/dev/net/tun 缺失但 tun 已加载:修复设备节点
已加载的 tun 模块并不证明设备节点存在。把驱动和节点分开看,按顺序重建,并用接口验证。
OpenVPN 因 /dev/net/tun 报出 errno 2 而停止,可与此同时 lsmod 却若无其事地列出了 tun 模块。两件事同时为真。模块提供的是驱动,而客户端打开的那条路径,是必须由别的东西创建的设备节点。把它们当成同一个对象,正是重载模块这个显而易见修法毫无效果的原因。
两个对象,两个不同的归属
内核模块和设备节点是两个独立对象,各有各的生命周期。加载模块只是注册了一个驱动;在有 devtmpfs 的系统上,内核会要求创建对应节点。在没有 devtmpfs 的系统上,没有任何东西去创建它,驱动就待在那里,却没有一条指向它的路径。模块完全可以同时处于已加载、状态正常、却从文件系统完全够不着的状态。
所以错误文本和模块列表并不矛盾。客户端打开一条路径,路径不存在,内核在询问 tun 驱动之前就返回了 ENOENT。重载模块修不好缺失的路径,创建路径也修不好缺失的驱动。
lsmod | grep -w tun
stat -c "%F %t:%T" /dev/net/tun
grep " /dev " /proc/mounts
devtmpfs 与显式 mknod 的区别
通常有两种机制创建这个节点,弄清楚本该由谁负责,就知道该修哪里。devtmpfs 让内核在已挂载的实例里创建设备节点,udev 再在其上施加属主和权限。而显式的 mknod 是镜像和命名空间里两条机制都不运行时的手工路径。
要比结果,不要比意图。如果 devtmpfs 挂在 /dev 上而节点仍然不见,那就说明驱动不是你假设的那个,或者启动后有什么把节点删掉了。如果 /dev 是解包后的 rootfs 里的普通目录,那就不存在任何自动机制,构建镜像的人必须自己创建节点。
stat -c "%F major=%t minor=%T" /dev/net/tun
# expect: character special file major=a minor=c8
两种机制都不运行的地方,节点只能手工创建,而且顺序很重要。先建父目录,因为 mknod 变不出目录来;再做字符设备;最后设一个只让 root 可写的权限。建节点之前先重载模块;只有在 ls 显示那是个目录而非设备之后,才可以删掉已存在的路径。
sudo modprobe tun
sudo mkdir -p /dev/net
sudo mknod /dev/net/tun c 10 200
sudo chmod 600 /dev/net/tun
ls -l /dev/net/tun
最快的判别方法是看 stat 报告的类型:该路径上是个目录,是精简镜像里最常见的意外。Dockerfile 里一个出于好意的 mkdir 恰好会留下这种结果,再多的模块加载也换不回一个字符设备。Major 10 与 minor 200 是 tun 的注册配对,用十六进制写作 a 和 c8。
四种痕迹,四种成因
几乎每个案例都能归入四种成因,而每种都在日志里留下不同的痕迹。先看启动环境。无盘镜像、嵌入式板卡和 initramfs 阶段往往既不挂载 devtmpfs,也没有由 udev 管理的 /dev,于是没有任何进程去写这个节点。然后去看承载 /dev的那个东西的挂载选项,因为 nodev 允许路径存在却拒绝打开它,而从加固主机上取来的 bind 挂载会悄悄带上这个标志。只读挂载失败得更早也更响:在那里 mknod 返回 EROFS;而 EPERM 说明进程缺少能力,不是文件系统在自我保护。最后,自带 /dev tmpfs 的容器运行时需要把节点注入它自己的命名空间,它的设备 cgroup 也必须放行 major 与 minor 这一对。
再次改动权限之前,先检查策略层。SELinux 和 AppArmor 可以拒绝一次所有文件属性都允许的打开,而这种拒绝出现在审计日志里,不在客户端输出里。一次从未出现在任何拒绝里的权限改动,是把时间花在了完全错误的层面上。
sudo journalctl -k -n 20 --no-pager
sudo ausearch -m AVC -ts recent | tail -5
用接口来证明,而不是用列表
列目录成功只能证明路径存在 ,仅此而已。打开一个健康的 tun 节点来读,调用会返回错误,因为该设备没有读语义,也不是普通文件。这个错误在这里恰恰是成功信号;认出它,你就不会把它当成故障去追。
head -c 1 /dev/net/tun
head: error reading '/dev/net/tun': File descriptor in bad state
决定性的测试是创建一个接口,因为它一步就同时检验了驱动、节点和你的权限。若添加成功、链路出现,三者就都在工作。失败时,errno 会把成因分开:ENOENT 说明没有可用节点,EPERM 指向能力、挂载标志或策略,EBUSY 则说明接口已被别的进程占用。
sudo ip tuntap add dev tun7 mode tun
ip -brief link show tun7
sudo ip tuntap del dev tun7 mode tun
要在真正失败的那个上下文里跑这个测试,因为服务单元、容器或定时任务携带的能力集可能和你的登录 shell 不同。在错误上下文里通过的测试,对真正失败的那个上下文什么也证明不了。
当设备不是问题所在
如果接口测试通过而客户端仍然失败,剩下的问题就是配置、服务上下文或路由,设备本身没问题。这时就该停止编辑设备节点,因为继续改只会离一个能工作的系统更远。
每次都跑同样的五项检查,而不是另发明一项:模块是否在没有竞争模块的情况下加载、路径是否是 major 与 minor 正确的字符设备、挂载选项是否干净、进程上下文是否具备相应能力、失败时刻是否有策略拒绝。五项都扛过去的故障,无论提示里提到什么,都不是设备问题。