手机重启,常开模式已启用,「在没有 VPN 的情况下封锁连接」已打开,隧道却始终起不来。配置并没有出错:封锁正在履行职责,而客户端自己连接端点的尝试同样受它约束。端点名称必须在隧道存在之前就被解析,而且是由一个在其他所有应用都不被允许时仍获准访问网络的客户端来完成。弄清楚是哪个组件在做这件事、以什么顺序做,这个谜团就变成了一个配置问题。

VPN 构建器究竟要求什么

Android VPN 应用不会把服务器地址交给平台。它构建一个本地接口:隧道的地址和路由、应用应在隧道内使用的解析器,然后 establish() 返回一个文件描述符,应用以原始 IP 包的形式读写它。远端端点根本不会出现在那个构建器里。

这是关键的结构性事实。平台创建 tun 接口并在其上执行策略;应用在用户空间实现传输,也就是说,解析端点名称和打开加密连接都由应用自己完成。因此以主机名形式出现的端点之所以可行,并不是因为平台支持名称,而是因为平台根本看不到端点。

构建器里确实有一个字段接收地址:DNS 服务器。它们必须是数字形式的解析器地址,通常是运营商希望通过隧道去查询的那些。该字段描述的是隧道建立之后应用把查询发往哪里。它对客户端如何找到端点只字不提,而把这两件事当成同一个设置,正是混乱的起点。

两个 DNS 问题,而不是一个

第一个问题是:隧道建立之后应用把查询发往哪里?答案是隧道内部的解析器地址,可经 tun 接口到达,而且从构造上就是数字。

第二个问题是:在那个隧道存在之前,客户端如何找到自己的端点?那次查找发生在隧道之外,在底层网络上,在客户端的进程里。它可以使用名称,并且必须使用设备在没有 VPN 时也能到达的解析器。

StageWho resolvesPathFailure looks like
Endpoint lookupThe VPN appUnderlying network, protected socketNever connects; no tunnel interface traffic
Peer authenticationClient and serverTunnel transportConnects then drops; certificate or key error
Private name lookupApps via the tunnelResolver inside the tunTunnel up, internal sites do not resolve

三个阶段,三种不同的症状。一条「无法连接」的错误信息把它们揉成一团,这也是人们转而去编辑 DNS 服务器字段的原因——而那个问题发生在该字段被读取之前。

为什么 bootstrap 会死锁

常开模式加上封锁生效,意味着不在 VPN 内的流量会被丢弃,包括拥有该 VPN 的应用自己的流量。逃生口是显式的:应用在套接字上调用 protect(),把它绑定到底层网络,从而绕过隧道获准出网。每个 VPN 客户端都必须为自己的传输这么做,否则永远到不了服务器。

现在设想一种部署:唯一知道端点名称的解析器本身就在隧道里。客户端需要隧道来解析名称,又需要名称来构建隧道。这是真正的循环依赖,任何客户端配置都解决不了。这是设计错误,不是程序缺陷。

时序会以更温和的形式暴露同样的依赖。启动时、首次解锁之前,设备可能还没有可用网络;在强制门户上,网络存在但除门户之外全部封锁;从 Wi-Fi 切换到蜂窝网络时,受保护套接字的底层网络发生变化,客户端必须察觉并重新解析。这些都是 bootstrap 事件,每一个都值得刻意测试,而不是在凌晨 3 点才发现。

为什么把答案钉死不是解决办法

诱人的做法是把名称解析一次,把得到的地址粘进数字字段,或者在定制构建里硬编码。它看起来能用,却把名称变成了快照。当运营商移动端点——迁移、anycast 变更、新的区域地址——客户端仍在拨旧的地址,而界面上没有任何东西解释原因。

DNS 记录带 TTL,正是为了让答案可以变化;缓存是按这个生存期定义的,而不是永久。钉死一个答案,就丢掉了让名称优于地址的机制,却保留了使用名称的运维成本。

钉死往往还伴随第二个坏习惯:因为连接现在以不同方式失败,就放松身份校验。服务器身份验证不属于 bootstrap 问题,削弱它等于用 DNS 的不便去换被中间人劫持的风险。

能挺过重启的设计

耐久做法并不光鲜。使用一个正确实现受保护套接字、并在底层网络上解析自己端点的客户端,并给它一个能公开解析、或能通过隧道建立前可达的解析器解析的端点名称。让封锁保持开启;为了 DNS 能用就整体关掉它,等于去掉了这套部署存在的意义。

在端点必须保持私密的地方,提供平台可以使用的显式 bootstrap 路径——一个无需隧道即可到达的解析器,或一条由运营商维护的管理连接——并把这一例外保持得尽可能窄,让它归策略所有,而不是归用户开关所有。Android VpnService 文档是平台在保护与生命周期方面保证什么的参考,而 VpnService.Builder 参考 也确认 DNS 字段接收数字地址。

然后在重启后的新连接上,按顺序验证三个阶段:端点名称解析成功,隧道建立且对端通过认证,私有名称经隧道的解析器解析成功。如果第一阶段失败,后面的一切都不可能工作,修复属于 bootstrap 路径——绝不属于解析器字段。

  • 端点解析发生在应用内、隧道之外、受保护的套接字上。
  • 构建器里的 DNS 字段是给隧道内的应用用的,不是给客户端自己的端点用的。
  • 只能通过隧道到达的解析器是死锁,而不是启动慢。
  • 重启后分别测试端点、认证和私有名称查找。

相关文章