Android 应用拒绝在 VPN 下运行:由哪一层决定
应用拒绝 VPN 可能源于本地传输检查、服务端地址规则或应用自身策略。先分清这三种情况,再决定是否更换出口。
当 Android 应用在隧道建立的那一刻开始罢工时,可能有三类不同的原因,它们造成的故障看起来很像。应用可能读取手机自身的网络状态,在本地直接拒绝。服务器可能对你来源的地址做分类,然后拒绝它。或者,这次拒绝与 VPN 根本无关。下面的顺序先收集证据,再动手改动,因为更换出口是最昂贵的一步,而且如果检查发生在本地,它最不可能起作用。
第 1 步:弄清应用在什么时候拒绝
时序是第一个判别依据。清空日志缓冲区,复现故障,然后看看应用在报错之前做了什么:
adb logcat -c
# reproduce the failure in the app
adb logcat -d | grep -iE 'vpn|network|connect|http' | tail -60
如果应用在发出任何出站请求之前就拒绝,你面对的是一个本地判断。如果它先连接、发出了内容,然后才显示错误,那么拒绝来自另一侧。如果关掉隧道后应用以同样的方式失败,VPN 就从来不是变量。
把这段时序当作证据,而不是结论。应用可能回放缓存中的响应,后台请求也可能在你查看之前已经成功或失败。记录最后一次成功请求和报错的时间戳,而不只是消息出现的那一刻。
第 2 步:读取手机自己对网络的看法
Android 把 VPN 状态作为一种网络能力暴露出来,因此应用不需要商业服务商清单,就能察觉隧道处于活动状态。它向连接服务询问当前网络是什么,而回答里包含一个 VPN 传输位:
val cm = context.getSystemService(ConnectivityManager::class.java)
val caps = cm.getNetworkCapabilities(cm.activeNetwork)
val onVpn = caps?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true
这个检查在本地完成、代价很低,它解释了整整一类“这个应用在 VPN 下不能用”的报告,而无需任何远端分类。应用能看到什么,仍取决于它自身的网络上下文,所以不要以为每个应用都会检查设备上的每个配置文件。
在工作站上,你可以用 adb shell dumpsys connectivity 并查找 VPN 网络条目,在测试设备上看到同样的状态。把隧道开启时的运行与隧道关闭时的运行作对比,并保留两份输出。
第 3 步:读取服务器实际看到的东西
另一个观察者是服务端。它看到到达的来源地址,并可能依据地址、地址背后的托管网络、地区、运营商,或与账号和设备绑定的信号来采取行动。这些都不是唯一身份,而共享地址让附带封禁变得正常:一个受限地址可能连带影响同一转换之后的无关用户。
要用应用所用的那条路径获取地址,而不是笔记本上的浏览器。在手机上,向地址回显服务发一个普通请求,就能看到远端收到的是什么;再把它与通过蜂窝网络的请求作比较,你就知道地址到底有没有变。
如果应用在蜂窝网络下正常、在相同账号下通过隧道失败,拒绝就在远端,或在应用自己的策略里。如果两种情况都失败,VPN 就不是差异所在,你应该停止测试出口。
第 4 步:把证据归入三种结论之一
三种组合几乎覆盖所有报告。隧道活动时没有任何出站尝试就拒绝,指向本地传输检查。请求发出后带着错误返回、某个特定主机超时,或出现地区提示,指向远端。在没有隧道的情况下于蜂窝网络重现的失败,指向账号、应用版本或服务自身的策略,换任何 VPN 都不会改变它。
把结论和产生它的证据一起写下来。这一步的价值在于,它终止了那种循环:有人为了修复一个在手机上运行的检查,反复更换了四次出口节点。
它也让说法保持诚实。自建服务器改变的是出口及其互联网出口;它不会让 Android 忘记 VPN 网络处于活动状态,也不保证能被做地址分类的服务接受。本地过滤器在你自有的 VPN 网络上触发的容易程度,与商业网络并无不同。
第 5 步:走受支持的路径并验证
从产品文档化的策略和它的支持负责人开始。发送客户端版本、准确的错误消息和一段较窄的故障时间窗;不要为了证明错误存在就提交完整的流量抓包或凭据。
如果服务允许直接访问、设备策略也允许,按应用排除是受支持的机制,它有意把该应用从隧道中移出,而不是隐藏隧道。该应用的流量随后走自己的路径,这是一个需要有人负责的策略决定。在无 VPN 即阻断的配置下,被排除的应用可能直接失去连接,所以在承诺它可用之前,先测试这种组合。
任何改动之后,重复第一步的时序测试,确认应用可用,并确认其他每个应用仍在使用隧道。然后恢复之前的配置,检查故障是否重现,因为一个你无法复现的修复,只是你还会再遇到的巧合。