在移动端VPN开发中,always-on(永久在线)是一个绕不开的话题。普通VPN应用在用户锁屏、切换网络或者应用被系统杀死后,隧道往往就会中断,数据流量可能回落到明文传输,带来安全隐患。安卓从7.0版本开始引入了VPN always-on能力,允许系统在VPN断开后自动重启VPN服务,配合lockdown模式甚至可以做到无VPN即无网络。本文将从原理、用户端配置和开发者代码实现三个层面,完整讲解如何落地移动端VPN always-on方案。

一、always-on的工作原理与两种模式
安卓的always-on机制由系统ConnectivityService统一管理,核心思想是把VPN提升为一种由系统托管的服务。当用户在设置中开启always-on后,系统会记录该VPN应用的包名,一旦检测到VPN断开,就会自动调用该应用的VpnService入口重新拉起连接,整个过程不依赖应用自身的保活逻辑,因此比应用层的心跳重连更可靠,也不会被厂商的后台清理策略误杀。
always-on其实包含两个层次。第一层是基础的Always-on:VPN断开后系统自动重连,但在重连的间隙里,应用流量仍然可能直接走物理网络,存在短暂的泄漏窗口。第二层是Block connections without VPN(国内常称锁死模式或lockdown):开启后,任何不经过VPN的连接都会被系统直接阻断,也就是没有VPN就没有网络。对安全要求高的场景,比如企业MDM管理、金融类App,推荐同时开启这两项。
两者的区别可以用一张表来概括:
| 模式 | 断开时行为 | 流量泄漏风险 | 适用场景 |
|---|---|---|---|
| 普通VPN | 保持断开,需手动重连 | 高 | 普通个人用户 |
| Always-on | 系统自动重连 | 低(重连间隙可能泄漏) | 一般安全需求 |
| Always-on + Block without VPN | 自动重连且阻断非VPN流量 | 无 | 企业、金融等高安全场景 |
二、用户端如何开启always-on
对于普通用户而言,开启always-on不需要root,也不需要额外工具,完全依赖系统自带的设置入口。以原生安卓为例,操作路径一般是:打开系统设置,进入网络和互联网,选择VPN,点击目标VPN配置右侧的齿轮图标,在详情页中打开始终开启的VPN开关,再按需打开在没有VPN的情况下阻止连接。首次开启时系统会弹出确认框,提示开启后所有流量都会经过该VPN,确认即可。
不同厂商的ROM入口略有差异。部分国产系统的VPN入口藏在更多连接方式或连接与共享里,个别系统还会要求先设置屏幕锁(PIN码或图案),这是系统的安全前提,因为VPN配置本身存储了敏感凭据。另外要注意,always-on对VPN类型有要求:系统只支持基于IPsec IKEv2和部分L2TP/PPTP的传统配置开启always-on,第三方App提供的VPN则需要该App正确声明并实现了VpnService,用户才能在应用详情页看到对应的开关。
开启后有一个实用的验证方法:手动停止VPN应用或断开连接,观察通知栏,系统会在几秒内自动弹出VPN已重新连接的提示;如果开启了lockdown,可以尝试关闭VPN后打开浏览器,会发现完全无法加载页面,说明阻断生效。
三、开发者实现:基于VpnService构建always-on应用
对开发者来说,应用侧要做的核心工作是基于VpnService实现VPN隧道,并在清单文件中正确声明权限,系统才能在always-on状态下拉起你的服务。首先在AndroidManifest.xml中声明:
<service
android:name=".MyVpnService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:exported="false">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>BIND_VPN_SERVICE权限是关键,它保证只有系统进程能够绑定这个服务,这正是系统自动重启VPN的安全基础。intent-filter中的action声明让系统识别这是一个VPN服务入口。
接下来是服务端的连接建立逻辑。系统拉起服务后,通过onStartCommand接收意图,典型流程如下:
public class MyVpnService extends VpnService {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent != null ? intent.getAction() : null;
if (VpnService.ACTION_SERVICE.equals(action)) {
// 系统主动建立的连接,无需再次请求授权
establishVpn();
} else {
// 用户手动启动,先请求VPN授权
Intent i = VpnService.prepare(this);
if (i != null) {
startActivityForResultPending(i);
} else {
establishVpn();
}
}
return START_STICKY;
}
private void establishVpn() {
Builder builder = new Builder()
.setSession("MyAlwaysOnVpn")
.addAddress("10.0.0.2", 32)
.addRoute("0.0.0.0", 0)
.addDnsServer("8.8.8.8")
.setMtu(1500);
ParcelFileDescriptor fd = builder.establish();
// 之后将fd交给native层或转发线程处理数据包
}
}这里有一个非常重要的细节:当系统因always-on自动重启VPN时,会以ACTION_SERVICE拉起服务,此时授权已经由系统记录,应用不能再弹出授权对话框,否则会抛异常。而用户手动启动时才需要走prepare()流程。区分这两种场景是always-on应用最容易踩的坑。
还要注意Builder中setBlocking(true)与setMtu()的配合,以及安卓10之后对setHttpProxy的支持变化。如果应用希望被企业MDM批量配置always-on,还可以通过DevicePolicyManager的setAlwaysOnVpnPackage接口由管理员强制开启,用户无法关闭,这在企业设备管理场景中非常常见。
四、常见问题与排查建议
第一个常见问题是always-on开关置灰或找不到。这通常是应用没有声明BIND_VPN_SERVICE权限,或者服务没有响应onRevoke回调导致系统判定异常。务必在服务中实现onRevoke(),在其中关闭文件描述符并停止隧道,否则用户手动断开VPN时会出现残留连接。
第二个问题是lockdown模式下设备完全断网。这往往是因为VPN服务器不可达,或应用启动崩溃导致系统反复拉起失败。排查时可以先用adb shell dumpsys connectivity查看VPN状态,确认服务是否被系统成功绑定。第三个问题是重启手机后VPN没有自动恢复,这需要确认应用没有被用户或安全中心设置为禁止自启,同时服务要以前台服务形式运行并声明对应的Service启动方式。
总结来说,移动端VPN always-on = 系统级托管重连 + 可选的强制锁死模式,开发者只要正确实现VpnService的生命周期回调、区分系统拉起与用户启动两种意图,并处理好授权流程,就能获得远比应用层保活更稳定的连接体验。对安全敏感的业务,强烈建议引导用户同时开启Block connections without VPN,从系统层面堵住流量泄漏的最后一道口子。
VPN always-onAndroid VPNService永久VPN修改时间:2026-09-02 00:42:45