25

To reach an isolated network I use an -D .

In order to avoid having to type the details every time I added them to ~/.ssh/config:

$ awk '/Host socks-proxy/' RS= ~/.ssh/config
Host socks-proxy
  Hostname pcit
  BatchMode yes
  RequestTTY no
  Compression yes
  DynamicForward localhost:9118

Then I created a service unit definition file:

$ cat ~/.config/systemd/user/SocksProxy.service 
[Unit]
Description=SocksProxy Over Bridge Host

[Service]
ExecStart=/usr/bin/ssh -Nk socks-proxy

[Install]
WantedBy=default.target

I let the daemon reload the new service definitions, enabled the new service, started it, checked its status, and verified, that it is listening:

$ systemctl --user daemon-reload
$ systemctl --user list-unit-files | grep SocksP
SocksProxy.service   disabled

$ systemctl --user enable SocksProxy.service
Created symlink from ~/.config/systemd/user/default.target.wants/SocksProxy.service to ~/.config/systemd/user/SocksProxy.service.

$ systemctl --user start SocksProxy.service 
$ systemctl --user status SocksProxy.service 
● SocksProxy.service - SocksProxy Over Bridge Host
   Loaded: loaded (/home/alex/.config/systemd/user/SocksProxy.service; enabled)
   Active: active (running) since Thu 2017-08-03 10:45:29 CEST; 2s ago
 Main PID: 26490 (ssh)
   CGroup: /user.slice/user-1000.slice/[email protected]/SocksProxy.service
           └─26490 /usr/bin/ssh -Nk socks-proxy

$ netstat -tnlp | grep 118
tcp     0    0 127.0.0.1:9118        0.0.0.0:*             LISTEN     
tcp6    0    0 ::1:9118              :::*                  LISTEN

This works as intended. Then I wanted to avoid having to manually start the service, or running it permanently with , by using for on-demand (re-)spawning. That didn't work, I think (my version of) ssh cannot receive socket file-descriptors.

I found the documentation (1,2), and an example for using the systemd-socket-proxyd-tool to create 2 "wrapper" services, a "service" and a "socket":

$ cat ~/.config/systemd/user/SocksProxyHelper.socket 
[Unit]
Description=On Demand Socks proxy into Work

[Socket]
ListenStream=8118
#BindToDevice=lo
#Accept=yes

[Install]
WantedBy=sockets.target

$ cat ~/.config/systemd/user/SocksProxyHelper.service 
[Unit]
Description=On demand Work Socks tunnel
After=network.target SocksProxyHelper.socket
Requires=SocksProxyHelper.socket SocksProxy.service
After=SocksProxy.service

[Service]
#Type=simple
#Accept=false
ExecStart=/lib/systemd/systemd-socket-proxyd 127.0.0.1:9118
TimeoutStopSec=5

[Install]
WantedBy=multi-user.target

$ systemctl --user daemon-reload

This seems to work, until ssh dies or gets killed. Then it won't re-spawn at the next connection attempt when it should.

Questions:

  1. Can /usr/bin/ssh really not accept systemd-passed sockets? Or only newer versions? Mine is the one from up2date Debian 8.9.
  2. Can only units of root use the BindTodevice option?
  3. Why is my proxy service not respawning correctly on first new connection after the old tunnel dies?
  4. Is this the right way to set-up an "on-demand ssh socks proxy"? If, not, how do you do it?
2
  • autossh should take care of reconnecting in case the connection fails (though it is not the system-way).
    – Jakuje
    Commented Aug 3, 2017 at 12:54
  • @Jakuje : Thx for the comment, but I do not want the connection to be permanent. I want it to be spawned when I use it, and then (ideally after a no-data-sent-in-x-minutes timeout) self-terminate. Also, my previous solution used autossh. Commented Aug 3, 2017 at 12:58

4 Answers 4

11
  • Can /usr/bin/ssh really not accept systemd-passed sockets?

I think that's not too surprising, considering:

  • OpenSSH is an OpenBSD project
  • systemd only supports the Linux kernel
  • systemd support would need to be explicitly added to OpenSSH, as an optional/build-time dependency, so it would probably be a hard sell.

  • Can only units of root use the BindTodevice option?

User systemd instances are generally pretty isolated, and e.g. can not communicate with the main pid-0 instance. Things like depending on system units from user unit files are not possible.

The documentation for BindToDevice mentions:

Note that setting this parameter might result in additional dependencies to be added to the unit (see above).

Due to the above-mentioned restriction, we can imply that the option doesn't work from user systemd instances.


  • Why is my proxy service not respawning correctly on first new connection after the old tunnel dies?

As I understand, the chain of events is as follows:

  • SocksProxyHelper.socket is started.
  • A SOCKS client connects to localhost:8118.
  • systemd starts SocksProxyHelper.service.
  • As a dependency of SocksProxyHelper.service, systemd also starts SocksProxy.service.
  • systemd-socket-proxyd accepts the systemd socket, and forwards its data to ssh.
  • ssh dies or is killed.
  • systemd notices, and places SocksProxy.service into a inactive state, but does nothing.
  • SocksProxyHelper.service keeps running and accepting connections, but fails to connect to ssh, as it is no longer running.

The fix is to add BindsTo=SocksProxy.service to SocksProxyHelper.service. Quoting its documentation (emphasis added):

Configures requirement dependencies, very similar in style to Requires=. However, this dependency type is stronger: in addition to the effect of Requires= it declares that if the unit bound to is stopped, this unit will be stopped too. This means a unit bound to another unit that suddenly enters inactive state will be stopped too. Units can suddenly, unexpectedly enter inactive state for different reasons: the main process of a service unit might terminate on its own choice, the backing device of a device unit might be unplugged or the mount point of a mount unit might be unmounted without involvement of the system and service manager.

When used in conjunction with After= on the same unit the behaviour of BindsTo= is even stronger. In this case, the unit bound to strictly has to be in active state for this unit to also be in active state. This not only means a unit bound to another unit that suddenly enters inactive state, but also one that is bound to another unit that gets skipped due to a failed condition check (such as ConditionPathExists=, ConditionPathIsSymbolicLink=, … — see below) will be stopped, should it be running. Hence, in many cases it is best to combine BindsTo= with After=.


  • Is this the right way to set-up an "on-demand ssh socks proxy"? If, not, how do you do it?

There's probably no "right way". This method has its advantages (everything being "on-demand") and disadvantages (dependency on systemd, the first connection not getting through because ssh hasn't begun listening yet). Perhaps implementing systemd socket activation support in autossh would be a better solution.

3
  • I have added BindsTo=SocksProxy.service to the Unit section of the file ~/.config/systemd/user/SocksProxyHelper.service, after the After=SocksProxy.service line. Manually restarting the SocksProxy Service is now not required anymore, when SSH dies/gets_killed. Is there a way for systemd to "hold" the initial connection, so that it does not get a TCP reset? Commented Sep 4, 2018 at 9:43
  • 1
    @Vladimir Panteleev one point I would like to clarify: support for systemd socket activation can be implemented relatively easily by parsing $LISTEN_FDS without adding a dependency on sd_listen_fds(), so it may still be a hard sell, but not too hard.
    – Amir
    Commented Oct 13, 2018 at 9:48
  • @AlexStragies To confuse matters more, the naming scheme in this conversation as a whole is misleading. What you are calling SocksProxyHelper is a proxy service. What you are calling SocksProxy is proxied service. Coupled with the fact that your proxied service is running an executable offering a SOCKS5 Proxy, it gets confusing really fast. Commented May 7, 2020 at 11:28
8

For future reference, i'm pasting below the systemd --user configuration files for on-demand ssh-tunnel using systemd-socket-proxyd daemon, with various enhancements and explanation comments:

~/.config/systemd/user/ssh-tunnel-proxy.socket

[Unit]
Description=Socket-activation for SSH-tunnel

[Socket]
ListenStream=1000

[Install]
WantedBy=sockets.target

~/.config/systemd/user/ssh-tunnel-proxy.service

[Unit]
Description=Socket-activation proxy for SSH tunnel

## Stop also when stopped listening for socket-activation.
BindsTo=ssh-tunnel-proxy.socket
After=ssh-tunnel-proxy.socket

## Stop also when ssh-tunnel stops/breaks
#  (otherwise, could not restart).
BindsTo=ssh-tunnel.service
After=ssh-tunnel.service

[Service]
ExecStart=/lib/systemd/systemd-socket-proxyd --exit-idle-time=500s localhost:1001

~/.config/systemd/user/ssh-tunnel.service

[Unit]
Description=Tunnel to SSH server

## Stop-when-idle is controlled by `--exit-idle-time=` in proxy.service
#  (from `man systemd-socket-proxyd`)
StopWhenUnneeded=true

[Service]
Type=simple
## Prefixed with `-` not to mark service as failed on net-fails;
#  will be restarted on-demand by socket-activation.
ExecStart=-/usr/bin/ssh -kaxNT -o ExitOnForwardFailure=yes  hostname_in_ssh_config  -L 1001:localhost:2000
## Delay enough time to allow for ssh-authentication to complete
#  so tunnel has been established before proxy process attaches to it,
#  or else the first SYN request will be lost.
ExecStartPost=/bin/sleep 2

Customizations

In the scripts above, you have to replace the following strings:

  • 1000 - (ssh-tunnel-proxy.socket file)
    what (host:)port to listen locally for socket-activation of the tunnel, e.g. to emulate a local MYSql port.
  • 1001 - (ssh-tunnel-proxy.service & ssh-tunnel.service files)
    what local (host:)port to use when proxy-forwarding to ssh service-process;
    just choose an unused port.
  • hostname_in_ssh_config - (ssh-tunnel.service file)
    the host-group to connect to, referred in your ssh-config (any SOCKS configuration belong there).
  • localhost:2000 - (ssh-tunnel.service file)
    the ssh tunnel's remote host:port endpoint, e.g. where the remote MYSql binds to.
  • x2 delay timings (ssh-tunnel-proxy.service & ssh-tunnel.service files)
    they are explained in the comments.
  • ssh-tunnel-... - (the prefix of all unit-files)
    make it descriptive of your need for the tunnel, e.g. mysql-tunnel-....
  • SSH - (in the Description= directives of all unit-files)
    make it descriptive of your need for the tunnel, e.g. MYSql.

Commands for controlling the tunnel

# After any edits.
systemctl --user daemon-reload
# If socket's unit-file has been edited.
systemctl --user restart ssh-tunnel-proxy.socket

# To start listening for on-demand activation of the tunnel
systemctl --user start ssh-tunnel.socket

# To enable on-demand tunnel on Boot
systemctl --user enable ssh-tunnel-proxy.socket

# To gracefully stop tunnel (any cmd will do)
systemctl --user stop ssh-tunnel.service
systemctl --user stop ssh-tunnel-proxy.service

# To gracefully stop & disable tunnel (till next reboot)
systemctl --user stop ssh-tunnel-proxy.socket

# To view the health of the tunnel
systemctl --user status ssh-tunnel-proxy.{socket,service} ssh-tunnel

# To reset tunnel after errors (both cmds may be needed)
systemctl --user reset-failed ssh-tunnel ssh-tunnel-proxy
systemctl --user restart ssh-tunnel-proxy.socket
3
  • 1
    Excellent! I'm using this for plain point-to-point tunneling myself, not SOCKS, and it seems to be working as expected so far, except for one little detail. I'm pretty sure -L 1002:localhost:2000 should read -L 1001:…. The port that systemd-socket-proxyd forwards to has to match the local end of the ssh tunnel in order for any proxied connections to work.
    – Askeli
    Commented Feb 9, 2022 at 14:58
  • 2
    Thanks! This excellent answer helped me a lot! Here's a small improvement I made. Instead of using a port, use a unix socket (/tmp/ssh-tunnel for example). I generally prefer this over just port numbers. Then, this allows to replace the sleep, which may not be enough and may be too much, with waiting for the file: ExecStartPost=bash -c "while [ ! -e /tmp/ssh-tunnel ]; do sleep 0.1; done"
    – Noam
    Commented Jul 20, 2022 at 9:50
  • 1
    Thank you @noam. I suspect that systemd has options to avoid the use of a bash sleep-loop.
    – ankostis
    Commented Jul 21, 2022 at 11:19
4

Due to the reputation system, I'm not able to comment @ankostis solution...

I suspect that systemd has options to avoid the use of a bash sleep-loop

Yes, there is systemd-notify.

Also incorporating @noam suggestion to use use a socket between systemd-socket-proxyd and ssh to not waste local ports, it would look like the following:

~/.config/systemd/user/ssh-tunnel-proxy.service

[Unit]
Description=Socket-activation proxy for SSH tunnel

## Stop also when stopped listening for socket-activation.
BindsTo=ssh-tunnel-proxy.socket
After=ssh-tunnel-proxy.socket

## Stop also when ssh-tunnel stops/breaks
#  (otherwise, could not restart).
BindsTo=ssh-tunnel.service
After=ssh-tunnel.service

[Service]
ExecStart=/lib/systemd/systemd-socket-proxyd --exit-idle-time=500s ${XDG_RUNTIME_DIR}/ssh-tunnel-proxy

~/.config/systemd/user/ssh-tunnel.service

[Unit]
Description=Tunnel to SSH server

## Stop-when-idle is controlled by `--exit-idle-time=` in proxy.service
#  (from `man systemd-socket-proxyd`)
StopWhenUnneeded=true

[Service]
Type=notify
NotifyAccess=all
## Prefixed with `-` not to mark service as failed on net-fails;
#  will be restarted on-demand by socket-activation.
ExecStart=-/usr/bin/ssh -kaxNT -o ExitOnForwardFailure=yes -o ControlMaster=no -o StreamLocalBindUnlink=yes -o PermitLocalCommand=yes -o LocalCommand="systemd-notify --ready" hostname_in_ssh_config -L ${XDG_RUNTIME_DIR}/ssh-tunnel-proxy:localhost:2000

The ssh_config man page says about LocalCommand "Specifies a command to execute on the local machine after successfully connecting to the server".

ControlMaster=no ensures that the ssh session never gets a master in case multiplexing is used, since it's town down automatically and would kill all other multiplexed ssh sessions to the same host.

Finally, it might be a good idea to bind the socket port to localhost.

~/.config/systemd/user/ssh-tunnel-proxy.socket

[Unit]
Description=Socket-activation for SSH-tunnel

[Socket]
ListenStream=127.0.0.1:1000
ListenStream=[::1]:1000

[Install]
WantedBy=sockets.target
1
  • Thanks for answering! +1. I'll give your suggestions a spin :) Commented Feb 19, 2023 at 21:40
2

Still testing this (as I am writing this answer, I am using it), but I think the missing ingredient is -o ExitOnForwardFailure=yes as an option to your ssh binary.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .