Self-hosted reversed tunnel for integration-heavy network application development

Are you developing some kind of chatbot, or perhaps integrating your app with Facebook Messenger?
You may find yourself in need of a reverse HTTP or TCP tunnel, ngrok is an example of such tunnels.
They are great at the marketing stuff, turning other solutions to dust, but lesser-known open-sourced self-hosted solutions do exist.

The solution is called SSH

SSH has every network tool majority of people want, including a reverse TCP tunnel that lets you expose your local webserver to the internet.

Swiss Army Knife

You may surprise at the quality and features provided by the open-sourced software called OpenSSH.
If you have a spare server with SSH connections, you can turn it into a reverse tunneling server without installing any software. OpenSSH got you covered.

Cutting to the chase, the command to be run on your localhost is:

ssh -R <remote-listen-address>:<remote-listen-port>:<local-listen-address>:<local-listen-port> -q -C -N <ssh-user>@<ssh-host>

Flags explained

  • -R: Forward TCP to the local side
  • -q: Quiet mode
  • -C: Compress data
  • -N: No command, port forwarding only

You may want to add other options, like for authentications; for me, the actual command I use is:

ssh -i ~/.ssh/id_example_server -R localhost:8081:localhost:8081 -q -C -N user@tunnel.example.com

Curious why I only listen to localhost on the remote server?

Adding some context about the setup

TCP tunneling is hardly the only thing you need for your project. Most of the use cases also require TLS (HTTPS) connections.

certbot

That is why I also install NGINX and Let’s Encrypt’s certbot on my server.

I forward the traffic from NGINX to the remote listening address. The NGINX has been used as a TLS termination proxy.
Below is an example of the end configuration I use for my NGINX server.

server {
    listen 443 ssl; # managed by Certbot
    server_name tunnel.example.com;

    add_header Strict-Transport-Security "max-age=15552000" always;

    location / {
        proxy_pass http://localhost:8081;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto https;
    }

    ssl_certificate /etc/letsencrypt/live/tunnel.example.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/tunnel.example.com/privkey.pem; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
}

server {
    listen 80;
    server_name tunnel.example.com;

    if ($host = tunnel.example.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    return 404; # managed by Certbot
}

And that is also the reason why localhost is the address for listening on the remote server in my setup.

Happy coding!