任务本身很清楚:笔记本连上时,笔记本上的某个东西应该发生变化。可正在编辑的配置属于服务器,问题就出在这里。服务器文件里的路径指向的是那台服务器上的文件,无论推送多少内容,都不会改变由谁的进程来执行它。在两个钩子之间做选择,本质上是决定由哪台机器承担这项工作,并接受随之而来的限制。

两台机器,先回答两个问题

先问状态变化必须发生在哪里,再问哪个事件与之对应。如果变化是按用户授权或按客户端的配置决策,就该放在用户目录已知的那一端:服务器。如果变化是一条路由、一个 resolver 设置或端点上的本地辅助工具,就该放在端点上,而且只有端点才能做到。

把这两个问题的顺序颠倒过来,就成了很多人试图把批处理文件推送到 Windows 笔记本的原因。服务器知道谁连上了;笔记本知道自己那套网络栈长什么样。双方都不掌握对方的信息,这种尝试往往悄无声息地失败,而不是给出明确的错误。

在这两个问题之前,还值得问第三个:平台到底提供不提供你想要的钩子?自己生成配置的设备未必提供同名事件,导入 .ovpn 文件的桌面客户端也未必就是当初写这个文件时针对的那个客户端。先确认平台,就能避免围绕一个根本不存在的钩子做设计。它还会告诉你哪些文档真正适用于眼前这台机器。

服务端方案:每次连接一个事件

client-connect 脚本会在已认证客户端连接时,在服务器的进程上下文中运行,并收到有文档记录的连接信息,例如 common name 和被分配的地址。要按客户端写配置片段、执行策略决定或记录服务器自己拥有的状态,这里是合适的位置。

人们常忽略的区别,是这个钩子与服务器 up 钩子之间的差异。up 钩子在服务器打开自己的隧道设备时触发,在多客户端的服务器上,这只会在守护进程启动时发生一次。它不是每次登录都会触发的事件,放在那里的代码是在服务启动时运行,而不是在用户连接时运行。

在带 Web 界面的设备上,请使用受支持的扩展机制,而不要直接编辑自动生成的守护进程文件。生成的文件会在下一次保存时被重写,而这次重写会在最糟糕的时刻悄悄抹掉你的改动。把扩展文本另存一份在设备之外,这样固件更新也不会连意图一起丢掉。

客户端方案:必须在本机完成的工作

社区版客户端可以在隧道设备建立时运行本地脚本,也可以在路由安装完成后再运行一次。这是两个不同的时刻,选错就会造成经典的竞态:辅助工具在它所依赖的路由还不存在时就已经启动。

辅助工具必须存在于那台客户端上,必须能被运行客户端的账户读取并执行,还必须被显式的 script-security 设置允许。以管理员身份启动客户端时才有效的脚本,对开机前运行的服务或另一个用户的会话没有任何说明。

命名同样重要。up 钩子对应设备创建,route-up 对应路由安装,但需要私有网络可达的任务应当去验证可达性,而不是假定设备创建就意味着每条路由、每条 resolver 记录和每个应用都已就绪。

为什么推送可执行代码不在选项里

通过 push 下发的选项是被刻意限制的,而执行脚本的选项属于服务器无权下发的类别。这道限制之所以存在,是因为否则被入侵或配置错误的服务器就能在每一台曾经连接过的客户端上执行任意命令。

在本地启用脚本执行并不能改变这一点。你的 script-security 设置允许的是你自己选择的本地脚本,它并不授权服务器去提供新的脚本。

如果目标只是安装 VPN 路由,就使用受支持的路由配置,而不是发明一套远程执行方案。如果目标确实需要本地代码,请通过组织的端点管理工具分发,并像审查其他任何以用户权限运行的软件一样审查它。

信任任何一个钩子之前要验证什么

为每个钩子写下四件事:机器、事件名、以哪个账户运行,以及它应当产生的状态变化。然后测试连接、重连和清理路径,因为在重连时泄漏状态的钩子比从未运行过的钩子更糟。

要在真正执行它的账户下、在它真正执行的那一刻测试。openvpn(8) 手册记录了两类钩子和 push 的限制;你所部署的平台则记录了它是否提供这些钩子。在交互式管理员终端里成功的脚本只是一个假设,而不是验证。

相关文章