Docker 端口为什么绕过防火墙?讲透 iptables 链路与 5 种收口方案

举报
yd_213781742 发表于 2026/10/30 11:45:33 2026/10/30
【摘要】 从 DNAT 发生在 PREROUTING 讲起,解释为什么开了 firewalld/ufw 容器端口仍对公网可达;拆解 DOCKER-USER 等 iptables 链的顺序陷阱,给出绑定回环、DOCKER-USER 规则、firewalld/ufw 等 5 种收口方案与上线自检清单。

一、先说现象

很多人在云主机上遇到过同一幕:

  1. 装好 firewalld 或 ufw,只放行了 22 和 443;
  2. firewall-cmd --list-ports 里确实没有 8080;
  3. docker run -p 8080:80 nginx 起来之后,从公网直接就能访问 8080。

防火墙像是被"穿透"了。于是开始怀疑 Docker 有后门、怀疑 firewalld 没生效、怀疑云厂商安全组。

其实三者都没坏。真正的原因是:你配的规则和 Docker 的包,走的根本不是同一条链路。

二、根因:DNAT 发生在防火墙之前

一个从公网进来的包,在 Linux 协议栈里的顺序是这样的:

网卡 → raw PREROUTING → nat PREROUTING(DNAT 在这里发生)
     → 路由决策
     → filter FORWARD(转发过滤)
     → 到达容器

Docker 发布端口时,会在 nat 表的 PREROUTING 里挂一条规则,把发往宿主机 8080 的包在进入过滤之前改写目的地址:

# 示例:Docker 写入 nat 表的端口映射规则
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A DOCKER -d 172.17.0.2/32 -p tcp --dport 80 -j DNAT --to-destination 172.17.0.2:80

关键在于:包在 PREROUTING 阶段就已经"不是给宿主机"的了,它的目的地变成了容器 IP。因此后续走的是 FORWARD,而不是 INPUT。

而 ufw 和 firewalld 的端口放行规则,绝大多数都挂在 INPUT 上(ufw-before-input、zone 的 input 链)。包压根不会经过那里。

Docker 官方文档对这件事的表述很直接:Docker 和 ufw 在规则的使用方式上互不兼容;发布端口时流量在 nat 表就被转发,早于 INPUT/OUTPUT 链,实际上忽略了你的防火墙配置。

所以不是防火墙失效,是它被"绕过去"了——合法地、按设计地绕过去。

三、那我把规则加到 FORWARD 上行不行?

也不行,原因在链的顺序。

Docker 启动时会在 filter 表的 FORWARD 链里插入若干跳转,且插在最前面。典型结构(Docker 28+ 的 iptables 后端):

FORWARD
 ├─ -j DOCKER-USER        ← 唯一留给用户的钩子,位置最前
 ├─ -j DOCKER-FORWARD     ← Docker 自己的转发处理
 │    ├─ 已建立连接直接 ACCEPT
 │    └─ -j DOCKER-BRIDGE → -j DOCKER(按端口放行)
 └─ -j DOCKER-INGRESS     ← Swarm 相关

你在 FORWARD 末尾追加的 -A FORWARD -p tcp --dport 8080 -j DROP,永远轮不到执行——包在 DOCKER-USER 或 DOCKER 链里就已经被 ACCEPT 掉了。

这里有个容易踩的细节:DOCKER-USER 里的规则,看到的是 DNAT 之后的包。也就是说 --dport 匹配的是容器端口(80),不是宿主机端口(8080)。想按宿主机端口匹配,得用 conntrack:

# 按"原始目的端口"匹配,即用户实际访问的 8080
iptables -I DOCKER-USER -p tcp -m conntrack --ctorigdstport 8080 -j DROP

这个坑我见过太多次:规则写了 --dport 8080 却完全不生效,因为包到这里时端口已经被改写成 80 了。

四、五种收口方案,按推荐度排序

### 方案 1:只绑回环地址(最简单,优先考虑)

如果容器只需要被本机的 Nginx / Caddy 反代,根本不需要对外暴露:

docker run -d -p 127.0.0.1:8080:80 nginx

Docker 生成的 DNAT 规则会带上目的地址限制,只有访问 127.0.0.1:8080 的包才会被转发,外部流量连匹配都匹配不上。

Compose 里同理:

services:
  web:
    image: nginx
    ports:
      - "127.0.0.1:8080:80"

优点:零防火墙知识、零维护、不会因为 Docker 重启丢规则。

限制:必须有反代或本机客户端;纯 TCP 服务(如数据库)也能用,但要注意反代层要支持对应协议。

### 方案 2:DOCKER-USER 链(最灵活,生产推荐)

DOCKER-USER 是 Docker 明确保留给用户的链,位置在 FORWARD 最前,且不会被 Docker 重建时清空(Docker 只保证这条链存在,不碰你的规则)。

只允许办公网访问 8080:

iptables -I DOCKER-USER -i eth0 -p tcp --dport 80 \
  ! -s 203.0.113.0/24 -j DROP

允许已建立连接回流(否则响应包会被自己拦掉):

iptables -I DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

只允许特定源 IP 段访问某个"原始端口":

iptables -I DOCKER-USER -i eth0 -m conntrack --ctorigdstport 8080 \
  ! --ctorigsrc 203.0.113.0/24 -j DROP

注意顺序:-I 是插入到链首,多条规则时后插入的先生效。建议先写 ACCEPT 类规则,再写兜底 DROP,或者用 -I DOCKER-USER <序号> 精确控制。

持久化是这套方案唯一的麻烦:iptables 规则重启即失。别用 iptables-persistent 保存——它会把 Docker 动态生成的 DOCKER、DOCKER-FORWARD 链一起存下来,下次开机 Docker 还没建链,恢复就会失败。

干净的写法是挂一个 systemd 单元,等 Docker 起来之后再灌规则:

# /etc/systemd/system/docker-firewall.service
[Unit]
Description=Apply custom DOCKER-USER rules
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/docker-firewall.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
#!/bin/sh
# /usr/local/sbin/docker-firewall.sh
set -e
iptables -I DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -I DOCKER-USER -i eth0 -p tcp --dport 80 ! -s 203.0.113.0/24 -j DROP
chmod +x /usr/local/sbin/docker-firewall.sh
systemctl daemon-reload
systemctl enable --now docker-firewall.service

### 方案 3:firewalld 的 docker zone 与 direct 规则

新版 Docker 在检测到 firewalld 时,会自动创建一个 target 为 ACCEPT 的 docker zone,并把 docker0 等网桥接口塞进去,同时建立 docker-forwarding 策略。

这带来一个副作用:容器相关的流量在 firewalld 层面被整体放行了。你在 public zone 里删端口、加 rich rule,都管不到已经发布出去的容器端口。

想在 firewalld 体系里拦,可以用 direct 规则直接落到 DOCKER-USER:

firewall-cmd --permanent --direct --add-rule ipv4 filter DOCKER-USER 0 \
  -i eth0 -p tcp --dport 80 -j DROP
firewall-cmd --reload

坑:direct 规则在 firewalld 启动时下发,如果此时 DOCKER-USER 链还没被 Docker 创建,规则会下发失败。要么保证 docker.service 先于 firewalld 启动,要么接受"重载一次 firewalld"的操作习惯。相比之下,方案 2 的 systemd 单元更稳。

### 方案 4:ufw 的 after.rules

Ubuntu 上用 ufw 的话,得在 /etc/ufw/after.rules 的 *filter 段末尾补一条链声明和规则:

*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -i eth0 -p tcp --dport 80 ! -s 203.0.113.0/24 -j DROP
-A DOCKER-USER -j RETURN
COMMIT

然后 ufw reload。注意 -A DOCKER-USER -j RETURN 一定要放在最后,否则链尾默认策略是 DROP,会把容器的正常出网也一起掐了。

同样存在"链未创建"的时序问题,且 ufw 与 Docker 的兼容性官方并不承诺,属于能跑但别指望长期稳定的方案。

### 方案 5:关掉 Docker 的 iptables 管理(不推荐,除非你很懂)

在 /etc/docker/daemon.json 里:

{
  "iptables": false
}

关掉之后 Docker 不再写规则,端口映射由你自己用 NAT 实现。但代价是:容器默认上不了外网(没有 MASQUERADE),容器间隔离没了,-p 参数基本失效,还得自己处理 DNS。

官方对这件事的评价是:这选项对大多数用户不合适,很可能直接搞坏容器网络;而且也没法完全阻止 Docker 建规则。

除非你在做 SDN、需要完全接管转发面,否则别碰。

五、方案对比

方案 生效位置 维护成本 适用场景 主要风险
绑定 127.0.0.1 nat DNAT 目的地址 极低 有反代、只需本机访问 必须改造调用链路
DOCKER-USER filter FORWARD 最前 中(需持久化) 生产环境精细控制 规则顺序、conntrack 匹配
firewalld direct filter DOCKER-USER 中 已统一用 firewalld 启动时序
ufw after.rules filter DOCKER-USER 中 Ubuntu 单机 官方不承诺兼容
关闭 iptables — 高 特殊网络架构 容器断网、隔离失效

六、上线前的自检清单

1. 看谁在监听

ss -lntp | grep -E ':(8080|80)\s'

如果看到 docker-proxy 监听在 0.0.0.0:8080,说明端口是对全网开放的。

2. 看 DNAT 规则

iptables -t nat -S DOCKER | grep 8080

没有 -d 127.0.0.1 之类的限制,就是全量转发。

3. 看用户链是否真的生效

iptables -S DOCKER-USER
iptables -L DOCKER-USER -n -v --line-numbers

-v 的包计数是判断规则有没有被命中的最直接证据。计数一直是 0,说明规则位置不对或者匹配条件写错了。

4. 从外部验证,不要只看配置

# 在另一台机器上执行
nc -zv <公网IP> 8080
curl -m 3 -I http://<公网IP>:8080

配置对不对,以外部实际能否连通为准。

5. 别忘了 IPv6 和云安全组

  • 如果 daemon 开了 IPv6,ip6tables 的 DOCKER-USER 要单独配一遍,两套规则互不影响。
  • 主机防火墙是最后一道,不是唯一一道。云上务必同时收紧安全组,两层都拦才是真的收口。

七、几个常见误解

"关掉 userland-proxy 就安全了"

不成立。--userland-proxy=false 只是不启 docker-proxy 进程,DNAT 规则照旧存在,端口依旧可达。它不是安全开关。

"firewalld 里把 docker0 加进 trusted zone 就解决了"

方向反了。那是放开容器流量,不是限制。

"容器没发布端口就一定安全"

也未必。--network host 的容器直接用宿主网络栈,端口监听等同于宿主机进程;macvlan/ipvlan 的容器在二层直接暴露,同样绕开 Docker 的转发规则。

"重启 Docker 后规则还在"

DOCKER-USER 里的规则通常能活下来,但如果你用 iptables-save/restore 做持久化,很可能在 Docker 重建链时被冲掉或者恢复失败。用 systemd 单元按依赖顺序下发,是省心的做法。

八、小结

一句话总结这套机制:Docker 在 nat 表把包改道了,防火墙的 INPUT 规则从设计上就看不到这些包;而 FORWARD 里 Docker 的链又排在你的规则前面。

所以正确的姿势只有三个方向:

  1. 不让它暴露——绑 127.0.0.1,用反代对外;
  2. 在 DOCKER-USER 里拦——这是 Docker 留给你的唯一官方钩子,注意 DNAT 后的端口语义;
  3. 在云安全组再兜一层——主机规则会写错,安全组不会跟着一起错。

把这三层叠起来,容器端口才算真正"收口"。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。