Tunnel all the things

When you need to take control over a remote server or device, problems start to arise. Whether the device is behind a corporate firewall, a restrictive set-top box, or is just not publicly available from the Internet, it's always difficult to gain and keep access over the lifespan of the service the device is supposed to provide.

SSH tunnelling is one option for this that is dead simple, super powerful, and yet not very well known.

Ngrok and LocalTunnel provide this functionality somehow, and allow global access to "local" devices through a SSL tunnel. Ngrok link allows remote management of devices. All this services rely on a private central server that the SSH tunnel goes through

What I'm presenting here is a much bare bone solution that only relies on a simple SSH tunnel to gain and keep access to a remote device through a central public server.

What do we need

For the SSH tunnel to work, the only requirement is a server, publicly accessible, that runs a SSH daemon, and a user that can access this machine. We'll use the user foo, and the server will be central.example.org.

The user just needs to exist and be able to login.

To create it, a simple sudo useradd foo and passwd foo afterwards. For connecting, it's a good idea to add the connecting device's public key to the ~/.ssh/authorized_keys file of this foo user so that the tunnel creation can be non-interactive.

It will be mandatory if you use that technique to access remote devices (on which, by definition, you have no way to interactively input a password ...)

Of course you need some kind of device that we will access and manage remotely;

We're going to use basic Linux functionality and some NodeJS for easy creation of the tunnel.

Making local global

The first easy use-case is to make a local port available to the world thanks to SSH. This is easily done.

Suppose you have a local machine on which you develop, and you have a node service running on port 3000.

To make that port available to anyone (for remote testing for instance), just create a tunnel that forwards your local port to a random port on the central server :

ssh foo@central.example.org \
  -o PasswordAuthentication=no \
  -o ServerAliveInterval=30 \
  -N -R 0:localhost:3000

The output will be something like :

Allocated port 42671 for remote forward to localhost:3000

The port is allocated randomly. You can use a predefined port but make sure that you know that this port is not used somewhere else :

ssh foo@central.example.org \
  -o PasswordAuthentication=no \
  -o ServerAliveInterval=30 \
  -N -R 13245:localhost:3000

The output will be empty, don't expect anything — you already know the port you're forwarding to !

As long as the process is alive, the forwarding is effective. Just CTRL + C to stop the whole thing.

Accessing remote devices

The exact same principle can be used to gain remote access to your devices in the wild. The tunnel must be initiated on the remote device, to your central server.

If you choose to use a random port number, there is an extra step for you to retrieve the allocated port, but as your remote device is connected (since it just opened an SSH connection), you can either send the port via mail (quick and dirty) or use any other method to retrieve it (send a GET request to your backend for your devices for instance).

To use that, I have created a simple Node wrapper that I now use when I need to create a tunnel between remote devices and a central 'command' center.

The full code is available on Github as usual : tchapi/node-simple-ssh-tunnel

To use, it's pretty simple. It's just a wrapper around the aforementioned SSH stanza :

const tunnel = require('ssh_tunnel')

tunnel.setConfig({
    user: "foo",
    server: "central.example.org",
    port: 22,
    timeout: 10000, // in ms
})

tunnel.start(function() {
    console.log(tunnel.getState())

    // Later ...
    tunnel.stop()
}, function() {
    console.log("There has been an error ...")
})

And you would get something like that :

$ node examples/index.js
[info] Starting tunnel to foo@central.example.org
[info]  Allocated port 43557 for remote forward to localhost:22
[success] Tunnel active at 43557
{ port: '43557' }

Now, up to you to send this tunnel.getState() info to somewhere you can retrieve it.

Easy and robust.

Now, to connect to your remote device from anywhere, you just have to do :

ssh device_user@central.example.org -p 43557

which is strictly equivalent to :

ssh device_user@remote_device

... if you were on the same local network as the device.

🎉