策略路由回答的问题,和多数排障笔记问的不是同一个。主表说明目的地的去向;策略规则说明究竟该查询哪张表,而它们可以依据源地址、数据包标记或数据包到达的接口来做出决定。当隧道表现不一致时,原因往往是规则集,而路由表往往不是。

规则先选表,表再选路由

每条规则都带有优先级、匹配条件和目标表。内核按优先级顺序遍历规则,命中第一条就停下,然后在该表中执行通常的最长前缀查找。指向空表的规则会落到下一条规则所选的表上,精心写好的规则之所以看起来毫无作用,原因就在这里。

也正因如此,导出主表很少能解释一个正在工作的 VPN 客户端。客户端通常把规则插在较低的优先级编号上,远在主表默认规则之前,因此数据包永远到不了你盯着看的那条路由。降低主表里的度量值,对已经被改道的流量毫无影响。

规则也可以故意把某个前缀变成黑洞或不可达。有些客户端把这种规则当作终止开关加入,于是丢包看起来像隧道坏了,而不是被配置成了这样。

把规则集当成序列来读,而不是清单

把规则连同它们的优先级和所引用的表一起打印出来,然后再打印每张带默认路由或隧道路由的表。顺序才是关键,而截图通常恰好省掉的正是这部分:

ip rule show
ip route show table all | sed -n '1,60p'
ip route get 198.51.100.20 from 10.8.0.2
ip route get 198.51.100.20 from 10.8.0.2 mark 0x1

最后两条命令把理论变成观察,因为它们问的是内核对该来源和该标记会怎么做。如果标记改变时答案也跟着改变,那就是有基于标记的规则在起作用,任何漏掉标记的测试都在测另一个数据包。外部连接值得单列一条规则。让全部流量穿过自己的隧道,必须排除它连接的那个对端地址,否则加密后的数据包会被送回它们刚刚离开的隧道。多数客户端会自动加入这条排除规则,而手工编辑的规则集正是它容易失踪的地方。

标记与套接字:客户端分流的两条路

有些客户端在防火墙里给数据包打标记,再加一条匹配该标记的规则。另一些使用套接字级路由,在连接之前给套接字设置路由。前者在 ip rule 中看得见,后者看不见,也不会出现在普通查询里。

如果测试程序的表现和它要代表的应用程序不同,检查该程序是打了标记,还是绑定到了某个设备。机制一旦被命名,这个差异就不再神秘,它也解释了为什么同一台机器上,从 shell 跑的 curl 和浏览器标签页可以走不同的路径。两种技术都属于广义的策略路由:它们用目的地之外的东西来选路。两者都不授权流量,也都不替代防火墙规则或对端所期待的凭据。

在再加一条规则之前,先追查归属

规则是被什么东西安装上去的。VPN 客户端、网络管理器和手写脚本都可能在启动时添加规则,而重连逻辑会把上一组规则留在原地。早先配置里的两条陈旧规则,会在配置被删掉很久之后继续左右流量。

根据优先级和形态找出哪条进程拥有这条规则,并检查该所有者是否在每次连接时重写规则集。如果是,今天加的手工规则明天就会消失,修复应该落在客户端的配置里,而不是 shell 的历史文件里。为了测试而清空整套规则是很诱人的捷径,代价也可预测:它同时删掉了客户端维持自身隧道所需的规则。一次只删一条,或者在一台可以重启的机器上测试。

用两条流来证明范围,而不是一条

只有用一条本应受影响的流和一条本不应受影响的流,才能验证一次修正。选一个策略会送进隧道的目的地,再选一个必须留在隧道外的目的地,然后比较两者的查找和抓包结果:

ip route get 198.51.100.20 from 192.0.2.44
ip route get 198.51.100.21 from 192.0.2.44
tcpdump -ni any host 198.51.100.21 and port 443

当两次查找返回不同的接口,说明规则起了作用。当它们返回同一个接口,说明规则的匹配范围比预期更宽,而这正是把整个子网悄悄塞进为单个应用准备的隧道的故障。

把规则优先级和拥有它的服务写进工单,附上两个测试地址。这样下一个人拿到的是一对可复现的地址,而不是一段症状描述,并且能在一分钟内判断策略路由是否还在起作用。

相关文章