昨天隧道还正常,今早 unit 却死了,journal 里写着 Failed at step EXEC spawning /usr/local/bin/vpnclient。凭据、路由和账户从不进入这句话。systemd 把 ExecStart 解析成一个路径,请内核执行它,内核拒绝了,最常见的原因是该路径上什么都没有,或者文件不可执行。在解析后的命令证明相反之前,请把这一故障当作文件与 argv 问题。

为什么 EXEC 在认证开始之前就失败

systemd 不通过 shell 运行 ExecStart。它 fork,从该行构造参数向量,并对第一个元素调用 execve。如果该调用失败,服务就永远到不了 VPN 客户端自己的代码,所以修改配置文件或检查凭据都无济于事。

状态码把这一类与普通崩溃区分开。以 status=203/EXEC 退出的 unit 从未启动过进程;先启动、随后以 status=1 退出的 unit 可能是在认证、DNS 或路由上失败了。先读状态码再看客户端日志,因为前一种情况下客户端日志会是空的。

journal 里的路径是 systemd 使用的路径,不一定是您改过的那个。drop-in 文件和生成器输出可以覆盖厂商文件,而过期的 daemon-reload 会把旧行留在内存里。因此第一步诊断是问 systemd 它认为命令是什么,而不是打开您记得改过的文件。

二进制路径被写下的三个位置

从发行版软件包、厂商脚本和发布 tarball 安装的 VPN 客户端,可能落在三个不同的前缀下。unit、权限规则和安装程序各记录了一个前缀,三者中有两个不一致时故障就出现了。

RecordCommon valueHow to read it
Unit or drop-in/usr/local/bin/vpnclientsystemctl cat vpnclient.service
Installed binary/usr/bin/vpnclientcommand -v vpnclient, then ls -l the result
Sudoers grant/opt/vpnclient/bin/vpnclientsudo grep -r vpnclient /etc/sudoers.d/

具体的不一致是这样:厂商脚本装到 /usr/local/bin,tarball 解压在 /opt 下,而 unit 仍写着上一个软件包的旧路径 /usr/bin。这些文件只有一个是存在的,而且很少是 ExecStart 里的那个。

在改动任何一个文件之前,先确定哪个路径是规范的。如果二进制归发行版软件包所有,规范路径就是软件包路径,unit 应当跟随它。如果归厂商脚本所有,同一条规则适用于厂商前缀。在前缀之间复制二进制会产生两份副本,下一次升级时它们就会分道扬镳。

当引号里的命令变成文件名

第二种失败模式藏在同一行 journal 后面。有人把这个写进 unit,并期待客户端在开机时连上:

ExecStart=/usr/bin/"vpnclient connect office"

systemd 会把该值拆成单词,于是第一个单词变成 /usr/bin/vpnclient connect office,引号作为分组语法被去掉。随后内核去找一个名字里含空格的文件,而它并不存在。即使二进制已经安装,报错也与缺少二进制一模一样。

受支持的形式是真正的可执行文件,后面跟分开的参数,例如 ExecStart=/usr/bin/vpnclient connect office。如果工作流确实需要管道或命令链,就把它放进脚本并调用脚本,因为 ExecStart 没有 &&,没有管道,也没有 glob 展开。

修 unit,而不是修文件系统

诱人的修法是从期望路径到真实二进制建一个符号链接。它能工作到软件包升级、链接指向被删除的文件,或者 SELinux 标签不再匹配目标为止。更好的做法是通过 drop-in 修改 unit,让厂商文件保持原样,升级也保持可预测。

权限规则必须与同一路径、同一批子命令对应。为 /usr/local/bin/vpnclient 写的 sudoers 条目并不授权 /usr/bin/vpnclient,而允许任意参数的条目会让任何本地进程重新配置或停止隧道。只授予 unit 需要的那两个动词,如果停止服务已经会关闭接口,就把拆除动作排除在外。

比较的价值就在这里:unit、软件包数据库和 sudoers 应当指向同一个路径。做到这一点,EXEC 错误就会消失,既不需要重启,也不需要在第三个位置放一份二进制副本。

在重启之前证明解析后的命令

sudo systemctl daemon-reload
sudo systemctl cat vpnclient.service
systemctl show -p ExecStart vpnclient.service
systemd-analyze verify /etc/systemd/system/vpnclient.service
sudo systemctl restart vpnclient.service
systemctl status vpnclient.service --no-pager

systemctl show 是决定性的输出,因为它会打印 systemd 实际解析出的 argv,包括任何 drop-in。systemd-analyze verify 会在您重启之前抓住缺失或不可执行的路径,重启后的 status 命令则告诉您进程现在是否存在。

如果 unit 启动后又失败,您已经离开 EXEC 这一类,进入了客户端自己的错误处理。请在 journal 里找客户端的消息,而不是又一次 execve 失败。把 unit 的 diff 和 sudoers 的 diff 一起写进变更记录,这样下一次升级就不会悄悄把被拆开的路径带回来。

相关文章