Reverse SSH through 3G/NAT

When the Raspberri Pi is connected via the 3G/UMTS network you can no longer ssh into it because 3G uses NAT. Only the Pi itself can open a connection to other machines. Fortunately we can use a feature built into ssh called "remote port forwarding" which works like this:

The Pi's ssh client connects to your Linux desktop's ssh server and tells it to forward connections from a local port (e.g. 2222) back to a remote port on the Pi. In this case the remote port is 22 so we can connect to the Pi's ssh server. The first connection is the tunnel through which the second connection in the reverse direction is established. Each side acts both as client and server.

If your desktop doesn't run Linux you can simply use another Pi! :-) There probably are ssh servers for Windows that support port forwarding - contact me if you're aware of one.

We'll relax security a bit to make things work smoothly:

  • On the Pi we will store the password to be used for the desktop's ssh server in plaintext. Do not use this password for any other account. On the desktop, restrict the user account so that it can't do anything harmful. Be aware that if someone physically steals or remotely breaks into the Pi they can use these credentials to ssh into your desktop. (This would be the same for certificate based authentication, so it doesn't help us here.)
  • We'll also disable strict host key checking on the Pi so the connection doesn't break when your desktop's host key changes.

Unless you have a static IP you'll need to configure your router to use a dynamic DNS service such as noip.com (or myfritz.net if you use a FritzBox). Set up port forwarding in the router (cf. Wikipedia's list of ports). Let's assume your dynamic DNS address is foobar.noip.com and you are forwarding port 12345 to port 22 on your dekstop.

To secure your desktop's ssh server you might want to restrict the users that may log in at all. For example, to allow only the john account add this line at the top of /etc/ssh/sshd_config:

AllowUsers john

Test your setup by ssh-ing from the Pi to your desktop:

ssh john@foobar.noip.com -p 12345

Next install sshpass on the Pi. sshpass avoids the interactive ssh password prompt so the connection can be established automatically.

sudo apt-get install sshpass

Now initiate the reverse ssh connection from the Pi with the following script:

#!/bin/bash

REMOTE_PORT=12345
REMOTE_ADDRESS=john@foobar.noip.com
PASSWORD="johnspassword"

sshpass -p "$PASSWORD" \
ssh -o ServerAliveInterval=60 \
    -o ServerAliveCountMax=2 \
    -o StrictHostKeyChecking=no \
    -o UserKnownHostsFile=/dev/null \
    -o ConnectTimeout=15 \
    -N -R 2222:localhost:22 $REMOTE_ADDRESS -p $REMOTE_PORT

The ServerAlive... options make sure that when the desktop is shut down the connection is also shut down. This is required so that it can be re-established when the desktop is up again. The ...Host... options avoid key verification failures when your desktop's dynamic IP changes. See here for the full documentation of all options.

On the desktop you can now connect to the Pi as user pi:

ssh pi@localhost -p 2222

Of course we really want the Pi to initiate the connection automatically. So we wrap the above call in a loop and add an @reboot line to the crontab to run this script at startup:

#!/bin/bash

FWD_PORT=2222
WAIT_SECONDS=60

if [ "$#" != "3" ]; then
  echo "Maintain a reverse ssh connection, forwarding port $FWD_PORT on the remote machine."
  echo "If the connection fails or is dropped, wait $WAIT_SECONDS seconds and retry."
  echo "Usage: $(basename $0) [remote_user@]remote_server port password"
  echo "Then, on the remote ssh server: ssh $USER@localhost -p $FWD_PORT"
  exit
fi

REMOTE_ADDRESS=$1
REMOTE_PORT=$2
PASSWORD=$3

# By default the tunnel never seems to timeout. This is bad because if the connection to the
# server has been established once, then the server disconnects (link down, or maybe it's a laptop
# that doesn't run 24/7), it could never be re-established. So it's important to set a timeout for
# the tunnel. Note that keepalive is handled transparently by ssh; it does not mean any payload data
# has to be sent through the tunnel at these intervals.
SERVER_ALIVE_INTERVAL=60
SERVER_ALIVE_COUNT_MAX=2

while true; do
  sshpass -p "$PASSWORD" \
    ssh -o ServerAliveInterval=$SERVER_ALIVE_INTERVAL \
    -o ServerAliveCountMax=$SERVER_ALIVE_COUNT_MAX \
    -o StrictHostKeyChecking=no \
    -o UserKnownHostsFile=/dev/null \
    -o ConnectTimeout=15 \
    -N -R $FWD_PORT:localhost:22 $REMOTE_ADDRESS -p $REMOTE_PORT
  sleep $WAIT_SECONDS
done

The crontab entry:

@reboot ./reverse_ssh.sh john@foobar.noip.com 12345 johnspassword

Keep in mind that after starting up the desktop you'll have to wait up to a minute (or whatever interval you chose) before the connection is established. In my experience there is no harm in setting WAIT_SECONDS as low as 60 - it doesn't use up bandwidth, doesn't bog down the system or cause any other trouble.