Motivation
Long as the list of VPN technologies in my NOS of choice is, it doesn’t include SSH. It is the aim of this document to convince you that this is an oversight. SSH may be used in many cases that might seem to call for a traditional VPN.
Reverse Tunnels
One of the key advantages of using SSH as a lightweight VPN is that it’s easy to set up NAT traversal. Instead of arranging for port-forwarding to an internal host in order to manage it from across the Internet, you can have it SSH out to a public tunnel host, where it waits for inbound connections, then tunnels that traffic back inside, where it hands it to the remote private host on your behalf.
Let’s say we want to expose an internal web server to the public without using port-forwarding or a traditional VPN, and without actually moving that host out onto the public Internet. This systemd unit will do that:
[Unit]
Description=Expose My Web Server
After=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/ssh \
-NT \
-oKeepAlive=yes \
-R *:%i:127.0.0.1:80 \
my-public-tunnel-host.example.com
Restart=always
[Install]
WantedBy=default.target
Place that in ~/.config/systemd/user/remote-tunnel@.service
, but hold off trying to enable it. There are things we must do before it can start without error:
Set up SSH keys between this internal client and the remote tunnel host. You should be able to say
ssh my-public-tunnel-host.example.com
and have it log you in directly, without needing a password.If the server has a firewall running, open a high-numbered port for the tunnel. We’ll use 12345 in this example, but anything in the 1024-49151 range is legal for this purpose. Example commands for this are:
$ sudo ufw allow 12345 # Ubuntu $ sudo firewall-cmd --add-port 12345/tcp # RHEL family; repeat with --permanent
Enable the
GatewayPorts
setting on the tunnel server, thensudo systemctl reload ssh
.
At this point, a manual run of the tunnel-starting command on the internal host should succeed:
$ /usr/bin/ssh -NT -oKeepAlive=yes -R \*:12345:127.0.0.1:80 \
my-public-tunnel-host.example.com
It will appear to do nothing, but if you then put http://my-public-tunnel-host.example.com:12345
into a web browser, it will connect through the tunnel to the internal host on port 80. As soon as you Ctrl-C out of the tunnel command above, the public tunnel host will stop listening on TCP port 12345; the tunnel is down.
If all that worked, set it to run in the background with:
$ sudo loginctl enable-linger $UID
$ systemctl --user daemon-reload
$ systemctl --user enable remote-tunnel@12345
$ systemctl --user start remote-tunnel@12345
$ systemctl --user status remote-tunnel@12345
That should bring the tunnel back up, allowing your web browser test to succeed again. Calling “stop
” on the service will bring it right back down.
The reason we parameterized this systemd unit with the @
sign is so we can have more than one, either on the same box, or with multiple client boxes, each with their own unique tunnel port. You can imagine deploying a fleet of boxes with a serial number that sets an offset within the 1024-49152 range on the public tunnel server, allowing you to log into each one by knowing which port to connect to.
Do realize, you are exposing the internal host to the public Internet now! Whatever mechanisms the protocol you’re tunneling has for making it secure, you need to be looking into now, hard.
Remote Login
I believe the most elegant service to forward through this remote tunnel is SSH itself. Simply replace the “:80” in the example above with “:22”. Done on a box with an SSH server running, this gives you the ability to log into that box using the same powerful protocol that enabled the initial tunnel connection.
One of the things to realize is that when you do this, you’re double-encrypting everything from the public tunnel server back into the private network. This might give enough extra security that you can tolerate an old client device running a weak ciphersuite, provided you bind the tunnel to localhost
instead of to *
. In this scenario, the private client still unavoidably uses weak crypto to establish the outbound tunnel, but if your attacker cannot establish a MITM position in that link, the client may be protected by upgrading the tunnel server to modern crypto. Then, the only way to get at the public end of the tunnel is to log into the tunnel server.
And how do we do that seamlessly? By using SSH’s -L
flag instead:
$ ssh -NT -L 2222:localhost:12345 my-public-tunnel-host.example.com
Now you’ve got a port open on your client’s localhost (2222) that, when you connect to it, tunnels first to the public server in the “forward” direction, then through that in the “reverse” direction to the private server!
Multiple Private Hosts, One Tunnel
Have you noticed that between these two flags, -L
and -R
, that we aren’t limited to connecting to the private host running the remote-tunnel@.service
unit? You might have multiple devices at that remote site needing to be managed, but you need only this one outbound tunnel connection to get at them all through this single tunnel.
The sanest way to manage that for sets of ports that don’t change often is to tunnel SSH from the remote side, then put something like this into your local client’s ~/.ssh/config
file:
Host remote-tunnel
Port 12345
HostName my-public-tunnel-host.example.com
LocalForward 8291 192.168.88.1 # WinBox mgmt UI on Chateau, main GW router
LocalForward 8080 192.168.88.6 # SwOS web UI on CSS106 switch
LocalForward 8022 192.168.88.9 # another host running SSH
Now you can say ssh remote-tunnel
on your local client to quickly establish three local listening ports that will forward traffic through that tunnel to the remote host, which will then forward it on to other hosts on that private LAN.
But Setting Up Many Ports Is a Pain!
That list of LocalForward
directives above is fine when the list is short, or at least when it doesn’t change often. When the number of devices starts growing at each site, SSH has a solution for that, too:
$ ssh -NT -D 1080 my-public-tunnel-host.example.com
This starts a SOCKS proxy, once a quite popular method for allowing clients access to the Internet through a single shared connection. Modern routers have largely supplanted SOCKS proxies, but the clients remain available if you go looking.
The single most widely deployed category of SOCKS clients are web browsers. All the major ones have a way to request that it make connections indirectly, via a SOCKS proxy, and if you point it at localhost
, it will happily tunnel all web traffic through SSH to the public tunnel host, emerging on the Internet as if the connection was made from your tunnel host.
If you combine this with the ideas above, you can instead connect to the private SSH server through a -R
tunnel, at which point your browser’s connections are made on the private LAN where the remote-tunnel@.service
unit is running. The connections will appear as if coming from that host, not from your local client, two NAT barriers away.
But “VPN” Means Routing Traffic!
There are those who will quibble that a single tunnel connecting two hosts cannot be a VPN without a routing layer in there somewhere.
But lo! What have we here? It is the -w
option to our rescue, allowing us to attach these SSH tunnels to Linux tap/tun devices, at which point we have as much power as any other Linux-based VPN.
This gets complicated, but it is doable. I recommend this path only if one of the simpler methods above doesn’t suffice.
And let’s be real: if you’re at this point, you might need to be thinking about traditional VPN protocols. That public SSH tunnel host could just as well be set up to run WireGuard tunnels instead, for instance.
What Has All This to Do with RouterOS?
“This is the MikroTik Solutions site!” I hear you yell. “You haven’t given a single RouterOS command yet!”
True, and that’s largely because when I go reaching for an SSH tunnel, I first look around for a Linux box to do it on, because it’s likely to be running full-strength OpenSSH. If I can’t have that, I look for a macOS box, and then failing that, even Windows 10 comes with the option to add OpenSSH with a single click, once you dig down into the Control Panel to discover the place they’ve hidden it away this week.
But although RouterOS runs a nerfed SSH server based on Dropbear, a stripped-down SSH server meant to be embedded in systems like RouterOS, it does at least offer a subset of the features described above:
/ip ssh
set forwarding-enabled=local # client may use OpenSSH's -L flag
set forwarding-enabled=remote # ditto -R flag
There does not appear to be a way to make it establish that outbound connection to the public tunnel server, nor are there equivalents to OpenSSH’s -D
and -w
flags. For that, you will either need to run proper OpenSSH on the router in a container, fob the job off on another box as previously described, or switch to a VPN protocol RouterOS supports natively.
License
This work is © 2024 by Warren Young and is licensed under CC BY-NC-SA 4.0