SSH IP VPN Tunnel on Ubuntu 20.04

Today we will create an virtual interface to which you can assign an IP address and use like any other IP interface on Ubuntu. It’s transmissions are encrypted by SSH. This is not SSH port-forwarding. I repeat, this is not layer 4 SSH port-forwarding or what is commonly known as SSH-tunneling. This is full layer-3 connectivity on top of SSH.

SSH is a common tool for network engineers and systems administrators to securely access the CLI (command-line interface) of various systems. OpenSSH is an open-source implementation of the protocol and is included or available to install on most Linux distributions. While it’s a great tool for CLI access, SSH has other, darker powers that some consider to be hacking tools or black magic.

One of OpenSSH’s tools that is somewhat well known is the “SSH Tunnel”, and is basically a port forwarding technique that allows the sending of a single TCP or UDP port through an SSH connection. A Much less known feature is OpenSSH’s ability to create a virtual Ethernet adapter on top of an SSH connection. This allows full layer-3 IP connectivity, not just a single layer-4 TCP or UDP port. You can add routes that point through this virtual connection, just like you would any other Ethernet interface. You can even run a routing protocol across it.

Topology

We are setting up an SSH IP tunnel from Ubuntu20.04Server-1 on the left side at private physical IP 192.168.0.2 to Ubuntu20.04Server-3 on the right with a public physical IP of 12.0.0.2. The SSH tunnel will use a network of 10.0.0.0/30. One fun fact here is that this tunnel is traversing a NAT (PAT) that I set up on the Cisco router that is connecting Ubuntu20.04Server-1 to the Internet. No issues traversing NAT for SSH IP tunnel. Finally, we will add some static routes to allow Ubuntu20.04Server-4 to ping Ubuntu20.04Server-3 through the SSH tunnel.

Installation

Ubuntu20.04Server-3

You probably installed Ubuntu’s OpenSSH server when you installed the OS but you’ll also need a tool called autossh, so run this command on Ubuntu20.04Server-3:

apt-get install openssh-server

Now we’re going to make some changes to the SSH server configuration. Root login is required from the client in order to create a TUN adapter, so we’ll be enabling that. Edit the /etc/ssh/sshd_config file. You will make these changes:

  • Uncomment and change “PermitRootLogin prohibit-password” to “PermitRootLogin without-password”
  • Uncomment and change “PermitTunnel no” to “PermitTunnel yes”
vi /etc/ssh/sshd_config
PermitRootLogin without-password
PermitTunnel yes

The above configuration is confusing – it will allow login as root, but not without a key. It’s relatively secure. Then restart the OpenSSH server:

systemctl restart sshd

Ubuntu20.04Server-1

On Ubuntu20.04Server-1, you’ll need a tool called “autossh” that watches SSH sessions and restarts them if they die. Run this command:

apt-get install autossh

Let’s set up key authentication, so we can log in as root to the server. :

ssh-keygen -t rsa #create an RSA key
cat ~/.ssh/id_rsa.pub | ssh james@12.0.0.2 "mkdir -p ~/.ssh && cat >>  ~/.ssh/authorized_keys" #Copy key to server

Connecting the tunnel

We’re ready to build our tunnel! From Ubuntu 20.04Server-1 (the client at 192.168.0.2), run the following magical command:

autossh -M 0 -o "ServerAliveInterval 30" -o "ServerAliveCountMax 3" -NTC -o Tunnel=point-to-point -w 0:0 12.0.0.2 &

There’s a fair amount going on here, I’ll break it down:

  • ‘-M 0’ refers to monitoring tcp port, do not use
  • ‘-o “ServerAliveInterval 30”’ sends a keepalive every 30 seconds
  • ‘-o “ServerAliveCountMax 3″’ retries keepalive a maximum of 3 times. Autossh ends here, SSH native commands start from next option.
  • ’-N’ instructs SSH not to execute a remote command
  • ’-T’ disables pseudo-tty allocation
  • ’-C’ compression, may improve performance, may degrade
  • ‘-o Tunnel=point-to-point’ creates a virtual interface
  • ’-w 0:0’ gives the local and remote tun adapters a number, in this instance 0. Left side of ‘:’ is local, right side is remote.
  • 12.0.0.2 is the tunnel destination
  • The final ampersand runs the command in the background so you can get your shell back.

If you have done everything correctly, you now have a “tun0” device on both the server and client:

ip link

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 0c:88:6c:69:00:00 brd ff:ff:ff:ff:ff:ff
9: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UNKNOWN mode DEFAULT group default qlen 500
    link/none 

Now you can configure it with IP settings. You can use “ip route” commands for testing, or netplan to survive reboot. On the server, we’ll add a static route for 192.168.0.0/24 pointing through the newly created tunnel so it can access that network. And on Ubuntu 20.04Server-4 (an innocent bystander in the 192.168.0.0/24 network at 192.168.0.3), we’ll also add a route for the 10.0.0/30 network pointing to 192.168.0.2 so we can see that the tunnel works to route all IP traffic, not just traffic between the client and server. Make sure the client has IP forwarding enabled or that won’t work.

#On Ubuntu 20.04Server-1 (the client)

ip addr add 10.0.0.1/30 dev tun0
ip link set tun0 up

#On Ubuntu 20.04Server-3 (the server)
ip addr add 10.0.0.2/30 dev tun0
ip link set tun0 up
ip route add 192.168.0.0/24 via 10.0.0.1

#On Ubuntu 20.04Server-4 (innocent bystander at 192.168.0.3)
ip route add 10.0.0.0/30 via 192.168.0.2

Let’s trying pinging from Ubuntu 20.04Server-4 to Ubuntu 20.04Server-3:

james@u20vm:~$ ping 10.0.0.2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=63 time=3.87 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=63 time=3.73 ms

It works! Now let’s try to ping from the server Ubuntu 20.04Server-3 all the way through to Ubuntu 20.04Server-4, going right through that NAT at the Cisco router:

james@u20vm:~$ ping 192.168.0.3
PING 192.168.0.3 (192.168.0.3) 56(84) bytes of data.
64 bytes from 192.168.0.3: icmp_seq=1 ttl=63 time=3.30 ms
64 bytes from 192.168.0.3: icmp_seq=2 ttl=63 time=3.69 ms

It works!

Hope you enjoyed this one, SSH IP tunnels are one of my favorite Linux hacks.