Docker 基础体系 · 第 46/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。

Docker Bridge 与 iptables/nftables:NAT、端口发布、转发和冲突

Docker Bridge 网络不是“容器直接连到物理网卡”。在 Linux 容器中,Docker 通常创建一个 Linux bridge,例如默认的 docker0,再通过 veth pair 把每个容器的网络命名空间连接到这个 bridge。iptables 或 nftables 则负责在这个二层网络之上实现三类关键行为:

  1. 容器访问外部网络时的地址转换,即 SNAT/MASQUERADE;
  2. 外部或宿主机访问容器发布端口时的 DNAT;
  3. 是否允许数据包跨越宿主机网络接口转发,即 forwarding/filtering。

这里的“iptables”既可能指传统的 xtables 内核接口,也可能指运行在 nftables 后端上的 iptables-nft 命令。两者命令形式相似,但实际规则存储和与原生 nftables 规则的交互方式可能不同。现代 Docker Engine 的具体防火墙后端取决于 Engine 版本、发行版和 daemon 配置;Linux 容器默认应先按“Docker 管理 iptables 兼容规则”理解,再通过实际规则集确认。


一、先建立网络模型:Bridge、Namespace 和 veth

1. Linux Bridge 是什么

Linux bridge 是内核中的虚拟二层交换机。它有多个端口,可以转发以太网帧。Docker 的典型拓扑如下:

graph LR
    C1["容器 netns<br/>eth0 172.17.0.2"]
    V1["veth pair"]
    B["docker0 bridge<br/>172.17.0.1"]
    H["宿主机网络栈"]
    U["物理网卡<br/>192.0.2.10"]
    R["外部网络"]

    C1 --- V1 --- B --- H --- U --- R

容器内部看到的是自己的 eth0,宿主机通常看到的是对应的 veth 一端。另一端被加入 docker0

例如:

docker network inspect bridge
ip addr show docker0
ip link
bridge link

典型结果中可能看到:

docker0: inet 172.17.0.1/16

以及某个容器:

容器 eth0: 172.17.0.2/16
默认网关: 172.17.0.1

这里的地址只是默认配置示例,不是 Docker 的规范固定值。自定义 bridge、Compose 网络或其他 Docker 网络都可能使用不同子网。

2. Network namespace 决定“从哪里看网络”

容器的网络设备、路由表和部分 iptables 视图属于容器自己的 network namespace。宿主机执行:

ip route

看到的是宿主机路由,不是容器路由。

可以用以下方式查看容器内网络:

docker exec <container> ip addr
docker exec <container> ip route
docker exec <container> cat /etc/resolv.conf

如果镜像中没有 ip 命令,可以使用宿主机进入容器 namespace:

PID=$(docker inspect -f '{{.State.Pid}}' <container>)
nsenter -t "$PID" -n ip addr
nsenter -t "$PID" -n ip route

nsenter 要求宿主机具备相应权限,并且容器正在运行。

3. Bridge 网络的三种通信方向

对于一个典型 bridge 网络,需要区分三个方向:

  • 容器到容器:数据包通常只经过 bridge 的二层转发,是否允许同一 bridge 上的容器互通还受 Docker 网络配置和过滤规则影响;
  • 容器到外部网络:数据包离开 Docker bridge,经过宿主机三层转发,通常需要 SNAT/MASQUERADE;
  • 外部网络到容器:如果使用端口发布,宿主机上的目标端口会被 DNAT 到容器 IP 和容器端口。

“容器有 IP”不等于“外部主机可以直接访问这个 IP”。容器子网通常只存在于宿主机及其二层网络内部,外部路由器并不知道如何返回 172.17.0.0/16 这类 Docker 私有网段。


二、iptables 与 nftables 在这里分别负责什么

1. iptables 是规则管理接口,不是单一的数据包处理机制

iptables 命令用于管理 Linux 内核中的 netfilter 规则。常见表和职责如下:

主要用途 Docker 场景
nat 地址转换 容器出站 MASQUERADE、发布端口 DNAT
filter 放行或丢弃 转发控制、bridge 间隔离
mangle 修改标记、TTL、DSCP 等 通常不是 Docker Bridge 的核心
raw conntrack 前置控制 诊断或特殊网络策略

常见链包括:

  • PREROUTING:路由判断前处理收到的数据包;
  • INPUT:目标是本机的数据包;
  • FORWARD:需要经过本机转发的数据包;
  • OUTPUT:本机进程发出的数据包;
  • POSTROUTING:路由判断之后、离开接口之前处理。

Docker 通常还会创建自定义链,例如:

DOCKER
DOCKER-USER
DOCKER-FORWARD
DOCKER-BRIDGE
DOCKER-CT

具体链集合和顺序随 Docker Engine 版本、配置和防火墙后端变化。不要把某一台机器上看到的链顺序当成稳定 API。

2. nftables 是内核规则体系,iptables-nft 是兼容入口

现代 Linux 发行版常见两种情况:

iptables --version

可能输出:

iptables v1.8.x (nf_tables)

这表示 iptables 命令通过 nftables 兼容后端工作,规则通常会映射到 nftables 的 ipip6 或兼容表中。

也可能输出:

iptables v1.8.x (legacy)

这表示使用传统 xtables 后端。

直接查看 nftables 规则:

sudo nft list ruleset

查看 iptables 规则及计数器:

sudo iptables -t nat -vnL --line-numbers
sudo iptables -vnL --line-numbers

使用 iptables-nft 时,不能简单认为“iptables 规则”和“nft 规则”是两套完全独立且互不影响的机制。它们可能共同进入 netfilter,但表、优先级、兼容链和原生 nftables 链之间的关系依赖实现。反过来,如果系统仍使用 legacy 后端,原生 nftables 规则也不一定会与 legacy 规则按预期协同。

因此,排障时应同时确认:

iptables --version
sudo nft list ruleset
sudo iptables-save

3. Docker 的防火墙后端必须以实际版本为准

Docker Engine 长期以来主要通过 iptables 管理 bridge 网络所需的 NAT 和过滤规则。较新的 Engine 版本可能提供 nftables 防火墙后端或相关实验能力,但可用性、配置项、与现有防火墙管理器的交互属于版本相关行为。

不能仅因为系统安装了 nftables,就推断 Docker 一定使用原生 nftables;也不能仅因为执行了 iptables 命令,就推断规则一定存储在 legacy 后端。应检查当前 Engine 文档、daemon 配置和实际规则集。


三、容器出站:为什么需要 MASQUERADE

设:

  • 容器地址为 C = 172.18.0.2
  • Docker bridge 地址为 B = 172.18.0.1
  • 宿主机外部地址为 H = 192.0.2.10
  • 外部服务器为 S = 198.51.100.20
  • 容器访问外部 TCP 443 端口。

容器最初发送的数据包可以表示为:

源地址:端口 = 172.18.0.2:40000
目的地址:端口 = 198.51.100.20:443

如果宿主机直接把这个包转发出去,而不做源地址转换,外部服务器的响应会发给:

172.18.0.2:40000

外部网络通常没有到 172.18.0.0/16 的路由,因此响应无法返回容器。

Docker 常见的规则逻辑是:

源地址属于 Docker 子网
并且出口接口不是该 Docker bridge
    => 对源地址执行 MASQUERADE

发送到外部网卡后,数据包变成:

源地址:端口 = 192.0.2.10:53000
目的地址:端口 = 198.51.100.20:443

这里端口可能被保留,也可能因为冲突被改写。MASQUERADE 是一种动态 SNAT,适合宿主机外部 IP 可能变化的场景。若宿主机有固定地址,也可以从设计上使用 SNAT,但 Docker 自动管理的规则通常会选择 MASQUERADE。

1. conntrack 让返回包能够还原

NAT 不是“每个包独立改地址”。内核通过 connection tracking,也就是 conntrack,保存连接映射:

内部方向:
172.18.0.2:40000 -> 198.51.100.20:443
转换为:
192.0.2.10:53000 -> 198.51.100.20:443

返回方向:

198.51.100.20:443 -> 192.0.2.10:53000

根据 conntrack 状态还原为:

198.51.100.20:443 -> 172.18.0.2:40000

因此,一条 TCP 连接通常只在第一个数据包经过 NAT 规则时决定映射,后续数据包依靠连接跟踪状态处理。这也解释了两个现象:

  • 修改 NAT 规则后,已有连接可能继续沿用旧映射;
  • 清空 conntrack 或重启相关网络组件可能中断已有连接。

查看连接跟踪需要安装 conntrack 工具:

sudo conntrack -L

权限、内核模块和发行版配置可能导致该命令不可用。

2. 转发开关是另一个独立条件

即使 NAT 规则存在,Linux 也必须允许 IPv4 转发:

sysctl net.ipv4.ip_forward

预期通常是:

net.ipv4.ip_forward = 1

如果结果为 0,容器可以访问自己的 bridge 网关,却可能无法访问外部网络。临时修改:

sudo sysctl -w net.ipv4.ip_forward=1

永久配置应由系统网络管理规范维护,而不是只依赖一次手工执行。IPv6 使用不同的 sysctl 和地址转换模型,不能把 IPv4 的结论直接套用到 IPv6。

3. 出站访问的必要条件

对一个典型 IPv4 bridge 出站连接,至少需要满足:

容器有可用默认路由
∧ bridge 能把包送到宿主机
∧ 宿主机允许 FORWARD
∧ 出口路径存在
∧ 外部回程可达
∧ 若外部没有 Docker 子网路由,则存在 SNAT/MASQUERADE

这是一个合取条件。只缺任意一项,都可能出现“容器能解析 DNS,但不能访问互联网”或“TCP SYN 发出但没有 SYN-ACK”的故障。


四、端口发布:-p 到底改变了什么

1. EXPOSE 不等于端口发布

Dockerfile 中的:

EXPOSE 8080

主要是镜像元数据,表达应用预计监听 8080 端口。它不会自动在宿主机打开端口,也不会创建 DNAT。

以下命令才会创建发布行为:

docker run -d --name web -p 8080:80 nginx

其语义通常是:

宿主机地址:8080
    DNAT
容器地址:80

如果未指定宿主机地址,Docker 常见行为是绑定宿主机所有 IPv4 地址:

0.0.0.0:8080 -> 容器:80

显式绑定本机回环地址:

docker run -d --name web-local -p 127.0.0.1:8080:80 nginx

此时外部机器不能通过宿主机的物理地址访问该发布端口,宿主机本地进程仍可以访问。

IPv6、协议类型和绑定地址应明确指定。例如:

docker run -d --name dns-tcp -p 127.0.0.1:8053:53/tcp nginx

这只是端口映射语法示例;Nginx 默认并不提供 DNS 服务,不能把它当作可工作的 DNS 容器。

2. 外部数据包的实际路径

假设:

宿主机外部地址: 192.0.2.10
发布端口:       8080
容器地址:       172.18.0.2
容器端口:       80

外部客户端发送:

203.0.113.50:50000 -> 192.0.2.10:8080

在宿主机的 nat PREROUTING 阶段,Docker 规则将其转换为:

203.0.113.50:50000 -> 172.18.0.2:80

随后进行路由判断。因为新目标 172.18.0.2 位于 Docker bridge 子网,数据包进入 FORWARD 路径,经过 veth 和 bridge,到达容器。

完整路径可以概括为:

sequenceDiagram
    participant C as 外部客户端
    participant N as 宿主机 PREROUTING
    participant D as Docker DNAT
    participant F as FORWARD/filter
    participant B as docker bridge
    participant A as 容器应用

    C->>N: 192.0.2.10:8080
    N->>D: 进入 nat PREROUTING
    D->>F: DNAT 到 172.18.0.2:80
    F->>B: 允许转发
    B->>A: veth -> eth0
    A-->>C: 返回流量按 conntrack 反向 NAT

关键点是:发布端口的包在 DNAT 后通常不是“宿主机本地服务接收”,而是“宿主机转发到容器”。因此它主要经过 FORWARD,不应只检查 INPUT

3. Docker 自定义链与规则位置

在传统 iptables 视图中,可能看到类似:

sudo iptables -t nat -S DOCKER
sudo iptables -S DOCKER-USER
sudo iptables -S DOCKER-FORWARD

具体输出可能类似:

-A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.18.0.2:80

不要手工依赖容器 IP 编写永久规则,因为容器重建后 IP 可能变化。Docker 会根据容器生命周期创建和删除相关规则。

DOCKER-USER 的意义是给管理员放置自定义过滤规则。它通常位于 Docker 允许规则之前,但一个非常重要的事实是:

到达 DOCKER-USER 时,发布端口的数据包通常已经完成 DNAT。

因此,在 DOCKER-USER 中匹配发布端口时,常常要匹配容器目标端口,而不是原始宿主机端口。例如原始目标是 192.0.2.10:8080,进入过滤路径时目标可能已经是 172.18.0.2:80

如果必须匹配 DNAT 前的原始地址和端口,应使用 conntrack 扩展,例如:

sudo iptables -I DOCKER-USER -p tcp -m conntrack \
  --ctorigdst 192.0.2.10 --ctorigdstport 8080 -j ACCEPT

这条规则依赖内核和 iptables 扩展支持,且读取原始连接元组通常比普通端口匹配更昂贵。规则真正生效前应先用计数器验证,不要直接把它用于生产全局策略。


五、宿主机本地访问发布端口与 Hairpin NAT

发布端口不只服务于外部客户端。宿主机自身访问:

curl http://127.0.0.1:8080

通常经过 nat OUTPUT,然后被 DNAT 到容器。

如果容器访问宿主机的外部地址和发布端口,例如:

容器 172.18.0.3
访问 192.0.2.10:8080
目标容器 172.18.0.2:80

这属于 hairpin NAT,也称 NAT loopback。数据包可能需要:

  1. 对发布端口执行 DNAT;
  2. 对源地址执行额外 SNAT,避免目标容器直接把响应发给原始容器而绕过 conntrack 的预期路径。

Docker 是否、何时以及如何为特定 bridge 场景创建 hairpin 相关规则,取决于网络模式、端口发布方式和 Engine 实现。出现以下现象时,不应只测试外部访问:

  • 外部访问 宿主机地址:发布端口 正常;
  • 宿主机本地访问正常;
  • 容器访问同一宿主机地址的发布端口失败。

应分别从宿主机和容器内抓包,并检查 conntrack 与 NAT 规则。对于容器间调用,通常更可靠的方式是直接使用 Compose 服务名或容器网络别名,而不是绕回宿主机发布端口。


六、转发、过滤与 NAT 不是同一个概念

1. NAT 决定“地址如何改写”

NAT 解决的是:

这个连接的源地址或目标地址如何转换

它不等价于访问控制。存在 DNAT 规则不意味着一定放行,存在 MASQUERADE 规则也不意味着一定允许转发。

2. filter 决定“数据包是否允许通过”

容器访问外部网络通常涉及:

docker0 -> eth0

外部访问发布端口通常涉及:

eth0 -> docker0

这两种方向都可能经过 FORWARD。检查默认策略和规则:

sudo iptables -L FORWARD -vn --line-numbers
sudo iptables -S FORWARD

如果默认策略是 DROP,则必须存在明确允许规则。Docker 通常会创建自己的转发链,但宿主机的 firewalld、云初始化脚本、安全产品或管理员规则也可能改变最终结果。

3. 默认 bridge 的容器互通并非单纯依赖 IP 路由

同一 bridge 上的容器通常可以通过二层交换互通。Docker 的 bridge 网络还涉及:

  • 是否允许同一网络上的容器间通信;
  • 不同 Docker bridge 之间是否隔离;
  • bridge 数据包是否经过 netfilter;
  • 宿主机上的转发策略是否允许该方向。

不同网络的容器不能仅因为都属于 172.18.0.0/16 就假定可以互通。实际应检查:

docker network inspect <network>
docker inspect <container>

并从容器内部测试目标服务,而不是只测试目标 IP 是否存在。


七、iptables 与 nftables 的冲突机制

1. “两边都写规则”可能造成重复或顺序不确定

以下组件都可能操作防火墙:

  • Docker Engine;
  • firewalld;
  • ufw;
  • Kubernetes 或其他容器运行时;
  • 云主机安全代理;
  • 手工执行的 iptables/nftables 脚本;
  • 网络管理服务在启动时恢复的规则。

冲突不一定表现为命令报错。更常见的是规则都成功写入,但因为优先级或链跳转顺序不同,最终数据包被提前接受、提前丢弃或进入错误的 NAT 规则。

在 nftables 中,链具有 hook 和 priority。两个规则可能挂在同一个 hook,但 priority 不同;iptables 兼容规则还可能出现在兼容表中。不能只看某条规则“存在”,必须确认它在目标数据包的实际路径上被执行。

2. ufw 的常见误判

许多系统中,管理员以为:

sudo ufw deny 8080/tcp

就一定阻止了 Docker 发布的 8080 端口。

但发布端口的数据包可能先在 nat PREROUTING 中被 DNAT 到容器,再进入 FORWARD,而不是进入宿主机的 INPUT。如果 ufw 只管理 INPUT,它可能看不到预期的目标。

验证方法不是查看 ufw 状态,而是:

sudo iptables -t nat -vnL --line-numbers
sudo iptables -vnL FORWARD --line-numbers
sudo iptables -S DOCKER-USER

并从另一台主机测试:

nc -vz 192.0.2.10 8080

生产环境中,应明确由哪一个组件负责 Docker 流量的入口控制,并把规则放在正确的转发路径,而不是只修改 INPUT

3. firewalld、zone 与 Docker bridge

firewalld 可能把 Docker bridge 接口加入某个 zone,也可能通过 direct rules 或 nftables backend 管理过滤路径。接口归属、zone 的 target、转发策略和 masquerade 设置会共同影响结果。

可检查:

sudo firewall-cmd --get-active-zones
sudo firewall-cmd --get-zone-of-interface=docker0
sudo firewall-cmd --list-all --zone=<zone>

命令是否存在取决于系统是否使用 firewalld。不要在已经由 firewalld 管理的系统中长期运行一套互不知情的规则恢复脚本,否则重载 firewalld 可能覆盖或重排规则。

4. nftables 的“默认 drop”会影响 Docker

原生 nftables 规则可能有:

chain forward {
    type filter hook forward priority filter; policy drop;
}

即使 Docker 的 DNAT 正确,数据包仍可能在 forward hook 被丢弃。此时现象通常是:

  • 宿主机端口监听或发布状态看起来正常;
  • NAT 计数器可能增加;
  • 容器应用没有收到连接;
  • 客户端超时或收到连接被拒绝之外的错误。

应使用计数器和抓包确认丢包点,而不是重复执行 docker restart


八、网络地址冲突:最隐蔽的 Bridge 问题

1. Docker 子网与物理网络重叠

假设 Docker bridge 使用:

172.18.0.0/16

而企业 VPN 也使用:

172.18.0.0/16

容器访问企业服务 172.18.10.20 时,容器路由表可能认为该地址就在本地 bridge 上:

172.18.0.0/16 dev eth0

于是数据包被送到 Docker bridge,而不是 VPN。此时 NAT 规则可能完全正确,但路由选择已经错误。

检查宿主机和容器路由:

ip route
docker exec <container> ip route
ip route get 172.18.10.20
docker network inspect <network>

解决方式是为 Docker 网络规划不与企业网、云 VPC、VPN 和宿主机路由重叠的地址池,例如在 daemon 配置中统一规划 default-address-pools。修改地址池通常只影响新建网络;已有网络可能需要重建,重建前应确认服务依赖和数据卷不受影响。

2. 端口冲突不是 NAT 冲突

两个容器都执行:

-p 8080:80

通常不能同时成功,因为同一宿主机地址、同一协议和同一端口不能被两个发布端口占用。Docker 会在创建或启动阶段报告端口分配失败。

这与“两个 NAT 规则都存在、匹配顺序冲突”不同。端口发布通常还涉及宿主机监听 socket 或端口分配检查。可以检查:

docker ps
docker port <container>
sudo ss -ltnp

以下两种发布可以共存:

127.0.0.1:8080 -> 容器 A:80
192.0.2.10:8080 -> 容器 B:80

因为宿主机绑定地址不同。但是否能从预期网络访问,还要考虑路由和过滤规则。

3. 多个 NAT 规则的匹配顺序

如果管理员手工添加一条比 Docker 规则更早匹配的 DNAT 规则:

192.0.2.10:8080 -> 其他目标

那么数据包可能根本不会到达 Docker 的 DOCKER 链。NAT 的第一包处理具有顺序性;“Docker 的规则存在”不能证明它获得了数据包。


九、一个可运行的端到端实验

以下实验在 Linux 宿主机上运行,要求:

  • Docker Engine 已安装并运行;
  • 当前用户有 Docker 权限;
  • 主机允许创建 bridge 和配置防火墙;
  • 端口 18080 未被占用。

创建一个监听 HTTP 端口的容器:

docker run -d \
  --name bridge-demo \
  -p 127.0.0.1:18080:80 \
  nginx:alpine

检查发布关系:

docker port bridge-demo

预期类似:

80/tcp -> 127.0.0.1:18080

检查容器 IP:

docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' bridge-demo

测试宿主机访问:

curl -v http://127.0.0.1:18080/

如果成功,说明至少满足:

  1. Nginx 在容器内监听 80;
  2. 容器 bridge 网络正常;
  3. 宿主机本地发布路径的 DNAT 或代理路径正常;
  4. 返回流量能够回到 curl 进程。

查看对应规则和计数器:

sudo iptables -t nat -vnL --line-numbers
sudo iptables -vnL FORWARD --line-numbers
sudo nft list ruleset

再次执行 curl,再观察计数器是否增长。计数器不增长通常说明:

  • 请求没有走到所查看的规则;
  • 实际使用了另一套防火墙后端;
  • 请求被本地 socket、代理或其他路径处理;
  • 查看的是错误的地址族,例如 IPv4 规则而请求使用 IPv6。

查看连接套接字:

sudo ss -ltnp | grep 18080

Docker 的具体实现可能使用宿主机端口监听辅助机制、内核 DNAT,或二者结合;因此不能把 ss 中是否存在某个 Docker 进程当作所有版本的固定判断标准。

最后清理:

docker rm -f bridge-demo

十、按数据包路径排障,而不是按组件猜测

1. 先确认应用是否真的监听

docker exec bridge-demo ss -ltn

如果容器内没有 ss,可以:

docker exec bridge-demo cat /proc/net/tcp

更直接的方式是从另一个同网络容器测试:

docker run --rm --network container:bridge-demo \
  curlimages/curl:latest -v http://127.0.0.1:80/

这个测试绕过了 bridge 到宿主机发布端口的路径,只验证应用是否在容器 namespace 内监听。

2. 确认 Docker 网络和路由

docker inspect bridge-demo
docker network ls
docker network inspect bridge
ip addr show docker0
ip route

重点观察:

  • 容器是否加入预期网络;
  • 容器 IP 是否变化;
  • 默认网关是否是对应 bridge 地址;
  • Docker 子网是否与 VPN 或主机路由重叠。

3. 在正确接口抓包

宿主机上可以同时抓 bridge、veth 和外部网卡:

sudo tcpdump -ni any 'tcp port 18080 or tcp port 80'

更细致地:

sudo tcpdump -ni docker0 tcp
sudo tcpdump -ni <外部网卡> tcp port 18080

从容器访问外部服务时:

sudo tcpdump -ni docker0 host 172.18.0.2
sudo tcpdump -ni <外部网卡> host <外部服务器IP>

常见判断方式:

  • 外部网卡能看到 SYN,docker0 看不到转换后的 SYN:DNAT 或路由前规则有问题;
  • docker0 能看到请求,容器 veth 看不到:bridge、veth 或过滤路径有问题;
  • 容器收到 SYN 并发出 SYN-ACK,但外部收不到:回程路径、SNAT 或过滤规则有问题;
  • 请求和响应都到达,但应用无响应:应用监听地址、协议或容器内部策略有问题。

4. 检查 ip_forward 和过滤策略

sysctl net.ipv4.ip_forward
sudo iptables -L FORWARD -vn --line-numbers
sudo nft list chain inet filter forward

最后一条命令中的表名和链名不一定存在;nftables 规则可能使用不同命名,应先执行:

sudo nft list ruleset

5. 检查发布端口是否仅绑定回环地址

如果执行:

docker run -d -p 127.0.0.1:18080:80 nginx:alpine

那么远程主机访问:

curl http://192.0.2.10:18080/

失败是预期行为,不是 Docker 网络故障。若目标是对外提供服务,应使用明确的外部绑定地址:

docker run -d -p 192.0.2.10:18080:80 nginx:alpine

或者根据实际需求绑定所有 IPv4 地址:

docker run -d -p 0.0.0.0:18080:80 nginx:alpine

绑定所有地址会扩大暴露面,必须同时考虑主机防火墙和云安全组。


十一、Docker 规则被覆盖或不应直接修改时怎么办

1. 不要把 Docker 生成链当作永久配置入口

容器启动、停止、网络创建和 daemon 重启都会使 Docker 重新计算规则。直接修改:

sudo iptables -t nat -I DOCKER ...

可能短期有效,但不一定能在容器重建后保留,也可能与 Docker 后续添加的规则顺序冲突。

管理自定义过滤策略时,优先考虑 Docker 提供的管理链,例如:

sudo iptables -I DOCKER-USER 1 ...

但仍必须验证当前 Docker 版本和防火墙后端是否提供该链,以及数据包在该链时是否已经 DNAT。

2. 规则更改应配合回滚

添加规则前保存当前状态:

sudo iptables-save > /root/iptables.before-docker-test
sudo nft list ruleset > /root/nft.before-docker-test

如果使用原生 nftables,应通过发行版支持的配置文件或服务管理规则,而不是只执行一次临时命令。iptables 与 nftables 混用时,回滚必须恢复对应后端,不要用 iptables-restore 试图恢复一份原生 nftables 配置。

3. 先验证,再重载防火墙

firewalld、ufw 或系统网络服务重载可能清理临时规则、改变链顺序或重新设置默认策略。修改前后都应检查:

docker ps
docker port <container>
sudo nft list ruleset
sudo iptables-save
curl ...

如果重载后容器发布端口立即失效,优先比较重载前后的规则差异和计数器,而不是直接重建所有容器。


十二、Bridge 模式的边界

本文讨论的是 Linux 容器使用的 bridge 网络。以下场景不能直接套用同一结论:

  • host 网络模式:容器直接使用宿主机网络 namespace,通常没有独立容器 IP,也不需要同样的 bridge DNAT 路径;
  • none 网络模式:容器几乎没有自动网络配置;
  • macvlan/ipvlan:数据路径和宿主机与容器之间的通信限制不同;
  • overlay 网络:通常还涉及 VXLAN、节点间封装和分布式控制面;
  • rootless Docker:常使用用户态网络方案,可能没有 rootful Docker 的内核 bridge、iptables 和端口发布路径;
  • Docker Desktop:Linux 容器可能运行在 Desktop 管理的 Linux VM 中,宿主机看到的端口转发还可能经过 Desktop 自身的虚拟化网络层。

此外,IPv4 的 MASQUERADE 经验不能直接推导 IPv6。IPv6 通常强调全局可路由地址和过滤策略,Docker 是否启用 IPv6、是否使用 NAT66 以及外部路由如何配置,都需要单独验证。


十三、几个容易混淆的结论

“端口发布成功,所以外部一定能访问”

不成立。端口发布成功只表示 Docker 接受了配置,并创建或准备了相应映射。外部访问还受以下因素影响:

绑定地址
∧ 地址族
∧ 宿主机路由
∧ DNAT
∧ FORWARD 过滤
∧ 云安全组
∧ 应用监听地址
∧ 返回路径

“容器能访问宿主机,所以容器能访问互联网”

不成立。访问 bridge 网关是本地路径,不需要外部路由和出站 MASQUERADE。互联网访问还需要宿主机转发、出口路由、源地址转换和外部回程。

“iptables 里有规则,所以 nftables 不会影响它”

不成立。iptables 可能使用 nftables 兼容后端,也可能与原生 nftables 规则共同参与 netfilter。最终结果取决于实际 hook、priority、链跳转和策略。

“把 FORWARD 默认策略设为 ACCEPT 就能解决所有问题”

也不成立。它可能绕过某个过滤问题,却不能修复:

  • Docker 子网与 VPN 重叠;
  • DNAT 目标错误;
  • 应用没有监听;
  • 宿主机端口绑定错误;
  • conntrack 状态异常;
  • 外部网络没有回程路径。

更严重的是,全局放开转发可能扩大容器和其他网络的暴露范围。


Docker Bridge 的核心可以用一条数据流概括:

容器出站:
容器地址 --bridge--> 宿主机转发 --MASQUERADE--> 外部网络

端口发布入站:
外部地址:宿主机端口 --DNAT--> 容器地址:容器端口 --FORWARD--> bridge --> 容器

返回流量:
依靠 conntrack 反向还原 NAT 映射

其中,Bridge 提供二层连接,路由决定下一跳,NAT 改写地址和端口,filter 规则决定是否允许通过,conntrack 保存连接状态;iptables 或 nftables 只是这些内核机制的规则管理入口。排障时沿着这条路径逐段验证,通常比单独检查某个 Docker 命令或某条防火墙规则更可靠。


系列导航与关联阅读

官方资料

本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。