Mac 开机时的 VPN:launchd 监管的是作业,不是隧道
通过 lint 的 plist 并不能证明凭据或就绪状态。让作业匹配其会话,让客户端保持在前台,并在该上下文中测试。
登录时作业会运行,日志显示客户端正在启动,但隧道始终没有建立。或者建立后几秒就消失了。两种情况下列表文件本身可能都没问题。launchd 在接受作业时做出了两个承诺:它会监管一个进程,并且该进程会持续运行。VPN 两者都需要,而一份正确的 XML 文件一个都提供不了。
有效的属性列表不等于可用的服务
plutil -lint 检查的是结构。它无法告诉你二进制文件是否存在、这些参数是否受该客户端版本支持,或者作业是否在凭据可用的上下文中启动。这些都是语义问题,而大多数看起来像 launchd 故障的情况正是由此产生。
plutil -lint /Library/LaunchDaemons/com.example.vpn.plist
launchctl print system/com.example.vpn
请阅读第二条命令的输出,而不是假定作业已经加载。它会报告最后的退出状态以及服务是否在运行,从而在改动配置文件之前区分“从未启动”和“启动后死亡”。
代理继承用户会话,守护进程则不会
Apple 的 launchd 文档把按用户的代理与系统守护进程区分开来。代理在已登录用户的上下文中运行,可以使用该用户的钥匙串和环境。守护进程没有该会话,把作业移入系统目录也不会重建用户的交互式凭据。
决定设计方案的正是这一区别。如果连接需要有人登录或批准 MFA 提示,受支持的安排是 登录后运行的用户会话作业。如果必须在无人登录的情况下工作,客户端就需要一个经过批准、自带续期和吊销流程的无人值守身份。选错行会得到一个能干净启动、却在第一步认证就失败的作业。
不要用在参数数组里放密码或关闭证书校验的方式来掩盖这一区别。两者都会把认证要求变成可读文件里的永久秘密,而后者意味着隧道会认证任何做出响应的对象。
只有进程留在前台,监管才有效
自行守护化的客户端会立刻把控制权交还给 launchd,于是监管者盯着的已是一个退出的进程。隧道可能还在,但 launchd 认为作业失败,其重启策略会做出你并不想要的动作。
使用客户端有文档记录的前台模式或服务模式。如果产品自带受支持的启动功能,该功能已经知道如何留在前台以及如何交接给网络栈;自定义作业是另一套设计,而不是对它的改进。
给作业一个唯一的标签、预期的可执行文件和经过斟酌的参数数组。数组不是 shell 命令行,所以管道和重定向在那里不起作用,引号的行为也不同。在管理员终端里能跑通的命令,充其量只是初步检查。
凭据必须能在无提示的情况下工作
无人值守运行只是转移凭据问题,而不是消除它。必须有人拥有该身份、决定它的有效期,并记录它如何被替换。没有到期监控的证书,会变成日历上有日期的故障。
把秘密挡在参数之外,也挡在作业的标准输出之外。如果客户端支持从受保护的存储中读取凭据,就走那条路径,并确认作业的执行上下文能够访问该存储。以 root 运行的守护进程通常读不到登录钥匙串中的条目,这类细节只有在系统上下文中才会暴露。
先定义何为就绪,再在那里测试
启动成功只证明进程开始了。它不能证明网络已就绪、网关接受了身份,或私有应用可以访问。如果还有其他东西依赖隧道,它需要一个反映连接状态的就绪信号,而不是作业的启动时间。
重试应当有上限,并应区分网络缺失和凭据被拒。每次失败都重启无法修复过期证书,还会产生满是相同尝试的日志,把真正的那一次掩盖掉。
要在它真正会运行的上下文中验收作业:开机时无网络、正常登录、从睡眠唤醒以及受控停止。然后把作业标签、客户端版本和配置文件修订一起记录下来,这样下一个人继承的就是可归因的服务,而不是一个曾经能用的文件。