Android 的 VpnService 没有排除路由,无论怎么读 API 都造不出一个。Builder.addRoute() 只会向隧道添加目标,因此让一个 LAN 网段保持本地,就意味着该网段根本不进入路由列表。你真正要加的是该网段的补集,用普通前缀表示。工作是算术,而且会在少数可预测的地方失败。

按顺序做完下面的检查。每一项都假设前一项成立,所以在第一处失败就停下,不要往下修补症状。

从隧道声称的范围开始

把 0.0.0.0/0 交给 addRoute(),等于告诉 Android 每个 IPv4 目标都属于隧道。这就是很多客户端看起来做不了分离隧道的原因:它们先声称整个地址空间,再试图从中减去。构建器里没有任何东西会做减法。为你原本想保留在本地的网络设置更具体的路由,效果与预期相反,因为最长匹配前缀获胜,那个网络反而落进隧道里。

路由列表就是全部策略。它背后没有按目标逐条否决的机制,Builder 参考文档也没有列出任何能提供该机制的方法。

用前缀算术生成补集

被排除前缀的补集,是它在通往地址树根部的路径上的兄弟集合。对从 1 到被排除长度的每个长度,取出该长度下包含你前缀的块,翻转前缀正下方的那一位,然后输出结果。听起来很抽象,直到你真正运行它。

排除 10.0.0.0/8 会得到八条路由:128.0.0.0/1、64.0.0.0/2、32.0.0.0/3、16.0.0.0/4、0.0.0.0/5、12.0.0.0/6、8.0.0.0/7、11.0.0.0/8。它们都不包含 10.0.0.0/8,合起来则覆盖其余全部。

八行代码就能为任意前缀生成这份列表。

import ipaddress
def complement(addr, length):
    base = int(ipaddress.IPv4Address(addr))
    out = []
    for k in range(1, length + 1):
        block = base & (0xFFFFFFFF << (32 - k))
        sibling = block ^ (1 << (32 - k))
        out.append(f"{ipaddress.IPv4Address(sibling)}/{k}")
    return out
print(complement("10.0.0.0", 8))

路由数量等于前缀长度,那正是需要盯住的数字。单次 /21 排除要花费 21 条路由,仍然可读。十次排除可能花费几百条,这通常意味着需要改的是网络设计,而不是生成器。

检查地址族与隧道范围

IPv4 路由对 IPv6 毫无作用。如果隧道声称了 IPv6 地址,IPv6 的 LAN 前缀就需要自己的补集,用同样的算术在 128 位上生成。排除在纸面上成立、本地设备却仍不可达,一个常见原因正是:主机是通过 IPv6 访问的,而当时只切掉了 IPv4。在断定算术有误之前,两种地址族都要检查。

范围是检查的另一半。Android 按网络、按应用跟踪路由,因此同一个前缀可能对一个应用走隧道、对另一个应用走直连,取决于 VPN 是如何声明的。

把 DNS 与按应用规则放到该放的位置

路由排除不会改变解析器。本地主机名仍可能由隧道的 DNS 服务器应答,而它的地址却在本地路由,这种不一致表现为名字解析到 LAN 无法到达的东西。要有意识地设定解析器策略,用搜索域或分离 DNS 规则。不要以为路由已经替你决定好了。

addDisallowedApplication() 把一个应用挡在隧道外,但它不会为留在隧道内的应用把某个目标挡在外面。protect() 让你打开的套接字绕过隧道,隧道正是靠它避免捕获自身流量。两者都不能替代补集。

用查询来验证,而不是用自己的列表

隧道起来之后,去问设备某个地址实际去了哪里,而不是重读你自己构建的列表。在自带 iproute2 的版本上,adb shell ip route get 10.1.2.3 会指出内核选择的接口。对公网地址运行同一条命令,确认它指向隧道。

还有两项检查可以闭环。先解析一个本地名字,然后对解析出的地址运行 adb shell ip route get,要求接口不是隧道,因为 adb shell ping -c 1 host.lan 成功只能证明可达,不能证明路径。如果隧道声称了 IPv6,就在其上重复地址族测试。要在你实际发布的 API 级别上测试,因为 VPN 网络暴露给路由表的方式在各个版本之间变过。纸面上正确的路由列表和选错接口的查询是两种不同的缺陷,而只有后者会到达用户。

如果排除列表持续增长,就停止生成路由,改为给远端网络重新编号,或者干脆收窄隧道。生成器只是缺失 API 的变通办法,不是设计工具。

相关文章