Creating ssh proxies

Fri, 08 July 2022 :: #ssh :: #linux

I always forget what are the proper arguments to create a ssh proxy in some specific way, so I'm leaving a small cheatsheet here for my future references. I've already written a blog post about it a few years ago, but now I need a fast reference instead of a descriptive blog post.

Example 1 (ssh -L)

There's a VM (heravm) running on my MacMini (hera) which doesn't have access to the Internet, only has network access to the host machine. I want to connect to SSH service on this VM, but I want to do it from my desktop instead of MacMini. The desktop machine is in the same network as MacMini (although it doesn't really matter).

I can issue this command from the <desktop> machine:

desktop$ ssh -Nf -L 7722:<heravm>:22 <hera>

This will open port 7722 on the Local machine (desktop). When connecting to localhost:7722 from my desktop, ssh will connect to <hera>:22, and then it will forward all traffic from <hera>:22 to <heravm>:22. So make sure the heravm is reachable from hera.

Example 2 (ssh -R)

There's a VM (heravm) running on my MacMini (hera) which doesn't have access to the Internet, but has network access to the host machine. I want to connect to this VM from another house (so, from a different network). I have a VPS server ostarion with a public IP reachable from both networks that can help with NAT traversal.

hera$ ssh -Nf -R 12345:<heravm>:22 <ostarion>

This will open port 12345 on the Remote machine <ostarion>. After relocating to another house and connecting to <ostarion>:12345, I will effectively connect to <heravm>:22, so directly onto the VM that has no Internet access.

Example 3 (ssh -D)

I have a VPS ostarion. I would like to browse the web so that websites will think I'm browsing from ostarion instead of my local ISP.

desktop$ ssh -D8080 <ostarion>

This will create a SOCKS5 server on host Use browser plugins like SwitchyOmega to use this proxy for chosen websites.


Temporary tunnels are fun, but persistent tunnels that spawn automatically and restart on network failures are what we're after. It's still a matter of empirical verification, but one persistence method is described below.

The example scenario I'm trying to accomplish is this: there's a virtual machine heravm running on some host (hera) that is behind NAT. I want to be able to connect to this VM from another house. I can use an external VPS that I control (ostarion) to setup ssh tunelling. The heravm VM doesn't have Internet connection enabled, it's just able to connect to its host (hera) and nothing else.

This basically means that I need to create reverse tunneling from hera:

hera$ ssh -R 12345:<heravm>:22 ostarion

This means that when I connect to <ostarion>:12345, I will effectively connect to <heravm>:22.

Now we need to make sure that this tunnel will be created for as long as it's possible, and will be re-created when the machine will be restarted from some reason (e.g. power failure).

Persistence on macOS (tested on Monterey)

We're gonna utilise launchd to manage our tunnel. The tunnel will be (re)started when needed.

But first, we're going to create a new ssh identity (a private and a public key) that won't use any password. If that sounds like a security risk, then I agree, but the proper solution for this depends entirely on your setup, and is out of scope for this blog post.

Remember to NOT overwrite your main identity!

hera $ ssh-keygen -t ecdsa
Generating public/private ecdsa key pair.
Enter file in which to save the key (/home/antek/.ssh/id_ecdsa): /path/to/unattended
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /path/to/unattended
Your public key has been saved in /path/to/
The key fingerprint is:
SHA256:xPB9hGEfqxhI2041ioN7urHxYt2rtQEAmvuOsArGDdA antek@host
The key's randomart image is:
+---[ECDSA 256]---+
|  .   o   =oo    |
| + . o O =.+ o   |
|+ E o = O . +    |
|..   o = o o     |
|..  . o S .      |
|..o  o .         |
|oo..+. .o        |
|++  o*...o       |
|= ..o.o.o.       |

Make sure to remove group and other read rights from /path/to/unattended file, so that only the owner will be able to access the private key.

Having an "unattended" private key created we can create a new ssh configuration entry inside ~/.ssh/config:

host host_unattended
    hostname $hostname$
    user antek
    identityfile /path/to/unattended
    identitiesonly yes

The $hostname$ variable points to an external server that is always on, has a publicly reachable IP address and of course it's running sshd (e.g. a VPS).

After the ssh config is ready, we can proceed with creation of the new launchd descriptor for our service (named servicename.plist):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "">
<plist version="1.0"><dict>
    <string>$username$</string>   <!-- HERE -->
        <string>-R 12345:$heravm$:22</string>   <!-- HERE -->
        <string>-o ExitOnForwardFailure=yes</string>
        <string>-o ServerAliveInterval=60</string>
        <string>-o ServerAliveCountMax=10</string>

Make sure you notice the $heravm$ and $username$ variables! Change them according to your requirements. You should probably change the org.anadoxin.servicename as well.

In the example above, the ssh binary is called with the following options:

  • -N -- don't execute any command. This option is used often when forwarding ports (so, perfect for our case),
  • -T -- disables pseudo-terminal allocation (no point if we're not spawning any interactive sessions),
  • -C -- enable data compression -- useful only for slow links. Disable if you have fast Internet connection.
  • Remember to NOT use the -f flag here. This flag will fork ssh to the background. Normally this would be useful, but used in this descriptor it will confuse launchd; it will think that sshd has exited, and will try to start it again (over and over again).

After the launchd descriptor has been created, it can be started with launchctl:

hera$ sudo launchctl load servicename.plist

If you're getting errors, please verify that the syntax of the XML file is OK (plutil -p servicename.plist), the access rights are not too loose, and inspect both stderr and stdout streams created in /tmp.

Because the descriptor uses RunAtLoad=true, it should be executed on load. Verify the tunnel is working by e.g. using ps:

hera$ ps aux | grep "/usr/bin/ssh"
antek             5011   0.1  0.0 408667760   5472   ??  Ss    8:31PM   0:00.23 /usr/bin/ssh -R 12345: -NTC -o ExitOnForwardFailure=yes -o ServerAliveInterval=60 -o ServerAliveCountMax=10 ostarion_unattended

If from any reason the tunnel will collapse (e.g. network failure, power failure), it should be automatically re-created by launchd.


The -L, -R and -D switches accept also the listening interface. Default listening interface settings open up the port for everyone (binding the socket to This may not be what you want, and you might limit the bind address to So, for "example 3", the more secure command would be:

$ ssh -D <ostarion>

You can find the proper syntax for all options in man ssh.