你有两条上行链路,ip -6 route show在两条链路上打印出相同的下一跳:default via fe80::1 dev uplink0 和 default via fe80::1 dev uplink1。这并不是地址重复冲突,而且把裸下一跳 fe80::1 直接抄进策略表,也无法告诉内核你指的是哪条链路。真正的问题比症状看起来更窄:第二张表需要一条链路还是两条,以及重启之后是什么让这些路由继续存活。按顺序检查三个条件,就能解决大部分情况。

同一个 fe80::1 是两个不同的邻居

链路本地地址的作用域限于单条链路,因此 fe80::1 并非同一个地址打印了两次,而是两个碰巧共用后缀的邻居。区域索引是主机用来表明自己指哪一个的方式,所以 fe80::1%uplink0 和 fe80::1%uplink1 对内核而言就是不同的目的地,而这个后缀从不会传到远端路由器。RFC 4007 描述了这背后的区域模型,这也是为什么策略表里的每个链路本地下一跳都必须带上设备。

把某台主机上的数字区域 ID 复制进模板的 VPN 配置生成器,是把本地上下文当成了地址的一部分,它写出的文件在任何接口名或接口索引不同的机器上都是错的。策略表也不会把两条链路合并成一条:路由式 VPN 在网络之间传递数据包,但不会把远端以太网邻居的链路本地身份带过隧道,因此链路本地流量仍留在自己的链路上。

去掉设备,这条路由就无法写入。ip -6 route add default via fe80::1 table 200 无法解析网关,iproute2 会以 Error: Nexthop has invalid gateway. 拒绝。加上 dev uplink0 之后命令就成功了,因为正是设备把下一跳固定在这两个邻居之一。设备无法告诉你的是 uplink0 是否就是这张表该用的链路,而从另一台主机复制来的路由会连那台主机的接口名一起带过来,真正的错误往往就出在这里。

条件一:表里放一条链路还是两条

先弄清第二条上行链路的用途。如果两条链路承载同样的流量、其中一条是备用,那么备用表里应放两个网关并配上不同的度量值:相同的目的地,一条首选路径,一个明确的次选。如果这两条上行链路存在,是因为不同流量必须经不同运营商出去,或者因为这是一个分流隧道——部分流量绕过 VPN,其余仍留在隧道内——那么这两条链路根本不该放在同一张表里。它们应该放在两张表里,由一条规则来选择,而规则选择的是表,不是隧道:绕行流量在路由之前先被打上标记,标记才是把它送进第二张表的东西。

在后一种情况下,规则决定何时查阅某张表,而路由决定数据包被查表之后去哪里:

ip -6 rule add fwmark 0x2 table 201
ip -6 route add default via fe80::1 dev uplink1 metric 100 table 201
ip -6 route add 2001:db8:100::/48 dev uplink1 metric 100 table 201
ip -6 route show table 201

这两个对象谁也替代不了谁,而一张只放默认路由的表仍可能解析不出网关。如果添加被拒绝,或者路由存在却不可用,就把该接口的直连前缀放进同一张表,或者把这个下一跳标记为 onlink(当该前缀在别处被路由时)。在上面的代码块里,2001:db8:100::/48 代表 uplink1 上的那个直连前缀,而不是任意目的地。fwmark 规则只消费由别处设置的标记,可以来自转发路径上的 nftables 或 iptables 规则,也可以来自 SO_MARK 作用于套接字,因此照抄一段没有标记来源的配置,什么也匹配不到。

条件二:度量值察觉不到运营商已死

度量值只是在已有路由之间表达偏好,并不是健康检查。运营商掉线时,内核会移除该设备上的路由,第二条默认路由接管。如果网线仍插着、其后的运营商却死了,表里什么都不会变:首选路由照样安装,邻居在第 2 层仍可达,数据包就这么进了黑洞。如果这个区别对你的环境很重要,故障切换就必须来自某种探测路径并删除路由的东西,无论是路由守护进程,还是一个负责删除并重装该条目的短脚本。

表建好之后要验证选择,而且要按内核看它的方式去验证。ip -6 route get 2001:db8:100::1 会打印将要使用的路由,包括设备和选中的源地址,而且它遵守策略规则。源地址属于错误的上行链路,通常说明某条规则比你预想的更早匹配上了。

条件三:决定由谁负责持久化

通过 ip -6 route add 添加的路由会一直存活到重启,或被某个操作刷掉为止。从路由器通告学到的默认路由带有生命周期,路由器可以将其撤回;而手工添加的默认路由永远不会自行过期。把两者混在一张表里,就会出现一条过期永久路由压过新通告路由的情况,哪怕它所描述的上行链路早已不存在。

每张表只指定一个负责人。如果网络管理器或路由守护进程已经在管理该接口,就在那里描述策略路由,别去碰内核。如果你用脚本实现,就让脚本幂等,并在同一个地方写好拆解步骤,因为第一次改坏时你就会用到它:

ip -6 route flush table 201
ip -6 rule del fwmark 0x2 table 201

在为其他主机做转发的路由器上,先检查 net.ipv6.conf.all.forwarding,然后再去怀疑这张表。转发关闭时,无论表 201 多么正确,发往其他网络的数据包都会被丢弃。

把设计记录下来,方便下一个人审计

只保存 fe80::1 的清单会丢掉真正重要的字段。要把目的地、下一跳、设备、表和度量值一起记录下来,还要记下每条路由的来源:是通告的、管理器生成的,还是静态的。日后有人问第二条上行链路是否真的在承载流量时,这份记录不用猜就能回答。

在宣布这张表完工之前,先用 ip -6 neigh show dev uplink0 确认邻居,再重读 ip -6 rule show,看看有没有顺序上的意外,然后用匹配该规则的流量去测试,而不是用一个走默认路由的 ping。接着故意把一条上行链路停掉,观察内核回退到哪条路由;答案应该与你写下的那条一致。

相关文章