启动时,绑定固定 VPN 地址的套接字单元可能在隧道分配该地址之前就失败,其背后的服务永远起不来。两个开关看起来都像修复,却各自解决同一问题的不同一半。FreeBind 让 systemd 在地址仍缺失时就创建监听者。After= 只负责排序,所以它改变的是启动时机,而不能让地址更早到达。

两个不同的问题,两个不同的开关

第一个问题是绑定失败:内核拒绝在主机并不拥有的地址上监听,于是套接字单元立刻失败,服务永远不会被激活。第二个问题是就绪失败:地址最终存在,但某个东西在隧道承载流量之前就试图使用它。FreeBind 解决第一个,对第二个毫无作用。排序指令并不直接解决任何一个,因为它们只决定谁按什么顺序启动。为真正存在的失败命名,就是全部判断。

要从日志而不是服务描述确认属于哪一种。绑定失败会点名某个地址,并报告无法分配。就绪失败通常出现得更晚,表现为超时或对端看到的连接被拒,而日志里的套接字单元一切正常。

FreeBind:监听者先于地址存在

FreeBind 让内核允许绑定到非本地地址,于是 systemd 可以在启动时创建监听套接字,并在应用启动时把它交给应用。这正是套接字激活的意义,也是套接字能比其背后的服务活得更久的原因。

[Socket]
ListenStream=
ListenStream=10.8.0.1:8443
FreeBind=yes

空赋值会在添加新条目之前清除从厂商单元继承的监听条目,从而避免残留的通配监听者在每个接口上应答。把地址和端口换成你自己的,并核对生效单元,而不仅是 drop-in 文件。要用以单元名命名的 drop-in,而不是就地修改软件包文件,因为下一次软件包更新会覆盖这次修改,不会留下任何意图的痕迹。然后按照套接字单元预期的重载与重启流程应用它。

systemd 套接字参考记录了该选项背后的 IPv4 与 IPv6 行为,重要细节在于它作用于绑定而不是路由。其中没有任何内容会分配地址或启动 VPN。FreeBind 是授予监听者的许可,而不是接口的替代品。

After= 与 Wants=:顺序不等于就绪

顺序与要求是两条不同的轴,把它们混为一谈就会造出按正确顺序启动却仍然失败的单元。After=vpn-client.service只意味着如果两个任务都被启动,这一个会等另一个被启动。它不会把 VPN 拉进来,也不会等 VPN 完成连接。

像Wants=和Requires=这样的要求指令负责激活,它们在依赖失败时的行为不同:一个继续,另一个停止。两者都不知道已连接的隧道意味着什么。VPN 进程可以在没有分配地址时处于活动状态,也可以在有地址但没有到对端路由时处于活动状态,这两种状态都满足依赖。如果应用需要地址存在,就显式建模该条件,而不是把进程启动当作就绪的证明。

用systemctl cat和systemctl show -p After -p Wants -p Requires检查单元实际声明了什么。来自三个不同目录的 drop-in 可能加入你从未写过的指令,而 systemd 只读取生效单元。

network-online.target 不做什么

排在network-online.target之后的单元等待网络管理器宣布网络可用,这是一个启动同步点,而不是持续条件。它对某条隧道是否在线只字不提,也不会在 VPN 于午后重连时再次触发。把它当作重连监视器,是对该目标最常见的误读。

systemd 项目直接记录了这一点,实际后果是:任何需要隧道地址的东西都必须自己持有该依赖。等待 network-online 之后再绑定隧道地址的单元,只有在隧道碰巧先起来时才是对的,而那是巧合而不是设计。

测试你真正选择的生命周期

测试干净启动、地址出现后的请求,以及 VPN 断开再回来之后的行为。这三种情况把能用的监听者和能用的服务区分开来。

systemd-analyze verify myapp.socket
systemctl cat myapp.socket
ss -ltn "sport = :8443"

第一条命令在重启前捕获单元文件错误,第二条显示合并后的单元,第三条证明监听者是否存在、在哪个地址上。在你期待隧道地址的地方却出现通配地址上的监听者,正是空 ListenStream 本该清除的残留。VPN 重连后重复第三条命令,看看地址是否发生了移动。

有意识地选择生命周期:要么是自由绑定的监听者,等待稳定地址回归;要么是随隧道停止、在恢复时重新启动的服务。它们是不同的设计,失败方式也不同。然后把访问策略绑定在你预期的来源与路径上,因为绑定目标地址并不要求被接受的报文来自可信接口。

相关文章