OpenSSH >= 6.7 can use Unix domain socket forwarding
That means the reverse tunnel endpoint will be an UNIX listening socket instead of a traditional TCP listening socket. You can then manage more easily the flotilla of RPIs with an easy naming scheme: the socket's name will be the RPI's chosen (and fixed) name, like OfficeDevice1991
. It could be even be an unique property from the RPI as long as it's a valid filename (since unix socket names adhere to filename conventions). For example its hostname, the MAC address of its ethernet or wifi card ...
SSH can handle unix sockets for tunnels, not for connecting itself. It will need the help of a ProxyCommand
to be able to work as unix-socket client. socat can handle many kind of connections, including unix sockets.
UPDATE:
There is also a specific issue to handle: the unix socket file is not deleted on clean exit, nor would it have been be deleted anyway for example after a crash. This require the option StreamLocalBindUnlink=yes
. I didn't find initially that, as the name perhaps imply, this option must be set on the node creating the unix socket. So in the end it's set on the client with a local forward (-L
) or else on the server (in sshd_config
) with a remote forward (-R
). OP found it there. This solution uses a remote forward.
Configuration on VPS:
mkdir /rpi-access
(as root) edit the sshd_config
file (/etc/ssh/sshd_config
). It requires this additional option:
StreamLocalBindUnlink yes
Depending on default options it might also require AllowStreamLocalForwarding yes
UPDATE2:
Also set in sshd_config
the parameters ClientAliveInterval
and ClientAliveCountMax
, thus allowing to detect a disconnect in a reasonable time, eg:
ClientAliveInterval 300
ClientAliveCountMax 2
Stale ssh connections should then be detected earlier on the VPS (~10mn with the example), and the corresponding sshd process will then exit.
Usage on RPI:
ssh -fN -R /rpi-access/OfficeDevice1991:localhost:22 -i /privatekeyfile useraccount@ip
In a config file this would be similar to this:
Host ip
User useraccount
RemoteForward /rpi-access/OfficeDevice1991:localhost:22
IdentityFile /privatekeyfile
Repeating it again: the StreamLocalBindUnlink yes
set on sshd
on VPS side option is important: the socket just created is not removed, even upon normal exit. This option ensures that the socket is removed if it exists before use, thus allowing to be reused for further reconnections. This also means one can't consider the mere presence of the socket as meaning the RPI is connected (but see later).
Now this allows to do on VPS:
ssh -o 'ProxyCommand=socat UNIX:/rpi-access/%h -' rpiuseraccount@OfficeDevice1991
As a configuration file, considering for example RPIs have all a name starting with OfficeDevice:
Host OfficeDevice*
User rpiuseraccount
ProxyCommand socat UNIX:/rpi-access/%h -
To keep the link, just use a loop
The RPI can run a loop reconnecting ssh to the VPS whenever the connections ends. For this it musn't use the background mode (no -f
). A keepalive mechanism should also be used. TCPKeepAlive (system level) or ServerAliveInterval (application level) are available. I think TCPKeepAlive is useful only on the server (the side receiving the connection), so let's rather use ServerAliveInterval.
Its value (as well as ServerAliveCountMax) should probably be adapted depending on various criteria: a firewall dropping inactive connections after a certain time, the wished recovery delay, not generating useless traffic, ... let's say 300s here.
OfficeDevice1991 RPI:
#!/bin/sh
while : ; do
ssh -N -o ConnectTimeout=30 -o ServerAliveInterval=300 -R /rpi-access/OfficeDevice1991:localhost:22 -i /privatekeyfile useraccount@ip
sleep 5 # avoid flood/DDoS in case of really unexpected issues
done
Even if the remote side didn't yet detect the previous connectivity failure, and for a few more time has the old ssh connection still running, StreamLocalBindUnlink yes
will anyway forcefully refresh the unix socket to the new connection.
Best Answer
I love explaining this kind of thing through visualization. :-)
Think of your SSH connections as tubes. Big tubes. Normally, you'll reach through these tubes to run a shell on a remote computer. The shell runs in a virtual terminal (tty). But you know this part already.
Think of your tunnel as a tube within a tube. You still have the big SSH connection, but the -L or -R option lets you set up a smaller tube inside it.
Every tube has a beginning and an end. The big tube, your SSH connection, started with your SSH client and ends up at the SSH server you connected to. All the smaller tubes have the same endpoints, except that the role of "start" or "end" is determined by whether you used
-L
or-R
(respectively) to create them.(You haven't said, but I'm going to assume that the "remote" machine you've mentioned, the one behind the firewall, can access the Internet using Network Address Translation (NAT). This is kind of important, so please correct this assumption if it is false.)
When you create a tunnel, you specify an address and port on which it will answer, and an address and port to which it will be delivered. The
-L
option tells the tunnel to answer on the local side of the tunnel (the host running your client). The-R
option tells the tunnel to answer on the remote side (the SSH server).So... To be able to SSH from the Internet into a machine behind a firewall, you need the machine in question to open an SSH connection to the outside world and include a
-R
tunnel whose "entry" point is the "remote" side of his connection.Of the two models shown above, you want the one on the right.
From the firewalled host:
This tells your client to establish a tunnel with a
-R
emote entry point. Anything that attaches to port 22222 on the far end of the tunnel will actually reach "localhost port 22", where "localhost" is from the perspective of the exit point of the tunnel (i.e. your ssh client).The other options are:
-f
tells ssh to background itself after it authenticates, so you don't have to sit around running something on the remote server for the tunnel to remain alive.-N
says that you want an SSH connection, but you don't actually want to run any remote commands. If all you're creating is a tunnel, then including this option saves resources.-T
disables pseudo-tty allocation, which is appropriate because you're not trying to create an interactive shell.There will be a password challenge unless you have set up DSA or RSA keys for a passwordless login.
Note that it is STRONGLY recommended that you use a throw-away account (not your own login) that you set up for just this tunnel/customer/server.
Now, from your shell on yourpublichost, establish a connection to the firewalled host through the tunnel:
You'll get a host key challenge, as you've probably never hit this host before. Then you'll get a password challenge for the
username
account (unless you've set up keys for passwordless login).If you're going to be accessing this host on a regular basis, you can also simplify access by adding a few lines to your
~/.ssh/config
file:Adjust
remotehostname
andremoteusername
to suit. Theremoteusername
field must match your username on the remote server, butremotehostname
can be any hostname that suits you, it doesn't have to match anything resolvable.See also:
ControlMaster
to maintain your tunnel