分流出口:usb0 上的 VPN 池,eth0 上的网关流量
被转发的 VPN 客户端可以从 usb0 出去,而网关保留自己的 eth0 默认路由。先建一张完整的选择表,再让故障变成关闭而不是泄漏。
客户端连上来并通过认证,之后它们的流量却从错误的链路上出去,因为机器上唯一的默认路由属于网关自己。替换那条默认路由会破坏机器自身的更新和管理访问,所以选择范围必须比「全部流量」更窄。可行的形态是一条策略规则,以客户端池的内网源地址为键,指向拥有第二条 WAN 的第二张路由表。从那之后,检查清单关心的是完整性和故障行为,而不是规则本身。
确认源地址能保留到路由决策那一刻
匹配某个源地址范围的策略规则,只有在查表时数据包仍带着该源地址才会命中。被转发的客户端流量满足这一点,只要上游没有在路由之前改写它。网关自己产生的流量则不满足,这正是网关自己的默认路由可以原地不动的原因。
本地测试在这里会误导。在网关上跑的 curl 是本机产生的流,用的是网关地址,所以它走主表,对客户端路径什么也证明不了。请从已连接的客户端测试,或者用真正属于该池的源地址生成流量。
先把真实数值写下来:池前缀、承载它的隧道接口、第二条 WAN 的地址与下一跳,以及你打算使用的表号。在添加新规则之前清掉任何一次尝试留下的旧规则,因为不同优先级上的重复规则会产生看起来随机的故障。
建一张完整的选择表
一张表不是带个朋友的默认路由。 它需要足够多的路由,让每次查表都在表内得到解析,包括 WAN 网段的直连路由,好让下一跳可达。对于 tun1 上的 10.77.0.0/24 池、WAN 地址 203.0.113.20/24 与下一跳 203.0.113.1,以及表 600:
ip -4 route replace 203.0.113.0/24 dev usb0 src 203.0.113.20 table 600
ip -4 route replace 10.77.0.0/24 dev tun1 table 600
ip -4 route replace default via 203.0.113.1 dev usb0 table 600
ip -4 rule add priority 1500 from 10.77.0.0/24 lookup 600
这条规则落在任何现有兜底规则之下,所以匹配的客户端先命中表 600,其余流量落回主表。ip-rule 参考完整描述了按序查表的模型与选择器语法。
然后想一想表里没有什么。只在主表里有路由的目的地,比如经 eth0 可达的管理网段,在表 600 里找不到,而且一旦那里有了默认路由,也不会回落到主表。如果这些路径重要,就把它们显式加进选定的表,这通常是 VPN 出口表的正确做法。
把监听器自身的流量挡在策略之外
OpenVPN 监听器有自己的外部连接,走客户端抵达它时所用的那条链路。那部分流量必须保持来时的回程路径,而覆盖服务器公网地址或整个隧道接口的宽规则会把它拖进另一张表。把规则限定在内网池,别的都不要。
反向路径过滤也值得一看,原因相同。当回给客户端的响应经 usb0 出去、而请求从另一条链路进来时,严格的 rp_filter 设置会丢掉路由策略认为有效的包。在断定规则失败之前,先在两块接口上确认实际生效的 sysctl 值。
顺序相对机器上的其它东西同样重要。Docker 网桥、第二个 VPN,以及来自厂商镜像的任何既有规则都会添加优先级,所以要读完整份规则列表,而不是假定优先级 100 是唯一的。
让转发和地址转换与路由保持一致
路由只是选一条链路,并不授予转发的许可。内核必须开启 IPv4 转发,防火墙必须放行客户端到 WAN 的方向以及回程流量。一条正确的表配上丢弃该流的 FORWARD 链,症状与缺少路由完全相同,所以这两项要在同一轮里一起查。
如果第二条 WAN 不会把私有池回程过来,就在那条出口上做源地址转换。对于动态地址的接口,通常的做法是一条限定在该池与该接口上的窄 masquerade 规则:
iptables -t nat -A POSTROUTING -s 10.77.0.0/24 -o usb0 -j MASQUERADE
这里的范围比目标更重要。匹配任意出站接口的 masquerade 规则也会改写本来正确经 eth0 出去的流量,那样只是掩盖路由错误,而不是修正它。地址转换发生在路由决策之后,所以它永远修不好已经发到错误链路上的包。
路径不同之后,隧道开销也要留意。如果客户端协商了较大的 TCP MSS,而替代 WAN 的 MTU 更低、或里面还套着隧道,大分段会卡住而小分段能走。在转发路径上钳制 MSS 可以把这变成可预期的行为,而不是时断时续的挂起。
决定 usb0 消失时会发生什么
空表并不是故障关闭状态。如果匹配的路由消失,查表会继续走到下一条规则,进入主表并取 eth0 的默认路由。本该从第二条 WAN 出去的流量,悄悄从第一条出去了;对策略路由部署来说,这通常是你最不想要的结果。
在无法接受这种回落的地方,就在表 600 里加一条显式的终结路由,比如 blackhole 默认路由,或者在接口关闭期间在防火墙里丢弃该池。两种做法都是你可以写下来的决定;沉默不是。
持久性是最后一项。谁拥有这两条链路,不管是网络管理器、调度脚本还是连接生命周期,都必须每次重连时重建路由和规则,且不能追加重复项。用真实客户端的请求和来自网关的另一条请求来验证,确认两边观察到的出口,然后拔掉第二条 WAN,看看你设计的两种结果里究竟哪一种会发生。IPv6 需要自己的表、规则和地址转换;上面那条 IPv4 规则对它只字未提。