TT Lab
Get started
Learn Learning paths Courses

SSH and File Transfer

How Not to Confuse -L, -R and -D

Continue in TT Lab

In one line

-L opens a port on my side and sends it over there, -R opens a port on the other side and pulls it back to my side, and -D sets up a SOCKS proxy on my side. Once you get the direction right, the rest is syntax.

Why this exists

You need to connect to a database on the internal network, but that database cannot be reached from outside. You can connect to the bastion server with SSH. What you need here is local forwarding. A connection coming in on port 15432 of my laptop is sent through the bastion to db.internal:5432.

ssh -L 15432:db.internal:5432 bastion

How to read it: -L <내가 열 포트>:<배스천이 볼 때의 목적지>:<그 포트> (the port I open, the destination as the bastion sees it, and that port). The key point is that the host name in the middle is resolved from the SSH server's point of view. It does not matter if db.internal does not resolve on my laptop.

How it works

Option Where the port opens Traffic direction Typical use
-L Local Local → SSH server → destination Reaching an internal database or admin console
-R Remote (the SSH server) Remote → SSH server → local → destination Exposing a service behind a firewall to the outside, receiving webhooks
-D Local (SOCKS) Any destination the application picks Routing a whole browser through the internal network

The three directions of SSH forwarding. -L opens a port on my computer and sends it through the SSH server to a destination in the other network; -R does the opposite, opening a port on the SSH server and pulling what comes in to a destination on my computer's side; and -D opens a SOCKS proxy on my computer and goes out to the various destinations the application picks

For a tunnel-only connection, use -N -f together. -N does not run a remote command, and -f sends it to the background.

ssh -N -f -L 15432:db.internal:5432 bastion

-R has a trap. By default it binds only to the SSH server's loopback. That is, other people on the server cannot use that port. To open it to the outside, you have to turn on GatewayPorts yes on the server, and this calls for caution for security reasons.

The directives that control forwarding on the server side.

AllowTcpForwarding no
AllowAgentForwarding no
GatewayPorts no
PermitOpen 10.0.5.20:5432

PermitOpen is especially practical. Without blocking forwarding entirely, it restricts the destinations to a whitelist. It fits exactly the situation "a developer must connect to the production DB but must not reach other internal services".

Agent forwarding is dangerous

-A (agent forwarding) is convenient, but someone with root privileges on the intermediate server can authenticate to other servers with my key through my agent socket. If the goal is to go through a bastion, use ProxyJump (-J). You get the same result without exposing the agent.

ssh -J bastion deploy@10.0.3.14

Escape sequences

When you use a tunnel, there are times the connection looks frozen. What you use then is an escape sequence. At the very start of a line, press ~ and then type the next character.

Sequence Action
~. Force-close the connection (escape a frozen session)
~^Z Put SSH in the background
~# List of forwarded connections
~C Open a command line (add/remove -L while running)
~? Help

Surprisingly few people know that with ~C you can add forwarding during a connection. It is useful when you need to dig one more tunnel without dropping the session.

How not to mix up the three directions

-L, -R, and -D differ only in the letter but do entirely different things. If you grasp just which side listens and which side goes out, you will not mix them up.

Option Where it listens Where it goes out When you use it
-L 5432:db:5432 My computer The remote server Connecting to a DB I cannot reach
-R 8080:localhost:3000 The remote server My computer Letting others outside see my development server
-D 1080 My computer (SOCKS) The remote server Many destinations at once

The db:5432 of -L is a name as the remote server sees it. It is resolved by the server's DNS, not my hosts file. This is where it goes wrong most often.

Why the remote port does not open to the outside

If you set up -R 8080:localhost:3000 and get refused when connecting from another machine, it is not a configuration problem but the default. Under GatewayPorts no, sshd opens the forwarded port only on the remote server's loopback. You have to change it to GatewayPorts clientspecified in the server's /etc/ssh/sshd_config and specify the address, like -R 0.0.0.0:8080:localhost:3000, for it to open outward. Turning this value on means that anyone who can connect to that server can expose a port on my laptop to the internet, so you do not turn it on on a shared jump server.

It is not ssh that revives a dropped tunnel

A tunnel always drops. The NAT table expires, the wireless drops, the server restarts. ssh itself does not reconnect, so reopening is the job of something outside.

autossh -M 0 -N \
  -o ServerAliveInterval=15 -o ServerAliveCountMax=3 \
  -o ExitOnForwardFailure=yes \
  -L 5432:db.internal:5432 jump.example.com

ServerAliveInterval is a device for checking whether a quiet connection is dead. It asks every 15 seconds and drops the connection after three unanswered tries. ExitOnForwardFailure=yes makes it so that if the port could not be opened, the connection does not count as a success. If you leave this out, an ssh that is alive without a tunnel remains, and you get a state where the connection is up but nothing works.

Saving connections with multiplexing

If you keep connecting to the same server, you share one connection.

Host jump
  ControlMaster auto
  ControlPath ~/.ssh/cm-%r@%h:%p
  ControlPersist 10m

From the second connection on, the handshake and authentication are skipped, so the difference you feel is large. However, ControlPath is a socket file, so if the home directory is on NFS, it does not work. In that case, move it under /run/user/$UID.

What it looks like in the field

Opening a tunnel and forgetting it. An ssh process sent to the background with -N -f stays quietly alive. A few weeks later it becomes "who opened this port?". If the process shows as ssh in ss -ltnp, it is a tunnel. Some places have a team rule to leave the purpose in ControlPath or the process name.

What you will do in the next lab

You build all three directions of forwarding, local, remote, and dynamic, and use ss to check where each tunnel opened its port. At the end you submit a comparison table of the three methods together with real evidence.