Gentoo Linux 使用 xtables-nft-multi 之后,docker wg-easy 不能正常使用

上一篇记录了 Gentoo Linux 在新内核里把 iptableslegacy backend 切换到 xtables-nft-multi 的过程。宿主机切完之后,大部分自己写的防火墙脚本都能继续工作,因为命令还是 iptables,只是背后翻译成了 nftables 规则。

不过还有一个容易忽略的地方:Docker 容器里的程序也可能会调用 iptables。这次出问题的就是 Docker 版 wg-easy

背景

Linux Kernel 6.17 之后,iptables legacy 这条路已经不适合继续依赖了。对于自己编译内核的 Gentoo 用户来说,如果没有再打开 legacy 相关配置,系统里继续使用旧的 iptables backend 就会遇到各种规则写入失败的问题。

比较合适的做法是把 iptables 命令切到 nft backend:

1eselect iptables list
2eselect iptables set 2

切换后确认一下:

1iptables --version

正常应该看到类似:

1iptables v1.8.x (nf_tables)

这表示现在执行 iptables 命令时,用户态工具会把规则翻译成 nftables 规则再写入内核。

wg-easy 为什么会出问题

wg-easy 本身是一个很好用的 WireGuard Web 管理工具。它的问题不在 Web UI,而在容器启动和 WireGuard peer 生效时,仍然会执行一些 iptables 命令,例如:

1iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
2iptables -A FORWARD -i wg0 -j ACCEPT
3iptables -A FORWARD -o wg0 -j ACCEPT

这些规则的作用很直接:

  • 允许 wg0 接口转发流量
  • 给 WireGuard 客户端做 MASQUERADE
  • 让客户端能通过服务器访问外部网络

问题在于:容器内看到的是自己的文件系统,但防火墙规则最终写入的是宿主机内核的 netfilter。也就是说,容器里的 iptables 如果还是 legacy 版本,它并不会因为宿主机已经切到 xtables-nft-multi 就自动变好。

于是就会出现一种很别扭的现象:

  • 宿主机执行 iptables 正常
  • 宿主机的 Docker 网络基本正常
  • wg-easy 容器能启动
  • 但 WireGuard 客户端连上后不能正常转发流量

本质原因是容器里的 iptables backend 和宿主机内核实际支持的 backend 不一致。

处理思路

最稳妥的方案不是改 wg-easy 的历史数据,也不是重建 WireGuard 配置,而是只改镜像里的 iptables 命令入口。

也就是说,让容器内的这些命令:

1iptables
2ip6tables
3iptables-save
4iptables-restore

都指向 xtables-nft-multi

这样 wg-easy 原来生成的配置、已有的 peer、历史数据都不需要动。它仍然以为自己在执行 iptables,但实际写入内核时已经走 nf_tables backend。

自定义 Dockerfile

可以在本地创建一个很薄的派生镜像,只替换 iptables 相关命令:

 1FROM ghcr.io/wg-easy/wg-easy:latest
 2
 3RUN set -eux; \
 4    if command -v xtables-nft-multi >/dev/null 2>&1; then \
 5      ln -sf "$(command -v xtables-nft-multi)" /usr/sbin/iptables; \
 6      ln -sf "$(command -v xtables-nft-multi)" /usr/sbin/ip6tables; \
 7      ln -sf "$(command -v xtables-nft-multi)" /usr/sbin/iptables-save; \
 8      ln -sf "$(command -v xtables-nft-multi)" /usr/sbin/ip6tables-save; \
 9      ln -sf "$(command -v xtables-nft-multi)" /usr/sbin/iptables-restore; \
10      ln -sf "$(command -v xtables-nft-multi)" /usr/sbin/ip6tables-restore; \
11    else \
12      echo "xtables-nft-multi not found in image" >&2; \
13      exit 1; \
14    fi

如果基础镜像里没有 xtables-nft-multi,需要先安装对应的 iptables 包。不同基础镜像的包管理器不一样,可以先进入容器确认:

1docker run --rm -it ghcr.io/wg-easy/wg-easy:latest sh
2command -v xtables-nft-multi
3cat /etc/os-release

如果是 Alpine 系镜像,可以改成:

 1FROM ghcr.io/wg-easy/wg-easy:latest
 2
 3RUN set -eux; \
 4    apk add --no-cache iptables; \
 5    ln -sf /usr/sbin/xtables-nft-multi /usr/sbin/iptables; \
 6    ln -sf /usr/sbin/xtables-nft-multi /usr/sbin/ip6tables; \
 7    ln -sf /usr/sbin/xtables-nft-multi /usr/sbin/iptables-save; \
 8    ln -sf /usr/sbin/xtables-nft-multi /usr/sbin/ip6tables-save; \
 9    ln -sf /usr/sbin/xtables-nft-multi /usr/sbin/iptables-restore; \
10    ln -sf /usr/sbin/xtables-nft-multi /usr/sbin/ip6tables-restore

构建镜像:

1docker build -t wg-easy:nft .

然后在 docker compose 里把原来的镜像替换掉:

 1services:
 2  wg-easy:
 3    image: wg-easy:nft
 4    container_name: wg-easy
 5    cap_add:
 6      - NET_ADMIN
 7      - SYS_MODULE
 8    sysctls:
 9      - net.ipv4.ip_forward=1
10      - net.ipv4.conf.all.src_valid_mark=1
11    volumes:
12      - ./data:/etc/wireguard
13    ports:
14      - "51820:51820/udp"
15      - "51821:51821/tcp"

这里的关键点只有一个:./data:/etc/wireguard 继续使用原来的数据目录。这样已有的 WireGuard 配置和 peer 信息都保留下来,不需要重新生成客户端配置。

验证

容器启动后,先确认容器内的 iptables backend:

1docker exec -it wg-easy iptables --version

期望输出里包含:

1nf_tables

再看宿主机上的规则是否已经写入:

1iptables -t nat -S
2iptables -S

如果能看到 wg-easywg0 相关的 MASQUERADEFORWARD 规则,再用客户端连接测试外网访问,基本就可以确认问题解决。

小结

这类问题容易误判成 DockerWireGuardwg-easy 的配置错误,但核心其实还是 iptables backend 不一致。

宿主机切到 xtables-nft-multi 之后,容器内凡是会直接调用 iptables 的程序也要一起处理。对 wg-easy 来说,最小改动就是做一个派生镜像,把容器里的 iptables 命令替换成 xtables-nft-multi

这样做的好处是很明确的:

  • 不需要迁移 wg-easy 历史数据
  • 不需要重建 WireGuard peer
  • 不需要改已有客户端配置
  • 继续兼容原来基于 iptables 的启动逻辑
  • 实际规则写入时走新的 nftables backend

从长期看,原生 nftables 当然是更干净的方向。但对于这类已经封装好 iptables 调用的 Docker 应用,先用 xtables-nft-multi 做兼容翻译,是一个成本低、风险小、维护起来也比较舒服的过渡方案。

Posts in this series