Access Raspberry Pi behind router or firewall remotely

In this tutorial I will explain a method to log into your Raspberry Pi remotely to gain access even if it is behind a router or firewall.

Reverse SSH Tunnel

We will set up a script on the Raspberry Pi to start or stop a reverse SSH tunnel to a VPS server and provide a means to command the tunnel to turn on and off from the server.

Persisting the tunnel can be problematic with either the Raspberry Pi or the server dropping the connection. So we want to be able to command it from a server where we always have SSH access.

Method to turn on and off the tunnel service

We will run a PHP script on the server that indicates on or off when the http endpoint URL is requested.

This script will look for a file called ton in the root of our home directory on the server.

So, the script outputs on or off depending on if the ton file is present or not. It may also log a timestamp for when the script is accessed.

A script on the Pi will regularly probe this URL (say every 20 minutes). So it will take from zero to 20 minutes to update the status of our tunnel on/off, and the logged timestamp helps us to estimate how long we need to wait before our command change takes effect.

To simply create the ton file we may type touch ton or touch ~/ton (if we are not positioned at our home directory).

To remove it, we may type rm ton

Maybe you can think of a better way to set this kind of status flag? Maybe via a front-end App?

Script to signal the command to turn on/off

You can put this script anywhere on your server where the script may run like a web page. It could be in a specific folder for say, commands. This will determine the URL used to reach it in a browser or via a GET request from a remote script.

So here is the simple PHP script:

<?php
file_put_contents(__DIR__ . "/tunnel-log", date("Y-m-d H:i:s"));
if (file_exists("/home/YOUR_USER_NAME/ton"))
    echo "on";
else
    echo "off";

You can see that it saves the timestamp log locally, but it might be better to save it somewhere else where it has permission to write to such as /var/log/ssh.

Of course, you should test that it works in a browser whilst adding and removing the ton file.

To display the current time on the server, you can use the date command on the command line.

Linux command to start SSH reverse tunnel in the background

This command sets the port and runs the process in the background so that it doesn’t stop soon after the user exits.

The port simply needs to be one that is not used by any other service that is set up on your Pi. Port numbers are best chosen in the range of integers from 49152 to 65535 (private ports).

Here is the command that starts the SSH reverse tunnel in the background, returning the user to the command line:

nohup ssh -f -N -R PORT_NUMBER:localhost:SSH_PORT user@my-server-ip-address

Now, you might specify the user and ip address of the server, but a better way I think is to give it an ID by adding the details to .ssh/config. This makes it less likely that the server will reject your connection attempt because some incorrect authentication details might be sent.

An example entry is:

Host myserver
    Hostname 123.123.1.12
    Port 22
    User me
    IdentityFile ~/.ssh/rpi.ppk

You would test this with: ssh myserver to check that you connect ok to your server command shell.

And enter exit to close down the connection.

Turning off the reverse tunnel service

To facilitate this, we will extract the process ID and kill the associated process.

The process may have shut down when we come to turn it off, so rather than storing the process ID when we start it, we will detect it later or not find it.

Here is a command to find our running process:

ps aux | grep -v grep | grep PORT_NUMBER

This will show what the process ID number is and we may terminate it with the kill command.

PHP Script to turn the reverse tunnel service on and off

Now we know what commands we need, our port, and the URL of the command script, we can put together a script to run on the Raspberry Pi.

Here is the code:

<?php
define("LOCAL_PORT", "65303");
define("SSH_PORT", "22");
define("LOGIN", "user@myserver");
define("SERVER_ENDPOINT", "http://123.123.1.12/command/tunnel.php");

$pid = get_pid();

$turn_on = turn_on();

if ($pid > 0 && !$turn_on) {
    // Turn off
    // Kill the tunnel process
    exec("kill $pid");
    log_msg("stopped");
}

if ($pid < 1 && $turn_on) {
    // Turn on
    // Start the process in the background and ignore any text output
    exec('nohup ssh -f -N -R LOCAL_PORT:localhost:SSH_PORT LOGIN > /dev/null 2>&1 & echo $!', $op);
    log_msg("started");
}

function turn_on() {
    // Get the output from the server script
    $ch = curl_init(SERVER_ENDPOINT);
    curl_setopt($ch, CURLOPT_HEADER, 0);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    $on = trim((string)curl_exec($ch)) == "on";
    if(curl_error($ch)) {
        $on = false;
        log_msg("Error: " . curl_error($ch) . "\n");
    }
    curl_close($ch);
    //$on = false; // To test
    return $on;
}

function get_pid() {
    $pid = 0;
    // Filter out our process ID from the running processes
    exec('ps aux | grep -v grep | grep LOCAL_PORT', $op);
    if (count($op) > 0) {
        $pid = (int)preg_split("/\s+/", $op[0])[1];
    }
    return $pid;
}

function log_msg($txt) {
    // Add a message to the log file
    file_put_contents(__DIR__ . "/log", date("Y-m-d H:i:s") . "\t" . $txt . "\n",  FILE_APPEND);
}

When the script runs, it extracts the PID number or zero if the process is not running.

Then, it fetches the on or off value from the server end point.

Then, it decides what action if any it should take (do nothing / kill process / start tunnel).

Also, we log the start and stop times.

Scheduling the script

We should run the script at regular intervals.

We will add an entry to our crontab. This command opens our crontab in a text editor. Typically, we might use nano as our editor.

crontab -e

To run the script every 20 minutes, you may append this to the end of the crontab:

*/20 * * * * /path/to/script.php

There is no need for any restarting for this to take effect.

Work flow

So now we have all the pieces in place for a working reverse tunnel system to access our Pi from a server.

So our work flow could be:

  • ssh to our server from a PC
  • create the ton file
  • cat /var/www/path-to-log-file
  • see when the server script was last probed
  • wait until it should have been re-probed
  • check if the tunnel is available
  • connect to the localhost port
  • enter password for our pi user
  • do stuff on the pi
  • exit
  • delete ton

To create the ton file: touch ton

To check for the tunnel on the server: sudo lsof -i tcp

This will list the open ports supporting the tcp protocol and we should be able to see our tunnel entries like so:

sshd 66209 user 10u  IPv6 642432  0t0  TCP ip6-localhost:65303 (LISTEN)
sshd 66209 user 11u  IPv4 642433  0t0  TCP localhost:65303 (LISTEN)

Where 65303 in this case is our localhost port.

To connect: ssh localhost -p LOCAL_PORT

To delete ton: rm ton

If the tunnel does not appear to be available on the server when we had previously set it to be on (perhaps hours before), we may command it to be off, then after the next update time, command it to be back on again to re-establish the link.

In my experience, the connection was broken after around 2 hours.

Conclusion

So, we have presented a method to SSH to a Raspberry Pi which may be behind a firewall or router where there is no port-forwarding to it. Also, we provided a mechanism to remotely control the establishment of a reverse tunnel.

This contrasts to setting up a supposedly persistant reverse tunnel on the Pi and it being subject to unexpected loss of syncronization with the remote server where we have no way to recover it unless we may directly access the Pi via SSH.