编写 VPN 客户端脚本:先选层次,再谈密码
厂商 CLI、rasdial、应用 intent 或 UI 自动化各有各的失败方式。选好层次,别让密码进 argv,等状态而不是等退出码。
脚本把 VPN 客户端起在作者随手选的那一层,而这一层往往不是任务真正需要的那层。厂商命令行、系统自带拨号器和移动应用的 intent,在连接生命周期的不同时点返回。它们对密码的处理也不同,而这个差别决定了密码是否对机器上每个用户可见。写第一行脚本之前,先把层次选好。
第 1 层:厂商命令行
多数桌面客户端提供命令行,接受配置名,还常常接受密码。思科的老客户端接受 connect 动词并带上用户名和密码参数;macOS 通过 networksetup 暴露内置服务;NetworkManager 则响应 nmcli。这些调用在命令结束时返回,并不等于隧道已经可用。请查阅厂商针对你所部署版本的文档,因为这类接口会变,有些会被整个移除。
nmcli connection up "Work VPN"
networksetup -connectpppoeservice "Work VPN"
好处是厂商了解自家的配置库,所以调用使用的设置与图形界面一致。坏处是每家厂商都自创动词,为一个客户端写的脚本换不到另一个上。当客户端和版本都固定时,可以把这一层当作最短路径。
第 2 层:系统自带服务
在 Windows 上,拨号器属于操作系统,它面向电话簿条目,而不是厂商的配置库。这使它更容易跨客户端移植,也更容易从服务账户里写脚本。这也意味着连接可能存在于一处而不在另一处,让查错清单的人摸不着头脑。
rasdial "Work VPN" user pass
Get-VpnConnection -Name "Work VPN" | Select-Object ConnectionStatus
需要创建或删除条目、而不是拨号时,请用 PowerShell 这一面。WMI 与 cmdlet 两层以不同名字暴露同一批对象,而删除是与断开分开的操作。只负责拨号的脚本在迁移后会留下过期条目。在承诺清理会发生之前,先确认你的自动化写进哪个库。
Remove-VpnConnection -Name "Old VPN" -Force
Get-VpnConnection
在 macOS 上,对应物是网络服务列表:已有服务按名称连接,而不是临时创建。
凭据在每一层都会泄漏,所以要选通道
凡是在命令行上传的东西,都会出现在每个本地用户的进程表里,通常还会留在 shell 历史中。OpenVPN 支持的替代做法是 从文件读取密码 并配合 --askpass,文件本身也需要与信任程度相匹配的权限。命名管道避免了把密码写进磁盘,但任何能打开它的进程都能往里写。在 Windows 上,把密码放进凭据管理器,或放进只有服务账户可读的文件,绝不要作为字面量写进批处理文件。
openvpn --config /etc/openvpn/work.conf --askpass /etc/openvpn/work.secret --auth-nocache --daemon
ls -l /etc/openvpn/work.secret
同样的规则适用于令牌和一次性验证码:如果自动化需要它们,就需要一条不是命令行参数的通道。换机制时要轮换密码,因为旧值已经在某处日志里躺过。能跑但会泄漏的脚本,不算能跑的脚本。
等状态,然后只由一个所有者监督
这些调用都在隧道就绪之前返回,所以要轮询客户端暴露的状态,或在隧道内探测目标并设置超时。连接调用之后立刻复制文件或打开数据库连接,就是竞态,它会间歇性失败,看起来像网络故障。
ip link show tun0
nmcli -t -f GENERAL.STATE connection show "Work VPN"
curl -sS --max-time 5 https://app.internal.example/health -o /dev/null
断开则正好相反:进程可能在路由消失之前就退出。客户端如果提供钩子,就用钩子,例如 OpenVPN的 up 和 down 脚本,或 systemd 单元的 ExecStartPost,因为它们运行在你真正关心的时间点。钩子以调用者的权限运行,所以重启网络的钩子需要明确规则,而不是想当然。连接的生命周期里只保留一个所有者。
当两个监管者同时启停同一配置,结果就是重连循环,读起来像服务商故障。当层次有文档、密码走非 argv 通道、等待条件是带超时的状态检查、且只有一个进程拥有生命周期时,runbook 才算写完。