在 Android 上添加到 main 表的路由,在任何列表里看起来都没问题,却仍然不承载流量。原因是 main 表只是若干选项之一,而且它不是最先被查询的。Android 的网络服务会构建策略规则,用目的地址并不包含的信息来选择路由表——应用的 uid、数据包上的标记、数据包到达的接口——目的查找随后才在那些规则所选中的表里进行。

固定顺序上的三个决定

一个外发数据包会做三次彼此独立的决定。第一,策略规则决定使用哪张路由表。第二,该表内部的某条路由决定下一跳和出口接口。第三,netfilter 钩子决定数据包是被放行、修改还是重定向。

顺序之所以重要,是因为每个阶段只能回答自己的问题。接受数据包的防火墙规则并不选择路由表,而添加到没有任何规则选中的表中的路由是无效的。围绕 Android VPN 的大量困惑,都来自只修好了三者之一却看不到变化——真正的故障在另一个环节。

StageSelectorWhat it cannot decide
Policy ruleuid range, mark, incoming interfaceWhich route exists inside the table
Route lookupDestination prefix, longest matchWhether the packet is allowed out
Netfilter hookChain position, match conditionsWhich table was selected

标记是打包的字段,而不是你拥有的标签

Android 把多个事实打包进同一个标记值:选了哪个网络、应用是否显式选择了网络、套接字是否受 VPN 保护,以及与权限相关的位。AOSP 的 fwmark 头文件定义了当前代码树中的这种布局,而该布局属于平台内部细节,会随版本变化。

正因如此,在 Android 上用 iptables -j MARK --set-mark 写入整个标记值是危险的。它不是追加标签,而是替换系统已经做出的网络选择。随后数据包会匹配到不同的规则:取决于被覆盖的位,看起来可能像 VPN 开始工作了,也可能像某个无关应用失去了连接。

如果确实必须改动标记,先读取当前值,并使用掩码,只修改你理解的那些位:

ip rule show
ip route show table all | head -40
ip route get 10.20.4.11

连接标记与数据包标记是两个不同的对象,二者都不是路由。在应答包上恢复标记,与在出方向选择路由表是两回事,这就是单向修复常常产生“连接能建立却随即挂起”的原因。

规则和表归谁管

Android 的网络服务 netd 负责创建规则以及按网络划分的表。AOSP RouteController 正是为 VPN、物理网络和 uid 范围组装这些规则的地方,它的生命周期绑定在网络事件上,而不是绑定在配置文件上。

由此有两个推论。第一,你在某台设备上看到的优先级和表号属于编译期内部细节,而不是接口:在另一个 Android 版本上看起来相同的编号,含义可能完全不同。第二,网络变化时状态会被重建,因此今天从 root shell 插入的规则,可能在下次 Wi-Fi 切换后就不复存在。

这种消失常被报告成“系统忽略了某条路由”。实际发生的是:选择该表的那条规则已不存在,表内那条路由从数据包的角度变得不可达。当原本可用的配置在重连后失效时,请检查规则列表,而不是路由列表。

钩子顺序类问题没有放之四海而皆准的答案

iptables 是否在 VPN 路由之前运行这个问题,不指明数据包路径就无法回答。本地生成的数据包会经过 output 钩子,标记变更会在此触发一次重新路由检查——这是第二次查找,而不是回头修改第一次。来自通过 tethering 接入的客户端的数据包走 prerouting 路径并被转发,看到它的链是另外一些。

所以同一条规则可能对某个应用的流量有效,却对 tethering 流量完全不可见;一直停留在零的计数器告诉你数据包走了哪条路径,而不是规则是否写错。在修改任何匹配条件之前,先用计数器确定路径。

这也正是策略路由错误难以察觉的阶段。路由到一张存在但不包含目的地址的表,会产生“网络不可达”,看起来像被防火墙丢弃;而防火墙丢弃会产生超时,看起来像路由黑洞。

在系统内部诊断,或者干脆别动

对于获授权的排查,在动任何东西之前先确定四个事实:应用的 uid 与配置文件、Android 为它选择的网络、VPN 是否包含该应用,以及问题是否在两个地址族上都存在。按应用排除就能解释大量“VPN 不工作”的报告,而根本不存在路由故障。

然后读取真正生效的规则、掩码和表,并把一次网络切换纳入观察窗口。如果行为在重连前后发生变化,你面对的是被管理的生命周期,而不是缺失的路由。

对于常规需求,请使用受支持的控件:按应用包含与排除 在 VPN 应用里,以及平台自身的网络选择,用于任何需要特定路径的场景。如果特权集成确实必须与路由机制协作,请针对你所支持的 Android 版本的确切实现,并通过拥有它的组件来维护其生命周期。清空平台链或插入高优先级规则,会把一次诊断变成第二个网络管理器,手机的后续行为也会如其所愿。

相关文章