Skip to content

Instantly share code, notes, and snippets.

@perfecto25
Last active Nov 27, 2021
Embed
What would you like to do?
sshuttle:
pkg.installed: []
group.present:
- gid: 2024
user.present:
- fullname: sshuttle
- uid: 2024
- gid: 2024
- allow_uid_change: True
- allow_gid_change: True
- createhome: True
- shell: '/bin/bash'
- home: /home/sshuttle
## Manage SSH Keys
ssh_auth.present:
- user: sshuttle
- names:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKlQ2xxxxxxxxxxxxxxxx sshuttle
file.managed:
- names:
- /home/sshuttle/.ssh/id_ed25519:
- source: salt://{{ slspath }}/files/id_ed25519
- mode: 600
- /home/sshuttle/.ssh/id_ed25519.pub:
- source: salt://{{ slspath }}/files/id_ed25519.pub
- mode: 644
- user: sshuttle
- group: sshuttle
/etc/sshuttle:
file.directory:
- user: sshuttle
- group: sshuttle
- dirmode: 744
- makedirs: True
sshuttle_config:
file.serialize:
- name: /etc/sshuttle/config.json
- formatter: json
- create: True
- user: sshuttle
- group: sshuttle
- mode: 644
- makedirs: True
- dataset:
{{ salt['pillar.get']('sshuttle') }}
sshuttle_runscript:
file.managed:
- name: /etc/sshuttle/sshuttle.py
- source: salt://{{ slspath }}/files/sshuttle.py
- user: sshuttle
- group: sshuttle
- mode: 744
sshuttle_sudoers:
file.managed:
- name: /etc/sudoers.d/sshuttle
- source: salt://{{ slspath }}/files/sudoers
- user: root
- group: root
- mode: 644
sshuttle_service_file:
file.managed:
- name: /etc/systemd/system/sshuttle.service
- source: salt://{{ slspath }}/files/service
- user: root
- group: root
- mode: 644
sshuttle_service:
service.running:
- name: sshuttle
- enable: True
- watch:
- file: sshuttle_config
#!/usr/bin/env python
## REQUIRES MINIMUM PY VERSION 2.7
from __future__ import print_function
import os
import sys
import json
import signal
import time
import socket
import subprocess
from subprocess import CalledProcessError
import logging
import logging.handlers
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)
handler = logging.handlers.SysLogHandler(address = '/dev/log')
formatter = logging.Formatter('%(module)s.%(funcName)s: %(message)s')
handler.setFormatter(formatter)
log.addHandler(handler)
conf = "/etc/sshuttle/config.json"
ssh_user = "sshuttle" ## username thats used for SSH connection
def precheck():
if len(sys.argv) < 2:
print("need to pass argument: start | stop | restart | status ")
sys.exit()
if sys.argv[1] in ["help", "-h", "--help", "h"]:
print("sshuttle.py start | stop | restart | status")
sys.exit()
if not sys.argv[1] in ["start", "stop", "restart", "status"]:
print("usage: sshuttle.py start | stop | restart | status")
sys.exit()
if not os.path.exists(conf):
print("no sshuttle config file present, exiting.")
sys.exit()
# check if sshuttle is installed
try:
subprocess.check_output(["which", "sshuttle"]).strip()
except CalledProcessError:
print("sshuttle is not installed on this host")
sys.exit()
def start():
with open(conf) as jsondata:
data = json.load(jsondata)
keys = sorted(data.keys())
for rhost in data.keys():
if ":" in rhost:
relay = rhost.split(":")[1]
else:
relay = rhost
netrange = ""
# if single network, turn into List
if not type(data[rhost]) is list:
networks = data[rhost].split()
else:
networks = data[rhost]
for network in networks:
# check if CIDR format
if "/" in network:
netrange = netrange + " " + network
else:
netrange = netrange + " " + socket.gethostbyname(network)
netrange = netrange.strip()
# build rpath
rpath = "-r {0}@{1} {2} -l listen '0.0.0.0' --ssh-cmd 'ssh -o ServerAliveInterval=60' --no-latency-control".format(ssh_user, rhost, netrange)
try:
print("starting sshuttle..")
log.info("starting sshuttle for networks: %s via %s" % (netrange, rhost))
subprocess.Popen("sshuttle {}".format(rpath), shell=True)
except CalledProcessError as err:
log.error("error running sshuttle: %s" % str(err))
# sleep to give connection time to establish SSH handshake, in case other connections use this conn as a hop
time.sleep(3)
def get_pid():
search = "ps -ef | grep '/usr/bin/python /usr/share/sshuttle/main.py /usr/bin/python -r' | grep -v grep | awk {'print $2'}"
pids = []
for line in os.popen(search):
fields = line.split()
pids.append(fields[0])
return pids
def stop():
pids = get_pid()
for pid in pids:
print("stopping sshuttle PID %s " % pid)
log.info("stopping sshuttle")
os.kill(int(pid), signal.SIGTERM)
def status():
pids = get_pid()
if pids:
print("sshuttle is running..")
else:
print("sshuttle is not running..")
if __name__ == "__main__":
precheck()
cmd = sys.argv[1].lower()
if cmd == "start":
start()
if cmd == "stop":
stop()
if cmd == "restart":
print("restarting sshuttle..")
stop()
start()
if cmd == "status":
status()
[Unit]
Description=sshuttle service
After=network.target
[Service]
User=sshuttle
Restart=always
Type=forking
WorkingDirectory=/etc/sshuttle
ExecStart=/etc/sshuttle/sshuttle.py start
ExecStop=/etc/sshuttle/sshuttle.py stop
[Install]
WantedBy=multi-user.target
#!/bin/sh
### SSHUTTLE INIT.D SERVICE SCRIPT
### REQUIRED MINIMUM Python version of 2.7
SCRIPT=/etc/sshuttle/sshuttle.py
RUNAS=sshuttle
LOGFILE=/var/log/messages
start() {
su -c "$SCRIPT start" $RUNAS >&2
}
stop() {
su -c "$SCRIPT stop" $RUNAS >&2
}
restart() {
stop
start
}
status() {
su -c "$SCRIPT status" >&2
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
stop
start
;;
status)
status
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
esac
sshuttle ALL=(root) NOPASSWD: /usr/bin/python /usr/share/sshuttle/main.py /usr/bin/python --firewall 12*** 0
@fake-name

This comment has been minimized.

Copy link

@fake-name fake-name commented Jun 6, 2020

Patched for modern python:

#!/usr/bin/env python3
from __future__ import print_function

import os
import sys
import json
import signal
import socket
import subprocess
from subprocess import CalledProcessError
import logging
import logging.handlers

log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)
handler = logging.handlers.SysLogHandler(address = '/dev/log')
formatter = logging.Formatter('%(module)s.%(funcName)s: %(message)s')
handler.setFormatter(formatter)
log.addHandler(handler)

conf = "/etc/sshuttle/config.json"

def precheck():
    if len(sys.argv) < 2:
        print("need to pass argument: start | stop | restart | status ")
        sys.exit()
    
    if sys.argv[1] in ["help", "-h", "--help", "h"]:
        print("sshuttle.py start | stop | restart | status")
        sys.exit()

    if not sys.argv[1] in ["start", "stop", "restart", "status"]:
        print("usage: sshuttle.py start | stop | restart | status")
        sys.exit()
    
    if not os.path.exists(conf):
        print("no sshuttle config file present, exiting.")
        sys.exit()
    
    # check if sshuttle is installed
    try:
        subprocess.check_output(["which", "sshuttle"]).strip()
    except CalledProcessError:
        print("sshuttle is not installed on this host")
        sys.exit()
        
def start():

    with open(conf) as jsondata:
        data = json.load(jsondata)

    assert 'user' in data, "'user' key (for the SSH user) needs to be present in json config file"
    assert 'path' in data, "'path' key needs to be present in json config file"

    ssh_user = data['user']

    for rhost in data['path'].keys():
        netrange = ""

        # if single network, turn into List
        if not type(data['path'][rhost]) is list:
            networks = data['path'][rhost].split()
        else:
            networks = data['path'][rhost]

        for network in networks:
            
            # check if CIDR format
            if "/" in network:
                netrange = netrange + " " + network
            else:
                netrange = netrange + " " + socket.gethostbyname(network)
        netrange = netrange.strip()
        
        # Modern kernels have iptables changes will kill and try to then route the connection to the 
        # emote server through the connection to the remote server, breaking the link. Therefore
        # so we need to exclude the direct connection to the remote server.
        # See https://github.com/sshuttle/sshuttle/issues/191
        exclude_host_direct = socket.gethostbyname(rhost.split(":")[0])

        # build rpath
        rpath = "-r {0}@{1} {2} -x {3}".format(ssh_user, rhost, netrange, exclude_host_direct)
        try:
            print("starting sshuttle..")
            log.info("starting sshuttle for networks: %s via %s" % (netrange, rhost))
            command = "sshuttle --dns {}".format(rpath)
            log.info("Command invocation: '%s': ", command)
            subprocess.Popen(command, shell=True) 
        except CalledProcessError as err:
            log.error("error running sshuttle: %s" % str(err))

def get_pid():
    search = "ps -ef | grep '/usr/bin/python3 /usr/bin/sshuttle --dns -r' | grep -v grep | awk {'print $2'}"    
    pids = []
    for line in os.popen(search):
        fields = line.split()
        pids.append(fields[0])
    return pids

def stop():
    pids = get_pid()
    for pid in pids:
        print("stopping sshuttle PID %s " % pid)
        log.info("stopping sshuttle")
        os.kill(int(pid), signal.SIGKILL)

def status():
    pids = get_pid()
    if pids:
        print("sshuttle is running..")
    else:
        print("sshuttle is not running..")

if __name__ == "__main__":

    precheck()

    cmd = sys.argv[1].lower()

    if cmd == "start":
        start()

    if cmd == "stop":
        stop()
    
    if cmd == "restart":
        print("restarting sshuttle..")
        stop()
        start()
        
    if cmd == "status":
        status()

Also, the remote SSH user is now a json parameter:

{
	"user" : "remote_user",
	"path" : {
		"remote_host:remote_port": [
			"0/0"
		]
	}
}
@aayla-secura

This comment has been minimized.

Copy link

@aayla-secura aayla-secura commented Jun 14, 2020

I think it's better to leave out the remote_user from the config completely and let sshuttle use the user specified in ~/.ssh/config.

Also, and more importantly, please send SIGTERM rather than SIGKILL, so that sshuttle can clean up the iptables rules. Otherwise the host is left in a messy state with non-working connections, and requires a restart (or manual removing of iptables rules). In general, you should never send SIGKILL unless the process has become unresponsive and there is no other way to stop it.

@perfecto25

This comment has been minimized.

Copy link
Owner Author

@perfecto25 perfecto25 commented Jul 29, 2020

updated to use SIGTERM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment