☰
Docker网络核心:默认bridge驱动与iptables交互机制全解析
2026/10/8 8:59:57 网站建设 项目流程

很多人第一次认真研究Docker网络,多半是因为线上出了“怪事”:容器明明起来了,端口映射也没写错,可从外面就是访问不了;或者安全同事在防火墙上做了访问控制,Docker容器立刻开始“耍脾气”。这些问题的幕后推手,其实就是标题里这两个词——Docker默认网络驱动,以及它和iptables的那套交互逻辑。把这层关系看透了,绝大多数容器网络故障都能一眼定位,这篇文章就专门聊聊这个。

1. 默认网络驱动:为什么Docker偏偏选了bridge

不带任何网络参数跑一个容器,Docker会默认把容器挂到bridge网络上。这不是随手选的默认值,而是综合了隔离性、易用性、跨宿主机迁移成本之后的最优解。理解了这个设计初衷,后面看iptables规则时思路会清晰很多。

1.1 bridge网络的三个核心设计目标

bridge网络在Linux里就是虚拟网桥,相当于一台纯二层的交换机。Docker默认的bridge网络会在宿主机上创建一张名为docker0的虚拟网卡,并分配一个私有网段(通常是172.17.0.0/16)。每个新容器启动后,会拿到这个网段里的一个IP,同时通过veth虚拟网线把容器内部的eth0和docker0桥接起来。

第一个设计目标是无感联网。容器内不需要做任何手工IP配置,Docker自动从docker0的地址池里分配地址,容器起来就能通信。它不像host网络那样依赖宿主机的IP,也不像overlay网络那样需要额外的键值存储。

第二个目标是安全隔离。不同容器之间的流量默认是隔离的,除非你显式地用--link或者加入同一个自定义bridge网络,否则容器A无法直接访问容器B。这个隔离不是靠二层VLAN实现的,而是靠iptables的FORWARD链规则,这是Docker和防火墙产生交集的关键原因。

第三个目标是端口映射的可移植性。容器内部的IP是私有的,宿主机外部网络根本不可达。Docker解决这个问题的标准手段是用iptables做DNAT,把宿主机端口上的流量转发进容器。这个机制在单机上、在虚拟机里、在云主机上都通用,不需要依赖具体的云平台SDN能力。

1.2 docker0网桥的工作机制

可以把docker0想象成一台嵌在Linux内核里的交换机,但它有个特殊之处:这台“交换机”本身还带了一个IP(172.17.0.1),并且这个IP被当作容器流量的默认网关。

当容器访问外部网络时,数据包从容器eth0发出,经过veth对进入docker0,然后由内核的路由判断走宿主机eth0出去。这里有两个关键点:

  • 容器eth0和veth对是一对虚拟网卡,网卡A收到的包,网卡B原封不动地收下,反之亦然。
  • docker0网桥的MAC地址学习、转发逻辑和物理交换机一模一样,同网段容器互访时,数据包在docker0内部就完成交换了,根本不会上到宿主机的iptables FORWARD链。

所以你看,同网段容器互访和对外访问的流量路径完全不同,前者是纯二层交换,后者才涉及三层的路由和iptables。这个区分在排查问题的时候特别重要,很多人一看到容器访问不了外网就想去动FORWARD链,结果把同网段互访也带崩了。

1.3 为什么不默认选host或overlay

host网络驱动把容器直接塞进宿主机网络栈,没有独立IP,性能确实好,但有两个致命缺陷:端口冲突要靠人工管理,容器之间没有网络隔离,任何一个端口监听都是全局的。这跟Docker“轻量隔离”的核心理念冲突,所以只能手工指定使用。

overlay网络是跨宿主机容器通信的标配,但它依赖一个外部的键值存储(Swarm模式下内置,单机Docker得自己搭),还得配置VXLAN隧道。对单机场景来说,引入的复杂度和收益完全不成比例。bridge网络则只要Linux内核自带bridge模块就能跑,零额外依赖。

提示:docker-compose如果不显式指定network_mode,会默认创建一个独立的bridge网络而不是复用默认的docker0。二者行为基本一致,但自定义bridge网络额外支持基于IP的DNS解析。

2. iptables交互的核心机制:三种数据包路径里的规则链

Docker和iptables的交互,围绕三个关键环节展开:入站的DNAT转发、出站的MASQUERADE源地址转换、以及容器间流量的FORWARD隔离。把这三条路径在心里过一遍,你会突然发现防火墙那堆规则不再是孤立的条目,而是一张有因果关系的网。

2.1 端口映射的本质是一组DNAT规则

先看一个最常见的命令:

docker run -d -p 8080:80 nginx

Docker实际做了两件事:一是启动容器,二是往iptables里写规则。你执行iptables -t nat -L -n --line-numbers,能看到类似这样的规则:

Chain DOCKER (2 references) target prot opt source destination DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80

这条规则挂在NAT表的DOCKER链上,表示所有访问宿主机8080端口的TCP包,目标地址都被改写为172.17.0.2:80。而DOCKER链被PREROUTING链和OUTPUT链跳转引用:

Chain PREROUTING (policy ACCEPT) target prot opt source destination DOCKER all -- 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL

PREROUTING是所有从外部进入宿主机的流量第一站,在这里做DNAT意味着外部流量还没走上路由决策就被“劫持”进容器了。为什么还要挂OUTPUT链?因为宿主机本机的进程访问localhost:8080时,不经过PREROUTING,只经过OUTPUT,不加这条跳转,本机访问自己的映射端口会失败。这是很多人容易忽略的细节。

注意:这里的DNAT规则没有指定接口,所以只要宿主机任何接口收到8080端口的流量,都会被转进容器。如果服务器有多个网卡、多个IP,而你只想让某个IP的8080能访问容器,默认规则做不到这个粒度,后面会讲怎么用DOCKER-USER链细化。

2.2 FORWARD链:Docker桥接流量的主战场

DNAT只是改了目标地址,它并没有改变数据包的“走向”。外部流量DNAT以后,目标地址变成172.17.0.2,宿主机发现这个地址是本机的docker0网段,于是走路由把包从docker0发出去。关键在于,从一个网卡转发到另一个网卡,必须过FORWARD链。

Docker在FORWARD链上做了两处修改:

Chain FORWARD (policy ACCEPT) target prot opt source destination DOCKER-USER all -- 0.0.0.0/0 0.0.0.0/0 DOCKER-ISOLATION-STAGE-1 all -- 0.0.0.0/0 0.0.0.0/0 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED DOCKER all -- 0.0.0.0/0 0.0.0.0/0 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0

这套规则解决三个层面的问题:

  • 让已建立的连接流量放行(ctstate RELATED,ESTABLISHED),这是DNAT之后回程流量能被正确接受的基石,没有这条,容器只能发请求,收不到响应。
  • 让真正要进入容器的流量放行(一对ACCEPT),Docker用DOCKER链来统一管理,链里只放行那些命中了DNAT规则的流量。
  • 隔离不同docker网络之间的流量(DOCKER-ISOLATION-STAGE-1),这条链会检查目标地址是否属于某个docker网段,然后跳去stage-2,最终用一条DROP把所有跨网桥的容器流量拦下。

这套FORWARD链的设计,保证了入站流量经过DNAT后,不会被默认策略误杀。但反过来也引入了一个副作用:如果系统防火墙(firewalld或手动iptables)把FORWARD策略改成了DROP,Docker的ACCEPT规则仍然放行匹配的流量,但非Docker管理下的宿主机转发流量就会被拦截,导致宿主机的IP转发功能失效,顺带也会影响容器对外的转发链路。

2.3 DOCKER-ISOLATION和DOCKER-USER:隔离与自定义的边界

DOCKER-ISOLATION-STAGE-1是Docker实现跨网络隔离的关卡。可以在filter表里查看到它:

Chain DOCKER-ISOLATION-STAGE-1 (1 references) target prot opt source destination DOCKER-ISOLATION-STAGE-2 all -- 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL RETURN all -- 0.0.0.0/0 0.0.0.0/0

大意是:所有即将进入DOCKER-ISOLATION-STAGE-2的包,如果目标地址是本机地址,继续检查;否则直接RETURN。Stage-2里针对每个bridge网络,会加一条“从A网段到B网段”的DROP规则,确保跨网络的容器无法互相通信。如果你发现不同bridge网络的容器能ping通,大概率是这段DROP规则因为某些原因被冲掉了。

DOCKER-USER链是一个非常值得用的自定义入口。它挂在FORWARD链的最前面,优先级高于Docker内置的所有规则,而且Docker引擎重启不会清空它。这意味着你可以在DOCKER-USER里写自己的白名单、黑名单,而不用担心docker重启把规则刷掉。

举例,只想允许192.168.1.0/24访问映射到容器里的8080端口,可以这么做:

iptables -I DOCKER-USER -p tcp --dport 8080 -s ! 192.168.1.0/24 -j DROP

这条规则放在DOCKER-USER链里,会在所有Docker内置逻辑之前执行,所以能精准拦截。这是我在生产环境里最常用的“容器访问控制”手法。

2.4 出站流量:MASQUERADE与回程路径

容器主动访问外部网络时,源地址是容器自己的172.17.0.2,这是一个私网地址。如果这个包不加处理就发到宿主机外面的网络,对端设备根本无法回应,因为路由表里没有172.17.0.0/16的路由。

Docker在NAT表的POSTROUTING链上挂了一条MASQUERADE:

Chain POSTROUTING (policy ACCEPT) MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0

这条规则把从docker0网段出来、要走向外部网络的包,源地址改写为出站网卡的IP。对端设备看到的源地址就是宿主机IP,回包自然能正常回来。这条链对容器出站访问来说必不可少,你要是发现容器能访问外网,但外网设备回访不了容器,除了检查DNAT,也要看看这条规则是否健在。

提示:如果容器只跟宿主机本机通信(访问宿主机的某个进程),流量不经过POSTROUTING,也基本不经过FORWARD,它走的是INPUT链。Docker会在INPUT链上添加一条允许来自docker0网段的流量通过的规则,否则容器连宿主机上的服务都访问不了。

3. 实战验证:把Docker和iptables的“暧昧关系”看个清楚

讲原理容易,真正排障还是得靠手上命令。我整理了一套验证流程,照着敲一遍,你对这套交互机制的印象会深得多。

3.1 检查当前网络驱动和网桥状态

先用三个命令确认基础状态:

docker network ls docker network inspect bridge --format '{{json .IPAM.Config}}' ip addr show docker0
  • docker network ls:确认默认的bridge网络存在,并且没有其他自定义网络干扰。
  • inspect里的IPAM.Config会显示子网,正常是172.17.0.0/16,掩码是16位。如果你的机器上这个网段变了(比如因为和现有网段冲突,daemon.json里配置过bip),后续看iptables规则时要把地址对应起来。
  • ip addr show docker0:确认docker0网卡有IP且状态是UP。如果DOWN,所有bridge网络容器的对外通信都会挂掉。

如果你的机器上还跑着多个自定义bridge网络,比如docker-compose的项目,你会在docker network ls里看到一堆名字以项目名命名的网络。每一个网络都有自己独立的网段,也就意味着会有对应的MASQUERADE规则和ISOLATION规则。排查时要小心别对着错误的网段找规则。

3.2 逐个解读关键iptables规则

启动一个容器并映射端口(比如前面那个nginx例子),然后执行:

iptables -t nat -L -n -v --line-numbers iptables -L -n -v --line-numbers

看的时候重点关注几条链:

  • NAT表的PREROUTING链:应该能看到一条跳转到DOCKER链的规则,以及一条MASQUERADE规则(对应172.17.0.0/16)。这印证了“入站DNAT”和“出站MASQUERADE”两条路径。
  • NAT表的DOCKER链:能看到具体的DNAT规则,目标容器IP和端口一目了然。
  • FILTER表的FORWARD链:能看到DOCKER-USER、DOCKER-ISOLATION-STAGE-1开头的若干规则,这是容器流量的必经之路。
  • FILTER表的DOCKER链:里面会有一条或两条ACCEPT规则,它们精确对应DNAT目标。

用-v选项能看到计数器。如果你敲容器映射端口,DOCKER链里那几条规则的计数会快速增加,这能帮你快速确认流量到底有没有走到这条链。如果PREROUTING链计数在涨,但DOCKER链计数不涨,说明DNAT没命中,多半是规则被覆盖或docker服务异常。

3.3 抓包验证DNAT前后的数据包形态

理论的最终验证方式是抓包。开三个终端:

终端一在宿主机外面(或直接从另一台机器)访问映射端口:

curl http://<宿主机IP>:8080

终端二抓宿主机外部网卡的流量:

tcpdump -i eth0 -nn -e -t port 8080

终端三抓docker0网卡上的流量:

tcpdump -i docker0 -nn -e -t port 80
  • eth0上的抓包会看到:源IP是外部客户端IP,目标IP是宿主机IP,MAC地址是客户端网关的MAC。
  • docker0上的抓包会看到:源IP还是外部客户端IP,但目标IP已经变成172.17.0.2:80,MAC地址是容器eth0的MAC(或者是docker0的MAC,取决于抓包位置)。

两相对照,DNAT前后的变化非常直观。另外留意MAC地址的变化:包从宿主机外部网卡进来时,MAC帧是为了到达宿主机;到了docker0再出去时,MAC帧则是为了到达容器。这证明数据包确实在系统内部走了一条“路由+转发”的路径,而不是被某个进程代理转发的。

注意:默认情况下,用-p映射端口时,Docker还会启动一个docker-proxy用户态进程,它也会监听8080端口。你抓包时可能会看到除iptables DNAT之外的连接来源,这是正常的。docker-proxy主要用于解决来自宿主机本机的访问以及一些特殊场景,实际生产流量转不经过它,但别被它误导。

4. 高频事故与排查实录:防火墙、iptables与Docker的相爱相杀

原理搞清楚了,接下来看真实世界里的坑。我碰到的几乎所有Docker网络故障,都集中在三类场景里,下面逐个拆解,附上当时的排查路径和最终解决方案。

4.1 firewalld启动或重启后,容器网络突然全断

这是一个极其经典的场景。CentOS 7/8默认带firewalld,而firewalld启动时会清空iptables规则然后按自己的策略重新加载。Docker的规则是直接写在iptables里的,firewalld一重启,Docker的规则就没了,后果包括:

  • 容器无法访问外部网络(MASQUERADE丢了)。
  • 外部无法访问映射端口(DNAT丢了)。
  • 不同容器间通信被默认的FORWARD策略误伤。

排查步骤先看现象触发的时机。如果是firewalld reload或reboot之后发生的,基本可以直接定位是这个原因。进一步的证据可以查看iptables -t nat -L -n里是否还有DOCKER链,以及FORWARD链策略是不是变成了DROP。

解决办法有两个主流方向:

方案A:禁用firewalld,改用iptables-service。适合那些不需要动态zone管理的纯Linux服务器环境:

systemctl stop firewalld systemctl disable firewalld systemctl enable iptables systemctl start iptables

方案B:保留firewalld,但让Docker和它共处。Docker检测到firewalld运行时,会尝试调用firewalld的接口来插入自己的规则。但需保证启动顺序:先启动Docker,再启动firewalld,或者firewalld重启后重启Docker。

实操心得:如果公司安全基线要求必须用firewalld,我一般在daemon.json里加"iptables": false配合ip_forward手动配置,但这会丢掉Docker的端口映射能力,很不推荐。真正靠谱的做法是把firewalld的默认zone规则里显式放行需要暴露的容器端口,同时在Docker服务启动后立即重启firewalld,让两边规则按固定顺序落地。

4.2 安全软件清理iptables后,端口映射失效

云监控、安全加固脚本、自研防火墙管理程序,有时候会执行iptables -F或iptables -t nat -F来“恢复干净状态”。这一清,Docker的规则全军覆没。

这种场景的排查特征是:容器本身运行正常,docker ps显示状态是Up,docker logs也看不到报错,但外部访问就是不通。而且iptables规则里找不到任何DOCKER链或DOCKER-USER链。

定位方法很简单:执行iptables -t nat -L DOCKER -n,如果提示“Chain 'DOCKER' does not exist”,说明规则被清了。

修复手段也很直接:

systemctl restart docker

重启Docker后它会重新写入全部iptables规则。但也注意,如果FORWARD默认策略被改成了DROP,重启Docker也不会自动改回去。需要手工确认FORWARD策略,或者把ip_forward和默认策略一起调整到符合Docker预期的状态。

4.3 容器间互访失败,但映射端口正常

另一种典型故障是“外部访问正常,容器A访问容器B超时”。通常的原因有两个。

第一个原因是不同bridge网络之间的隔离。Docker用DOCKER-ISOLATION-STAGE-1/2的DROP规则来阻止跨网络通信。如果你把两个服务放进了不同的docker-compose网络,它们天然就不能互通。解决方法要么把服务放在同一个自定义bridge网络里,要么直接用docker network connect把容器加到同一个网络。

第二个原因是DOCKER-USER链里自己写的规则误伤。比如你为了做IP白名单,写了一条:

iptables -I DOCKER-USER -i eth0 -s 0.0.0.0/0 -j DROP

这里的-i eth0限制了入站接口,流量从docker0过来时不会命中这条规则,但如果你误写成-i docker0或者不加-i条件,连容器间的转发流量都会被拦掉。我踩过一次这个坑,最后是用iptables-save对比规则后才发现的。

排查时可以临时看一下FORWARD链里DOCKER-USER的计数,如果容器A访问容器B的包都卡在DOCKER-USER上,计数会疯狂增长,一眼就能锁死问题。

4.4 一套自定义bridge网络的典型故障排查顺序

把前面这些经验浓缩成一套可供复用的排查路径,我自己在遇到容器网络“看起来正常但实际不通”的时候,按这个顺序走:

  1. 先看docker network inspect确认IPAM网段、网关、已连接的容器。
  2. 确认宿主机的ip_forward已开启,且FORWARD默认策略符合预期。
  3. 检查iptables -t nat -L DOCKER里DNAT规则是否存在,计数器是否有流量。
  4. 检查iptables -L DOCKER-USER -v里有没有误伤容器的拦截规则。
  5. 用tcpdump分别在外部网卡和docker0上抓包,确认包是否到达了正确的位置。
  6. 最后看Docker服务日志和容器内路由表(docker exec ip route)确认容器默认网关是docker0。

这个过程快则几分钟,慢也不会超过半小时。而且这套顺序基本不依赖具体发行版,Debian、CentOS、Ubuntu都适用。

5. 经验沉淀:和iptables打交道时的一些建议

做容器网络运维几年下来,最深的一点体会是:Docker的bridge网络之所以让人感觉“玄学”,是因为它的规则分散在NAT表和FILTER表的好几条链里,没有统一视图。所以哪怕只是临时调试,我也建议动手之前先iptables-save > /tmp/iptables.bak留个底,出问题随时恢复。

第二点建议是,生产环境尽量不要把自定义的容错规则直接加到FORWARD链或DOCKER链上。FORWARD链会被Docker重启清空重建,DOCKER链属于Docker内部实现,下次升级可能会变。要写业务访问控制,就写进DOCKER-USER链,它是专门为这种情况预留的,稳定性和语义清晰度都好得多。

第三点,也是最想提醒的:很多人遇到“关了防火墙就好了”,第一反应是关掉firewalld或清空iptables。但Docker的正常运行恰恰依赖iptables里那套规则,你“关了防火墙”其实是把容器网络的规则也一起关没了。更好的做法是找清iptables和Docker之间互相打架的规则,而不是简单粗暴地全清。真到了必须关防火墙的地步,记得重启一次Docker让规则恢复,别留着裸奔状态。

最后分享一个写规则的小技巧:如果你用DOCKER-USER链做端口限制,规则里尽量带上明确的接口和协议条件,不要写成全局的all traffic drop。宁可多写几条,也比一条宽泛规则把整个转发路径拦出问题要好。而且在生产环境改iptables之前,最好把docker network inspect的输出和iptables-save的内容存成文件放在手边,真出了问题,对照着排查比看任何文档都顺手。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询