在 iOS 上,Network Extension VPN 的大多数首次尝试都可以用两个错误码解释:NEVPNErrorDomain Code=4 和 Code=5。它们都不是服务器端的抱怨。一个出现在应用把设置写入从未从磁盘读取过的对象时,另一个出现在写入本身失败时,最常见的原因是配置文件在仍处于禁用状态时被保存。两者都靠改变四个调用的顺序解决,而不是去动 IKEv2 参数。

先加载,否则保存就没有可比对的对象

管理器持有一份已存储配置文件的副本,你编辑的对象在读取之前并不是那份副本。先调用 loadFromPreferences,在其完成处理程序内编辑,然后再保存。跳过读取,框架就会看到它手上的内容与你正在写入的内容不一致。在 Swift 中每一步的形状都相同,而每一步都属于它自己的闭包:

let mgr = NEVPNManager.shared()
mgr.loadFromPreferences { loadErr in
    guard loadErr == nil else { return }
    let proto = NEVPNProtocolIKEv2()
    proto.serverAddress = "vpn.example.net"
    proto.username = "alice"
    proto.useExtendedAuthentication = true
    mgr.protocolConfiguration = proto
    mgr.isEnabled = true
    mgr.saveToPreferences { saveErr in
        // do not start here; reload, then connect
    }
}

从保存闭包内部启动隧道是同一个错误换了个位置。它在配置文件被重新读取之前运行,因此启动调用看到的是你构建的状态,而不是已持久化的状态。保存闭包以第二次 loadFromPreferences 结束,只有那个完成处理程序才调用 startVPNTunnel。

启用标志是保存的一部分

已存储的配置文件在被打开之前是惰性的。在同一次写入中设置 isEnabled = true,否则配置文件会以禁用状态落到磁盘上,即使“设置”显示出一个看起来完整的配置,startVPNTunnel 也会拒绝。

外部编辑是另一半。如果用户在“设置”中改了什么,或者设备在网络之间移动,在信任仍留在内存里的值之前要重新读取。对象不会替你观察这些变化。过期的副本正是应用写出一个悄悄回滚用户编辑的配置文件的方式。两个相邻字段会同时造成安静的麻烦:on-demand 规则必须是规则对象的数组,而不是空占位符;disconnectOnSleep 应当反映你选定的行为,而不是复制来的默认值。

第三方应用只能使用两种协议

Network Extension 为应用提供 IPsec 和 IKEv2。L2TP 和 PPTP 留在系统内部,所以需要它们的应用拿不到,现实可行的答案是支持 IKEv2 的网关或受管理的配置描述文件。

凭据也遵循同样的模式。把密码放进 Keychain,并传递持久引用而不是字符串。保存时出现空引用,通常说明查找发生在条目写入之前,而不是管理器坏了。

这两者都是设计决策,而不是变通办法。把密码留在自己偏好设置里的应用,选择的是框架不鼓励的路径,它产生的问题读起来像保存错误。

在重写代码之前先排除授权问题

当 loadFromPreferences 完全不回调时,可能的原因是缺少 Network Extension 能力,或描述文件不携带该能力。这种失败会挂起而不是报错,所以能正常工作的代码被重写了几个小时。在你打算发布的已签名构建上检查授权,因为模拟器运行与设备运行可能不同,而真正要紧的目标是设备。打开目标的签名设置,确认该能力正好出现在那个构建上。

缺少授权的描述文件在真机上同样失败,而同一个构建在模拟器中却可能正常加载。在重写调用顺序之前,先在真实设备上测试已签名的归档。

观察状态变化,而不是想当然

观察 NEVPNStatusDidChangeNotification,并在每一步之后记录 manager.connection.status。状态始终不离开 invalid,说明配置文件没有被存储,或者授权缺失。连接到达 connecting 却始终连不上,指向的是提议或凭据,那是另一项调查、另一套证据。

确认配置文件出现在“设置”、“通用”、“VPN”下,并记录每个闭包的错误。这两件事能不作猜测地把顺序问题和权限问题分开。

相关文章