为什么你的 Android 应用无法添加系统 VPN 配置文件
VpnService 建立的是归你的应用所有的隧道;它无法新增或修改手机上的 PPTP、L2TP 或 IPsec 条目。看清边界,选对路径。
你在设置里点开 VPN 那一行,看到一个别人装好的配置,然后开始找那个能新增配置的调用。没有这样的调用。应用可以通过 VpnService 打开自己的隧道,但那份列表里的条目属于平台,也属于当初创建它们的客户端。清楚边界在哪里,能省下一周追查隐藏类的时间。
先从设置界面显示的东西看起
VPN 列表是系统或厂商客户端安装的配置登记表:PPTP、L2TP/IPsec、IKEv2,以及 OEM 应用或 MDM 推送进来的行。每一行都带着服务器地址、凭据,以及一份跑在你进程之外的协议实现。用户在那里点连接时,拉起隧道的是平台客户端,不是你的代码。这就是这一行能熬过重启、而卸载你的应用永远删不掉它的原因。
从用户角度看,这份列表像一层 API 表面,仿佛添加一个 VPN 只差一个 intent。并不是。唯一能碰到它的公开入口是导航 intent,而所有写入它的东西都属于平台或有特权的组件。
实际后果:如果一份支持文档让用户去设置里添加配置,那你的应用根本不在这个流程里。
VpnService 实际交到你手上的是什么
VpnService 和 VpnService.Builder 描述的是归你的应用所有的一条隧道。你添加地址、路由、DNS 服务器和 MTU,用 addAllowedApplication() 或 addDisallowedApplication() 划定流量范围,然后 establish() 返回一个指向 TUN 接口的 ParcelFileDescriptor。从那一刻起,应用自己读写 IP 包,也就是说协议由你实现或内嵌:OpenVPN、WireGuard,或者自定义传输。
这条链路里没有任何一处会讲 PPTP、L2TP/IPsec 或 IKEv2。Builder 没有写着使用系统 L2TP 客户端的开关,也没有任何回调让你去驱动一份平台已经拥有的配置。这才是真正的边界,而且它是设计上的边界,不是权限的缺口。
所以应用完全可以在 Android 上做一个 VPN 客户端。它只是没法成为手机自带客户端的管家。
同意弹窗不是装饰
因为你的隧道能捕获其他应用的流量,系统要求用户明确授权。prepare() 返回一个必须由你启动的 Intent,授权绑定在你的包上,隧道运行期间状态栏会一直显示指示图标。第二个 VPN 起来时第一个会被停用,因为 Android 同时只跑一个活动 VPN。
这条只能有一个的规则在测试时最容易被踩。你在公司客户端连着的时候拉起自己的隧道,然后判定代码坏了,其实只是平台从另一个客户端切走了。授权之后可以被撤销,而在缺少同意界面的设备上 prepare() 可能返回 null,所以 establish() 仍要处理失败。
隐藏 API 与设置 intent
旧答案会指向内置客户端使用的 com.android.internal 类和 AIDL 接口。它们不在 SDK 里,跨版本随时会变,应用商店也把这种访问视为风险。给设备 root 同样不能让它们稳定,因为 root 和隐藏 API 访问是两件不相关的事。
受支持的导航辅助是 Settings.ACTION_VPN_SETTINGS,它的值是 android.net.vpn.SETTINGS。启动之前先解析它,因为有些 OEM 固件并不带这个 Activity;解析失败就退回到书面步骤。它只把用户带到那个界面,什么也不配置。
设备所有者与 MDM 路径才是人们通常说的例外。DevicePolicyManager 可以指定一个常开 VPN 包,受管配置可以为支持它们的客户端预置设置,但那仍然不是公开 SDK 在写 PPTP 配置。
从用户这侧走一遍决策
如果你的产品需要承载流量,就按 VpnService 加一套真实协议栈来规划,并为授权流程和单 VPN 规则留出时间。如果目标只是连上已有的公司配置,你的工作是引导:深链到设置界面、把字段写进文档,让平台客户端去做隧道。如果应用只需要访问一个私有端点,它根本不需要自己的隧道。
在写代码之前先把这个决定写下来,因为三条路走过第一个迭代后几乎没有共同点。
开工前最后确认一句:需求说的是我们的应用连接,还是设备连接?前者是 VpnService,后者是你造不出来的配置。把这句话说准,比任何重构都便宜。