nc -uz 来探测一台 OpenVPN 服务器几乎测不出任何东西,OpenBSD 手册也在其注意事项中说明了这一点:该模式下的 UDP 扫描无论是否有程序监听都会报告成功。退出状态反映的是本地发送,而非远端状态,因此信任它的监控程序只要进程表看起来还像回事,就会一直把已经死掉的服务器标记为健康。替换它要从确定这项检查应当回答哪个问题开始,因为进程、监听者、认证和应用可达性是四种不同的观察。能说出其中之一的探测是有用的,暗示了全部四项的探测则是负担。

UDP 探测能观察到什么、不能观察到什么

nc -uz 会发送一个数据报然后退出,它返回的状态描述的是本地发送。关闭的端口可能以 ICMP port unreachable 回应,但该扫描模式的文档行为并不会把它变成失败,而被过滤的端口根本不会有任何回应。因此对调用方来说,沉默与成功是一回事。

这让探测变成了本地协议栈的测试,而不是服务器的测试。它并不能确认主机可达、端口已绑定、OpenVPN 进程正在运行,也不能确认客户端能够通过认证。TCP 连接测试更强,因为握手会给出确定的答案,但前提是部署确实在 TCP 上监听。把探测改成同一端口号上的 TCP,测的可能是并不存在的监听者;对于只跑 UDP 的服务器,它还会让健康的服务被判为失败。

让探测与配置的传输方式相匹配,先于相信任何一种结果。UDP 部署需要的是另一种信号,而不是另一种协议。

三类调用方,三个不同的问题

就绪与否取决于谁来问。重启崩溃进程的服务管理器需要知道实例已经启动并绑定了监听者。负载均衡器需要知道此刻到来的客户端会被接受。监控系统需要知道来自真实网络的路径仍能把会话送到被允许的资源。

这些并不是同一种检查,而一个探测无法诚实地覆盖全部三类。先选调用方,再选证据。

用当前实例构建探测

OpenVPN 会在实例完成初始化时写入一行启动记录,这行记录是有关本地启动的有用证据。请从当前这次启动中读取它,而不是从被追加的日志文件末尾读取,因为上周的成功读起来和一分钟前的成功一模一样。服务器也可能在任何客户端连接之前就完成初始化,所以这行记录并不能说明认证是否可行。

对于启动检查,要把进程状态与当前日志证据、以及与配置传输方式相符的监听者配合起来。当进程在运行但尚未完成初始化时,检查应当失败,这意味着日志查询必须限定在这次启动和这个单元之内。

systemctl is-active openvpn@server
sudo journalctl -u openvpn@server -b --no-pager | grep "Initialization Sequence Completed"
ss -lunp | grep 1194

三项输出,三项主张:单元处于活动状态、本次启动产生了初始化记录、有东西占着 UDP 端口。它们都没有说客户端可以连接。请在状态页上把它们分开,免得一次事故被压缩成一个绿点。

故意弄坏它,然后观察探测

只有亲眼看过它失败,探测才值得信任。停掉服务器,运行探测,确认你预期的结果。然后再启动它,确认探测在没有其他改动的情况下恢复。

sudo systemctl stop openvpn@server
nc -uz 203.0.113.10 1194; echo "exit=$?"
ss -lunp | grep 1194
sudo systemctl start openvpn@server
ss -lunp | grep 1194

逐行读这个代码块。netcat 命令会打印 exit=0 ,而此时并没有任何绑定,这一行就是全部问题所在。第一个 ss 什么都不打印,重启成功,第二个 ss 又显示了监听者。如果你的探测在服务器已停止时仍然通过,那它测的只是本地协议栈,仅此而已。

反方向的失败也很重要。对健康服务器失败的探测会引发本不需要的重启,而重启循环本身就是一次故障。在把探测接到任何会据此行动的东西上之前,两个方向都要测。

状态行应该说什么

记录每项检查覆盖的是哪条边界:进程、监听者、认证还是应用。一项检查很少能覆盖多于一条,而把这些边界压成一个“up”的状态页,会掩盖事故中真正重要的差别。网络路径方面,请使用带专用身份的真实客户端,让它完成握手和一次无害的允许操作,然后断开。这一步同时演练了控制通道、认证策略和监听者。

把这个身份与真实用户分开,并只授予检查所需的最小权限。权限过宽的就绪探测是为图方便而新增的攻击面。像对待任何账号一样轮换它的凭据,因为停止认证的探测看起来和停止接受客户端的服务器一模一样。

在告警文本中写明边界,好让第一响应者知道该看单元、套接字、证书还是路由。说不清自己证明了什么的探测会被以错误的理由信任,netcat 检查当初也正是这样活下来的。

相关文章