单元看起来没问题,systemctl start 也报告成功,十秒后服务却停在那里不动。或者它每半分钟重启一次,直到达到启动次数上限。这两种表现都不是网络故障:单元描述的进程生命周期与客户端的实际行为不符。客户端可能 fork 之后退出,也可能配置完会话就立即退出。

为什么单元在启动成功几秒后就变成非活动

systemd 根据它启动的进程来推断状态,因此 Type= 是对该进程的声明,而不是说明文档。用 Type=simple 时,命令运行期间单元被视为活动,命令返回后即变为非活动。因此,把连接交给辅助程序后立刻退出的客户端看起来就像崩溃,即使隧道已经建立。随后日志里会出现一条启动又停止的记录,读起来像是二进制文件坏了。

在动网络配置之前,先把单元类型和客户端行为放在一起读。

选择与客户端匹配的类型

两种模式覆盖了大多数厂商 CLI:一种是配置完会话就退出的命令,另一种是在返回前 fork 辅助程序的命令。两者需要不同的单元形态。

[Service]
Type=oneshot
RemainAfterExit=true
ExecStart=/usr/bin/protonvpn-cli connect -f
ExecStop=/usr/bin/protonvpn-cli disconnect

oneshot 配合 RemainAfterExit=true 能让单元在命令返回后仍保持活动,正好符合配置完就退出的模式,而停止命令让服务干净收尾。会 fork 的客户端保持同样的形态,但需要 Type=forking 才能让 systemd 跟随子进程。还要设置 Restart=on-failure、RestartSec=30 和有上限的 StartLimitBurst,这样供应商中断才不会变成反复重启、把报错埋掉的循环。

永远不要让被监管的客户端自行守护化

systemd 之外的监管器同样会撞墙。runit 的运行脚本启动一个守护化的客户端后会立刻返回给监管器,监管器把这当成崩溃,再起一份。每个监管器都希望自己掌握前台进程,就像 sshd -D 一直留在前台那样。对被监管的命令去掉守护化标志,把后台的事交给监管器。在隧道存在之前就返回的包装脚本会造成同样的混乱。

自行脱离的进程,对本来该盯着它的东西来说是不可见的。

会话结束,隧道也跟着没了

桌面会话拥有它启动的客户端。注销之后会话总线消失,隧道也随之消失,这解释了为什么关掉最后一个窗口时图形客户端的连接经常会断。服务器应该用系统级单元或 NetworkManager 配置,让它自己能起来。用户级单元只在隧道本该随登录结束时才合理,所以把它绑定到图形会话的目标上,并在描述里写清楚。

服务用户是同一个问题的另一半。属主为 root、权限为 0600 的密钥,对以其他账户运行的单元来说读不到,无论你在 shell 里用 sudo 做成了什么。

systemctl cat openvpn-server@work
sudo chown -R openvpn:network /etc/openvpn/server
sudo find /etc/openvpn -type f -perm /077 -ls

地址出现之后再排依赖

当守护进程绑定到尚未创建的隧道地址时,它会以 Cannot assign requested address 退出。修法是显式排序,而不是重试循环。

[Unit]
After=network.target [email protected]
Requires=sys-devices-virtual-net-wg0.device

After= 负责排序作业,而设备单元上的 Requires= 把依赖方绑到接口的存在上,这比只等网络目标更强。改完要重新加载管理器并重启依赖服务,然后去读它的日志,而不是假定顺序已经生效。用 StandardOutput=journal 或日志选项把客户端输出送进日志,因为被监管的客户端如果写进一个没人打开的文件,看起来和从未启动过一模一样。

然后重启一次,确认隧道设备出现之后单元是活动的。只用 systemctl start 验证过的修复,并没有测试它本该修好的启动顺序。

相关文章