LXD 是通过添加设备节点把宿主机的字符设备暴露给容器的,而不是教来宾自己去创建。如果容器里没有 /dev/net/tun,宿主机可能确实有该设备,来宾仍然打不开。unix-char 条目是这条路径在管理端的另一半,而且它刻意比特权容器更窄。所以宿主机节点和容器节点值得分开检查。

机制很短,值得记住:LXD 解析宿主机路径,在实例内创建一个对应的节点,再把设备加入容器的设备 cgroup。此后来宾进程还需要自己的权限和能力,才能真正使用这个节点。

unix-char 条目到底改变了什么

unix-char 设备让 LXD 把宿主机的一个字符设备暴露在容器内的指定路径上。条目有三个关键部分:管理器使用的设备标签、宿主机上的 source 路径,以及来宾看到的 path。只有后两者描述设备本身。任一处拼错,节点就会出现在客户端永远不会打开的位置。

在宿主机上,这个设备通常是 /dev/net/tun。容器内可以沿用同一路径,也可以另选一个,但路径必须与 VPN 客户端的预期一致。沿用 /dev/net/tun 可以避免第二个配置问题,因为大多数客户端不加询问就会打开该路径。

管理器在实例启动时或设备热插拔时应用该条目。它不会向来宾复制驱动,也不会创建隧道接口。它让节点可见,并通过容器的设备控制组放行访问。

在实例上声明设备,而不是在来宾里

从 LXD 管理上下文添加条目,写明实例和一个日后能认出的标签。对名为 vpnbox 的容器,命令是:

lxc config device add vpnbox tun unix-char source=/dev/net/tun path=/dev/net/tun
lxc config device show vpnbox

标签 tun 只对实例配置有效。它不是接口名,也不必与来宾里的任何东西对应。如果某个 profile 已经提供同一设备,再加一条会制造冲突而不是冗余,所以先看实际生效的配置。

在容器里改文件是错的层面。启动脚本里的 mknod 在特权容器上也许看似有效,重建后就消失,或在权限更低的容器上失败。实例的设备集合归管理器管;节点也让它来管。

把属主和模式与来宾进程对齐

unix-char 条目可以为容器内的节点设置 uid、gid 和 mode。用它们去匹配运行 VPN 客户端的账号,而不是把设备开放给所有用户或把容器切到特权模式。

可见不等于允许。属主为 root、模式 600 的节点,以非特权用户运行的服务看得见,却依旧打不开。先确认来宾账号,再把属主设为该账号,模式不要比客户端所需更宽。

进程还需要 配置隧道接口的能力,通常是容器网络命名空间里的 CAP_NET_ADMIN。传入设备不会授予该能力,授予能力也不会提供设备;这是两项独立要求。

知道条目不授予什么

节点可见并不等于 VPN 能用。 客户端软件包仍要安装,profile 仍要有效,服务仍要启动。认证、路由和 DNS 仍是客户端的事,那里失败并不能证明设备条目有错。

systemd 还可能加一层限制。带 DeviceAllow 或加固设备策略的 unit,可以拒绝访问容器管理器提供的节点。当节点存在但服务报权限错误时,检查该 unit 的设备策略,改策略而不是放宽容器。

热插拔是可选项,而且很明确。带 required=false 的条目需要源路径,会随宿主机设备变化而出现或消失。这种生命周期适合来去不定的硬件;对 VPN 启动时就要用的 TUN 设备,可选条目可能让服务在前置条件尚未出现时就先启动。

按可见、可打开、有接口的顺序验证

先确认目标容器内的节点。路径、主次设备号和属主都应与你添加的条目及宿主机设备一致。节点缺失说明管理器没有应用条目,或者应用到了别的实例。

lxc exec vpnbox -- ls -l /dev/net/tun
lxc exec vpnbox -- sh -c 'test -c /dev/net/tun && echo char-device-ok'

再确认服务账号能打开它。以该账号做一次短测试,可以区分属主问题和服务问题。最后启动客户端并确认出现隧道接口;只有第三步能证明整条路径,因为前两步都通过时,profile 或能力仍可能不对。

变更获批时,记下实例、设备标签、宿主机源路径、来宾路径和属主账号。这五个值就是日后重建、迁移或改 profile 时必须保住的;它们也比一条没有上下文的权限报错容易比对得多。

相关文章