Skip to content

Instantly share code, notes, and snippets.

@perfecto25
Last active September 1, 2023 18:33
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save perfecto25/6e9a0c982fb76401f720b661f1a8a9f1 to your computer and use it in GitHub Desktop.
Save perfecto25/6e9a0c982fb76401f720b661f1a8a9f1 to your computer and use it in GitHub Desktop.
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
Copy link

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
Copy link

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
Copy link
Author

updated to use SIGTERM

@linuxacademyir
Copy link

Hello
thank you for good job
but where i add remote server credentials like username / password and port?

@perfecto25
Copy link
Author

can add to /home/sshuttle/.ssh/config

Host abc1
Hostname abc1.corp.com
User joe
Port 22
IdentityFile /home/joe/.ssh/id_rsa

Using passwords for ssh is not recommended , its insecure, and theres no way to store them, you have to manually enter them during ssh handshake, use keypairs instead

@linuxacademyir
Copy link

Thank you for your reply
I have already added it to config file but by questions is
How does this script know which host in config to connect to
Should I call the “Host” abc1?
Shouldn’t I add my “Host” name into script ? Or the script reads it as “Host” abc1?

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