解决 Docker+nftables 端口转发失效: 原因分析与配置方案
2025-04-13 22:31:19
Docker 遇上 nftables:端口转发踩坑与解决方案
搞 Docker 和 nftables
的时候,端口转发不生效?明明 nftables
规则看着没毛病,容器也跑得好好的,但就是从外面访问不了。特别是防火墙一关(比如 OUTPUT
或 FORWARD
链放行所有流量)就能通,一开就歇菜。这情况不少人都遇到过,感觉就像 Docker 和 nftables
八字不合。
举个例子,就像这位朋友遇到的:想把宿主机 ens18
接口(IP 192.168.16.215
)的 8080
端口收到的流量,转发给 docker0
网桥上的容器(IP 172.17.10.2
)的 80
端口。他写了类似下面的 nftables
NAT 规则:
table inet nat {
chain prerouting {
type nat hook prerouting priority -100; policy accept;
# 把 192.168.16.215:8080 (ens18接口) 的流量转给 172.17.10.2:80 (docker0接口)
tcp dport 8080 iifname "ens18" ip daddr 192.168.16.215 dnat to 172.17.10.2:80
}
}
这规则本身没啥大问题,确实是在 prerouting
钩子做目标地址转换(DNAT)。但为啥一开防火墙(特别是关闭 OUTPUT
链的默认放行策略)就不工作了呢?真的是 Docker 和 nftables
不兼容吗?
咱们来挖挖根源,再看看怎么把这事儿搞定。
问题根源:为什么 Docker 和 nftables 会“打架”?
这事儿吧,不能简单甩锅给“不兼容”。Docker 和 nftables
本身可以一起用,但需要你明白它们各自是怎么管网络规则的,特别是涉及到数据包流转路径上的各个关卡。
关键点往往在这里:
-
Docker 的“老习惯”:iptables
Docker 被设计出来的时候,Linux 世界的主流防火墙是iptables
。所以,Docker 默认会自己动手去创建和管理一堆iptables
规则,来实现容器网络(创建docker0
网桥、分配 IP、设置 NAT 规则让容器能访问外网、处理端口映射-p
参数等)。它会在nat
表的PREROUTING
、OUTPUT
链和filter
表的FORWARD
链里加规则。 -
nftables 上位,但 Docker 可能没跟上(或者你没配置好)
nftables
是iptables
的正统接班人,设计更优、性能更好、语法更骚气。新版的 Linux 发行版都在逐步用nftables
替换iptables
作为默认防火墙后端。但 Docker 的核心逻辑可能还是认iptables
的命令行接口。这就可能造成几种情况:- 系统底层用的是
nftables
,但你装了iptables-nft
这个兼容层。Docker 调用iptables
命令时,实际上是iptables-nft
在帮你把规则翻译成nftables
规则。这一般能工作,但可能会和你手动写的nftables
规则混在一起,有点乱。 - 你完全禁用了
iptables
,只用纯nftables
。这时候 Docker 发现iptables
命令没了,或者没效果,它就没法自动帮你配置网络了。 - Docker 还在尝试操作
iptables
,而你同时在用nftables
手写规则,两者可能对同一个数据包的处理产生冲突或者遗漏。
- 系统底层用的是
-
被忽略的
FORWARD
链:转发的核心
开篇那个问题,“关闭OUTPUT
链后不工作”,这很可能是一个误导。NAT (dnat
) 发生在prerouting
链,改变了目标 IP 地址。但是,数据包从外部接口 (ens18
) 进来,要想到达内部的docker0
接口,它需要经过 Linux 内核的转发 (Forwarding)路径。控制这个路径的,主要是filter
表里的FORWARD
链。如果你设置了
FORWARD
链的默认策略是drop
(拒绝),并且没有明确写规则允许从ens18
到docker0
(以及反向,用于响应包)的流量通过,那即使 NAT 成功了,数据包也会在转发这一步被丢掉。同理,如果OUTPUT
链策略设为drop
,并且没有为响应包 (从容器经docker0
回来,再从ens18
出去)放行,也可能导致连接建立不完整。不过,对于外部访问容器,FORWARD
链的问题更常见。 -
连接跟踪(Connection Tracking)的重要性
现代防火墙都是状态化的。对于一个 TCP 连接,第一个包(SYN)过来,经过 NAT 和FORWARD
规则检查后到达容器。容器回包(SYN/ACK)时,内核需要知道这个包是属于刚才那个连接的,这样才能自动允许它通过FORWARD
和OUTPUT
链,并且在需要时做反向 NAT(SNAT)。这是通过连接跟踪 (conntrack
) 模块实现的。你的nftables
规则里必须包含允许related
和established
状态的连接通过的规则。
总结一下,问题大概率不是 Docker 和 nftables
水火不容,而是你的 nftables
规则没有完整覆盖数据包从进入到转发再到出去(以及响应包回来)的整个流程,特别是 FORWARD
链的规则,以及可能存在的 Docker 自动规则与手动规则的冲突。
解决方案:让 Docker 和 nftables 和谐共处
针对上面的原因,有几种思路可以解决这个问题:
方案一:纯手动精细化管理 nftables 规则
这是控制欲最强的方案。完全接管 Docker 相关的网络规则,不让 Docker 插手(或者忽略它可能产生的 iptables
规则)。
原理:
在 nftables
里,不仅要写 NAT 规则,还要写 FORWARD
链规则,明确允许目标流量在接口间转发。同时,利用连接跟踪,简化规则。
操作步骤:
-
确保 IP 转发已启用:
检查/proc/sys/net/ipv4/ip_forward
的值是否为1
。如果不是,执行:sudo sysctl -w net.ipv4.ip_forward=1 # 若要永久生效,编辑 /etc/sysctl.conf 或在 /etc/sysctl.d/ 下创建配置文件 # 添加或修改: net.ipv4.ip_forward = 1 # 然后执行 sudo sysctl -p
-
编写完整的
nftables
规则集:
你需要至少包含以下部分的规则:- 定义表和链: 创建
filter
表(用于过滤)和nat
表(用于地址转换)。 - 基础 INPUT/OUTPUT/FORWARD 策略: 先设置默认拒绝策略,更安全。
flush ruleset table inet filter { chain input { type filter hook input priority 0; policy drop; # 允许回环接口流量 iifname "lo" accept # 允许已建立和相关的连接 (非常重要!) ct state established,related accept # 根据需要允许其他入站流量, 比如 SSH tcp dport 22 accept } chain forward { type filter hook forward priority 0; policy drop; # 允许已建立和相关的连接转发 (非常重要!) ct state established,related accept } chain output { type filter hook output priority 0; policy accept; # 或者 drop,然后按需允许 # 如果 policy 是 drop, 至少需要下面这句 # oifname "lo" accept # ct state established,related accept # ... 其他允许的出站规则 } } table inet nat { chain prerouting { type nat hook prerouting priority -100; policy accept; } chain postrouting { type nat hook postrouting priority 100; policy accept; # 如果容器需要访问外网,可能需要SNAT规则 # 例如:允许 docker0 网段出去时伪装成宿主机IP # ip saddr 172.17.0.0/16 oifname "ens18" masquerade } }
- 添加针对 Docker 的 NAT 规则: 就是你之前写的那个。
table inet nat { chain prerouting { type nat hook prerouting priority -100; policy accept; # 把 192.168.16.215:8080 (ens18接口) 的流量转给 172.17.10.2:80 tcp dport 8080 iifname "ens18" ip daddr 192.168.16.215 dnat to 172.17.10.2:80 } # 如果容器需要访问外网,也需要 SNAT/Masquerade chain postrouting { type nat hook postrouting priority 100; policy accept; ip saddr 172.17.0.0/16 oifname "ens18" masquerade } }
- 添加关键的 FORWARD 规则: 这是最容易漏掉的!
table inet filter { chain forward { type filter hook forward priority 0; policy drop; # 默认拒绝 # 允许已建立和相关的连接,涵盖大部分双向流量 ct state established,related accept # 明确允许从外部访问容器的新连接 # 从 ens18 进来,目标是容器 IP 和端口,去往 docker0 iifname "ens18" oifname "docker0" ip daddr 172.17.10.2 tcp dport 80 ct state new accept # (可选) 如果容器需要主动访问外部网络 # iifname "docker0" oifname "ens18" accept # 过于宽松,下面是更精确的 # (更精确的可选规则) 允许 docker 网段访问外部,通常结合 postrouting 的 masquerade iifname "docker0" oifname "ens18" ip saddr 172.17.0.0/16 accept # (更精确的可选规则) 允许容器访问宿主机上的服务 (如果需要) # iifname "docker0" oifname "lo" accept # (更精确的可选规则) 允许宿主机访问容器上的服务 (如果需要, NAT通常已经处理了入口,这里是关于其他协议或直接访问) # iifname "lo" oifname "docker0" accept } }
- 定义表和链: 创建
-
加载规则:
将上述规则保存到文件(例如/etc/nftables.conf
),然后加载:sudo nft -f /etc/nftables.conf
确保
nftables
服务开机自启并加载该配置。
代码示例(合并后的 /etc/nftables.conf
片段):
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
iifname "lo" accept
ct state established,related accept
tcp dport 22 accept # 允许 SSH 访问宿主机
# 可以添加其他允许访问宿主机的规则
}
chain forward {
type filter hook forward priority 0; policy drop;
ct state established,related accept
# 允许从 ens18 转发到容器 172.17.10.2 的 80 端口的新连接
iifname "ens18" oifname "docker0" ip daddr 172.17.10.2 tcp dport 80 ct state new accept
# 允许 docker 网段访问外部 (配合下面的 masquerade)
iifname "docker0" oifname "ens18" ip saddr 172.17.0.0/16 accept
# 可以添加其他转发规则,比如容器间通信 (如果不在同一个docker network)
}
chain output {
type filter hook output priority 0; policy accept; # 或者更严格设为 drop
# 如果是 drop, 记得允许必要出站流量
# oifname "lo" accept
# ct state established,related accept
# ...
}
}
table inet nat {
chain prerouting {
type nat hook prerouting priority -100; policy accept;
# DNAT 规则: ens18:8080 -> container:80
tcp dport 8080 iifname "ens18" ip daddr 192.168.16.215 dnat to 172.17.10.2:80
}
chain postrouting {
type nat hook postrouting priority 100; policy accept;
# 让容器可以通过宿主机 IP 访问外网
ip saddr 172.17.0.0/16 oifname "ens18" masquerade
}
}
安全建议:
规则越精细越好。不要直接 accept
整个接口的流量转发,尽量指定源/目标 IP、端口、协议和连接状态。默认策略设为 drop
是个好习惯。
进阶使用技巧:
可以使用 nftables
的 set
来管理一组 IP 地址或端口,让规则更简洁。比如定义一个 docker_nets
集合包含所有 Docker 网络的地址范围。
使用 log
动作调试被拒绝的包:在 drop
规则前加一条 log prefix "FORWARD drop: " limit rate 5/minute
可以在内核日志中看到被阻止的流量信息。
方案二:禁用 Docker 的 iptables 管理
如果你想完全掌控 nftables
,不想让 Docker 在背后捣乱(比如创建它自己的 iptables
规则,即使是通过 iptables-nft
),可以明确告诉 Docker:“别碰 iptables
”。
原理:
修改 Docker 守护进程的配置,让它不执行任何 iptables
操作。这样一来,所有网络配置(包括基础的容器上网 NAT、端口映射等)都必须由你手动在 nftables
中实现。
操作步骤:
-
编辑 Docker 配置文件:
通常是/etc/docker/daemon.json
。如果文件不存在,就创建一个。{ "iptables": false }
-
重启 Docker 服务:
sudo systemctl restart docker
-
手动配置所有必要的 nftables 规则:
现在 Docker 不会自动配置网络了。你需要像方案一那样,手动编写nftables
规则来处理:- 容器访问外网的 SNAT/Masquerade(
nat
表postrouting
链)。 - 所有端口映射的 DNAT(
nat
表prerouting
链)。 - 所有必要的
FORWARD
链规则,允许流量在宿主机接口和docker0
(或其他 Docker 网络接口)之间流转。 - 甚至可能需要配置容器间的通信规则(如果它们不在同一个自定义 Docker 网络里)。
- 容器访问外网的 SNAT/Masquerade(
代码示例(daemon.json
):
{
"iptables": false
}
安全建议:
这种方法给了你最大的控制权,但也意味着最大的责任。任何 Docker 需要的网络连通性,如果没在你的 nftables
规则里明确允许,就会失败。务必仔细测试所有网络场景(容器访问外网、外部访问容器、容器间通信等)。
注意事项:
这个选项会让 Docker 的 -p
端口映射参数失效,因为它是通过 iptables
实现的。你必须手动在 nftables
里实现等效的 DNAT 和 FORWARD
规则。管理起来会复杂一些,特别是当你有很多容器和端口映射时。
方案三:拥抱 iptables-nft
兼容模式
这是在许多现代 Linux 发行版上比较“顺滑”的方式。系统底层用 nftables
,但提供 iptables
命令作为前端接口。Docker 继续使用它熟悉的 iptables
命令,这些命令被翻译成 nftables
规则执行。你可以同时使用 nftables
命令添加你自己的规则。
原理:
利用 Linux 提供的 iptables-nft
模式。让 Docker 按照它的老方式去管理 iptables
规则(这些规则实际上会被 nftables
内核子系统处理)。同时,你可以在 nftables
中添加补充规则,比如更精细的 FORWARD
链控制或者自定义的 NAT 规则。
操作步骤:
-
确认系统使用
iptables-nft
:
在 Debian/Ubuntu 系统上,可以通过update-alternatives
查看:sudo update-alternatives --config iptables
选择包含 "nft" 的选项(如
/usr/sbin/iptables-nft
)。其他发行版可能通过检查安装的包(比如iptables
包是否是基于nftables
的版本)来确认。 -
保持 Docker 的默认设置:
确保/etc/docker/daemon.json
中没有"iptables": false
的设置(或者明确设为true
)。让 Docker 继续管理iptables
规则。 -
理解 Docker 创建的规则:
Docker 重启后,用sudo nft list ruleset
查看nftables
规则集。你会看到 Docker 通过iptables-nft
创建了一些链(比如filter
表下的DOCKER
,DOCKER-ISOLATION-STAGE-1/2
,DOCKER-USER
链,以及nat
表下的DOCKER
链等)。Docker 的-p
映射规则通常会出现在nat
表的DOCKER
链(做 DNAT)和filter
表的DOCKER
链(做 FORWARD 允许)。 -
在
nftables
中添加你自己的规则:
你仍然可以用nft
命令或/etc/nftables.conf
添加规则。关键在于要理解你的规则和 Docker 自动生成的规则如何协同工作。-
利用
DOCKER-USER
链: Docker 在filter
表的FORWARD
链处理流程中,特意设置了一个DOCKER-USER
链。这个链默认是空的,并且会在 Docker 自身的规则之前被调用。官方推荐把用户自定义的、与 Docker 相关的过滤规则放在这里。这样可以确保你的规则优先执行,并且不会被 Docker 服务重启所覆盖。table inet filter { # DOCKER-USER 链是 Docker 预留给你添加自定义规则的地方 chain DOCKER-USER { # 在这里添加你自己的规则,例如更严格的访问控制 # 比如:只允许某个特定源 IP 访问那个转发的端口 # iifname "ens18" oifname "docker0" ip saddr 1.2.3.4 ip daddr 172.17.10.2 tcp dport 80 accept # 如果上面这条匹配了,下面的 docker 自己的规则可能就不再执行了 (取决于链的跳转方式) # 注意:默认情况下 DOCKER-USER 链最后是 RETURN,表示继续执行 Docker 后续的规则 # 如果你在这里加了 accept/drop/reject,处理就可能在这里结束 } }
-
直接添加
FORWARD
规则: 你也可以直接在filter
表的FORWARD
链添加规则。注意规则的顺序。如果你想覆盖 Docker 的某些行为,需要确保你的规则优先级更高(在nftables
中,链里规则按顺序匹配)。 -
管理 NAT 规则: 如果你需要做 Docker
-p
参数之外的、更复杂的 NAT,可以像方案一那样在nat
表的prerouting
和postrouting
链中添加规则。要小心不要和 Docker 自动生成的 NAT 规则冲突。
-
代码示例(检查 iptables
模式):
sudo update-alternatives --display iptables # 查看当前指向
sudo update-alternatives --config iptables # 如果需要,切换模式
安全建议:
利用 DOCKER-USER
链是管理自定义 Docker 相关过滤规则的最佳实践。查看 sudo nft list ruleset
理解 Docker 创建了哪些规则,可以帮你避免冲突和遗漏。
进阶使用技巧:
即使在这种模式下,用 sudo nft monitor
也能跟踪数据包如何经过 Docker 创建的以及你自己添加的 nftables
规则,是排查问题的利器。
调试小技巧
遇到问题别慌,试试这些方法定位:
- 检查规则是否加载:
sudo nft list ruleset
,仔细看你的规则是不是真的在里面,语法有没有写错。 - 看数据包怎么走:
sudo nft monitor trace
(需要内核支持并加载nft_trace
模块),可以实时看到每个数据包匹配了哪些规则。这个超级有用! - 记录被丢弃的包: 在
FORWARD
或INPUT
链的policy drop
之前,加上log prefix "DROP(forward): " drop
或log prefix "DROP(input): " drop
,然后在系统日志里(如dmesg
或/var/log/syslog
)看是什么样的包被丢了。 - 检查连接跟踪表:
sudo conntrack -L
,看看连接是否被正确跟踪,状态是什么(ESTABLISHED
,NEW
,RELATED
等)。sudo conntrack -E -p tcp --dport 8080
可以实时看新进来的连接。 - 简化规则: 暂时把防火墙规则大幅简化,比如
FORWARD
链临时改成policy accept
,看看能不能通。如果通了,再逐步加回规则,找到是哪条规则拦住了。 - 检查 Docker 网络:
docker network inspect bridge
(或其他网络名),确认容器 IP、网关等信息是否和你规则里写的一致。
选哪种方案取决于你的需求:想要完全控制、不怕麻烦,就用方案一或方案二;想省事点、利用 Docker 的便利性,同时还能自定义一些规则,方案三(iptables-nft
模式)通常是个不错的平衡点。关键是理解数据包的流转路径和 nftables
不同表链的作用。