重启应用,向管理器询问连接状态,它会报告已断开,而系统菜单却清楚显示隧道在运行。或者保存调用返回一个什么也不解释的错误,或者共享密钥在赋值几秒后就被判定为缺失。三种症状,一个机制:管理器是对系统存储的视图,而视图在有人加载它之前读出来是空的。

在调试其中任何一项之前,先确定该用哪个 API。系统自带的 IPsec 或 IKEv2 隧道适合内置管理器;系统没有实现的协议需要自带报文处理的隧道提供方,任何字段组合都改变不了这一点。

管理器是对存储的视图

创建或获取管理器,让它加载偏好设置,并在读取状态或修改协议设置之前等待完成处理器结束。如果任务是把已保存的配置列出来,就用全变体调用,而不是把一个实例复用于两个目的。在加载落地之前,状态通常是无效的,而把无效当成关闭,恰恰就是让应用只在重启后看起来出错的 bug。

写入遵循同样的顺序。在一个从未加载过任何东西的管理器上尝试保存,可能会失败且没有有用的消息,因为不存在可让改动作用于其上的已存储身份。先加载、再修改、然后保存,最后重新读取状态。系统连接的是已保存的配置文件,而不是你在内存中编辑的那个对象。

状态变更只在你观察时到达

偏好设置加载完成后,状态会依次经过已断开、正在连接、已连接、正在重新确认和正在断开。用一次读取初始化界面,然后监听变更通知,而不是用定时器轮询。轮询在前台有效、在后台说谎,应用就是这样卡在一个永远不结束的转圈上。

通知只在应用位于前台时到达。任何在后台维持状态的东西在重启后都会显得过期,所以启动时刷新,应用回到前台时再刷新一次。如果隧道由提供方拥有,就通过对应的会话对象驱动它;普通的连接属性并不能建模提供方的生命周期。

密钥是引用,不是字符串

报告共享密钥缺失,很少意味着没人赋值。协议对象要的是引用:一个用于用户密码,一个用于 IPsec 预共享密钥。传 nil,或者传一个来自错误钥匙串条目的引用,得到的抱怨和空槽位一样。错误只说出症状而隐藏原因,所以人们会反复重输同一个密钥。

先把密钥写成一个通用密码条目,然后再读回指向它的持久引用。持久引用与内存中的指针不同,能跨重启存活,而配置文件序列化的正是这种表示。选一个在设备解锁后仍允许访问的可访问性类别,否则必须在锁屏上重连的隧道,即便密钥完全有效也会认证失败。

扩展有自己的钥匙串视图

网络扩展是独立进程,因此应用默认的钥匙串访问到不了它。在两个 target 的 entitlement 里给同一个钥匙串访问组,把该条目只存一次在这个组内,并让每个进程都能读到它。没有这个组,扩展什么也找不到,并报告应用无法复现的凭据失败。

如果提供方是通过它的提供方配置对象来配置的,就不要把密钥再嵌进应用里。两份副本会各自漂移,而漂移表现为凭据错误,而不是重复。widget 或第二个进程报告另一个状态,原因也一样:各自都需要那个组,而且各自都必须在读取之前加载偏好设置。

让 API 与协议匹配

内置管理器承载 IPsec 和 IKEv2 协议对象。它不会因为多加几个字段就变成通用的 SSL VPN 客户端,而不匹配的配置文件可以干净地保存下来却始终连不上。在调查状态之前先确定你属于哪一类,因为状态 bug 和不被支持的协议从外面看起来一模一样。

为下一次重启,检查清单很短:加载偏好设置、读取状态、把变更订阅保持在前台、从共享组重建引用,并确认协议对象与你实现的 API 相符。走一遍这张清单要一分钟;靠猜要花一下午。

相关文章