Android 应用在 VPN 之外解析域名:绑定检查清单
VPN 开启时,Android 仍可能通过默认网络解析域名。把进程绑定到 VPN 网络,并确认应用实际用了哪个 resolver。
一个 Android 应用无法解析内部域名,而 VPN 客户端自带的浏览器却能访问,同一个构建在另一部手机上又正常。URL 不是变量。变量是应用进程被允许用哪个网络做解析,而 Android 并不保证处于活动状态的 VPN 就是你的 socket 所用的网络。按顺序做完这些检查,当 resolver 快照与你的假设不符时就停下来。
第 1 步:证明应用在哪个网络上
读应用自己的视图,而不是状态栏。从 ConnectivityManager 取出活动网络、它的 LinkProperties、DNS 服务器列表以及私人 DNS 设置。如果列表里有 VPN 的 resolver,说明应用有通往它的路径;如果里面是 Wi-Fi 的 resolver,说明隧道不是该进程的默认网络。
VPN 图标只能说明存在一条隧道。它不能说明应用进程被绑定到了这条隧道,而 Android 也不总是把域名解析交给看起来活动的那个网络。转发到另一台设备的本地 VPN 应用、没有互联网的 Wi-Fi 网络以及厂商定制构建,都可能让应用去问默认 resolver 而不是隧道的服务器。
第 2 步:请求 VPN transport 并绑定进程
当应用必须使用隧道的 resolver 时,请求带 VPN transport 的网络,并在回调里绑定进程,这样代码就不必猜测网络何时出现:
val request = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.build()
connectivityManager.requestNetwork(request, object : NetworkCallback() {
override fun onAvailable(network: Network) {
val bound = connectivityManager.bindProcessToNetwork(network)
Log.i(TAG, "bound to VPN network: " + bound)
}
})
这次调用会改变整个进程的默认网络,因此之后创建的每个 socket 都会使用它。已经存在的 socket 会保留创建时所在的网络,这就是为什么在绑定之前打开的连接池可能继续走旧路径解析。返回值是 boolean,false 表示绑定没有发生;稍等片刻重试,并记录两次尝试。
第 3 步:把绑定限制在真正需要它的范围内
对整个进程做绑定过于粗暴。遥测、更新检查以及第三方 SDK 也会一并走隧道,如果隧道没有互联网,它们就开始失败。对单个请求,使用该网络的 socket factory,其余全部保留设备默认值。有意识地选一种模型:按连接做窄范围绑定,或者在 VPN 消失时显式解绑的进程级绑定。
resolver 的两项功能可以在没有任何报错的情况下让绑定失效。严格模式下的私人 DNS 会把查询发给指定的 DoT 服务器而不是网络 resolver,而自带 DNS-over-HTTPS 客户端的应用会完全绕过系统 resolver。在断定绑定失败之前,先检查这两点。永远到不了 VPN 的 DNS 服务器的查询,不会出现在任何隧道侧的日志里。
第 4 步:不要相信 CONNECTED 约束
WorkManager 里的 NetworkType.CONNECTED 只表示有网络处于连接状态,即使底层的 Wi-Fi 或蜂窝链路已经断了,VPN transport 也满足这个条件。任务照常运行,每个请求都失败,重试策略把它变成一场看起来像调度 bug 的风暴。隧道是一个网络;这个约束从来就不是互联网检查。
检查任务真正需要的能力,例如经过验证的互联网访问,并保持带退避的有界重试策略。如果任务需要私有域名,就在执行任务之前先在选定的网络里解析该域名,并带着清晰的错误快速失败,而不是重试一个不可能成功的请求。
第 5 步:记录证据并重新测试版本范围
记录所选网络的 transport 类型、LinkProperties 里的 DNS 服务器、绑定是否返回 true,以及第一次请求的确切失败。然后在同一绑定下分别运行一个私有域名和一个公共域名。这一对结果能说明应用用的是隧道的 resolver,还是悄悄退回设备默认值,而这正是用户感受到的差别。
在你支持的最旧 Android 版本和一个当前版本上重复。让默认路径看起来正确的 resolver 行为,恰好是设备与版本差异所在,而只在最新测试设备上通过的构建,并没有针对你要发布的版本范围做过验证。adb shell dumpsys connectivity 是快速确认设备报告了哪些网络与能力的方法,在你复现故障时用得上。