通过带有套接字激活的 systemd 用户单元的按需 SSH Socks 代理不会按预期重新启动

通过带有套接字激活的 systemd 用户单元的按需 SSH Socks 代理不会按预期重新启动

为了到达一个隔离的网络,我使用 -D

为了避免每次将详细信息添加到时都必须输入详细信息~/.ssh/config

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

然后我创建了一个服务单元定义文件:

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

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

[Install]
WantedBy=default.target

我让守护进程重新加载新的服务定义,启用新服务,启动它,检查其状态并验证它正在侦听:

$ 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

这按预期工作。然后我想避免手动启动服务,或者永久运行它, 通过使用 用于按需(重新)生成。那不起作用,我认为(我的版本)ssh无法接收套接字文件描述符。

我找到了文档(1,2), 和一个例子用于使用systemd-socket-proxyd-创建2个“包装”服务的工具,一个“服务”和一个“套​​接字”:

$ 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

似乎工作,直到ssh死亡或被杀。然后它不会在下次连接尝试时重新生成。

问题:

  1. /usr/bin/ssh 真的不能接受 systemd 传递的套接字吗?还是只有较新的版本?我的是更新的 Debian 8.9 中的一个
  2. 只有 root 单位才能使用该BindTodevice选项吗?
  3. 为什么在旧隧道失效后,我的代理服务无法在第一个新连接上正确重生?
  4. 这是设置“按需 sshocks 代理”的正确方法吗?如果不是,你怎么办?

答案1

  • /usr/bin/ssh 真的不能接受 systemd 传递的套接字吗?

我认为这并不太令人惊讶,考虑到:

  • OpenSSH 是一个 OpenBSD 项目
  • systemd 仅支持 Linux 内核
  • systemd 支持需要明确添加到 OpenSSH,作为可选/构建时依赖项,因此可能很难推销。

  • 只有 root 单位才能使用该BindTodevice选项吗?

用户 systemd 实例通常非常隔离,例如无法与主 pid-0 实例通信。像从用户单元文件依赖系统单元这样的事情是不可能的。

提及的文档BindToDevice

请注意,设置此参数可能会导致向单元添加额外的依赖项(参见上文)。

由于上述限制,我们可以暗示该选项在用户 systemd 实例中不起作用。


  • 为什么在旧隧道失效后,我的代理服务无法在第一个新连接上正确重生?

据我了解,事件链如下:

  • SocksProxyHelper.socket已开始。
  • SOCKS 客户端连接到 localhost:8118。
  • systemd 启动SocksProxyHelper.service
  • 作为 的依赖项SocksProxyHelper.service,systemd 也会启动SocksProxy.service
  • systemd-socket-proxyd接受 systemd 套接字,并将其数据转发到 ssh.
  • ssh死亡或被杀。
  • systemd 注意到并置于SocksProxy.service非活动状态,但不执行任何操作。
  • SocksProxyHelper.service继续运行并接受连接,但无法连接到ssh,因为它不再运行。

修复方法是添加BindsTo=SocksProxy.serviceSocksProxyHelper.service.引用其文档(强调已添加):

配置需求依赖关系,风格与Requires=.然而,这种依赖类型更强:除了Requires=它声明的效果之外,如果绑定的单元被停止,则该单元也会被停止。这意味着与突然进入非活动状态的另一个单元绑定的单元也将被停止。由于不同的原因,单位可能会突然、意外地进入非活动状态:服务单元的主进程可能会自行终止,设备单元的支持设备可能会被拔掉,或者安装单元的安装点可能会被卸载,而无需系统和服务管理器的参与。

当在同一装置上结合使用时,After=的行为BindsTo=甚至更强。在这种情况下,该单位绑定到严格必须处于活动状态才能使该单元也处于活动状态。这不仅意味着绑定到另一个单元的单元突然进入非活动状态,而且还意味着绑定到另一个单元的单元由于条件检查失败而被跳过(例如ConditionPathExists=,,,ConditionPathIsSymbolicLink=…… - 见下文),如果它正在跑步。因此,在许多情况下最好BindsTo=与结合使用After=


  • 这是设置“按需 sshocks 代理”的正确方法吗?如果不是,你怎么办?

可能没有“正确的方法”。这种方法有其优点(一切都是“按需”)和缺点(依赖于 systemd,第一个连接未通过,因为 ssh 尚未开始侦听)。也许在 autossh 中实现 systemd 套接字激活支持将是一个更好的解决方案。

答案2

为了将来参考,我将systemd --user使用守护进程的按需 ssh 隧道的配置文件粘贴在下面systemd-socket-proxyd,其中包含各种增强功能和解释注释:

~/.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

定制

在上面的脚本中,您必须替换以下字符串:

  • 1000-(ssh-tunnel-proxy.socket文件)
    什么(主机:)端口本地侦听隧道的套接字激活,例如模拟本地 MYSql 端口。
  • 1001-(ssh-tunnel-proxy.servicessh-tunnel.service文件)
    什么是本地的(主机:)端口当代理转发到 ssh 服务进程时使用;
    只需选择一个未使用的端口即可。
  • hostname_in_ssh_config-(ssh-tunnel.service文件)
    要连接的主机组,在您的ssh 配置 (任何 SOCKS 配置都属于那里)。
  • localhost:2000- (ssh-tunnel.service文件)
    ssh 隧道的远程主机:端口端点,例如远程 MYSql 绑定到的位置。
  • x2 延迟时间ssh-tunnel-proxy.servicessh-tunnel.service文件)
    它们在评论中进行了解释。
  • ssh-tunnel-...- (所有单元文件的前缀)
    使其描述您对隧道的需求,例如mysql-tunnel-....
  • SSH-(在Description=所有单元文件的指令中)
    使其描述您对隧道的需求,例如MYSql

控制隧道的命令

# 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

由于声誉系统,我无法评论@ankostis 解决方案......

我怀疑 systemd 有选项可以避免使用 bash 睡眠循环

是的,有 systemd-notify。

还结合 @noam 建议在 systemd-socket-proxyd 和 ssh 之间使用套接字,以免浪费本地端口,如下所示:

〜/.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

ssh_config 手册页谈到 LocalCommand“指定成功连接到服务器后在本地计算机上执行的命令”。

ControlMaster=no确保 ssh 会话在使用多路复用的情况下永远不会获得主控,因为它会自动关闭,并且会杀死同一主机的所有其他多路复用 ssh 会话。

最后,将套接字端口绑定到本地主机可能是个好主意。

〜/.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

答案4

仍在测试这个(当我写这个答案时,我正在使用它),但我认为缺少的成分是-o ExitOnForwardFailure=yes作为你的ssh二进制文件的一个选项。

相关内容