你在系统设置里有一个已保存的 VPN 服务,想用终端把它启动起来。在 macOS 上,受支持的路径是 scutil --nc,但仅限系统自带网络连接管理器所拥有的服务。菜单栏不是 API:第三方客户端通常有自己的自动化,而管理服务推送的配置可能拒绝本地启动。这是我执行的检查清单:找到确切的服务,读取其状态,启动它,等待状态变化,然后证明流量确实走隧道。在第一个不一致的步骤停下,因为后面每条命令都假设前一条已经成功。

在尝试启动之前先找到服务

从清单而不是你记得的配置名称开始:

scutil --nc help
scutil --nc list

每一行把状态与服务标识符、类型和显示名称配在一起。标识符才是写脚本要用的值,因为显示名称会重复,也可以被修改。Apple 的 scutil 手册记录了网络连接模式,而已安装的 --nc help 才是该 Mac 上子命令的权威清单;不要以为旧指南里的某个标志在你的 macOS 版本里仍然存在。

空清单是一个结果,不是错误。由厂商客户端或管理配置创建的服务可能正在运行,却在这里不可见;也可能可见,但类型是原生控制器无法驱动的。遇到这种情况,请使用厂商工具或配置文档。只是为了有东西可启动而创建一个并行的原生配置,是不划算的交易。

读取当前状态并确认选择器

启动之前先查询服务状态,这样你知道一次成功的启动应该改变什么:

scutil --nc status "Office VPN"

如果两个服务共用同一个显示名称,这个名字就有歧义。改用标识符;命令两者都接受,但只有标识符能保证选中唯一一个服务。在脚本里解析之前,确认状态词是你认识的——通常是 Connected、Disconnected、Connecting 或 Disconnecting。还要确认该配置是否被管理,因为被锁定的配置可能接受命令却仍然忽略它,或者要求确认。

这次状态查询很便宜,所以把它当成自动化里的守卫:只有状态为 Disconnected 时才启动,不要把 Connecting 当成失败。如果服务已经 Connected,就跳过启动,而不是打断一条正常工作的隧道。

启动连接并等待状态稳定

启动请求是异步的。命令在请求被接受时返回,而不是在认证完成或路由安装好之后。运行它,然后用有上限的等待轮询状态;如果服务退回 Disconnected,就退出:

scutil --nc start "Office VPN"
n=0
while [ "$n" -lt 30 ]; do
  state=$(scutil --nc status "Office VPN" | head -n 1)
  [ "$state" = "Connected" ] && break
  [ "$state" = "Disconnected" ] && { echo "start failed"; exit 1; }
  sleep 2
  n=$((n + 1))
done

一直不结束的 Connecting 状态通常指向凭据、MFA 或服务器,而不是 scutil。命令无法提供交互式登录,所以期待令牌或硬件密钥的配置会停在 Connecting,最终回退。在增加重试之前先读状态词;对着认证提示反复循环不可能成功。

不要把 Shell 的退出状态当成就绪。有用的信号是状态字符串,随后是隧道确实在承载流量的证据。一条安静返回的启动命令,仍可能留给你一个永远无法完成认证的连接。

证明隧道承载流量,而不只是显示 Connected

Connected 描述的是连接,不是路由决策。原生 PPP 和 IKEv2 服务会创建 ppp 或 utun 接口并安装路由,而拆分隧道会故意不动默认路由。检查接口和两个地址族:

ifconfig -l
netstat -rn -f inet
netstat -rn -f inet6
scutil --dns

不要猜编号,找到服务创建的接口,然后问路由表某个具体目标走哪个接口:route -n get 10.0.0.10,把它换成内部主机。如果它报告的是物理接口,隧道虽然开着,却没有承载那条流量。检查拆分隧道路由、解析器顺序,以及任何允许隧道接口却不允许该进程的本地防火墙规则。

名称解析是最常见的假阴性。隧道完全健康时,内部名称也可能经由错误的解析器解析,看起来像 VPN 故障,其实不是。scutil --dns 会显示 macOS 将使用的顺序;更换解析器后,用 dscacheutil -flushcache 和 sudo killall -HUP mDNSResponder 清空缓存。

把凭据和权限留在命令行之外

永远不要把密码、预共享密钥或令牌放进参数。参数在进程列表里可见,也会进入 Shell 历史,复制出去的排障记录可能泄露它们。配置自身的凭据处理——无论是 Keychain 条目、系统提示还是客户端的认证流程——才是放秘密的地方。

用 sudo 运行命令不会让交互式登录变成无人值守。它改变的是有效用户,而不是 GUI 提示、硬件令牌或登录钥匙串的可用性。LaunchAgent 可以在用户登录且钥匙串解锁后启动服务;LaunchDaemon 通常够不到用户会话的这些部分。如果组织管理该服务,它的生命周期规则优先于任何本地脚本。

停止服务并确认它保持停止

用启动时的同一个选择器停止,然后回读状态,而不是假定隧道已经消失:

scutil --nc stop "Office VPN"
scutil --nc status "Office VPN"

退回 Disconnected 的停止仍留下一件要检查的事:配置是否设为按需连接,这会绕过你的脚本把它重新拉起。如果同一台 Mac 上还有另一个客户端拥有路由,那些路由可能在原生停止后仍然存在。在断定机器已经离开 VPN 之前,重新做一次路由检查。

对于手动运行的封装脚本,保持固定顺序:清单、状态、启动、轮询、路由测试、停止、验证。有标识符可用时,不要从自由格式输出里解析友好名称,也不要把启动命令的退出当成就绪。连接管理器报告状态;你的证据是状态加上一条指向隧道的路由。

相关文章