OpenVPN 的 hook 脚本能使用它刚建好的隧道吗?
OpenVPN 在自己的启动流程内运行 hook,因此需要隧道的脚本会等待一条正在等待它的隧道。把工作拆出去,并按事件选择 hook。
hook 脚本能否到达隧道另一端,取决于三个条件:它挂在哪个事件上、脚本运行时 OpenVPN 是否仍在启动序列内,以及工作是否已从该序列中拆出。多数脚本败在第三条上。加个 sleep 是把生命周期问题当成时序问题,它只恰好有效到足以误导人。
先定事件,再定延迟工作的机制,最后定延迟工作运行的位置。这三个决定相互独立,混在一起正是两行 hook 变成一下午的原因。
为什么 hook 要等一条等它的隧道
OpenVPN在搬运数据包的同一个进程内同步调用 hook。up 在 tun 或 tap 设备已存在、但尚未添加任何路由时触发,route-up 在路由之后触发;这两个时刻都不保证对端会应答。只要任一 hook 的脚本还在运行,守护进程就不转发,于是期望流量穿过隧道的命令,会等待一条正在等待该命令的隧道。这种循环等待正是手动执行能通过、hook 却超时的原因。
显而易见的补丁,也就是睡几秒,偶尔奏效,是因为远端自己起来了,而不是 sleep 修好了什么。查询就绪状态的助手是另一种答案:hook 返回,转发开始,助手在真正就位后拿到需要的东西。
按事件选 hook,而不是图方便
把 up 绑到只需要设备的工作,route-up 绑到需要路由的工作,把 client-connect 和 client-disconnect 留给服务器端按客户端处理的任务。断开时的清理必须能承受运行两次,因为在持久接口上重连并不遵循与完整重启相同的顺序。在服务器上,client-connect 是在客户端认证时触发,而不是守护进程启动时,移动逻辑前值得确认。若工作属于服务生命周期而非单次连接,systemd 单元比任何 hook 都更合适。
hook 命令由守护进程直接执行,中间没有 shell,因此 up 行里的 ifconfig_remote 会以字面文本到达。把命令包进显式 shell,或把逻辑放进读取环境的脚本。ipchange 是一条会添加自身参数的指令:它把地址和端口交给命令,所以为“无参数”写的脚本会看到两个意料之外的参数。
拆出工作的三种方式
第一种是在 hook 内部把助手放到后台:用 setsid 或 systemd-run --scope 启动它,关闭继承的描述符,并立即以零退出。第二种是让 hook 写一个小状态文件,由 systemd 定时器或 path 单元对它采取行动,这使 hook 保持简单,并把重试策略放在有日志的地方。第三种是完全舍弃 hook,把工作移到一个排在 VPN 服务之后的单元,当工作根本不是按连接时,这是正确答案。三者共享一条规则:hook 必须在需要隧道之前返回。
无论选哪种,都要让助手具备幂等性且可观察。两次重叠的调用不应把同一个不可逆操作执行两遍,失败的助手必须留下人能读的东西。
把沙箱故障与生命周期故障分开
当脚本从 root 提示符运行、作为服务却失败时,原因通常是沙箱而不是网络。限制性的 ProtectHome、只读的 ProtectSystem,或不存在的工作目录都可能让脚本或其数据不可达,而在 SELinux 主机上,文件还需要允许执行的上下文。用 systemctl cat openvpn-server@server 读单元、用 journalctl -u openvpn-server@server -n 80 读日志,然后找权限或上下文错误,而不是连接超时。禁用沙箱应是最后一步,而不是第一步。
生命周期故障看起来不同:hook 启动、助手运行,套接字就是还没出现。从 hook 自身写带时间戳的一行,断开再重连,按顺序读事件。当助手确实需要隧道时,让它轮询就绪状态,而不是固定睡一段时间。
三个字段能让下一次报告有用:哪个 hook 跑了、用了哪个账户、停在哪里。没有它们,之后每次调查都要从头开始。