用 ip rule 添加一条策略规则,它会一直工作到链路消失,而在那一刻它就干脆不存在了。没有任何东西大声报错:内核状态变了,而没有任何配置持有者知道这条规则本该存在。这就是会保留下来和不会保留的规则之间的全部差别。在运行 systemd-networkd 的主机上,解决办法是给这条规则一个持有者,而不是每次重连后重新执行命令。

只有有人持有,规则才存在

内核路由策略是状态,不是配置。它存在于正在运行的系统里,任何拆掉它所依赖接口的操作都可能把它一起带走。shell 命令只改变那个状态,不留下任何意图记录。

网络管理器的工作方式相反:它们读取配置、应用配置,并在链路事件使先前状态失效时重新应用。写在它们配置里的规则会被重建,而放在它们旁边添加的规则是没有持有者的外来对象。

这个区别在 networkd 里有个名字,值得在启用任何东西之前读一读。较新的版本默认会删除外来规则,这让那些以为手工条目会保留下来的人措手不及。

相信某个设置之前先读版本

归属行为由 networkd.conf 中的一个设置控制,而该设置在旧版 systemd 中并不存在。围绕它做设计之前先确认已安装的版本,因为旧主机的行为与你正在读的文档不同。

有两个值很重要。一个决定是否删除未在 networkd 配置中声明的规则,另一个对路由做同样的事。两者都关于归属而非策略内容,且在当前版本中默认都选择更严格的行为。全局关掉它们会让每条外来规则继续存活,这比声明一条预期的 VPN 策略是宽得多的决定。

systemctl --version | head -1
networkctl --version | head -1
grep -R "ManageForeign" /etc/systemd/ /usr/lib/systemd/ 2>/dev/null

那条 grep 会告诉你本机设置了什么、厂商默认值是什么。缺失也是信息,因为它意味着编译时的默认值生效。把默认值写进笔记,下一个人就不必重新发现它。

声明规则以及它指向的表

.network 文件里的规则只给出一个表名,名字能做的就这么多。它不创建该表内的路由,也不检查这些路由是否合理。只声明规则而不声明路由,得到的策略会匹配流量然后把它丢掉。

在同一个文件里声明路由,给出可达的下一跳和正确的表。在声明的规则里保留优先级,让它在有序列表中落在你预期的位置。提前到达错误表的规则会改动比你想修的服务更多的流量。把下面的片段当作形状而不是完整文件来读,因为接口归属和连接的其他部分仍然必须存在。

[RoutingPolicyRule]
Family=ipv4
IPProtocol=tcp
SourcePort=443
Table=100
Priority=1000

[Route]
Destination=10.80.0.0/24
Gateway=10.80.0.1
Table=100

然后重新加载,并回读内核状态而不是文件。解析干净的配置仍可能产生你并不想要的规则,只有声明完整时两种视角才会一致。

为接口选定唯一的持有者

一条隧道上的两个管理器会引发一场在两边都像缺陷的争斗。把现有的 wg-quick 配置导入 networkd 是迁移而不是叠加,两者同时运行会让接口及其规则处在两个持有者之下。

一次完成迁移:停掉旧单元、禁用它、在 networkd 中声明接口及其路由,然后重新加载。确认旧单元不会在启动时回来。重启后再次激活的残留单元会覆盖你刚刚声明的状态。保留旧配置文件,直到新配置经受住一次重连,再把它退役。

标记的归属也值得同样的谨慎:如果规则匹配某个防火墙标记,就必须有东西去设置那个标记。引用一个没人设置的标记的规则,是看起来像故意的死配置。

演练生命周期事件

配置的好坏取决于它在各种事件中的行为。断开链路、重新拉起、重启服务、续租地址。每做一项之后,都读取规则列表和表的内容。

sudo networkctl reload
ip rule show
ip route show table 100
sudo systemctl restart systemd-networkd

预期结果是同一条有归属的策略被重建、过时状态被退役,这比每次重连后再加一条 shell 规则更耐久。在变更记录里留一行,写明持有该规则的文件以及支持它的版本。这样下一位工程师不必翻 shell 历史就能找到这条策略。没有持有者的规则,就是还会再丢一次的规则。

相关文章