Android 的 VPN 路由:先于 main 表的策略规则
Android 在任何目的查找之前,先用 uid 和标记规则选定路由表,因此加到 main 中的路由可能根本不会被查询。
在 Android 上添加到 main 表的路由,在任何列表里看起来都没问题,却仍然不承载流量。原因是 main 表只是若干选项之一,而且它不是最先被查询的。Android 的网络服务会构建策略规则,用目的地址并不包含的信息来选择路由表——应用的 uid、数据包上的标记、数据包到达的接口——目的查找随后才在那些规则所选中的表里进行。
固定顺序上的三个决定
一个外发数据包会做三次彼此独立的决定。第一,策略规则决定使用哪张路由表。第二,该表内部的某条路由决定下一跳和出口接口。第三,netfilter 钩子决定数据包是被放行、修改还是重定向。
顺序之所以重要,是因为每个阶段只能回答自己的问题。接受数据包的防火墙规则并不选择路由表,而添加到没有任何规则选中的表中的路由是无效的。围绕 Android VPN 的大量困惑,都来自只修好了三者之一却看不到变化——真正的故障在另一个环节。
| Stage | Selector | What it cannot decide |
|---|---|---|
| Policy rule | uid range, mark, incoming interface | Which route exists inside the table |
| Route lookup | Destination prefix, longest match | Whether the packet is allowed out |
| Netfilter hook | Chain position, match conditions | Which 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 版本的确切实现,并通过拥有它的组件来维护其生命周期。清空平台链或插入高优先级规则,会把一次诊断变成第二个网络管理器,手机的后续行为也会如其所愿。