从 Wi-Fi 切到蜂窝网络后,Kill switch 会哪里断掉
Kill switch 是按接口名写的,而网络切换会把这些接口换掉。在信任它之前,先检查外侧路由、地址族和解析器。
上一刻笔记本还在办公室 Wi-Fi 上,下一刻已经挂在手机上,而隧道指示灯在你切完窗口之前就重新变绿了。绿灯只说明隧道重新连上;至于这段空档里路由和拦截策略做了什么,是另一个问题。那套策略是上次安装时编译好的一组匹配条件,它写死了一个接口、一个地址和一个地址族,而这次切换刚好把这些都拿走了。于是同一次切换会产生三种看起来一样的结果:拦截继续生效;策略把自己的恢复路径也拦住了;或者一条谁都没覆盖的路径被打开了。
屏幕上没有任何东西能区分这三种状态,所以要问规则钉在哪些标识符上,而不是问指示灯是什么颜色。在 Linux 上,一条规则可以指定一个接口,或指定从该接口填进去的 ipset;在 Android 上,可以通过 setUnderlyingNetworks 告诉 VpnService 跟随哪些底层网络。每个绑定都指向一条即将被替换的链路,而且事后都必须重新评估。
切换究竟重写了什么
旧链路消失时,内核会删掉它的地址、撤回经由它指向的路由,并用剩下的东西重建源地址选择。默认路由转到新网关上,而旧规则引用的接口索引要么消失,要么被另一个设备复用。按链路划分的解析器也会刷新,只是和路由不在同一瞬间。这一切都发生在 VPN 客户端下面,所以只盯着自己隧道状态的客户端根本看不到。
麻烦在于规则编码的是一个具体标识符,而不是它背后的意图。一条丢弃所有不从隧道接口出去的流量的规则,编码的是接口名;一条放行端点地址的规则,编码的是新网络可能改路由到别处的地址。标识符消失时,规则不会明显报错,而是要么不再匹配任何东西,要么开始匹配错的东西。
在不打洞的前提下重建外侧隧道
外侧传输绑定在旧源地址上,所以必须从新地址开一个新套接字。当端点是主机名而不是字面地址时,解析必须在拦截仍然生效的情况下成功。这就是顺序陷阱:严格到只放行端点的策略,会把找到端点所必需的那次解析也一起掐掉,除非解析发生在拦截抬起之前,或者经由仍然存活的隧道带过去。两种顺序都可能成立,但前提是客户端知道自己处在哪一种。
客户端在这里的差别,在排查时很关键。有些先解析再装规则,切换会带来一段可见的断网,然后自行恢复;另一些保持拦截,指望存活下来的隧道把解析带出去,一旦隧道随链路一起死掉就彻底失败。为了恢复连通性而加一条宽泛的直连默认路由,永远能让测试通过,也永远把你想堵的漏点留着。
仍然指向旧网关的路由会造成同样卡住的恢复,只是原因不同,所以先读路由表再怪客户端。下面这些命令会指出内核真正会用的接口和源地址,一行就能判定旧路径是否还在。
ip route get 1.1.1.1
ip -6 route get 2606:4700:4700::1111新链路带来 IPv6,规则却没跟上
蜂窝网络和访客网络常常下发全球 IPv6 前缀,哪怕办公室有线 LAN 从来没下发过。只按 IPv4 规则写的策略——大多数手写 iptables 和 nftables 配方的默认形态——完全覆盖不到这些流量。隧道还连着,具备 IPv6 可达性的应用直接走到隧道外面,指示灯也看不出区别,因为隧道确实还活着。反过来同样常见:一套只针对 IPv6 的规则,在不提供 v6 的网络上什么都不做。
在切换的两侧,把链路提供了什么和策略提到了什么放在一起比较。下面这些命令打印的正是客户端在用的那些事实:
ip -4 route show default
ip -6 route show default
sudo nft list ruleset | grep -c 'ip6 '如果第二条命令打印出一条默认路由,而第三条返回零,新链路就能在没有任何规则看管的情况下把机器从 IPv6 送出网外。切换后把三条都重跑一遍,因为真正有意义的数字是两次运行之间的差。如果第二条什么都没打印,那就是相反的情形,那里任何偏重 v6 的策略都只是摆设。
解析器会在会话中途换手
现代系统里解析器是挂在链路上的,所以新链路带来的是一整套新的解析器和可能不同的 split-DNS 视图。systemd-resolved 为每条链路保存一个 scope,并按路由在它们之间选择;macOS 从动态存储里读取带 scope 的解析器,并遵循接口顺序。安装了自带解析器的客户端会与链路的解析器竞争,胜负由没人注意的优先级规则决定。这时名字解析可能已经坏了,而包过滤正在严格按它被写成的样子执行。
要读当前生效的解析器,而不是配置里的那个。在 systemd 主机上,status 子命令会显示每个 scope 来自哪条链路,以及 private-DNS 名称是否生效:
resolvectl status --no-pager
resolvectl query intranet.example.com如果查询是从物理链路而不是隧道出去的,说明客户端输掉了优先级竞争,再怎么调防火墙也修不好名字。在切换的两侧比较查询路径,看是哪一边动了。解析到新网络无法路由的私有地址,属于第三种情况,它表现为超时而不是报错。
反复演练切换,并把记录留下
与其等它发生,不如主动复现。先在第一条链路上把隧道拉起来,开始一个长连接传输和反复的名字解析,然后切换链路,看哪一个活下来。记下旧链路消失到隧道报告已连接之间的间隔,因为策略到底顶住了没有,就发生在这个窗口里。反过来也做一遍,免得结论只成立在一个方向上。
例外需要单独过一遍,因为旧网络上成立的绕行,在新网络上不会自动成立。家庭 LAN 上的打印机、NAS 或强制门户在蜂窝网络里没有对应物,当初放行它们的例外就变成了一条没有对应物的规则。
把每个方向上的接口名、下发的地址族、生效的解析器和断网时长都写下来。重连只能证明传输恢复了,不能证明空档期间拦截顶住了,这是要在工单里分开写的两件事。有了这份记录,下一次切换就可以和已知状态比,而不是和记忆比。