hook 报告成功,可同一个路径从 shell 里看仍然是空的。两种说法可以同时成立,因为 systemd 服务可能活在自己的挂载命名空间里,并带有一个私有的临时目录。理解这一点之后,有趣的问题就变成:写入到底成功了吗。

两个临时目录,一条路径

隔离设置让单元看到临时目录的私有视图,而不是把它们变成只读。单元之外的进程看不到它在里面写了什么,单元停止时它留下的东西都会被丢弃。因此,hook 打印的路径和从 shell 检查的路径,可能指向同名的不同对象。

这是可见性问题,看起来却像数据丢失问题。什么都没被删除,文件只是待在 shell 并不属于的命名空间里。同一个命令在单元内部运行时会表现不同,而这一观察通常就能结束争论。在下任何关于写入的结论之前,先读单元的设置。

把看不见与写不进分开

成功写入和打开失败留下的证据不同,这个区别决定怎么修。如果 hook 说文件已创建,外面却找不到,那你要问的是命名空间。如果 hook 报告权限错误或打开失败,文件从未被写入,改命名空间也无济于事。

systemctl show [email protected] -p PrivateTmp -p User -p Group -p RuntimeDirectory
systemctl show [email protected] -p MainPID --value
sudo ls -l /proc/PID/root/tmp/client-state

经由进程根的路径展示的是服务自己的文件系统视图,前提是进程仍在运行、并且对它有权限检查。在同一会话里读单元的日志,因为一旦有人先入为主地定了原因,写入错误和文件缺失就很容易混为一谈。

给共享数据一个明确的位置

只有服务需要的数据,属于它的私有临时目录。另一个进程必须读取的数据,需要一条为双方定义的路径,并写明所有者、权限和有效期。编辑单元之前,先判断你属于哪一种。这类事故大多源于把私有文件当成共享文件。

[Service]
RuntimeDirectory=openvpn-hook-office
RuntimeDirectoryMode=0750

运行时目录把文件夹放在系统运行时区域下,按单元配置的用户和组创建,并在单元停止时删除。用 drop-in 添加它,让软件包自带的单元保持完整,然后重新加载管理器并重启该实例,接受它的客户端会重新连接。

保留已安装单元本来就需要的任何运行时目录。替换清单而不是追加,是常见的做法,会弄坏没人联想到这次改动的无关功能。

两层权限可能对不上

服务管理器按单元配置的用户创建目录,而 VPN 守护进程在运行连接 hook 之前,可能另外切换到自己的账户。这是两个不同的层,可能指向不同的身份。

root 所有、权限严格的目录,对已经放弃权限的守护进程完全可见,却依然写不进去。这时错误会被读成权限问题,尽管路径正好在你预期的位置。把目录的所有者和 hook 报告的身份对比一下。两者不同时,权限从来就不是问题所在。

在改动所有权之前,先确认 hook 实际以什么身份运行。为了修目录而改服务的执行用户,可能改变别处的设备访问和网络权限,为小问题付出大代价。

验证整个交接

一次确认四件事:hook 以自己的真实身份写入,预期的读取方能用上文件,无关用户不能,停止单元时按设计清理。文件出现在宿主共享临时目录里并不是要求,通常反而是交换从未拿到自己路径的迹象。重启之后测试读取方,而不只是第一次运行。清理行为是设计的一部分,所以要观察而不是假设。

持久状态应当放在寿命匹配的位置,因为运行时目录本就该消失。按寿命而不是按方便来选位置。这个习惯能避免同一调查出现下一个版本。

相关文章