Here I explain how to use sshuttle to setup a VPN over SSH tunnel, and the reasons for this method. main image

Table of contents

Introduction

There are so many free and paid VPN providers available. But VPNs can be blocked in some countries like China.

In my country, the government uses advanced censorship techniques to block VPNs, one of those techniques is using DPI (Deep Packet Inspection) to block VPNs, so using VPNs is not possible. Even if I used a different port for it like 8080 or 443, it still will be blocked.

That’s where sshuttle comes in. sshuttle is a free open source software that can be used to setup a VPN over SSH tunnel, this means that if you already have a VPS where you can connect to it with SSH, you can make use of that connection to tunnel your traffic through it.

sshuttle need the root access on client side to modify the system firewall to tunnel all traffic through the SSH connection, But it doesn’t need root privileges on the server side, because all it automatically uploads its source code on the server to handle traffic.

So after installing sshuttle, via pacman, I ran it normally with sshuttle -r user@host:port 0/0 and it worked fine.

Gotchas

sshuttle needs the root password each time I run it, but there’s an easy solution for this, just running sshuttle --sudoers will auto generate the proper sudoers.d file using the current user. But It didn’t work for me!

Another annoying behaviour is when I put my machine into sleep mode, and open it up after some time, I find that sshuttle process is stopped! and most times I find myself browsing the web from my original IP address, without noticing!

So I thought that I may need a systemd service that works all the time… After some research, I found an old systemd service file, but it didn’t work for me!

So after some research I found another python script that controls the sshuttle process, with systemd.

But it still fails to run because sshuttle asks for password in an interactive way that cannot be solved with that script.

So I needed to solve all of this.

  • Getting rid of the SSH passphrase.
  • Using the sshuttle service without root password.
  • Controlling sshuttle process via systemd.

Going Passwordless

SSH without passphrase

SSHuttle uses the default private key used for authentication with the SSH server, it is usually located in ~/.ssh/id_rsa.

To make things a bit easier, so I don’t have to type SSH passphrase every time I use sshuttle, there are two methods:

1- Using a key without a passphrase:

This requires keeping the device safe or else the remote server may be compromised.

To minimize the risk of compromise, it would be good idea to create a new user in the remote server just for this purpose.

Creating a new user is fairly easy, just typing this command:

sudo adduser example -s /usr/bin/true

The -s /usr/bin/true is to disallow user from controling the server via SSH.

After this, comes the authentication step, with a new or a the current SSH key.

  • Generating a new key is easy using the command ssh-keygen and entering an empty password.

  • Or using the same key but after changing the password to an empty one with ssh-keygen -p

Finally, the regular connection goes as usual: ssh-copy-id username@remote_host then sshuttle -r username@remote_host 0/0

2- Storing the passphrase to system wallet.

For me, I’m using this method with the Gnome Seahorse app, this app is great and stores credentials like SSH, GPG private keys passwords.

SSHuttle without root password

To overcome the root password issue, I had to go another approach which is to use my local root user for all of this.

So I moved my SSH key to /root/.ssh and changed it with empty password, and now the passwordless step is completed.

The python script

Here’s the python script which handles sshuttle through systemd:

Credits: @perfecto25 for initial script and @fake-name for the python3 version.

#!/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 = f"{os.path.abspath(os.path.dirname(__file__))}/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 = f"-r {ssh_user}@{rhost} {netrange} -x {exclude_host_direct}"

        try:
            print("starting sshuttle..")
            log.info("starting sshuttle for networks: %s via %s" % (netrange, rhost))
            command = "sshuttle --no-latency-control --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()

This script requires a json file which contains the configuration for the sshuttle service:

{
	"user": "example",
	"path": {
		"server_ip:port": [
			"0/0"
		]
	}
}

Putting those files in any directory that exists in $PATH will do the trick, so I put them in /usr/local/bin.

SystemD

SystemD is a huge set of programs that manages the startup and shutdown of services, along with other system configuration like networking.

To make SSHuttle work with systemD, a service file should be created in /etc/systemd/system/sshuttle.service with this content:

[Unit]
Description=sshuttle service
After=network.target

[Service]
User=root
Restart=always
Type=forking
WorkingDirectory=/usr/local/bin
ExecStart=/usr/local/bin/sshuttle.py start
ExecStop=/usr/local/bin/sshuttle.py stop

[Install]
WantedBy=multi-user.target

Then, running sudo systemctl daemon-reload will add this service, after that, I ran sudo systemctl enable --now sshuttle.service to start sshuttle which will run on boot.

Conclusion

For people with specific requirements, sshuttle can be a great tool to allow users to maintain their privacy online without having to deal with government censorship.

Those scripts facilitate the process of creating a VPN connection to a remote server, which automatically works after boot, and can be easily configured and enabled/restarted, and all of this with the help of systemD.

Related topics