Docker 绕过防火墙的端口映射问题

问题发现

早上检查服务器时发现他妈的数据库被黑了,排查后才想起防火墙端口忘记关闭。立即登录服务器关闭了防火墙对应端口。

但远程测试时发现:端口居然还能连上!


问题原因

Docker 网络流量示意图

我的数据库服务是通过 Docker 启动的,这正是问题的根源。

Docker 默认把所有转发到容器的流量全部放行,无条件 ACCEPT。

Docker 的 iptables 行为

Docker 默认会在系统 iptables 中自动添加 NAT/FORWARD 规则,将映射端口直接放行。外部流量会绕过 INPUT 链,因此即使防火墙(UFW)没有开放端口,也能访问容器服务。

当 Docker 启动容器并映射端口(如 -p 3307:3306,等价于 -p 0.0.0.0:3307:3306)时,会自动执行以下操作:

  1. 在 nat/PREROUTING 链挂上 DOCKER 跳转,再由 nat/DOCKER 链添加 DNAT 规则,把宿主机端口转发到容器 IP 端口
  2. 在 filter/FORWARD 链插入 DOCKER-USER 和 DOCKER 跳转,由 filter/DOCKER 链添加 ACCEPT 规则,放行转发流量

实测一下,一眼就能看出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ sudo iptables -t nat -L DOCKER -n
Chain DOCKER (2 references)
num target prot opt source destination
1 RETURN all -- 0.0.0.0/0 0.0.0.0/0
2 DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:3307 to:172.17.0.2:3306

$ sudo iptables -L DOCKER -n
Chain DOCKER (1 references)
num target prot opt source destination
1 ACCEPT tcp -- 0.0.0.0/0 172.17.0.2 tcp dpt:3306

$ sudo iptables -L FORWARD -n --line-numbers | head -5
Chain FORWARD (policy DROP)
num target prot opt source destination
1 DOCKER-USER all -- 0.0.0.0/0 0.0.0.0/0
2 DOCKER-ISOLATION-STAGE-1 all -- 0.0.0.0/0 0.0.0.0/0
3 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
4 DOCKER all -- 0.0.0.0/0 0.0.0.0/0

注意 FORWARD 链的 policy 可能是 DROP,但第 4 条 DOCKER 跳转里的 ACCEPT 已经把到容器的流量无条件放行了——这正是”防火墙看着是关的,端口却是开的”的根因。

流量路径分析

UFW 规则作用于 filter/INPUT 链,而外部访问 Docker 映射端口时,流量走的是:

1
外网 → PREROUTING (DNAT) → FORWARD → 容器

报文在 PREROUTING 被 DNAT 之后,目的地址已经变成了容器 IP(172.17.0.2),不再是宿主机本机地址。内核一看目的地址不是自己,就判定为「转发」,于是走 FORWARD 链而不是 INPUT 链。

整个 iptables 链的跳转结构长这样,对照着看就清楚了:

Docker 端口映射在 iptables 中的链跳转结构 报文进入 nat/PREROUTING 后做路由判定:目的地址为本机则走 filter/INPUT,UFW 规则生效;被 DNAT 成容器 IP 则走 filter/FORWARD,依次经过 DOCKER-USER 和 DOCKER 链,最终到达容器。 nat / PREROUTING 路由判定 目的地址是本机吗 filter / INPUT UFW 规则在此生效 filter / FORWARD 转发流量走这里 DOCKER-USER 自定义白名单插这里 DOCKER 自动 ACCEPT(绕过点) 容器 172.17.0.2:3306 是 否(DNAT 命中)

由于流量不经过 INPUT 链,所以即使 UFW 禁止了端口,Docker 映射的端口仍然可以被外部访问。

注意 DOCKER-USER 链在 FORWARD 里的位置排在 DOCKER 链之前——这正是它能用来做白名单的原因:你写的 DROP 规则会先于 Docker 自动生成的 ACCEPT 被命中。

为什么绑定 127.0.0.1 就不一样

如果映射写成 -p 127.0.0.1:3307:3306,情况完全不同:

  1. DNAT 规则带上了 -d 127.0.0.1/32 的匹配条件,公网来的报文目的地址是你的公网 IP,匹配不上任何 DNAT 规则
  2. 匹配不上 DNAT,报文就还是「发给宿主机本机端口」的报文,正常走 INPUT 链,UFW 规则生效
  3. 退一步说,就算 UFW 放行了,docker-proxy 也只在 127.0.0.1:3307 上监听,公网 IP 上压根没有监听进程,照样连不上

这就是后面方案一为什么是首选的原因——一行参数,零副作用。


解决方案

方案一:端口只绑回环地址(推荐,最小改动)

1
2
3
docker run -d --name mysql \
-p 127.0.0.1:3307:3306 \
mysql:8

docker-compose 里同理:

1
2
3
4
5
services:
mysql:
image: mysql:8
ports:
- "127.0.0.1:3307:3306"

对外访问统一走本机的反向代理(Nginx / Caddy)转发到 127.0.0.1:3307,代理层再做鉴权和 IP 白名单。容器完全不出现在公网可达面里,这是最干净的做法。

方案二:在 DOCKER-USER 链上做白名单(推荐)

DOCKER-USER 是 Docker 官方预留的自定义链,挂在 FORWARD 链的最前面,优先级高于 Docker 自己写的 ACCEPT 规则,是放自定义规则的唯一正确位置。

只放行内网网段,其余全部丢弃:

1
2
3
4
5
# 注意:DOCKER-USER 链末尾有一条 -j RETURN,规则必须插在它前面
sudo iptables -I DOCKER-USER -p tcp --dport 3306 ! -s 10.0.0.0/8 -j DROP

# 查看结果
sudo iptables -L DOCKER-USER -n --line-numbers

因为 DNAT 只改目的地址、不改源地址,所以这里看到的 -s 就是真实客户端 IP,按 IP 过滤是可靠的。

有一点必须注意:手工加的 iptables 规则重启后会丢。Docker 重启时不会清空 DOCKER-USER 链,但宿主机重启后规则就没了,需要持久化:

1
2
sudo apt install iptables-persistent
sudo netfilter-persistent save

或者写个 systemd unit 在 docker.service 之后执行这段 iptables 命令。

方案三:使用 UFW-Docker 插件

不想手写 iptables 的话,用社区脚本让 UFW 能正确管理 Docker 端口:

1
2
3
4
5
sudo wget -O /usr/local/bin/ufw-docker \
https://github.com/chaifeng/ufw-docker/raw/master/ufw-docker
sudo chmod +x /usr/local/bin/ufw-docker
sudo ufw-docker install
sudo systemctl restart ufw

允许特定 IP 访问容器端口:

1
sudo ufw-docker allow mysql 3306/tcp 192.168.1.100

装完记得校验规则是否真的落到了 DOCKER-USER 链里:

1
2
sudo ufw-docker check
sudo iptables -L DOCKER-USER -n

另外:ufw-docker 和 iptables: false 不能同时用,如果之前改过 daemon.json,要先把 "iptables": false 删掉再重启 Docker。

方案四:关闭 Docker 的 iptables 管理(不推荐)

网上流传最广但坑最深的一个方案:

1
2
3
{
"iptables": false
}

改完 sudo systemctl restart docker,UFW 确实能管住端口了——但代价是:

后果 说明
容器出不了网 Docker 不再写 POSTROUTING 的 MASQUERADE 规则,容器发的包没有源地址转换,直接回不来
容器间跨网段不通 DOCKER-ISOLATION-STAGE-* 链不再创建,自定义 bridge 网络之间的隔离和转发全乱
端口发布变成用户态代理 端口实际由 docker-proxy 用户态进程转发,性能下降、连接数受限于进程模型
要手写全套 NAT 每次新建容器网络都得自己补 MASQUERADE 和 DNAT

真要用就得自己把整套 NAT 补齐:

1
sudo iptables -t nat -A POSTROUTING -s 172.17.0.0/16 -j MASQUERADE

结论:这是”用大炮打蚊子”,只有在完全自管 iptables 的场景才考虑。生产环境优先方案一或方案二。


常见坑排查清单

问题现象 可能原因 排查命令
防火墙关了端口还能访问 Docker 在 nat/DOCKER 写了 DNAT、在 filter/DOCKER 写了 ACCEPT iptables -t nat -L DOCKER -n
iptables -L DOCKER -n
UFW 状态显示正常但不生效 流量走 FORWARD 链,没经过 UFW 挂的 INPUT 链 iptables -L FORWARD -n --line-numbers
DOCKER-USER 里加的规则不生效 规则加到了链末尾的 -j RETURN 之后 iptables -L DOCKER-USER -n --line-numbers
容器无法访问外网 禁用 iptables 后未配置 NAT iptables -t nat -A POSTROUTING -s 172.17.0.0/16 -j MASQUERADE
容器间无法通信 网络模式或 DOCKER-ISOLATION 链问题 docker network ls
iptables -L DOCKER-ISOLATION-STAGE-1 -n
重启后规则失效 手工规则未持久化 sudo netfilter-persistent save
端口发布只在本机能连 正常现象,说明绑定了 127.0.0.1 docker port <容器名>

总结

Docker 默认的 iptables 管理机制虽然方便,但会绕过系统防火墙,带来安全隐患。生产环境建议:

  1. 敏感服务不要直接映射到公网,端口绑定写成 127.0.0.1:宿主机端口:容器端口,对外访问走反向代理
  2. 需要放行公网时,用 DOCKER-USER 链做源 IP 白名单,并做好规则持久化
  3. iptables: false 是最后手段,会破坏容器网络,非必要不用

安全无小事,别等被黑了才想起来补救。


Docker 绕过防火墙的端口映射问题
https://blog.newpon.top/2025/12/01/服务器端口拦截问题/
作者
Niezidong
发布于
2025年12月1日
许可协议