NEVPNManager 在 iOS 上能创建什么(以及不能创建什么)
iOS 的 VPN API 用三个协议类构建一份归应用所有的配置。L2TP、PPTP 和已签名描述文件不属于这一范围。
在 iOS 应用里搭建 VPN 界面时,第一个意外是“设置”应用里的协议列表并不是 API 的列表。Network Extension 框架只允许你的应用创建一份归该应用所有的个人 VPN 配置,而且只接受少数几个协议对象。人们期待的其他一切,从 L2TP 到下载来的描述文件,都在这层范围之外,或者属于 Apple 私有。随之而来的失败通常不是缺陷,而是 API 在拒绝一项从未交给它的工作。
迷思:API 是设置编辑器
人们以为,既然 iOS 能配置 IKEv1、IKEv2、L2TP 和 IPsec 描述文件,应用就能以编程方式创建其中任何一种。它不能。框架向调用方应用恰好暴露一个配置槽位,而这个槽位只接受一小组公开协议类。人们期待的其他一切,从 L2TP 到下载来的描述文件,都在这层范围之外,或者属于 Apple 私有。
随之而来的失败通常不是缺陷。而是 API 在拒绝一项从未交给它的工作,解决办法是围绕 API 所拥有的范围来设计界面。
三个协议对象以及不存在的那些
你的选项是通过 NEVPNProtocolIPSec 使用 IKEv1、通过 NEVPNProtocolIKEv2 使用 IKEv2,以及在你自带数据包隧道扩展时使用 NETunnelProviderProtocol。NEVPNManager 参考文档列出了可接受的协议对象。L2TP 和 PPTP 从不出现在该列表中,因为 Apple 没有为它们发布任何类。尝试其中任何一个都会在保存时报错,或者更糟,得到一份系统悄悄拒绝使用的配置。
桌面代码遵循另一套规则。在 macOS 上,SCNetworkInterfaceCreateWithInterface 会叠加一种接口类型(例如 IPv4 上的 L2TP)并将其附加到网络服务,networksetup -connectpppoeservice 则启动结果。这需要管理员权限和辅助工具,因为共享密钥和密码会进入系统钥匙串。把这套设计照搬到 iOS,什么都编译不过。
传入钥匙串引用,而不是密码
两个 IKE 类都不接受文本形式的密码。passwordReference 和 sharedSecretReference 这两个属性要的是持久的钥匙串引用,你可以通过请求 kSecReturnPersistentRef 从 SecItemCopyMatching 获得。传入条目数据,或把字符串强塞进字段,会导致保存失败或凭据为空的连接。引用的持久性取决于其背后的钥匙串条目,因此条目必须在组装配置之前就存在。在保存的同一事务中创建它,你就消除了一整类顺序缺陷。
证书认证走另一条路:为 PKCS#12 数据块准备 identityData 加 identityDataPassword。把该数据块打包进应用包是一种从示例代码沿袭下来的习惯,而不是发布决策,因为密码随它一起旅行,应用的每一份副本都携带同一身份。
授权与可用性把守着一切
管理器可能根本不存在,原因是配置描述文件。你的应用标识符需要 Network Extension 能力以及个人 VPN 授权,签署构建的描述文件必须同时具备两者。
跳过这一点,共享实例会返回 nil,或者保存失败并报出一个从不提及授权的错误。可用性是另一道门。如果目标版本早于 iOS 8,运行时就没有该符号,所以失败是崩溃或 nil 管理器,而不是没有配置的管理器。在调试对象图之前,先检查运行时和授权。
加载、修改、保存,然后留意按需连接顺序
把生命周期当作加载、修改、保存,永远不要跳过加载。在 loadFromPreferences 完成之前,管理器不持有任何东西,协议对象也无法读取或替换。
第二次保存会替换配置而不是再添加一份,所以半途而废的保存可能损坏原本可用的描述文件。按需规则构成有序数组,而这个顺序就是策略。把“随处连接”规则放在断开规则之前,手动断开后会立刻重连;这看起来像缺陷,其实遵循了文档化的求值顺序。
当用户操作必须取胜时,把断开规则移到前面。应用控制之外的因素同样重要。路由来自隧道或服务器策略,而不是你设置的偏好,数据包隧道提供程序在启动时会报告它们。应用无法列出或编辑由其他应用或配置描述文件安装的描述文件,所以不要承诺一个显示设备上所有 VPN 的界面。