如何在代码里判断 Android 上的 VPN 是否已连接
Android 没有查询 VPN 连接状态的 getter。向网络能力发问、监视传输类型的变化,并把应用授权、设备状态和服务端证据分开处理。
你没法直接问 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 不同,其中一个为真时,另外几个仍可能为假。
| Question | How you answer it | What a yes means |
|---|---|---|
| Did the user grant VPN consent to my app? | VpnService.prepare() returns null | Consent exists; the tunnel may still be down |
| Is the device sending default traffic through a VPN? | TRANSPORT_VPN on the active network | Something is tunneling, not necessarily your app |
| Did this request arrive through the VPN? | Source address seen by the server | Proof for that connection, nothing wider |
第三行是大家在客户端上试图回答的那一行,而它在客户端上答不了。给 VPN 客户端分配独立的地址池,在服务端读源地址;用户中途切换隧道时,只有这条证据还站得住。
排除误报的验证顺序
仍在流传的旧示例代码会调用 getNetworkInfo(ConnectivityManager.TYPE_VPN)。这个调用已废弃,而且在好几个版本上,即使 VPN 已连接它也返回 null,这就是那么多抄来的代码片段在能用的设备上看起来坏掉的原因。能力检查绕开了这个陷阱,从 Android 5.0 起行为一致。如果你维护的代码库还在调它,请直接替换这个调用,而不是包一层兜底,因为当初给出错误答案的正是那层兜底。
有一种捷径值得警告。把外部服务报告的公开地址拿来和 VPN 出口地址比对,确实能证明一点路径信息,但它给每次检查都加了一次第三方请求,依赖那个服务活着,还可能被缓存或屏蔽。把它当手动诊断,永远不要当应用内部的状态检查。
按这个顺序执行检查,并在第一处与用户描述不一致的地方停下:shell 里的传输列表、活动网络能力、开关隧道时的回调事件,最后是服务端的源地址。如果四项都对得上而应用仍然表现异常,缺陷在你自己的状态机里,而不在 Android 对网络的看法里。