TCP/IP Port Forwarding

TCP/IP port forwarding (also known as tunneling) is a functionality of SSH to proxy a TCP/IP connection through an encrypted SSH channel.

SSH provides three different TCP/IP forwarding modes:

  • Local port forwarding to forward connections coming to a port on the client host through SSH to an SSH server, and there to some port on a host reachable from the SSH server.
  • Remote port forwarding to make an SSH server forward connections coming to a port on the server back to the client, and there to some port on a host reachable from the client.
  • Dynamic port forwarding is a variation of local port forwarding: the SSH client starts a SOCKS proxy listening on the local port, and initiates a local port forwarding whenever that SOCKS proxy gets a request to connect to some port on some remote host reachable from the SSH server.

TCP/IP port forwarding is specified in RFC 4254.

Local Port Forwarding

Local port forwarding can be set up with OpenSSH with the -L command-line option on the client:

ssh -L A:faraway:B server

This results in the client SSH process listening on port A for incoming connections. When a connection arrives, the client SSH process opens an SSH channel to the server, and tells the server to connect to port B on host “faraway”. If the SSH server can make that connection to “faraway”, it confirms the channel creation.

Local Port Forwarding Overview

The SSH client then starts reading data from port A and sends it encrypted through the channel. Any data arriving on the channel from the SSH server is decrypted and written to port A. The server decrypts any data arriving on its end of the SSH channel and writes it to port B on host “faraway”, and starts reading data from that port and sends it encrypted through the channel to the SSH client.

Note that “faraway” is resolved on the SSH server. If “faraway” is “localhost”, this means that port B is on the SSH server itself.

The following interaction diagram illustrates the process of setting up a local port forward in a little more detail:

Local Port Forwarding Interactions

In Apache MINA sshd, the part of this interaction running on the SSH client (listening on the local port, and requesting the channel to be opened; highlighted with a blue background) is implemented in TcpipClientChannel and in DefaultForwarder. The part running on the server (connecting to port B on host “faraway”, confirming the channel creation; highlighted with a yellow background) is implemented in TcpipServerChannel.

(The diagram shows the message forwarding from/to the ports through the channel via two threads on each side; an “R” thread reading from the channel and writing to the port, and a “W” thread reading from the port and writing to the channel. The actual implementation is a little different and uses asynchronous I/O for both.)

Remote Port Forwarding

Remote port forwarding can be set up with OpenSSH with the -R command-line option on the client:

ssh -R A:near:B server

This makes the SSH server listen on a local port on the server for incoming connections. When a connection arrives, the server SSH process opens an SSH channel to the client. The client connects to port B on host “near”, and if that connection is successful, it confirms the channel creation.

Remote Port Forwarding Overview

Note that “near” is resolved on the client. If “near” is “localhost”, port B will be on the SSH client host.

Remote port forwarding is basically the inverse of local port forwarding. Basically the only difference is that the client first has to send a message to the SSH server telling it to play the “client” role in this forwarding:

Remote Port Forwarding Interactions

Note how similar this diagram is to the interactions shown for local port forwarding above. Much of it is just a mirror image, and this symmetry also shows in the code: the SSH server part of Apache MINA sshd uses a TcpipClientChannel for the server-side part, while the client code uses a TcpipServerChannel.

Some details specified in RFC 4254 have been omitted from this diagram. For instance, the client will accept the “forwarded-tcpip A” request only if it had previously indeed requested a remote forwarding for that port (the initial “tcpip-forward A” global request). A client can also cancel a previously made “tcpip-forward” request.

Dynamic Port Forwarding

Dynamic port forwarding can be set up with OpenSSH with the -D command-line option on the client:

ssh -D A server

This makes the SSH client start a SOCKS proxy listening on port A. Whenever this SOCKS proxy gets a request to connect to a port B on a host “faraway” (reachable from the SSH server), it sets up a local port forward.

Dynamic Port Forwarding Overview

The interactions for setting up the forwarding are the same as in local port forwarding, but at the beginning the SOCKS protocol runs to establish where to connect to.

Dynamic Port Forwarding Interactions

Buffering

The tunnel has on each end two handlers:

  • A “read” handler (marked “R” in the interaction diagrams) that reads from the SSH connection between the SSH client and the SSH server, and that writes any data read to a port.
  • A “write” handler (marked “W” in the interaction diagrams) that reads from a port and writes to the SSH connection between the SSH client and the SSH server.

There is no buffering in either handler. Each reads a fixed-size buffer, and then writes out that buffer. Only once that single buffer has been written the handler initiates the next read. This keeps memory usage constant in the handlers; there is only a single read buffer that needs to get read and written.

It also automatically throttles the throughput to the slowest part of the connection. If data is being produced at port A faster than it can be written to the channel, data doesn‘t pile up in some buffer chain inside the SSH client or server’s “write” handler. Instead we “push back” on the producer by not reading faster than we can write. At some point, the lower-level transport buffers will be full, and the producer will wait with sending more data until SSH has forwarded the previously produced data.

The size of the read buffers can influence the performance of the forwarding. SSH transfers data in packets, which have a certain maximum size (which in turn is limited by the network packet size of the underlying TCP/IP transport). Best results can be expected if these buffer sizes match, or if the read buffers for the ports are a multiple of the SSH packet payload size. If the read buffer is larger than the SSH packet payload size, writing it to the SSH channel will break it up into several SSH packets, and the next read on the port will occur only when all packets have been written.

Note that the SSH channel is subject to the normal channel windowing mechanism defined in RFC 4254. If the sending window (the “remote window”) is exhausted, the sender needs to wait until the receiver increases the window size again. (I.e., until it has received an SSH_MSG_CHANNEL_WINDOW_ADJUST message from the receiving side.)