连接后打开路由表,你会看到一条地址经隧道转发,而它周围的子网仍然走本地网关。这条记录就是主机路由:一个地址,由全为一的前缀精确匹配。它之所以重要,是因为路由决策使用最长匹配前缀,因此 /32 可以在不改变 /24 或默认路由的情况下覆盖它们。这究竟是谨慎的例外还是未来的故障,取决于它指向哪个地址、由谁维护。

一个地址,一条表项

IPv4 主机路由带有 /32 前缀,IPv6 主机路由带有 /128,这意味着该表项只能精确匹配一个目的地址。表中其他内容都不动:覆盖它的子网和默认路由保持原样。窄表项之所以对该地址胜出,是因为它的前缀更长,而不是因为它是后加的。

这一特性使主机路由既是好例外,也是坏习惯。两条指向某服务入口的 /32 表项既覆盖不了它的第二个地址,也覆盖不了它的 IPv6 地址,更覆盖不了它下个月迁往的地址。一张塞满来历不明的单地址表项的表格,通常意味着有人一直在围绕子网决策打补丁,而不是做出决策。先读前缀长度,再问这条表项该保护什么。

从客户端向外读这条表项

从出问题的机器开始,因为同一个地址在笔记本上和它前面的网关上可能有不同的路由。不要从上往下读表,而要问内核:对于应用实际使用的那个确切地址,它会选择哪条路径。答案里包含 接口和下一跳,两者合起来才是那个决策。

ip route get 203.0.113.25
ip -6 route get 2001:db8:4::25
ip route show | grep -F 203.0.113.25
Find-NetRoute -RemoteIPAddress 203.0.113.25

对应用真正选择的地址族重复查询,因为客户端可能偏好 IPv6,从而悄悄绕过 IPv4 例外。比较隧道建立前后的结果,并保留两份输出。这个比较能说明表项是客户端在连接时添加的,还是本来就在。

持久表项和运行时表项失败方式不同。运行时表项随会话消失,而持久表项能挺过重启,并继续把流量发往一个已不存在的网关。

端点例外与递归陷阱

VPN 客户端通常会在改默认路由之前,先通过原网关 安装一条指向服务器外部地址的主机路由。没有这条表项,加密包就会被送进它们正试图建立的隧道,连接会一直循环到超时。

当隧道接受的子网恰好包含那个端点地址,或第二个配置文件后来添加了更宽的路由时,陷阱就出现了。此时端点表项输给更长的前缀,会话一开始传流量就崩溃。把端点地址写在接受的子网旁边,是最省事的提前发现冲突的办法。它也解释了为什么同一个配置文件在某个网络能用、换一个网络就失败。

验证应用真正使用的路径

当应用连接的是另一个地址或另一个地址族时,能 ping 通主机名只是弱证据。要在运行客户端的机器上,验证地址、地址族、端口和接口的确切组合。

ip route get 203.0.113.25
sudo nft list ruleset | grep -A 4 'chain output'
curl -sS --max-time 8 -o /dev/null -w '%{remote_ip}
' https://app.example.net

如果路径不对,先判断需求是一个网络还是一个地址,再动手修改。单地址需求值得用一条带注释和负责人的主机路由;地址轮换的服务则需要基于域名的路由,或者一个诚实的子网决策。

删除不再对应真实目的地的表项,并记下保留的那些。一条来历不明的 /32 三个月后和 bug 无法区分,下一个人会在不知道谁依赖它的情况下把它删掉。

相关文章