Save each tunnel in ~/.ssh/config
Retyping -L and -R options is how ports get mixed up. Give each tunnel a name instead, as its own Host block:
Host db-tunnel
HostName 203.0.113.10
User deploy
LocalForward 127.0.0.1:5432 127.0.0.1:5432
ExitOnForwardFailure yes
Host dev-share
HostName 203.0.113.10
User deploy
RemoteForward 127.0.0.1:9000 127.0.0.1:3000
Host socks
HostName 203.0.113.10
User deploy
DynamicForward 127.0.0.1:1080
Each line is the config form of a command-line flag: LocalForward is -L, RemoteForward is -R and DynamicForward is -D. The listen address and port come first, then a space, then the destination. As with the flags, the server resolves the destination of a local forward, so 127.0.0.1 there means the server itself. If the three kinds are new to you, start with SSH tunnels explained: local, remote and dynamic.
Use a separate name for tunnels, not the Host you log in with every day. Otherwise every login tries to open the same ports, and the second one prints Address already in use.
Start a tunnel by name
ssh -f -N db-tunnel
-N runs no remote command, so the connection only carries the forwards. -f sends ssh to the background after login, so a passphrase or password prompt still reaches you first. Your database tool can now connect to 127.0.0.1:5432.
With ExitOnForwardFailure yes, a port that is already taken stops ssh with exit code 255 instead of leaving a connection with no tunnel. This is what starting the same tunnel twice looks like:
bind [127.0.0.1]:5432: Address already in use
channel_setup_fwd_listener_tcpip: cannot listen to port: 5432
Could not request local forwarding.
To start several saved tunnels at once:
for t in db-tunnel socks; do ssh -f -N "$t"; done
Stop a tunnel
A background tunnel is just an ssh process. Without anything else, you find it and kill it:
ps ax | grep '[s]sh -f -N db-tunnel'
pkill -f 'ssh -f -N db-tunnel'
pkill -f matches any process whose command line contains the text. In our tests it also killed the shell script that had started the tunnel, because the script contained the same words. A control socket is cleaner. Add three lines to the block:
Host db-tunnel
HostName 203.0.113.10
User deploy
LocalForward 127.0.0.1:5432 127.0.0.1:5432
ExitOnForwardFailure yes
ControlMaster auto
ControlPath ~/.ssh/cm-%C
ControlPersist yes
Now the tunnel answers to commands by name:
ssh -f -N db-tunnel
ssh -O check db-tunnel
ssh -O exit db-tunnel
Master running (pid=27)
Exit request sent.
The socket also lets other ssh and scp commands to db-tunnel reuse the same connection. The details are in SSH multiplexing with ControlMaster.
Add or remove a forward without reconnecting
With a control socket, you can change a running tunnel. The connection and its other forwards stay up:
ssh -O forward -L 127.0.0.1:8081:127.0.0.1:8000 db-tunnel
ssh -O cancel -L 127.0.0.1:8081:127.0.0.1:8000 db-tunnel
cancel needs exactly the same specification you used to add the forward. It works for -R and -D as well. With -R 0:..., the server picks a free port and ssh -O forward prints it.
Keep a saved tunnel up
A tunnel started with -f lives as long as its SSH connection. Sleep, a Wi-Fi change or a server restart ends it. Add these lines to the block so ssh notices a dead connection within about 45 seconds and exits, instead of hanging:
ServerAliveInterval 15
ServerAliveCountMax 3
Something then has to start it again: a loop, a service manager, or autossh for a persistent SSH tunnel.
Mistakes that break saved tunnels
- A colon instead of a space.
LocalForward 5432:127.0.0.1:5432is the command-line format, not the config format. ssh answersMissing target argument, and in our test that one bad line stopped every host in the file from connecting. - Listening on all interfaces.
LocalForward 0.0.0.0:5432 ...orGatewayPorts yeslets other machines on your network use the tunnel. Keep127.0.0.1unless you mean it. - Two tunnels on one local port. Only the first one starts. Give each saved tunnel its own port.
- Forwarding refused by the server. If the port opens but connections fail with
administratively prohibited, the server hasAllowTcpForwardingturned off for you.
Questions
How do I see which SSH tunnels are running?
Run ps ax | grep '[s]sh -f -N' to list background ssh processes with their options. For a tunnel with a control socket, ssh -O check name answers directly. Inside an interactive session, press Enter and type ~# to list its forwarded connections.
Can one command start several tunnels?
Yes. Put several LocalForward or DynamicForward lines in one Host block and they all open with one ssh -f -N name. If one port is busy and ExitOnForwardFailure is on, none of them stays open.
Why does my saved tunnel stop after sleep or a network change?
The tunnel lives inside one SSH connection, and that connection dies when the network drops. Add ServerAliveInterval so ssh notices, and restart it with a loop or with autossh.
Sources
- ssh(1), -f, -N and -O, OpenBSD manual
- ssh_config(5), LocalForward, RemoteForward, DynamicForward, ControlMaster
- sshd_config(5), AllowTcpForwarding and GatewayPorts