你把一个域名加进了允许列表,查询确实到达了你的应用,匹配器也返回了允许,可页面就是打不开。到这一步,多数人开始重写匹配器、添加通配符比较。以我的经验,匹配器通常是对的,故障其实晚了一层:在你合成的 DNS 应答里,在你安装的路由里,或者在你用来转发查询的 socket 里。列表是最容易的部分;允许列表真正出问题的地方在数据包的路径上。

匹配器很少是元凶

首先要区分“被拦截”和“被弄坏”。被拦截的域名应该快速、可见地失败,因为浏览器收到“该域名不存在”就会停下。被弄坏的域名则会挂起,改走 IPv6 重试,再换一种传输方式,最后只剩下一个转圈。看到转圈,基本说明你的允许列表回了某种客户端无法处理的东西。

空的 A 记录是典型错误。返回 NOERROR 却零条回答,看起来像合法响应,stub 解析器照单全收,浏览器重试、试 AAAA、换解析器,很久之后才放弃。而返回响应码 3(即 NXDOMAIN),并把问题段回显、事务 ID 抄回,会立刻结束这次对话。凡是你有意拒绝的域名,都用它。

IPv6 也应得到同样的决定,而不是默认放过。如果你对 A 回 NXDOMAIN 却放行 AAAA,客户端就能通过 IPv6 访问被拦截的域名,过滤器会漏。某些实现会对被拦截的 AAAA 查询回一个 loopback 地址,让连接立刻失败,而不是回退到别的路径。选定一种策略,对两种记录都执行,两种都记日志。

捕获全部的路由就拥有全部

只做 DNS 的方案从不会添加 addRoute("0.0.0.0", 0),因此只有查询进入 TUN 接口,其余流量继续走底层网络。这是安全形态,也解释了为什么有些允许列表在 Wi-Fi 下看着能用、在蜂窝网络下就失效。

一旦存在默认路由,每条流就都归你管。到达接口却既不转发也不应答的 TCP 包会一直排队,直到客户端超时,而且没有任何错误可记,因为根本没出错。我见过有人为一份坏掉的允许列表耗掉一下午,真正原因却是 443 端口少了转发器。凡是你决定丢弃的包,都要记录协议和目的地址。

解析器 socket 必须离开隧道

转发是下一个陷阱所在。运营商的解析器常位于隧道无法到达的私有地址上,你转发的查询就石沉大海,应用只报一个读超时。把已批准的查询发给你确认可达的解析器,并先把那个 socket 交给 VpnService.protect()。未受保护的 socket 会把查询送回你自己的隧道,于是它再次到达应用并反复循环,直到超时触发。

这种故障看起来和断网一模一样,所以它在缺陷报告里能存活很久。给已发查询和已收应答各留一个计数器:如果前者持续上涨、后者始终为零,你该看的是 socket,不是列表。

长度和校验和失败时没有提示

静默丢弃也来自长度。回答集很大的 DNS 应答,或带一堆选项的 TCP 握手,可能超过你从未显式设置过的路径 MTU;分片在上游消失,客户端要么用更小尺寸重试,要么干脆不再重试。把接口 MTU 设成 1280 是保守选择,能扛住大多数蜂窝路径;显式设定,总好过继承一个没人测过的默认值。

如果你自己构造数据包,校验和是另一个静默杀手。UDP 校验和若在伪首部里漏掉 UDP 长度或协议字段,算出来的包在你这边看着规整,到了接收端却被无声丢弃。把伪首部逐字段对照规范核对,调试时把某条已知流的计算值打印出来。

VpnService 不会替你做的事

有两处限制值得在设计之前知道。普通应用无法在 Wi-Fi 之外安装系统级代理或 PAC 文件,这部分没有变化。变化的是按应用划分的路径:API 29 之前,VpnService.Builder 根本不能携带代理;从 API 29 起,setHttpProxy(ProxyInfo.buildPacProxy(uri)) 会把一段 PAC 脚本交给平台,由平台为 VPN 网络求值。面向旧 API,或 PAC 模型无法表达规则时,请在自家代码里求值。UDP 也不是白送的:浏览器经 SOCKS 代理只发 TCP,而多数 SOCKS5 客户端从不实现 UDP ASSOCIATE,于是基于代理的方案在 TCP 看似健康时丢掉或泄漏 UDP。

第二条限制正是基于 UDP 的协议能在普通网络上工作、却在 VPN 连上的那一刻失效的原因。若指望隧道承载 UDP,转发器就必须为它实现转发路径,而不是只换一种 socket 类型走 TCP。

按这个顺序检查

照顺序走,遇到第一个失败的环节就停下,因为在前面那一环站住之前,后面的检查都只是噪音。

  1. 列出已安装的路由,确认非 DNS 的数据包到底有没有进入 TUN 接口。
  2. 对一个被拦截的域名和一个已批准的域名,记录查询名、判定结果和响应码。
  3. 确认解析器 socket 已受保护,且所选解析器能从隧道内部应答。
  4. 确认接口 MTU,以及你手工构造的某个应答的校验和。
  5. 在 Wi-Fi 和蜂窝网络下重复同一个页面,因为运营商解析器和路径 MTU 并不相同。

五项都站得住、页面仍然卡住,下一个怀疑对象就是转发器本身:把抓到的请求和抓到的应答对比,找缺失的字节计数或半关闭的 socket。VpnService.Builder 参考文档覆盖了路由、DNS 和 MTU 的约定,而 protect() 是独立的 VpnService 调用,不是 builder 的选项;接口已连接只说明第一条约定满足了。

相关文章