新 VPN 出口上的新连接,旧流保持原样
计划性排空只迁移未分类的流。将其与路由切换对比,并了解支撑它的标记、路由表和 conntrack 状态。
两个健康的 VPN 出口、一个已经在运行的长上传,以及下一个连接必须走另一条隧道的要求。满足这一要求有两种方式,而只有一种在生产网关上是安全的。切换默认路由会移动每一个数据包,包括属于该上传的包。计划性排空只改变新连接的选择,让已跟踪的流留在它们开始的路径上。
切换出口的两种方式
路由切换只有一条命令,几秒内即可回滚,因此常在事故中使用。它也是不加区分的:现有上传的数据包会突然从另一条隧道出去,使用不同的公网源地址和独立的转换状态。
如果远端按源地址跟踪流,这次上传可能干脆不再被识别。排空通过按连接只做一次决定并把它存下来,避免了这一点,因此旧上传会一直保留自己的出口直到结束。这正是这项工作的全部价值:远端看不到任何需要它解释的变化。它会继续收到与它已知连接相匹配的数据包。
代价是复杂性:两张路由表、一个在正确钩子上运行的分类器,以及必须与机器上其他使用它们的组件协调的标记。
给每个出口一张自己的表
为每个出口预留一张路由表,并为两个选择准备不同的标记值或位。每张表需要与其角色相符的路由,而不仅仅是一个名字,还需要为缺失情形准备一个刻意的失败结果。一张静默落入主表的空表 — 流量就是这样从没人选择的出口出去的。
ip rule add fwmark 0x10/0x10 table 101
ip rule add fwmark 0x20/0x20 table 102
ip route add default via 192.0.2.1 dev wg-a table 101
ip route add default via 198.51.100.1 dev wg-b table 102在每张表的末尾加一条 blackhole 路由,让找不到匹配的查找大声失败,而不是逃到你没有选择的默认值。
只选一次,在第一个数据包上
在连接跟踪已存在之后、在真正重要的路由查找之前进行分类。对于新的、未分类的 LAN 连接,选择出口并存入连接状态。后续数据包则把存储的值恢复到策略路由所读取的数据包标记中。
微妙之处在于“新”的含义。在看到回复之前,conntrack 对早期数据包和重传仍可能报告 NEW,因此只看该状态的分类器可能覆盖已经做出的选择。分配出口之前,请确认该连接尚未存储出口。
nftables 将数据包标记与连接标记分开保存,元信息指南记录了二者之间的显式复制。连接标记不会自己变成你规则所匹配的数据包标记,这正是精心编写的分类器毫无效果最常见的原因。
把恢复限制在你想要的方向和流量上。对返回的数据包套用互联网出口标记,可能把回复发向出口而不是 LAN,症状就是一条看起来像防火墙问题的单向连接。在动防火墙规则之前先读一读回复路径,因为数据包正在被路由,只是没有回到发送方。
排空的四个阶段
在不碰 A 的情况下准备 B:路由、转发、授权、NAT 和返回路径。通过网关现有的配置所有者安装它的表和选择策略。
切换分类器,让新的、未分类的连接选择 B,同时保留已存储的选择。原子的 nftables 事务避免了必须清理旧状态的窗口。
在 A 的流结束之前保持 A 在线。它的接口、路由表、远端出口和 NAT 映射仍然是这些流的一部分,而编辑 NAT 规则并不会重建已经存在的绑定。
当排空条件满足时退役 A,判断依据是连接状态和应用行为,而不是某一个平静的间隔。
排空无法承诺的事
浏览器请求不是新的传输连接;连接池会继续在旧连接上发送请求。反过来,UDP 流可能老化出 conntrack,之后的数据包会被重新分类。排空跟随的是已跟踪的流,而不是应用对会话的设想。
如果 A 在流结束之前就故障,存储的标记救不了它们。它们会通过 B 重连,或者根本不再重连。计划性排空买到的是原路径健康期间的连续性;它不是独立出口之间的状态复制。
验证两半
在变更前启动一条长寿命的允许连接,变更后再启动一条真正全新的连接。确认各自使用哪条隧道、远端看到哪个公网源地址,然后检查返回流量是否回来。
不要为了新选择出现而清空 conntrack 表。那恰好摧毁了排空想要保留的状态,并把一次受控迁移变成一次带干净假象测试的故障。