ExecStart 不是 shell:修复 VPN 辅助单元
ExecStart 只按参数运行程序,并不是 shell 管道。请使用显式解释器或真正的脚本,然后校验单元并查看日志。
把一条能用的命令行粘进 ExecStart,重载后单元要么启动失败,要么悄无声息地退出,参数被当成了文件名。systemd 本身没有任何问题。ExecStart 是参数列表而不是 shell,所以管道、重定向和条件判断一旦离开终端就失去意义。先决定 shell 应该放在哪里,再写一个明确说明要执行什么的单元。
为什么 ExecStart 里的管道不起作用
在终端里输入命令时,shell 会先解析它:建立管道、为重定向打开文件、展开变量,然后程序才开始运行。systemd 一概不做这些:它按自己的规则切分 ExecStart,再用这些参数执行切分后得到的程序。因此 > 会作为普通参数传进来,而 | 被交给程序,并不会连接两个进程。单元并没有出错,只是这段语法的含义本来就不同。
这是刻意为之而非疏忽,因为参数处理的可预测性正是单元可审计的前提。它只会让那些以为这里有 shell 的人意外。
写一个明确说明执行什么的单元
先从一个能在日志里看清目的的小单元入手,让解析问题和隧道问题分开。这个单元只报告某个二进制的版本。
[Unit]
Description=Report the installed OpenVPN version
[Service]
Type=oneshot
ExecStart=/usr/sbin/openvpn --version
StandardOutput=journal
StandardError=journal路径只是占位符,请换成实际安装的二进制。Type=oneshot 适合会运行到结束的命令,日志设置则把它的输出留在无需终端也能读到的地方。这里刻意没有安装段,所以单元要靠手动调用,而不是开机启用。启动前先校验文件,因为指令名里的拼写错误和路径里的错误表现并不相同。
systemd-analyze verify /etc/systemd/system/openvpn-version.service
systemctl start openvpn-version.service把 shell 逻辑放进脚本
当辅助程序确实需要条件逻辑时,把这段逻辑写进脚本并显式调用。脚本需要解释器行、可执行权限,以及服务能够报告的退出码。/bin/sh -c 加一段带引号的命令去启动确实可行,但它把引号规则藏进单元里,也让审阅变难。命名脚本可以手动测试,日后替换时不必改动单元。不要依赖交互用户的 shell 配置,因为服务启动时用的是它自己的环境。
当相对路径重要时,WorkingDirectory 要设置好;环境变量应通过单元支持的指令传入,而不是在终端里导出。机密应当放在受保护的文件或凭据中,而不是可能被日志记录的参数列表里。
检查真正存在的那份单元
drop-in 会在你不曾编辑的文件之外改变行为。要读管理器实际加载了什么,以及上一次运行报告了什么。
systemctl cat vpn-helper.service
journalctl -u vpn-helper.service -n 20 --no-pager单元转储会按加载顺序列出 drop-in,意外的 ExecStart 通常就来自那里。任何改动之后都要重载管理器,确认新定义正是 systemctl cat 打印出来的内容。日志会显示确切的命令行和退出状态,从而把参数问题和二进制缺失、权限错误区分开。接着判断这个辅助程序该并入现有 VPN 生命周期,还是保持为手动诊断,因为第二个后台隧道进程会制造一个互相竞争的所有者。
能按需输出正确结果的单元是很好的第一个里程碑。它还不是隧道,也不该假装自己是。