# SSH port forwarding

SSH carries arbitrary TCP traffic alongside the shell channel. Four flags pick which direction it goes and how it's exposed at each end. The flag names hint at where the *local* port lives in the address pair: `-L` puts it on the **L**eft, `-R` puts it on the **R**ight.

## Local (-L)

```
ssh -N -f -L <local_port>:<remote_host>:<remote_port> user@ssh_server
```

The SSH client opens a listener on `<local_port>` on the operator's machine. Connections to it are tunneled through the SSH server and exit toward `<remote_host>:<remote_port>` *as seen from the SSH server*. The classic case: a web service bound to `127.0.0.1:80` on the SSH server, accessed via `localhost:1337` on the laptop.

`<remote_host>` doesn't have to be the SSH server itself — it can be anything the SSH server can reach. That makes `-L` a two-hop proxy: operator → SSH server → arbitrary inside-host.

## Remote (-R)

```
ssh -N -f -R <remote_port>:<local_host>:<local_port> user@ssh_server
```

The SSH server opens the listener instead. Connections to `<remote_port>` on the SSH server are tunneled back through the existing connection and exit toward `<local_host>:<local_port>` as seen from the *client*. Used to expose a service that lives in the client's network to a network the client can't otherwise reach.

By default `-R` listeners on OpenSSH bind to 127.0.0.1 on the server; `GatewayPorts yes` in sshd_config widens that to 0.0.0.0.

## Dynamic (-D, SOCKS proxy)

```
ssh -N -f -D <local_port> user@ssh_server
```

The client becomes a SOCKS5 proxy. Anything that speaks SOCKS — `proxychains`, Firefox, curl with `--proxy socks5://...` — routes its traffic through the SSH server. Unlike `-L`, the destination is chosen *per connection* by the client, so the same tunnel works for any host the SSH server can reach.

DNS over SOCKS depends on the client. Firefox has "Proxy DNS when using SOCKS V5"; proxychains has `proxy_dns` in its config. Without it the local resolver answers, which leaks the lookup and may resolve to addresses the SSH server can't reach.

## Jumphost (-J)

```
ssh -J user1@hop1,user2@hop2 user3@target
```

Chains SSH connections through intermediate hosts without giving the operator a shell on the intermediates. Each hop authenticates separately. Identical in effect to nested `ProxyCommand` setups but readable in one line. Same as the `ProxyJump` config keyword.

## Gateway mode (-g)

Modifier on `-L` (and via `-L` on `-D`). `-g` makes the local listener bind 0.0.0.0 instead of 127.0.0.1, exposing the forward to anyone on the operator's LAN. Useful when the operator wants their laptop to be a shared pivot for a team; risky if the local network is hostile.

## When to use which

| Network shape | Use |
|---|---|
| Operator has SSH to target, target binds loopback-only | `-L` |
| Operator can't reach target, but target can reach operator | `-R` |
| Operator wants browser/CLI access to many things behind one foothold | `-D` |
| Operator must traverse N gates to reach final shell | `-J` |
| Operator wants others on LAN to share the forward | add `-g` |

## Runtime forwards

The `~C` escape sequence inside an active SSH session opens the client's command line. Typing `-D 8080` there adds a dynamic forward to a session that started as a normal shell, without disconnecting. The reverse works too — `-KD 8080` cancels it. Useful when discovery happens mid-session and reopening the connection would be noisy.

## See also

- [[ssh-port-forwarding-cheatsheet]] — Helton's walkthrough with worked examples
- [[ssh-agent-forwarding-risk]] — the orthogonal `-A` flag and why it isn't safe by default
- [[living-off-the-land]] — SSH is the canonical pre-installed lateral-movement tool
- [[bypassing-dpi-with-ebpf]] — when SSH itself is the protocol being blocked
