隧道已经断开,但机器仍表现得像它还开着:一次查询一直挂起到超时,一条路由指向早已不存在的接口。这就是没有运行断开辅助脚本就死掉的 openconnect 进程留下的特征。它创建的网络状态仍在,系统上也没有别的东西知道如何清除它。

按状态创建的先后顺序检查机器:进程、链路、路由、解析器。每一步都能告诉你客户端是真的消失了,还是只是从你的终端上脱离,而这个区别决定你要不要修复任何东西。

从进程开始,而不是从配置开始

在碰路由之前,先打开终端看看还有什么在运行。pgrep -a openconnect会列出每个实例及其参数,从而显示 pid 文件和传给它的脚本。一个你没预料到的进程,比任何日志都能更快解释这条幽灵隧道。

如果什么都没在运行,就看链路。ip link show tun0会告诉你接口是否还在,而ip route show dev tun0会列出仍绑定在它上面的路由。然后用下面的命令检查解析器resolvectl status。在改动之前,把三个输出都记下来,因为日后就是靠这份记录区分旧记录和新记录。

消失的接口通常会带走它的路由。链路消失后仍然存在的路由,通常意味着进程还活着,或者第二个客户端占用了这个设备。

撤销这些改动的是辅助脚本

openconnect 自己不会删除接口、路由或解析器记录。它会调用一个外部辅助脚本,/etc/vpnc/vpnc-script在上游构建中,以及/usr/share/vpnc-scripts/vpnc-script在 Debian 和 Ubuntu 软件包里,使用的选项是-s。装起隧道的是同一个脚本,拆掉它的也是这个脚本。

如果进程退出时没有调用那个辅助脚本,状态就会留下。OpenConnect 手册明确指出没有脚本时路由和名称服务无法正常工作,并把辅助脚本描述为退出时拆除接口状态的组件。正因如此,用下面的选项启动客户端时,路径才重要,使用的选项是-s。

哪条信号能走到那条路径,在不同版本之间变了。7.06 的手册把 SIGTERM 当作跳过脚本的直接退出。当前构建把 INT 和 TERM 记录为干净停止,此时会话会注销并运行辅助脚本。请阅读你手上那份二进制文件的 SIGNALS 一节,而不是假定两种行为中的任何一种。

man openconnect | sed -n '/^SIGNALS/,/^LIMITATIONS/p'

发送你的构建所记录的信号

知道信号之后,按 pid 停止进程,而不是按模式匹配。

pgrep -a openconnect
sudo kill -INT "$(cat /var/run/openconnect.pid)"

然后重复第一节的三项检查,而不是相信退出状态。ip link show tun0应当失败,ip route show dev tun0应当不输出任何内容,而resolvectl status应当不再列出 VPN 链路。

SIGHUP 不是停止按钮:它会断开连接、运行脚本,并让会话保持可恢复状态,使用--cookie。SIGUSR2 会强制立即重连。SIGKILL 完全跳过清理,而这正是本文要讲的状态。

在不替换 resolv.conf 的情况下修复解析器

在大多数 Ubuntu 系统上,/etc/resolv.conf是指向 systemd-resolved 的符号链接,而不是你应该编辑的文件。替换它会破坏所有网络的名称解析,而不只是 VPN。改为删掉隧道的登记:

resolvectl revert tun0
resolvectl query example.com

如果系统使用 resolvconf 而不是 systemd-resolved,那么每个接口的记录会一直保留,直到被告知放弃该链路,而resolvconf -d tun0就是等效的操作。在选择命令之前,先确认用的是哪个解析器。

仍指向死链路后面服务器的一次查询会挂起到超时,而这段延迟看起来像 DNS 服务器坏了,而不是残留路由。

让下一次断开变得平淡无奇

在启动和关闭时保持 pid 文件路径一致,每次都传入--pid-file,或者把 openconnect 放到一个单元下运行让它的KillSignal与你所用版本记录的信号一致。先在前台测试,可以看到辅助脚本输出的关闭信息。

连接、断开,并把三项检查跑一遍。干净退出不会留下 VPN 链路、VPN 路由和 VPN 解析器记录。如果三项中有一项还在,你就知道是哪个组件没有清理。下一次排查就从那个答案开始,而不是从一次挂起的查询开始。

相关文章