Port Forwarding & Tunnelling

Chapter 5 — Port Forwarding & Tunnelling

SSH is more than a remote shell. It can create encrypted tunnels that carry arbitrary TCP traffic between machines — forwarding ports across firewalls, exposing services on the internet, or routing your entire browser through a remote server. This chapter covers the three forwarding modes: local, remote, and dynamic.

What is port forwarding? When SSH forwards a port, it listens on a port on one machine and transparently delivers traffic to a port on another machine — through the encrypted SSH connection. The application sending or receiving traffic doesn't know there's a tunnel involved.

The Three Types at a Glance

Local Forwarding
A port on your machine forwards to a port the server can reach. ssh -L local_port:target_host:target_port server Use when: you want to reach a remote service from your machine — a database, internal website, or admin panel that isn't exposed publicly.
Remote Forwarding
A port on the server forwards back to a port your machine can reach. ssh -R remote_port:target_host:target_port server Use when: you want the server (or internet) to reach something on your local machine — exposing a dev server or a machine behind NAT.
Dynamic (SOCKS Proxy)
A local SOCKS proxy port routes any traffic through the server. ssh -D local_port server Use when: you want to browse the web (or run any app) as if you were on the server's network — bypassing geo-restrictions or local network filtering.

Local Port Forwarding (-L)

The most common use case: you want to reach something on the remote side that isn't directly accessible from your machine. Your laptop connects to localhost:local_port, and SSH silently delivers that traffic through the tunnel to target_host:target_port — as seen from the server.

Your machine
localhost8080
encrypted SSH tunnel
(internet)
SSH Server
server
plain connection
(local network)
Target
db.internal5432
# Syntax ssh -L LOCAL_PORT:TARGET_HOST:TARGET_PORT SSH_SERVER # Examples # Access a remote MySQL database on your local port 3307 ssh -L 3307:localhost:3306 server # Access an internal web service at db.internal:8080 via local port 8080 ssh -L 8080:db.internal:8080 server # Use the config alias (server must be defined in ~/.ssh/config) ssh -L 8080:db.internal:8080 server
philip@laptop — tunnel to remote MySQL
# Open the tunnel — SSH gives you a shell AND forwards the port philip@laptop:~$ ssh -L 3307:localhost:3306 server philip@server:~$ ← shell open on server, tunnel active # In a second terminal on your laptop — connect to remote MySQL locally philip@laptop:~$ mysql -h 127.0.0.1 -P 3307 -u philip -p Welcome to the MySQL monitor. Commands end with ; or \g. ← you are connected to the remote MySQL through the tunnel

Run the tunnel in the background (-f -N)

If you don't need an interactive shell — just the tunnel — add -f (background) and -N (no command / don't open a shell):

philip@laptop — background tunnel, no shell
# -f = fork to background after auth, -N = no shell, just forward the port philip@laptop:~$ ssh -fN -L 3307:localhost:3306 server philip@laptop:~$ ← prompt returns immediately; tunnel running in background # To find and kill the background tunnel later philip@laptop:~$ ps aux | grep ssh philip 12345 0.0 0.0 ssh -fN -L 3307:localhost:3306 server philip@laptop:~$ kill 12345
Bind to 0.0.0.0 to share the tunnel on your local network. By default, the local port listens only on 127.0.0.1 (just your machine). Use ssh -L 0.0.0.0:8080:target:80 server to let other devices on your network use the tunnel too.

Remote Port Forwarding (-R)

The mirror image of local forwarding. A port opens on the server and traffic arriving there is forwarded back through the tunnel to a port your machine can reach. The canonical use case is exposing a local development server to the internet without any router configuration.

Internet visitor
anyone
hits server
port 8080
SSH Server
server8080
encrypted SSH tunnel
Your machine
localhost3000
# Syntax ssh -R REMOTE_PORT:LOCAL_HOST:LOCAL_PORT SSH_SERVER # Expose your local dev server (port 3000) on the server's port 8080 ssh -R 8080:localhost:3000 server # Anyone hitting http://server:8080 now reaches your local port 3000
philip@laptop — expose local dev server to internet
# Your React/Node/Flask app is running on port 3000 philip@laptop:~$ npm start Local: http://localhost:3000 # In another terminal — open reverse tunnel philip@laptop:~$ ssh -R 8080:localhost:3000 server philip@server:~$ # Your client can now open http://server.example.com:8080 and see your app # No router changes, no port opening, no DynDNS — just SSH

Allow external connections to the remote port

By default, the remote port binds only to 127.0.0.1 on the server — reachable from the server itself but not from outside. To allow external access, set GatewayPorts yes in the server's /etc/ssh/sshd_config, or specify the bind address explicitly:

# Bind to all interfaces on the server — anyone can reach it ssh -R 0.0.0.0:8080:localhost:3000 server # On the server — enable GatewayPorts in sshd_config if needed # /etc/ssh/sshd_config GatewayPorts yes # Then: sudo systemctl restart sshd
Remote forwarding opens your local machine to the internet — only do it on a server you trust and control. Anyone who can reach the server's open port can connect to your local service. Always restrict access with firewall rules if exposing sensitive services.

Dynamic Port Forwarding — SOCKS Proxy (-D)

Instead of forwarding one specific port to one specific destination, dynamic forwarding creates a SOCKS5 proxy on a local port. Any SOCKS-aware application (browser, curl, git) can be pointed at it, and all its traffic will travel through the SSH connection — leaving the server and appearing to originate from the server's IP address.

Your browser
SOCKS51080
encrypted SSH tunnel
SSH Server
server
your proxy exit
to any URL
via server
Destination
any website
# Open a SOCKS5 proxy on local port 1080 ssh -D 1080 server # Or background it so you keep your terminal ssh -fN -D 1080 server

Then point your browser's proxy settings at SOCKS5 127.0.0.1:1080:

philip@laptop — SOCKS proxy via curl and Firefox
# Start the SOCKS proxy in the background philip@laptop:~$ ssh -fN -D 1080 server # Use it with curl — your traffic exits from server's IP philip@laptop:~$ curl --socks5 127.0.0.1:1080 https://ifconfig.me 203.0.113.42 ← server's IP address, not your laptop's # Use it with git — route all git traffic through the tunnel philip@laptop:~$ git config --global http.proxy socks5://127.0.0.1:1080 # In Firefox: Settings → Network → Manual Proxy → SOCKS5 Host: 127.0.0.1 Port: 1080 # Enable "Proxy DNS when using SOCKS v5" to also tunnel DNS queries

Practical Use Cases

Access a remote database securely
MySQL/Postgres only listens on 127.0.0.1 on the server (correct). Tunnel it to your laptop so your GUI client (TablePlus, DBeaver) can connect.
ssh -fN -L 3307:localhost:3306 server # Connect TablePlus to 127.0.0.1:3307
Browse an internal admin panel
Grafana / phpMyAdmin / Proxmox web UI is on an internal host not exposed to the internet.
ssh -fN -L 8080:grafana.internal:3000 server # Open http://localhost:8080 in your browser
Show a client your local dev site
Your Node app is running on localhost:3000. Client wants to see it now, no time to deploy.
ssh -R 8080:localhost:3000 server # Client opens http://server.example.com:8080
SSH into a machine behind NAT
Your home Raspberry Pi is behind a home router. You want SSH access from work without port forwarding the router.
# Run on the Pi at home: ssh -fN -R 2222:localhost:22 server # From work: ssh -p 2222 pi@server
Browse the web via server's network
You're on a restrictive hotel/coffee-shop Wi-Fi and want to route traffic through your VPS.
ssh -fN -D 1080 server # Set browser SOCKS5 proxy: 127.0.0.1:1080
Access entire remote subnet
Multiple services on a private network (192.168.10.x). Forward a different local port for each, or use SOCKS and configure the browser.
ssh -fN -D 1080 server # Point browser proxy to 127.0.0.1:1080 # All 192.168.10.x addresses now reachable

Port Forwarding in the SSH Config File

Frequently used tunnels belong in ~/.ssh/config — then you don't need to remember the flags, and the tunnel opens automatically when you connect to that host:

# ~/.ssh/config — permanent local tunnel for the database Host server HostName 192.168.1.100 User philip IdentityFile ~/.ssh/id_ed25519 LocalForward 3307 localhost:3306 ← always tunnel MySQL LocalForward 8080 grafana.internal:3000 ← and Grafana # Now ssh server automatically sets up both tunnels
# config directives for each forwarding type LocalForward local_port target_host:target_port ← -L equivalent RemoteForward remote_port local_host:local_port ← -R equivalent DynamicForward local_port ← -D equivalent

Quick Reference

Flag / directiveWhat it does
-L local:host:remote Local forward — local port → remote target (via server)
-R remote:host:local Remote forward — server port → your local target
-D port Dynamic SOCKS5 proxy on local port
-N No shell — just set up the tunnel, no interactive session
-f Fork to background after authenticating
-fN Background tunnel with no shell (use together for clean background tunnels)
LocalForward Put -L equivalent in ~/.ssh/config
RemoteForward Put -R equivalent in ~/.ssh/config
DynamicForward Put -D equivalent in ~/.ssh/config
GatewayPorts yes Allow external connections to remote-forwarded ports (in sshd_config)
curl --socks5 127.0.0.1:PORT url Use a SOCKS proxy with curl
ps aux | grep ssh Find background SSH tunnel processes to kill them
Next — Chapter 6: SFTP. SSH can transfer files as well as open shells. SFTP (SSH File Transfer Protocol) runs over the same connection and gives you an interactive file browser — upload, download, rename, delete, and navigate directories on the remote machine. Chapter 6 covers the sftp command, its most useful subcommands, batch transfers, and GUI clients like FileZilla and WinSCP.