systemd 服务里 %h、%p 会被展开:一次 SSH 隧道启动失败的排查

举报
yd_277098636 发表于 2026/11/13 08:20:44 2026/11/13
【摘要】 背景2026 年 11 月 12 日,我需要把一条经 HTTP CONNECT 代理的 SSH 动态转发隧道做成 systemd 常驻服务。隧道本身在命令行里已经跑通过:ssh -N -D 127.0.0.1:11080 -o ProxyCommand="/root/connect_proxy.py %h %p" root@18.219.168.124其中 connect_proxy.py...

背景

2026 年 11 月 12 日,我需要把一条经 HTTP CONNECT 代理的 SSH 动态转发隧道做成 systemd 常驻服务。隧道本身在命令行里已经跑通过:

ssh -N -D 127.0.0.1:11080 -o ProxyCommand="/root/connect_proxy.py %h %p" root@18.219.168.124

其中 connect_proxy.py 是一个 CONNECT 中继脚本,%h、%p 是 SSH 的占位符,ssh 会在建立连接时把它们替换成目标主机和端口。手动执行完全正常,出口固定为 18.219.168.124,Google 204 也通。

现象:写进 service 就起不来

把同一条命令原样搬进 unit 文件:

[Service]
ExecStart=/usr/bin/ssh -N -D 127.0.0.1:11080 -o ProxyCommand="/root/connect_proxy.py %h %p" root@18.219.168.124

systemctl start 后服务立即退出。journalctl 里看到的不是 SSH 的报错,而是中继脚本收到了完全错误的参数——它拿到的根本不是主机名和端口。

根因:systemd 先展开了一遍 %

问题不在 SSH,在 systemd。ExecStart 里的 % 开头序列是 systemd 的 specifier(说明符),服务启动前就会被 systemd 自己展开,根本到不了 ssh:%h 是本机主机名,%p 是 unit 名称前缀,%n 是完整 unit 名称,%u 是运行该服务的用户名,而 %% 转义后才还原成一个字面 %。
所以上面那行实际传给 ssh 的 ProxyCommand 已经变成了类似 connect_proxy.py 加本机主机名加服务名前缀,中继脚本去 CONNECT 一个不存在的目标,自然直接崩掉。而在交互 shell 里没有这一层展开,%h %p 原样进了 ssh,ssh 再按自己的规则替换,所以手动跑是好的——同一条命令,shell 里对、service 里错,这是最容易误导人的地方。

修复:写成 %%h %%p

[Service]
ExecStart=/usr/bin/ssh -N -D 127.0.0.1:11080 -o ProxyCommand="/root/connect_proxy.py %%h %%p" root@18.219.168.124

systemd 把 %% 还原成单个 % 再传给命令,ssh 拿到的就是它期望的 %h %p,隧道随即正常启动。改完务必 systemctl daemon-reload 后再启动,并用 journalctl 看进程实际收到的参数,不要只看 unit 文件原文。

两个顺带踩到的坑

  1. ExecStart 不经过 shell。systemd 默认不启动 shell 解释 ExecStart,管道、重定向、$变量都不会生效,需要 shell 语义时必须显式包一层 /bin/sh -c。但包了 shell 也救不了 % 的问题,因为 specifier 展开发生在 shell 启动之前,%% 该写还得写。
  2. 后台进程别指望 nohup。同一次排查里还发现:在受限的 exec 会话里用 nohup ssh 起的隧道,会话收尾时会被连带杀掉(日志里是 Broken pipe),测通只在同一次会话内成立。需要长期存活的隧道必须走 systemd 服务,而不是靠 nohup 顶着。

检查清单

  • ExecStart 里出现 % 先问一句:这是给 systemd 的,还是给目标程序的?
  • 给目标程序的 %h、%p、%u 等一律写成 %%h、%%p、%%u。
  • 改完 daemon-reload,用 journalctl -u 服务名看进程实际收到的参数,不要只看 unit 文件原文。
  • 凡是「shell 里能跑、service 里起不来」的命令,先查 specifier 展开,再查 shell 语义缺失。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。