SSH Tunneling Explained: Local, Remote, and Dynamic Port Forwarding

SSH Tunneling Explained: Local, Remote, and Dynamic Port Forwarding

Everyone uses SSH for shells. Fewer people know it also carries arbitrary TCP traffic through the same encrypted connection, which turns SSH into a Swiss-army networking tool: reach a database that only listens on localhost, punch through a NAT to show a coworker your dev server, or route your browser through a remote machine with one flag. Three flags, three directions.

Local forwarding (-L): bring remote things here

-L makes a port on your machine deliver connections somewhere the server can reach:

ssh -L 5432:localhost:5432 user@dbserver

Read the triplet right to left: connections to your local port 5432 go through the tunnel and are delivered to localhost:5432 from dbserver’s point of view. Now psql -h localhost on your laptop talks to the remote database, which never had to expose its port beyond loopback.

The middle element does not have to be localhost. The server can act as a jump-off to anything it can reach:

# Reach a router admin page on an office network, via the office bastion
ssh -L 8443:192.168.10.1:443 user@office-bastion
# Browse to https://localhost:8443

This is the standard pattern for admin interfaces that should never be exposed: bind them to localhost on the server, and let SSH be the only way in.

Remote forwarding (-R): send local things there

-R is the mirror image: a port on the server delivers connections back to something your machine can reach:

ssh -R 8080:localhost:3000 user@vps

Connections to the VPS’s port 8080 tunnel back to port 3000 on your laptop. Suddenly your NAT-bound dev server has a public address, which is exactly how quick demo tunnels and many “expose my homelab” setups work.

One catch: by default sshd binds remote-forwarded ports to the server’s loopback only, so external clients cannot connect. To publish them, the server’s /etc/ssh/sshd_config needs:

GatewayPorts yes

Restart sshd after. Without it, -R listeners are only reachable from the server itself.

Dynamic forwarding (-D): the instant SOCKS proxy

-D skips fixed destinations entirely and opens a SOCKS proxy on your machine:

ssh -D 1080 user@vps

Any application that speaks SOCKS (browsers, curl with --socks5, most torrent and chat clients) can route through port 1080, and its traffic exits from the VPS. Destination selection happens per-connection, so one tunnel covers everything:

curl --socks5-hostname localhost:1080 https://ifconfig.me
# prints the VPS address

The -hostname variant matters: it sends DNS resolution through the tunnel too, so lookups do not leak to your local network. This is a one-command approximation of a VPN for TCP traffic, ideal for untrusted hotel Wi-Fi; the honest limits are no UDP and application-by-application opt-in rather than whole-system routing.

Flags that belong on every tunnel

ssh -N -f -L 5432:localhost:5432 user@dbserver

-N says run no remote command, tunnel only; -f backgrounds ssh after authentication. Together they make a tunnel-only process instead of a shell you must keep open. Add keepalives so idle tunnels survive NAT timeouts, best set in ~/.ssh/config:

Host dbtunnel
    HostName dbserver.example.com
    User admin
    LocalForward 5432 localhost:5432
    ServerAliveInterval 30
    ServerAliveCountMax 3

With that block, the whole tunnel becomes ssh -N dbtunnel, and the config file, not your shell history, documents your tunnels.

Keeping tunnels alive

A tunnel in a terminal dies with the terminal. For persistent tunnels, autossh monitors and restarts the connection:

autossh -M 0 -N dbtunnel

Or express it as a systemd service with Restart=always and ExecStart=/usr/bin/ssh -N dbtunnel, which survives reboots and integrates with journal logging like any other service. That, plus a dedicated key restricted on the server side (command="",restrict,port-forwarding options in authorized_keys), is the difference between an ad-hoc trick and infrastructure.

Where tunnels end and VPNs begin

SSH forwarding moves TCP, port by port or via SOCKS. It does not carry UDP, does not create a network interface, and does not route whole subnets into each other. The moment the requirement becomes “my laptop should behave as if it is on the home network,” you have outgrown tunnels and want WireGuard. Until then, for the daily business of reaching one guarded port securely, SSH tunneling is already installed on every machine you own.

Frequently Asked Questions

What is SSH port forwarding?

A mechanism that carries arbitrary TCP connections inside an established SSH session. Traffic enters a port on one end, travels through the encrypted tunnel, and exits at a destination reachable from the other end.

What is the difference between -L and -R?

Direction. -L listens on your local machine and delivers connections to something reachable from the server. -R listens on the server and delivers connections back to something reachable from your machine.

What does ssh -D do?

It opens a local SOCKS proxy that routes any application traffic through the server, effectively a one-command VPN for TCP. Point a browser at the SOCKS port and it browses as the server.

Why can other machines not connect to my forwarded port?

Forwarded ports bind to localhost by default. Bind to 0.0.0.0 explicitly for -L, and for -R set GatewayPorts yes in the server sshd configuration or the listener stays loopback-only.

How do I keep an SSH tunnel running permanently?

Use autossh, which restarts the tunnel when it drops, or wrap the ssh command in a systemd service with Restart=always. Plain ssh -N in a terminal dies with the terminal.

Is SSH tunneling a full VPN replacement?

No. It forwards specific TCP ports, or TCP generally via SOCKS, but not UDP or whole-interface traffic. For routing all traffic of every protocol, use a real VPN such as WireGuard.