A compilation of documentation   { en , fr }

OpenSSH tunnels

Tags:
Created on:
Author:
Xavier Béguin
Version in French: Les tunnels avec OpenSSH

A few definitions

General definition of a tunnel

A tunnel is a special connection that makes it possible to reach a machine on a computer network that is not reachable directly (as illustrated in figure 1 below) but only through an intermediate host.

To reach this machine, instead of sending the data directly to the destination host, it should be sent to the entrance of the tunnel. The data will then be encapsulated in another protocol and transported to the intermediate host that forms its exit. From there, the data will be sent to the destination host as if it had been emitted from the intermediate host. (This general principle is illustrated in figure 2 below.)

This intermediate host located on the edge of the protected network is often called a getaway: following the principle of the bastion host, it is a host that can be reached from outside the network but that can also access the hosts only accessible from within. Its role is to allow a limited and authenticated access to the protected network from the outside. The use of a tunnel allows or eases this bounce on the getaway host and will even make this constraint mostly unnoticeable.

Diagram showing the impossible direct connection
Figure 1. Example of an impossible direct connection. [toggle description]
In our example, the client application running on your local host cannot connect directly from its local network to the remote server in its own network.
Diagram of a local forwarding static SSH tunnel
Figure 2. Principle of a local forwarding static SSH tunnel. [toggle description]
A tunnel is established between your local host and a SSH gateway on the remote network, stating a specific remote target service. The client application sends its data to the entrance of the tunnel on the local host. The data goes through the tunnel to the SSH gateway where it is re-emitted to the final target service.

Difference between static and dynamic tunnels in OpenSSH

As stated in the definition above, data sent to the entrance of a tunnel is then re-emitted from its exit to their final destination. Among the tunnels created using the OpenSSH client, we will distinguish two types of tunnels depending on whether the final destination of the data is provided at the creation of the tunnel (it is then referred to as a static tunnel) or if the software application sending the data must specify by itself the final destination for each data stream it sends (this is referred to as a dynamic tunnel).

Static tunnel

In a static tunnel, the incoming data passes through the tunnel and is then systematically sent to the final destination (host and port) specified when the tunnel was created.

This solution has the advantage of making the tunnel totally transparent for the application that sends the data: it can send it to the entrance of the tunnel the same way it would send it to the destination host. The tunnel will be responsible for addressing it, from the other side, to the destination specified at the time the tunnel was created. The disadvantage is obviously that the application cannot send the data to another destination, unless it uses another tunnel.

Two types of static SSH tunnels are possible with OpenSSH:

  • in a local forwarding tunnel (illustrated in figure 2 above), the tunnel listens for incoming data on the host where the SSH tunnel was initiated (the host on which the SSH client was run, which is often your local machine), forwards the data to the SSH server to which the SSH client is connected and, from there, sends it to its final destination;
  • in a remote forwarding tunnel (illustrated in figure 3 below), the data travels in the opposite way: the tunnel listens for incoming data on the SSH server (the host to which the SSH client is connected), forwards the data to the host running the SSH client and, from there, sends it to its final destination.
Diagram of a remote forwarding static SSH tunnel
Figure 3. Principle of a remote forwarding static SSH tunnel. [toggle description]
A tunnel is established from your local host to a SSH server on a remote network, stating a specific target service (reachable from your local host). A client application sends its data to the end of the tunnel on the SSH server. The data goes through the tunnel to the host where the SSH client is running (your local host) where it is then re-emitted to the final target service.

Dynamic tunnel

In the case of a dynamic tunnel, the tunnel is created without specifying a final destination for the data it receives on its entrance. The application sending data into the tunnel must therefore also specify to which destination (which host and which port) the data should be re-emitted. (See figure 4 below for an illustration of this principle.)

The SOCKS protocol is used to provide the destination information to the tunnel. The dynamic tunnel then acts as a SOCKS proxy that receives data on a host (the entrance of the tunnel) and re-emits it from another host (the exit of the tunnel).

Diagram of a dynamic tunnel
Figure 4. Principle of a dynamic SSH tunnel. [toggle description]
A dynamic tunnel is established from your local host to a SSH server on a remote network. The client application sends its data to the entrance of the tunnel on the local host. The data then goes through the tunnel to the SSH server where it is re-emitted to the final destination machine the client application specified using the SOCKS protocol. The application can therefore send data addressed to different destinations host through the same tunnel.

The possibility of accessing any host reachable from the other side of the tunnel and the fact that, from the point of view of the final target machine, the connection seems to come from the machine at the end of the tunnel (the SSH gateway in our example) make that type of tunnel look, in terms of functionality, like to a VPN (Virtual Private Network), except that only the applications configured to use this tunnel/proxy will be redirected to this pseudo-VPN.

Opening a static tunnel

Required information

In practice, to open a SSH tunnel that we will call static (as opposed to a dynamic tunnel described below), we need to provide three details:

  • the DNS name of the host and the account name to connect to using SSH (this host will be the getaway host in our example describing a connection to a remote protected network). This is the host on which the exit of the tunnel will be located. This information should have been provided to you after you requested remote access from the administrator of the network you are connecting to.
  • the TCP port and network interface of the local host (where the OpenSSH client is running) where the entrance of the tunnel should be located:
    • the choice of this port is free as long as it is not currently in use (and is over 1024 if you are using a non-privileged account, i.e. other than the root account, as this type of users can only use these ports),
    • you will also have to choose on which network interface the port will be opened. This choice is particularly important in terms of security, as, if you decide to use an interface connected to a network, any host on this network can potentially send data into the tunnel if the port is not protected by a local firewall (see the note below on security). If no interface is provided here, by default the loopback interface will be used (as if you had provided the address localhost) and the tunnel will only be reachable to the users of the local host.
  • the DNS name or IP address and the TCP port of the host to which the data that went through the tunnel will be re-emitted to from its ending: as we already mentioned above, this information is required when this type of tunnel is created (this is the reason we call it a static tunnel in this document).

Note on security: please note that, once the tunnel is open, no additional authentication is applied when receiving data at the tunnel entrance. This means that OpenSSH will encapsulate and transport through the tunnel all data received on the incoming port and send them to the target host from the other side of the tunnel.

When the entrance of the tunnel is bound to an interface connected to a network, keep in mind that any host on this network will be able to send data into the tunnel, and thus to the destination host, if the incoming port is not protected by a local firewall.

It is therefore recommended to bind the entrance of the system to the loopback interface localhost (corresponding to the address 127.0.0.1 or ::1), which is the interface used by default if none is specified when the tunnel is created, as this interface is only reachable by the users of the local host. Even in this case, you should consider the risks caused by potential malevolent system users.

As the creator of a remote access to the protected network on the other side of the getaway, you have the responsibility to make sure not to create a security risk by giving unknown users access to protected resources.

Command to open the tunnel

Local forwarding tunnel

In this document, we will assume that you own an account whose identifier is hkrustofski (from my beloved character Herschel Krustofski) accessible from the internet by SSH on the host named gateway.example.org that acts as a SSH gateway having access to a protected network whose hosts use the DNS suffix .example.org.

The following command will open a tunnel between your local host and the remote machine to which you are connecting using SSH (in our example, it is a gateway host located on a firewall-protected network) to reach the TCP port 80 of a host named host1.example.org:

ssh -N -L 3033:host1.example.org:80 hkrustofski@gateway.example.org &

Here is a description of the three arguments of the ssh command:

  • the option -N (which is optional, but I find it handy) is used here to prevent the opening of a command interpreter on the remote host. The tunnel will then remain open until the SSH command we ran on the background here is killed (using a command such as kill %1 in the same shell session, for example). Without this option, the SSH client would run a command interpreter (Unix Shell) and the connection, as well as the tunnel, would terminate when you exit this remote shell;
  • the option -L is the option requesting the opening of the tunnel and specifies its entrance and exit usually in the form [bind_address:]port:host:hostport:
    • in this example, the entrance will be set on TCP port 3033 (it could be any port available above 1024). As no DNS name nor IP address is specified before the port number, it is as if localhost:3033 would have been provided instead: it means that the tunnel will listen on TCP port 3033 on the local loopback interface of your host (of address 127.0.0.1 on IPv4, or ::1 on IPv6). (You can also specify *:3033 to listen on all of the network interfaces of the host.),
    • data sent to the tunnel (in our example received on TCP port localhost:3033 of your local host) will be transmitted to the host host1.example.org on port 80 through the gateway to which we are connecting using SSH. Note that the DNS name host1.example.org will be resolved at the exit of the tunnel, on the SSH getaway (this name will likely not be visible outside of the protected network). As a side note, if the service you are trying to reach is located on the very host the tunnel leads to, you should specify the name localhost as a destination host to reach it; this name would then refer to the local loopback interface of the remote host;
  • hkrustofski@passerelle.example.org represents the identifier of your account on the SSH gateway and its DNS name. This account must have been provided to you by the administrator of the gateway host. Upon connection, you will have to authenticate using a password or a SSH key previously set up.

The tunnel created by this command corresponds to the one illustrated by the figure 2 above.

Remote forwarding tunnel

To open a tunnel operating in the opposite way, meaning that it will transmit data sent to a port on the remote host to a host reachable from your local network, you would use the option -R on the command line, which is used the same way as -L.

The following command will thus open a tunnel between a remote host to which you are connecting using SSH and the host host2.example.net through your local host (and therefore potentially located on your own firewall-protected network):

ssh -N -R 3033:host2.example.net:80 hkrustofski@remote.example.org &

In the above example, the tunnel entrance will be set up on TCP port 3033 of the loopback interface of the host remote.example.org and will allow users of the remote host to reach the TCP port 80 on the host host2.example.net through your local host. This allows remote users to bypass the firewall of your own network and to connect to a host reachable from your local network. Just like the local forwarding tunnel (created using the -L option), this remote forwarding tunnel bypasses the security rules enforced on your network and should therefore be used with great care.

The figure 3 above illustrates the tunnel resulting from this command.

Using the OpenSSH client configuration file

Local forwarding tunnel

Should the tunnel be used on a regularly basis, its use can be made more convenient by adding an entry in the configuration file of the OpenSSH client, ~/.ssh/config. This will avoid having to specify the tunnel parameters on the command line.

A connection name must be provided with the entry defined in the configuration file to specify the parameters of the tunnel (in our example, it is simply tunnel, but could be about any name of your choice). This tunnel can later be created simply by providing the connection name to the OpenSSH client the same way a remote host name would. (Note that the option IdentityFile used below is only required if you need to designate a SSH key for the connection other than the default key file, which is something like ~/.ssh/id_ed25519).

Here is an example of configuration (that could be added to your file ~/.ssh/config) matching our command line example above:

Host tunnel
  Hostname gateway.example.org
  User hkrustofski
  IdentityFile ~/.ssh/id_hkrustofki
  LocalForward 3033 host1.example.org:80

Once this configuration set up, the following command will open the tunnel to the host gateway.example.org and it will be configured the exact same way as in the ssh command given as an example in the previous section of this document (the option -N is here again perfectly optional):

ssh -N tunnel &

Arguments provided with the -L option used on the command line will here be specified with the directive LocalForward that takes two arguments:

  • the first, [address:]port (here 3033) is the optional IP address (or the DNS name resolving to a IP address set up on the local host) and the TCP port of the entrance of the tunnel;
  • the second, address:port, is the IP address (or a DNS name that will be resolved at the end of the tunnel) and the TCP port to which the data that went through the tunnel will be forwarded to at the exit of the tunnel.

Remote forwarding tunnel

The directive corresponding to the -R option on the command line is RemoteForward and operates the same way as LocalForward shown above, except that the entrance of the tunnel will be located on the remote host (the SSH gateway in our example) and its exit will be on the local host.

The remote forwarding tunnel defined on the command line in the example of the earlier section can thus be defined using the following entry in the configuration file ~/.ssh/config:

Host tunnel
  Hostname remote.example.org
  User hkrustofski
  IdentityFile ~/.ssh/id_hkrustofki
  RemoteForward 3033 host2.example.net:80

Opening a dynamic tunnel

Difference with a static tunnel

As we have seen before, a static SSH tunnel created with the option -L only allows you to connect to a remote address and port statically specified at the tunnel creation.

In contrast, as we already discussed in the definition section above, a dynamic tunnel consists of a kind of generalization of this type of SSH tunnel: instead of specifying, when the tunnel is created, the recipient of all data that will pass through the tunnel, it is the programs sending data into the tunnel that must use the SOCKS protocol to specify the host and port to which OpenSSH will transmit their data at the exit of the tunnel. This is the reason why a dynamic SSH tunnel can be considered like a SOCKS proxy.

Just like with a static tunnel, to the destination host, everything happens as if the connection had been initiated from the host where the tunnel exit is located.

Command to open the tunnel

The dynamic tunnel, which will thus act as a SOCKS proxy, can be opened using the -D option of the OpenSSH client, which means running a command of the following type:

ssh -D 1080 hkrustofski@passerelle.example.org

In this example, the -D option only provides a port without specifying the local network interface to use. The TCP port 1080 of the loopback interface (of address 127.0.0.1) will then be used, as if the argument localhost:1080 was provided. To listen on another network interface of the host, a DNS name or a corresponding IP address bound to the host must be provided, for example -D 192.168.1.27:1080 (specify * to listen to all the network interfaces, for example -D '*:1080').

The directive DynamicForward 1080 (the equivalent of the -D option) can also be used in an entry of the OpenSSH client configuration file ~/.ssh/config like in the example below (where no DNS name nor IP address of a network interface of the host is specified, which is here again equivalent to specifying DynamicForward localhost:1080):

Host passerelle.example.org
	User hkrustofski
	DynamicForward 1080

Below is an extract of the documentation related to this directive, provided by the manpage ssh_config(5):

DynamicForward
	 Specifies that a TCP port on the local machine be forwarded over
	 the secure channel, and the application protocol is then used to
	 determine where to connect to from the remote machine.

	 The argument must be [bind_address:]port. (...)

	 Currently the SOCKS4 and SOCKS5 protocols are supported, and
	 ssh(1) will act as a SOCKS server.  Multiple forwardings may be
	 specified, and additional forwardings can be given on the command
	 line.  Only the superuser can forward privileged ports.

After opening the tunnel, the SOCKS proxy (supporting the versions 4 or 5 of the protocol) of our example is then available on port TCP 1080 on the loopback interface (using the address localhost or 127.0.0.1) for use by any applications supporting this type of proxy (see below).

Use of the SOCKS proxy

General use: the example of tsocks

If you want to use the SOCKS proxy of an OpenSSH dynamic tunnel within an application (such as a web browser for instance), first check in its settings whether it can use a proxy of this type. It is the case with many web browsers, like Firefox for instance (whose configuration is briefly covered below).

Applications that do not natively support SOCKS proxies can usually still be forced to use such a proxy (on GNU/Linux, and probably on other systems too) through tools like proxychains or tsocks that will intercept system calls relative to network connections initiated by the application and transparently redirect them to the configured proxy.

To use tsocks for example, you will first need to specify the SOCKS proxy you want to use in its configuration file /etc/tsocks.conf. Here is a minimal example:

server = 127.0.0.1
server_type = 5
server_port = 1080

Attention : with tsocks, the DNS lookup is performed on your local host, not on the other side of the tunnel. You might therefore need to use an IP address to designate the service you want to reach through the tunnel instead of the DNS name, if this name is not visible from your local host.

After configuration, you can then run the desired application using three different (but strictly equivalent in terms of results) ways:

  • prefix the command of your application with the command tsocks:
    tsocks ssh -v hkrustofski@192.168.102.1
    
  • modify by yourself the environment variable LD_PRELOAD, in the environment of your application:
    env LD_PRELOAD=/usr/lib/libtsocks.so ssh -v hkrustofski@192.168.102.1
    
  • run the command “. tsocks -on” (do not forget the leading dot, synonym of the source shell function) in your shell before running your application, to modify the LD_PRELOAD variable in the environment of your current shell (this environment is inherited by applications spawn from this shell). “. tsocks -off” can be used to revert the configuration:
    . tsocks -on
    ssh -v hkrustofski@192.168.102.1
    . tsocks -off
    

(Of course, running ssh using tsocks is purely demonstrative here, as the OpenSSH client can use a SOCKS proxy directly, as described further below.)

Update: the tsocks software is nowadays no longer officially maintained and could be unavailable on some recent systems. It can be replaced by other products that provide a similar or equivalent functionality like ts-wrap or proxychains-ng (I have not tested them and I cannot guarantee neither their functionalities nor their quality).

Using the proxy with Firefox

To use Firefox through a SOCKS proxy (which means all network connections will be forwarded to the proxy), its settings must be modified as follows (the instructions are given for Firefox 144 and may have changed in later versions):

  • in the tab General, go down to the bottom of the page, section Network Settings and click on the button Settings;
  • in the configuration window that pops up, choose the option Manual proxy configuration and specify the name or IP address of your proxy in the box SOCKS Host and the port in the box Port on the same line (in the example given above, the host would be localhost and the port 1080).

The DNS is by default proxied on Firefox (meaning the names are resolved on the other side of the tunnel), which is usually what you want to do. To disable the proxying of the DNS (and have DNS names looked up on the local host), uncheck the box Proxy DNS when using SOCKS v5 (a similar box exists for SOCKS v4) at the bottom of the Connection Settings window.

Once these parameters are set up, everything will work as if your browser were on the remote machine, on the other side of the dynamic tunnel.

Transparent use of the tunnel with OpenSSH

The OpenSSH client can easily and transparently use a SOCKS proxy (provided by a dynamic tunnel), which makes it possible to easily connect, using SSH, to hosts only reachable through a tunnel.

This is possible through the ProxyCommand directive in the configuration file ~/.ssh/config (or the option -o "ProxyCommand ..." on the command line, as no dedicated option exists) as demonstrated in the example below. Combined with the option -W of the OpenSSH client, it is even possible to automatically open the dynamic tunnel when OpenSSH initiates a connection. This enables the transparent opening and closing of the tunnel when using a connection that requires it.

To illustrate these possibilities and to continue with the example already developed above, we will define two entries in the OpenSSH client configuration file ~/.ssh/config:

  • the first one represents the dynamic tunnel connected to our SSH getaway (here getaway.example.org);
  • the second one represents a host (named host1.example.org) located behind the remote firewall, reachable through the tunnel/proxy:
Host gateaway.example.org
    User hkrustofski
    DynamicForward 1080

Host host1.example.org
    User hkrustofski
    ProxyCommand ssh -W %h:%p getaway.example.org

In the arguments given to ProxyCommand above, the values %h and %p are replaced by OpenSSH respectively by the host name and port number of the SOCKS proxy the OpenSSH client should use. For more details, refer to the documentation on ProxyCommand in the manual page ssh_config(5).

With this configuration, an OpenSSH connection to the host host1.example.org can then be transparently opened using the command:

ssh host1.example.org

This configuration can also be used with other OpenSSH client commands, such as scp:

scp fichier.dat host1.example.org:vers/le/repertoire/

Note that the DNS lookup of the provided host name will always be performed on the other side of the tunnel, which means, in our example, on the SSH getaway.

If you would like to connect to more than one host in the protected network, you would not have to create an entry for each of them like we did for the host host1.example.org above. Instead, you could define an entry valid for all the host names that match the pattern *.example.org which would trigger the opening and the use of the tunnel/proxy. An example of such a configuration is described in the following section.

Note: on older versions of OpenSSH where the client had no option -W, it was required to use the tool netcat (in its openbsd version, then provided on Debian by the package netcat-openbsd) to forward network connections to the tunnel/proxy, and the tunnel had to be manually opened before using an OpenSSH connection that required it.

The configuration then looked like this:

Host getaway.example.org
	User hkrustofski
	DynamicForward 1080

Host host1.example.org
	User hkrustofski
	ProxyCommand /bin/nc.openbsd -X 5 -x 127.0.0.1:1080 %h %p

And it had to be used with two commands similar to the ones below (the first one opens the tunnel before the second one initiates the OpenSSH connection that requires it):

ssh -N gateway.example.org &
ssh host1.example.org

Using the tunnel for a set of SSH connections

When we have a set of hosts for which the SSH connection requires the use of a tunnel (like in the example given above where a SSH gateway is used to connect to hosts in a protected network), it is possible to automate the use of the tunnel for the whole set of hosts using a single entry in the configuration file. This makes it unnecessary to create an entry for each and every host whose access requires the use of the tunnel.

For this, we take advantage of the possibility offered by the OpenSSH configuration file to apply a common configuration to a set of machines matching a specific criterion. In the simplest case we are presenting here, we can define an entry that will be applied to hosts having a specific common part in their DNS name, like, for example, a name matching the pattern *.example.org.

Below is an example of the ~/.ssh/config file setting up the transparent use of a dynamic tunnel (playing the role of a SOCKS proxy) and applying its use to connections to any host whose name ends with .example.org:

Host gateway
	Hostname getaway.example.org
	User hkrustofski
	IdentityFile ~/.ssh/id_hkrustofki
	DynamicForward 1080

Host *.example.org !getaway.example.org
	User hkrustofski
	IdentityFile ~/.ssh/id_hkrustofki
	ProxyCommand ssh -W %h:%p getaway

In the example above:

  • the first entry defines the access to a SSH server defining the dynamic tunnel allowing the access to hosts on a remote internal network (the server we earlier called the SSH getaway);
  • the second entry will be applied to all hosts whose name ends in example.org, except for getaway.example.org that represents the SSH getaway where the tunnel ends and that is by definition directly reachable (see the note below);
  • this second entry will automatically open and use the tunnel thanks to the ProxyCommand directive we described earlier;
  • the directive IdentifyFile is optional and designates a file holding a specific SSH key used to connect to the relevant hosts (useful when using a key different from the default one).

Once this configuration is set, any connection to a host such as host.example.org will open and use the tunnel/SOCKS proxy which will relay connections through the host passerelle.example.org.

Note concerning the exclusion of gateway.example.org in the second entry : in the example above, we had to exclude the host gateway.example.org from the second configuration entry (using a pattern beginning with !) as, starting with OpenSSH version 6.6 (and perhaps some other versions before), OpenSSH re-applies the configuration defined in ~/.ssh/config for the host name corresponding to an alias, like for gateaway.example.org above. Without this exception, any connection to a host having a name ending in .example.org would generate an infinite loop as the client would try to open the tunnel through the getaway to connect to the getaway itself.

About the criteria selecting the affected hosts: if you cannot define a criterion on the DNS name to designate the hosts that require the use of the tunnel (i.e. in cases where all hosts do not end with a common suffix such as .example.org or where only a subset of the hosts matching this criterion require the use of the tunnel), OpenSSH makes it possible to define more complex criteria to select the hosts for which a particular configuration is applied (up to the use of a customized external command responsible for deciding whether the connection to a host should use the corresponding configuration block). For more details, refer to the documentation of the keywords Host and Match of the OpenSSH client configuration in the manual page ssh_config(5).

Tip for an everyday use

Although a little out of context, this small complementary tip is provided for the sake of completeness in the description of the tools and configurations used in the concrete case which serves as an example throughout this document (that of a connection to a remote protected network accessible via a SSH gateway).

Advanced users comfortable with the Unix command line can skip this section.

In our example above, we use the DNS name to determinate whether the SSH access to a host requires the use of the tunnel (it is applied to hosts whose name matches the pattern *.example.org). When using the OpenSSH client, we therefore must use the full host name to trigger the opening and use of the tunnel.

In the everyday use of a command interpreter, typing the full name can be burdensome. To avoid typing it on each connection, a little bash function can be useful, like the following one that completes its last argument with the domain name of the hosts that use the tunnel:

sshi() { ssh ${@:1:$(($#-1))} ${!#}.example.org; }

The body of the bash function sshi above might seem abstruse, but is in reality rather simple in principle. It calls the ssh command while reusing the arguments given to the function:

  • it first reuses as-is the arguments given to the function, except the last one (the term ${@:1:$(($#-1))} selects in the array $@ that holds the arguments of the function, the elements 1 until $# - 1, meaning until the last but one, included);
  • it then replaces the last argument (which is argument number $# whose value can be obtained, in bash, using the expression ${!#}, which is similar to the command eval echo '$#') by suffixing it with the domain name ${!#}.example.org.

If this kind of syntax or functionalities brings you joy and make you drool, you can find out some more on my pages tagged “bash”.

Once this function definition is added to your file .bash_aliases or .bashrc, you only need to run the following command to connect to the host scruffy.example.org :

sshi scruffy

As for the client command scp, the following function, although perfectible, should work in most cases (be aware though that is replaces in all its arguments the character “:” by “.exemple.org”, which could cause a problem):

scpi() { scp "${@/:/.interne.example.org:}"; }

It works just like the scp command, but will suffix the name just before the “:” by “.exemple.org”. Here are some usage examples:

scpi file.txt scruffy:
scpi -r scruffy:/a/remote/directory/ /a/local/directory/