SSH Tunnel: The simplest VPN

How a single command can create a poweful VPN in Linux

I don't know about you, but I have a reasonably complex network setup. I have my home WiFi network, which is pretty normal. Within that I then also have a local wired Ethernet network on (and around) my desk. Then on top of all that I have a virtual server out on the internet somewhere that I rent for my websites and various other services. And of course I have my laptop that I like to have as part of my network wherever I happen to be on the planet.

To integrate everything together I quite simply run a couple of VPNs.

Connecting my WiFi and Ethernet segments at home I have a Banana Pi M5 (Just llike the Raspberry Pi 4 but with two added benefits: it isn't plagued with power problems, and you can actually buy them at the moment!). A spot of routing magic and everything all tallks together.

But having that Pi in there does have another benefit - I can use it as the endpoint to a VPN to my server in the cloud (you don't have to use a Pi, I just happpen to have one at a convenient location on my network. Any Linux computer can be used - a laptop, your desktop, anything).

There is a probllem, though - like almost everyone else on a home network I'm behind a NAT router. And things get worse when I'm out-and-about on my laptop through my mobile phone where the mobile ISP also uses "carrier grade" NAT. Plus randomly assigned, and randomly changing, IP addresses really doesn't help.

So setting up a "traditional" VPN using something like IPSEC is completely impossible.

What is needed is some system where I can just "dial" my server using a standard protocol and establish a full network connection. Enter SSH...

SSH is not just a way of connecting to a remote server and executing commands. It has a whole system of "tunnels" built in - a way of sending arbitrary data from one end of the connection to the other. And that can be used as a conduit for network traffic.

I'd like to introduce you to a "little" SSH command:

ssh -f  -o PreferredAuthentications=publickey \
-o NumberOfPasswordPrompts=0 \
-o Tunnel=ethernet -o ServerAliveInterval=10 \
-o TCPKeepAlive=yes -o TunnelDevice=0:0 \
-o User=root -o Port=22 \
-o HostName=myserver.example.com \
myserver.example.com \
"/sbin/ifconfig tap0 10.0.0.1 netmask 255.255.255.0 pointopoint 10.0.0.2" \
&& /sbin/ifconfig tap0 10.0.0.2 netmask 255.255.255.0 pointtopoint 10.0.0.1

Well, it's little for certain values of little...

Let's take a little look at what is going on in this command. We can spit it into four rough parts.

All the -o parameters are configuring the various options to SSH. The first two configure how we'd like to authenticate: only use private/public key pairs, and never ask for a password.

The next four configure the tunnel itself - we choose a tunnel type of ethernet, set some options to keep the connection awake, and connect to a pair of tunnel devices. That's the TunnelDevice=0:0 option. This is where most of the magic happens. It create a pair of tun network interfaces, one on your local computer and the other on the remote computer. The numbers represent what number tun device to make. In this case it makes tun0 on both systems. The first number is your local computer, and the second is the remote computer. If you want to establish multiple connections to your server you need to have a unique number for the server's tun device for each connection. If you want to connect your local computer to mulltiple servers then you will need a unique number for each connection for the local tun device.

Then we have detaills about the connection itself - where to connect to and who to connect as. It's important to note that since network interface manipulation is typically a privileged operation we need to perform this as root - not only on the local computer, but on the server as well. You have to SSH in to the server as root, and that is normally not allowed by default (and with good reason). So we need to enable that, but we don't want to compromise the security of the server.

The main security concern with enabling root SSH access is that of someone getting or guessing your root password. So we really don't want that. Luckilly there's a half-way house: enable root SSH access but don't enable root SSH password access. You can configure sshd to only allow root access using private/public key pairs, which is considerably more secure than a password. To do that, on the server, you need to edit /etc/ssh/sshd_config and find the line starting PermitRootLogin. This is most often commented  out, so un-comment it. Very often you willl find it has the parameter prohibit-password, which is convenient, because that's exactly what we want. If not that's what you want to set it to, so it reads:

PermitRootLogin prohibit-password

Restarting sshd should then allow root to log in with a public key pair.

To generate the key pair, as root on your local computer, run:

ssh-keygen

Which should then generate a key pair id_rsa and id_rsa.pub in /root/.ssh. Don't enter any passwords, just hit enter for every question it asks.

Now find the file /root/.ssh/id_rsa.pub and copy the contents into the file /root/.ssh/authorized_keys on your server (you may have to create it).

Once done you should be able to SSH in to your server as root from your local root user's account without having to enter a  password.

Now that's working - back to the SSH command...

The next (penultimate) line is a command to run on the remote server once the connection is established. This simply configures the remote tun0 network interface with an IP address and tells it it's communicating over a point-to-point tunnel. You may need to install the net-tools package to get the ifconfig command.

The final line is the exact same for the local interface, but with the IP addresses reversed.

So in summary it's simply:

  • SSH in to the remote server from root as root and establish an ethernet tunnel

  • Create and configure two tunnel network endpoints with IP addresses

And that's pretty much it. Assuming it all worked you now have a VPN set up. ifconfig should show you something like:

tap0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.0.0.2  netmask 255.255.255.0  broadcast 10.0.0.255
        inet6 fe80::3873:d1ff:fe52:2925  prefixlen 64  scopeid 0x20<link>
        ether 3a:73:d1:52:29:25  txqueuelen 1000  (Ethernet)
        RX packets 219367  bytes 27846594 (27.8 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 493850  bytes 358099827 (358.0 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

And you can test it with a simple ping:

ping -c 4 10.0.0.1
PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.
64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=21.7 ms
64 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=20.7 ms
64 bytes from 10.0.0.1: icmp_seq=3 ttl=64 time=21.3 ms
64 bytes from 10.0.0.1: icmp_seq=4 ttl=64 time=25.9 ms

--- 10.0.0.1 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3004ms
rtt min/avg/max/mdev = 20.725/22.396/25.898/2.050 ms

The great thing with this VPN is it's possible to run it with NAT at both ends of the connection. That is, if both you and your server are behind NAT routers you can still use it. All you have to do is enable port forwarding on the router your server is behind  to pass a port (it doesn't have to be 22 - you can chose another obscure port number if you like, just change the 22 in the SSH command to whatever you choose) through to port 22 of the server.

And if your server is on a dynamically assigned IP address you can use a dynamic DNS service to keep track of the IP so you can always connect from wherever you are.

Simply put: if you can SSH in to the server you can create a VPN network connection with no complex configuration or needing to install any fancy software.

This of course only establishes the most basic of network connections. Nothing will pass over this connection except data destined directly for one of the endpoints. It's ideal if all you want to do is access the server itself - just remember to use the IP address of the remote end of the tunnel (10.0.0.1 in the example I show).

Now my setup is a little more complex: I have a VPN from my Banana Pi to connect my home network in to the server in the cloud. I then have another VPN on my laptop connecting it to the server in the cloud. And I want my laptop to have access to everything on my home network. That's where the magic of routing comes in, but that's a whole other (massive) topic for another day. If you want a sneak preview you might want to look at the quagga routing system, which is what I use across my network.

USB 3.0 Blues?
A blue connector does not USB 3.0 make.