一个 OpenVPN 实例在开机时以你不认识的名字启动,而停止它只在下次重启前有效。这种模式说明你移除了进程,却没有移除激活方式。两者是不同的对象,状态界面上只能看到其中一个。要找到激活方式,需要四条只读命令和一个判断。

先确定准确的单元名

从 journal 里的单元名出发,而不是从配置文件文件名出发。实例可能叫 openvpn@、openvpn-client@、openvpn-server@,也可能是某个发行版特有的名字,凭猜测去假设会让你对着错误的模板排查。状态输出里记录的是实际运行过的单元,这才是这里唯一重要的名字。原样复制它,包括任何后缀。

下面这些命令只是描述该单元,不做任何改动。systemctl show 会报告片段路径、源路径、单元文件状态、所有 drop-in,以及是什么触发了它。list-dependencies --reverse 会显示哪些已加载的单元指向它。两者都是只读操作,所以在形成结论之前先运行它们。

把观察到的名字代入所有位置。直接粘贴示例名字,是查错服务最常见的原因,它还会给出关于一个从未运行过的单元的自信答案。

读模板才能知道文件名

实例单元是模板的展开,读哪个配置文件由模板决定(读哪个配置文件)。以实例占位符结尾的命令行会解析为实例名加后缀。后缀是 .conf、实例名是 old 时,文件名就是 old.conf。若把实例命名为 old.conf,同一个模板就会去要 old.conf.conf。

这个细节解释了一类重复。有人用错名字添加了第二个实例,第一个照旧运行,主机最终多出两条隧道和一份令人困惑的日志。

读生效中的模板,而不是你记忆中编辑过的文件。drop-in 可能替换命令行,而开机时运行的正是替换后的版本。

追查它如何进入开机事务

常见路径有三种:来自 target 的 wants 链接、另一个单元提出的要求,或包生成器生成的单元。反向依赖列表覆盖前两种,把它们列为已加载的图条目。它不覆盖定时器、hook 或外部请求,所以这份列表只是证据,不是完整审计。

/run/systemd/generator 下生成的内容是产物,不是源头。修改它只保留到下一次生成,所以在那里做的修复看起来成功了,随后又退回原状。

先查包文档里写明的输入,再假设生成器从哪里读取。未文档化的目录扫描,正是单元在没有激活链接的情况下出现的常见原因。

移除激活方式,而不只是进程

如果实例是被直接启用的,而且确实已经过时,systemctl disable --now 可以一步完成停止和移除开机链接。先确认这条隧道不是你通往主机的唯一管理路径。该命令只是生命周期示例,不是让你停掉机器上所有 VPN 的建议。检查要放在命令之前,而不是之后。

这两半是可以分开的,也常被混淆。停用不会停止正在运行的服务,停止也不会移除启用状态,所以半条命令解释了一个开机后复发的问题。

被禁用的单元依然可能被另一个单元或显式请求启动。当激活由生成器掌控时,应修正生成器的输入,而不是禁用它的输出。屏蔽能挡住更多路径,但可能破坏依赖,所以它不能替代理解。

用重启来验证,而不是看状态界面

如果单元定义或生成器输入发生变化,先重新加载 systemd,再检查结果状态。守护进程重载不会把新配置推进正在运行的隧道,所以这两项检查是分开的。安排一次重启,确认目标隧道仍然可用,而该实例不再出现。

在弄清归属之前保留旧配置,然后按既定流程将它退役。成功是单元消失且访问路径仍可用,而不是某个界面上少了一个失败单元。

相关文章