容器镜像可以带上它们所需的每一个 OpenVPN 二进制文件和配置文件,却仍然在第一次网络调用时失败。错误之所以提到 /dev/net/tun,是因为应用向内核请求了 TUN 接口,而容器既没有设备节点,也没有创建它的能力。这是两种不同的权限,修好一种,另一种会原地不动。

把它们分开各需要一条命令。之后的错误信息会告诉你,你实际站在哪一道边界上。

设备节点和能力是两扇门

设备节点是文件系统对象,所以容器必须能在自己的挂载命名空间里看到它。能力是一种权限,所以进程需要在创建接口的那个命名空间里拥有 CAP_NET_ADMIN。只把设备传进去而没有能力,你会得到一个能打开却让 ioctl 失败的节点。只给能力而没有设备,应用就什么都打不开,这也是两者在示例里通常一起出现的原因。

Docker 的 devices 设置会把宿主机设备映射进容器,而 cap_add 调整能力集合。对同一路径做 bind mount 只是把文件放进视野,并不应用设备访问语义。

在 Compose 文件之外复现它

在改动服务之前,先确认宿主机有该设备,且一个干净容器能创建接口。下面的命令刻意做到最小,唯一目的是把宿主机问题与 compose 问题分开。如果它失败,任何 compose 改动都帮不上忙。

ls -l /dev/net/tun
docker run --rm --cap-add NET_ADMIN --device /dev/net/tun:/dev/net/tun alpine sh -c 'apk add -q iproute2 && ip tuntap add dev tun9 mode tun && ip link show tun9'

宿主机上缺少节点意味着 tun 模块不可用,这是宿主机侧而不是容器侧的修法。节点存在但容器内打不开,指向的是设备访问,消息会说 permission denied 而不是 no such file。这两种失败的先后顺序就是全部诊断。改动任何开关之前,先写下你看到的是哪一种。

TUN 之后的错误是另一类问题

一旦接口出现,下一次失败就换了主人。认证错误意味着隧道到达了服务器而凭据被拒绝,所以改能力没有用。应用仍然失败而隧道已经建立,指向的是路由、转发、容器内的 DNS 解析,或容器自身的网络布局。这些都是 TUN 权限回答不了的普通网络问题。每次改动后重跑最初的测试,可以防止各种失败混成一个印象。

从可用的示例里复制每一项设置,正是把最小修补变成难读的服务定义的原因。只带上这次部署真正需要的那几项,并记下每一项为什么在这里。

在容器内验证接口

检查接口和能力集合,要从正在运行的容器内部,而不是从宿主机。下面的命令会显示设备节点、能力列表,以及到底能不能创建接口。在与运行客户端相同的容器里执行它们;如果镜像很精简,就先安装工具。

ls -l /dev/net/tun
capsh --print | grep -i net_admin
ip link add dev tun9 type tun && ip link show tun9

出现在 bounding set 却不在 effective set 里的能力,其行为与从未授予过的能力不同,capsh 会把两者都显示出来。如果节点存在、能力也在,创建仍然失败,那么运行时还在施加别的东西,比如限制该操作的 seccomp 配置或 AppArmor 策略。这是第三道边界,由运行时而非应用记录。

记下容器参数、镜像,以及每次尝试的确切错误。下一个读到这条记录的人,不必重做整套调查就能看出是哪扇门关着。

相关文章