两件截然不同的工作被归入程序化 VPN 这一名称之下,而其中只有一件在两个移动平台上对普通应用开放。第一件是启用用户已经保存的配置;第二件是在你自己的进程里构建隧道。Android 通过 VpnService 提供第二件,iOS 通过 Network Extension 权限提供第二件,而第一件仍归系统、设备所有者或 MDM 服务器掌管。弄清你真正需要哪一件,通常会让项目变小。

两件同名的工作

启用一份已保存的配置,意味着让一条已有的隧道建立起来:主机、凭据、协议,全都已经配置在用户看得见的地方。构建隧道则意味着你的代码拥有接口、数据包循环和协议。它们在产品待办列表里看起来相似,却几乎不共享任何实现。

两个移动平台都没有把第一件向 SDK 应用开放。一条在用户毫无操作的情况下出现的连接与恶意软件无从区分,因此两个系统都会在任何承载流量的应用前设置一道同意门槛。

Android:一道同意门槛,以及没有配置 API

VpnService 自 Android 4.0 起存在,允许应用创建 tun 接口,然后读写数据包。服务必须用 BIND_VPN_SERVICE 声明,VpnService.prepare() 会生成请求用户允许的系统对话框。平台示例 ToyVpn 是对预期形态最清晰的表述:你的代码拥有接口以及流经它的一切。

它没有做的是在系统 VPN 设置界面里添加条目。没有公开 API 可以做到这一点,而“设置”应用把配置存放在普通应用够不到的存储里。普通应用调用隐藏类所产生的配置,系统永远不会列出,这就是为什么在一个版本上看似能用的代码会在下一个版本上悄悄失效。

受管理的路径另成一路。设备所有者或配置文件所有者可以通过设备策略 API 指定一个始终开启的 VPN 包,企业工具也可以推送一份由用户随后批准的配置。这是设备管理权限,不是应用在安装时请求的许可。如果你的客户是组织,这就是需要规划的路径;如果你的客户是从商店安装的个人,这条路径根本不可用。

iOS:权限就是边界

iOS 对同一件事做了不同切分。VPN 客户端在 Network Extension 框架下实现数据包隧道提供程序或应用代理,相应的权限必须授予开发者账号。通过 NEVPNManager 的个人 VPN 配置适用于更简单的情形,它仍在同一套权限模型内运行。Apple 在 Network Extension 参考文档中记录了整片界面,在界定工作范围之前值得一读。

没有受支持的方式打开 VPN 设置界面、读取用户保存的配置,或启动其中之一。客户端能做的是提供一份配置描述文件让用户安装,然后通过普通网络调用使用由此建立的隧道。按应用隧道由 MDM 服务器依照按需规则分配,而不是由应用在运行时选择。

来自大厂商的 VPN 客户端并不能证明你的账号也能提供同样的东西。那些权限按开发者账号申请并审核,所以进度风险在 Apple 一边,而不在你的迭代计划里。

桌面走得更远,但所有权不变

Windows 是宽松的情形。Add-VpnConnection 从提权的 PowerShell 会话创建电话簿条目,rasdial 则从脚本中连接或断开它。应用也可以通过厂商文档化的命令行来驱动其客户端。创建配置仍然是管理操作,因为该条目位于整机范围的存储中。

macOS 以不同的工具表现出相同的行为。scutil --nc start 和 stop 作用于已经存在的服务,而新建服务属于系统管理任务。在笔记本上拥有进程并不会让你成为这份配置的所有者。

应该改做什么

如果真正的需求是让你的应用访问内部 API,用户已经在运行的隧道就足够了。发送普通的 HTTPS 请求,让系统来选择路径,并停止试图控制隧道。如果需求是只有你的应用流量走隧道,就通过管理通道申请按应用 VPN,或者在应用内对无需隧道即可访问的服务终止 TLS。

隐藏 API 不是值得采用的捷径。它们会在版本之间变化,通不过应用审核,而且它们制造的故障在接手代码的人看来与普通网络故障无异。真正的比较不是 Android 与 iOS,而是拥有隧道与请求平台转发流量,只有前者才是你该构建的。

相关文章