你没法直接问 Android“VPN 连上了吗”。VpnService 没有提供连接状态的 getter,状态栏里的钥匙图标是系统画的,不是你的应用能读到的数据。你能做的是向连接栈提一个更窄的问题:这个应用的流量走的是哪张网络,那张网络是否带 VPN 传输类型。本指南按避免误报的顺序走一遍检查,从设备上的 shell 命令一直到应用里常驻的回调。

先别动应用代码,从 shell 检查设备

先拿到基线。adb shell dumpsys connectivity 会打印系统已知的每一张网络,每个块都会列出它的传输类型,所以带 VPN 的网络会以 VPN 出现在那份列表里。先读默认网络那一块,然后再找其他带相同传输类型的网络。

两段输出回答的是不同问题。“存在一张 VPN 网络”和“你的应用在用它”不是一回事;在带工作资料或第二用户的设备上,你的进程直连互联网时,这两件事可以同时为真。决定你 socket 行为的是活动网络那一行。

问清楚应用正在用哪张网络

代码里的能力判断只有四行。在清单里申请 ACCESS_NETWORK_STATE,然后把活动网络的能力和 VPN 传输类型做比较:

ConnectivityManager cm = (ConnectivityManager)
    context.getSystemService(Context.CONNECTIVITY_SERVICE);
Network active = cm.getActiveNetwork();
NetworkCapabilities caps = cm.getNetworkCapabilities(active);
boolean vpnInUse = caps != null
    && caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN);

答案刻意很窄:应用的默认网络此刻是不是 VPN。它不说明是哪家 VPN、是不是你的、隧道有没有通到对端。把 true 理解成“流量从某条隧道出去”,自己的簿记另外放。一个布尔值悄悄同时表示三件事,正是设置界面在用户明明走 VPN 上网时却报告 VPN 已关闭的原因。

反过来,如果你要判断的是“有没有任何 VPN 网络存在”,就遍历 cm.getAllNetworks() 并逐个检查能力集。这两种答案会在运行按应用 VPN 的设备上分岔:那条 VPN 覆盖了别的应用,却没有覆盖你的应用。多数“应用看不到明明开着的 VPN”的缺陷报告,源头就在这里。

监视变化,而不是轮询

定时轮询对这个问题是错误的形状,因为状态变化是事件。注册一个匹配 VPN 传输类型的请求,让框架在匹配网络出现、消失或变化时通知你:

NetworkRequest request = new NetworkRequest.Builder()
    .addTransportType(NetworkCapabilities.TRANSPORT_VPN)
    .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
    .build();
cm.registerNetworkCallback(request, callback);

VPN 网络永远不带 NET_CAPABILITY_NOT_VPN,所以显式移除它是在记录意图。不写这一句请求照样匹配,但下一个读代码的人得自己推出来。

注册属于需要这次观测的作用域,通常是一个界面或前台服务。在 onStop 或 onDestroy 里注销,不要把回调挂到已经消失的界面上去。没有可见组件的应用不适合当常驻观察者,而配置变更之后崩溃的,往往就是你忘了注销的那些回调。

把三个问题分开

这片领域里的大部分混乱,来自把三种不同的检查当成一种。它们用的 API 不同,其中一个为真时,另外几个仍可能为假。

QuestionHow you answer itWhat a yes means
Did the user grant VPN consent to my app?VpnService.prepare() returns nullConsent exists; the tunnel may still be down
Is the device sending default traffic through a VPN?TRANSPORT_VPN on the active networkSomething is tunneling, not necessarily your app
Did this request arrive through the VPN?Source address seen by the serverProof for that connection, nothing wider

第三行是大家在客户端上试图回答的那一行,而它在客户端上答不了。给 VPN 客户端分配独立的地址池,在服务端读源地址;用户中途切换隧道时,只有这条证据还站得住。

排除误报的验证顺序

仍在流传的旧示例代码会调用 getNetworkInfo(ConnectivityManager.TYPE_VPN)。这个调用已废弃,而且在好几个版本上,即使 VPN 已连接它也返回 null,这就是那么多抄来的代码片段在能用的设备上看起来坏掉的原因。能力检查绕开了这个陷阱,从 Android 5.0 起行为一致。如果你维护的代码库还在调它,请直接替换这个调用,而不是包一层兜底,因为当初给出错误答案的正是那层兜底。

有一种捷径值得警告。把外部服务报告的公开地址拿来和 VPN 出口地址比对,确实能证明一点路径信息,但它给每次检查都加了一次第三方请求,依赖那个服务活着,还可能被缓存或屏蔽。把它当手动诊断,永远不要当应用内部的状态检查。

按这个顺序执行检查,并在第一处与用户描述不一致的地方停下:shell 里的传输列表、活动网络能力、开关隧道时的回调事件,最后是服务端的源地址。如果四项都对得上而应用仍然表现异常,缺陷在你自己的状态机里,而不在 Android 对网络的看法里。

相关文章