ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Docker默认bridge网络与iptables机制解析及端口故障排查

Docker默认bridge网络与iptables机制解析及端口故障排查 先给结论Docker 默认使用的网络驱动是 bridge。这句话背出来很容易但一旦你经历过“容器起来了、端口映射也写了外面却就是访问不通”这种现场就会明白问题从来不在于记住一个名字而在于搞清楚 bridge 驱动下的容器是怎么和宿主机上的 iptables 规则相互作用的。Docker 在 Linux 上大量依赖 iptables 来完成端口映射、NAT 转发和容器通信管控这套机制既是它方便好用的根源也是各种网络故障和防火墙冲突的高发区。这篇文章适合正在学 Docker 网络、或者在排查“映射端口不通”“防火墙规则冲突”的读者内容全部来自我实际部署和踩坑的记录。1. 先搞清楚Docker 的默认网络驱动到底是哪个1.1 默认驱动不是背个名字就行bridge 背后是一台虚拟交换机Docker 默认用的 bridge 网络驱动说白了就是在宿主机上创建了一个虚拟网桥名字叫docker0。这个网桥默认有一个私有网段172.17.0.0/16宿主机自己在网桥上有一个地址172.17.0.1每个容器启动时Docker 会从这段私网地址里分配一个 IP比如172.17.0.2。容器里的 “eth0” 网卡并不是真的物理网卡而是通过 veth pair虚拟网线对的一端放进容器另一端插在docker0网桥上。可以把docker0理解成一台虚拟交换机每个容器就是交换机上的一台设备veth pair 就是连接设备和交换机的网线。同一台宿主机上默认 bridge 网络里的容器天然可以互相通信因为它们都在同一个二层广播域里。我见过不少新手把“bridge 驱动”理解成“所有容器都共享宿主机的 IP”这是错的。bridge 模式下容器有自己独立的 IP、独立的网络命名空间只是借助宿主机内核的网络栈做转发。这一点是理解 Docker 和 iptables 关系的基础容器的流量走到 veth 之后实际上会变成宿主机内核协议栈处理的流量而内核协议栈决定“这个包该怎么走”的时候就会检查 iptables 规则。1.2 默认选 bridge 而不选 host是安全性、灵活性和性能的折中Docker 不是只有 bridge 一种驱动还有 host、none、overlay、macvlan。默认选 bridge 而不是 host是有明确考量的。host 驱动性能最好因为容器直接复用宿主机的网络命名空间没有 NAT、没有额外转发层启动容器后它就监听在宿主机的 IP 上。但是缺点同样明显容器之间没有网络隔离端口不能冲突比如宿主机 80 被占用那容器里想监听 80 就必须换端口根本谈不上“端口映射”的灵活性。生产环境里很少直接用 host 跑多容器应用。bridge 是隔离性和便利性之间的折中每个容器独立 IP端口可以通过-p参数映射出去外部访问的是一个经过 NAT 转换的“入口”内部细节被挡住了。即便容器被攻破它也只是一个内网网段里的节点默认不能直接访问宿主机的物理网卡所在局域网段以外的资源——当然这个安全边界比很多人想象的要弱后面我会细说。目前主流驱动可以简单总结成一张表网络驱动适用场景关键特点bridge单机多容器最常见独立 IP、端口映射、NAT 出网host性能要求高、端口不冲突的场景共享宿主机网络栈无 NATnone离线任务、自定义网络方案无网络接口完全隔离overlay跨主机容器集群如 Swarm 模式基于 VXLAN 封装跨物理机互通macvlan容器需要直接连入物理局域网容器获得物理网段 IP性能好但依赖交换机端口配置如果你只需要在单台服务器上跑几个容器默认的 bridge 就是最省事的方案。但省事不代表不用理解原理因为端口映射、容器外网访问、容器间通信全都要靠 iptables 在背后忙活。2. 端口能通全靠它Docker 与 iptables 的协作机制2.1 容器流量必须经过 FORWARD 链Docker 在这里做了“狠事”很多接触过 iptables 的同学习惯性关注 INPUT 链和 OUTPUT 链因为本机服务的进出都在这里过滤。但容器不一样容器里的进程发出的数据包先经过容器自己的网络栈然后通过 veth 进入宿主机这时候宿主机内核发现“目标 IP 不是本机地址”就会走路由转发逻辑也就是 FORWARD 链。外部流量访问容器映射端口时也一样数据包到达宿主机、经过 NAT 后目标变成容器 IP仍然不属于本机继续走 FORWARD 链。这就导致了一个关键结果Firewalld 或 ufw 默认给 INPUT 链加的限制规则对容器流量不生效。很多人以为“我关了防火墙Docker 端口就能访问我开了防火墙Docker 端口就不能访问”实际操作中经常不是这么回事。Docker 启动时会检查 FORWARD 链的默认策略如果发现是 ACCEPT它会把默认策略改成 DROP然后插入自己的一系列 ACCEPT 规则。这样做的初衷是安全默认拒绝所有不明转发流量只放行 Docker 管理的容器相关流量。Docker 具体写入 FORWARD 链的规则大致是从容器 veth 接口进来、目标是 docker0 网段的流量ACCEPT。从 docker0 出去、目标是容器网段的流量ACCEPT。与已有连接状态相关的流量ACCEPT。其他 FORWARD 流量落到默认 DROP。这条规则的含义是容器和宿主机之间的转发被 Docker 接管了任何没有进入 Docker 规则体系的流量都会被丢弃。对于只跑 Docker 的主机来说这个机制还能接受但如果你同时在这台主机上跑着其他需要转发的服务比如自建路由器、虚拟机桥接FORWARD 默认 DROP 会导致那些流量也被掐断这是我在实际运维中踩过最狠的坑之一。2.2 端口映射的本质是 DNAT 加 MASQUERADE 的组合拳docker run -p 8080:80这个命令写起来很简单但背后 iptables 做的事情非常具体。假设容器 IP 是172.17.0.2HTTP 服务监听 80 端口。执行完这条命令后iptables -t nat里会多出类似这样的规则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 Chain OUTPUT (policy ACCEPT) target prot opt source destination DOCKER all -- 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL 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外部主机的流量到达宿主机网卡后先进入 PREROUTING 链匹配到 DOCKER 链的 DNAT 规则目标地址和端口被改写为172.17.0.2:80。然后内核查路由表发现这个 IP 属于 docker0 直连网段于是把包从 docker0 转发进容器的 veth。容器处理完请求后返回包的目标地址是外部主机 IP内核根据 conntrack 表里记录的连接状态自动把源地址还原成宿主机网卡的 IP 和端口 8080。整个过程中外部主机眼里只有宿主机 IP:8080完全感知不到容器的存在。容器主动访问外网时走的是另一条 NAT 规则。iptables -t nat的 POSTROUTING 链里会有类似这样的 MASQUERADE 规则Chain POSTROUTING (policy ACCEPT) target prot opt source destination MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0容器发出的包源地址是172.17.0.x出宿主机前被改成宿主机网卡 IP。外部服务器只知道请求来自宿主机回包也只会回给宿主机再由 conntrack 转回给发起请求的容器。这套“出去时改源、进来时改目标”的机制就是 Docker bridge 网络里 NAT 的核心逻辑。2.3 DOCKER 链与 DOCKER-USER 链自定义规则到底该写在哪里Docker 没有把所有规则直接散落在 PREROUTING、FORWARD 等系统链里而是创建了自己的自定义链DOCKER和DOCKER-USER。PREROUTING 链里只有一条调用规则包进来后调 DOCKER 链去匹配 DNAT。FORWARD 链里也有一条对应规则调 DOCKER 链决定是否放行。Docker 启动容器、删除容器、重建端口映射时只操作 DOCKER 链里的内容保证系统链层面规则尽量最小化。DOCKER-USER 链则更特殊它是 Docker 专门给运维人员留的“自定义规则入口”。官方设计意图非常明确用户要做的入站访问控制、源 IP 白名单、黑名单应该写进 DOCKER-USER而不是直接改 DOCKER 链或者 FORWARD 链。原因很简单Docker 服务重启或容器重启时会重建 DOCKER 链你写进去的规则可能被清掉而 DOCKER-USER 链在 Docker 每次更新规则时不会被触碰规则可以长期保留。提示很多人第一次排障时习惯把自己想加的 iptables 规则直接插到 DOCKER 链里比如iptables -I DOCKER -p tcp --dport 8080 -s 192.168.1.0/24 -j ACCEPT。重启 Docker 后就会发现规则消失了然后开始怀疑 Docker 是不是有问题。其实规则就应该写到 DOCKER-USER 链这个习惯要早点养起来能省掉很多重复劳动。2.4 docker-proxy 进程另一条容易被忽略的数据通道docker run -p之后宿主机上还会多出一个docker-proxy进程。它监听在映射端口上比如 0.0.0.0:8080把收到的流量转发给容器 IP:80。很多人不解已经做了 DNAT为什么还要跑一个用户态转发进程原因主要有两个。第一早期的 Docker 网络实现里有些场景下 DNAT 对来自宿主机自身的回环流量处理不完整比如你在宿主机上curl 127.0.0.1:8080不走 PREROUTING而是走 OUTPUT 链NAT 规则可能不生效docker-proxy 就成了兜底方案。第二IPv6 和 NAT66 的支持在很长一段时间里不如 IPv4 完善docker-proxy 可以弥补。但 docker-proxy 也有代价每个映射端口对应一个用户态进程端口数量一多进程数就上去了转发性能也不如内核态 NAT。容器需要良好支持现代内核网络时可以考虑用--userland-proxyfalse关掉它但前提是你测试过宿主机自身访问映射端口、以及 hairpin NAT 场景都正常。实测下来大部分情况下保留默认值最省心真遇到性能瓶颈再调。3. 把规则捞出来看一次完整的实验记录3.1 动手前先看清楚当前网络布局理论说再多不如直接上服务器看一眼。最简单的三件事看网桥、看路由、看容器 IP。# 查看 docker0 网桥地址 ip addr show docker0 # 查看默认 bridge 网络的详细配置 docker network inspect bridge # 查看容器分配的 IP docker inspect -f {{.Name}} - {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}} 容器名默认情况下docker network inspect bridge输出里会看到 subnet 是172.17.0.0/16gateway 是172.17.0.1。宿主机上还会多一条路由172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1这条路由意味着任何目标地址落在172.17.0.0/16网段的数据包内核都认为是“直连网络”会通过 docker0 直接投递而不走默认路由。这是容器跨主机不通、需要额外配置的根本原因之一。3.2 抓 Docker 写入的 iptables 规则我用一个简单的 nginx 容器做实验命令是docker run -d --name web -p 8080:80 nginx。然后抓 NAT 表iptables -t nat -S输出的核心部分大致如下-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER -A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE -A DOCKER -i docker0 -j RETURN -A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80注意那条POSTROUTING规则-s 172.17.0.0/16 ! -o docker0 -j MASQUERADE。它只对从容器网段发出、并且出口不是 docker0 的包生效。换句话说容器访问另一个容器时源 IP 不会被 NAT 成宿主机 IP两个容器之间直接保留原始 IP 通信这样容器之间才知道对方是谁。再抓 filter 表iptables -S核心规则大致如下-P FORWARD DROP -A FORWARD -j DOCKER-USER -A FORWARD -j DOCKER-ISOLATION-STAGE-1 -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT -A FORWARD -i docker0 ! -o docker0 -j ACCEPT -A FORWARD -i docker0 -o docker0 -j ACCEPT -A FORWARD -j DOCKER -A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT看到这里你基本能理解一次外部访问映射端口要经过哪些判断包进宿主机PREROUTING 调 DOCKER 链做 DNAT目标改为 172.17.0.2:80随后进入 FORWARD 链走 DOCKER-USER 做用户自定义检查再进入 DOCKER 链匹配到-d 172.17.0.2/32 ! -i docker0 -o docker0 --dport 80 ACCEPT放行从 docker0 出去进容器。整个过程环环相扣缺一条规则端口就不通。3.3 用抓包验证数据包流向只看规则还不够直观我习惯配合tcpdump来验证。从另一台机器上访问宿主机的 8080 端口时分别在宿主机 eth0 和 docker0 上抓包tcpdump -i eth0 -nn port 8080 tcpdump -i docker0 -nn port 80抓包结果会很清晰地展示 DNAT 的效果。eth0 上看到的是宿主机 IP 的 8080 端口流量而 docker0 上看到的目标 IP 已经变成了172.17.0.2目标端口变成了 80。如果你发现 eth0 上有包docker0 上却没有对应的包基本可以断定 DNAT 规则没生效或者 FORWARD 链把包丢了。如果是在宿主机本机上执行curl 127.0.0.1:8080默认开启了 docker-proxy 的情况下流量走的是 docker-proxy 进程不一定完全走 DNAT 流程抓包结果会和外部访问不同。这是很多初学者在调试时感到困惑的地方误以为规则没生效。4. 翻过那些坑端口映射与防火墙故障排查4.1 端口映射不通但容器内服务正常先按这个顺序查遇到“映射端口外面访问不了但 docker exec 进容器 curl 一下服务又是好的”我建议按照下面的顺序来排查效率最高确认容器服务实际监听端口别只听 Dockerfile 说“暴露了 80”就以为真的在 80。用docker exec 容器名 ss -lntp看一下最靠谱。看 NAT 表规则存在不存在iptables -t nat -S DOCKER。看 FORWARD 链默认策略和 Docker 相关 ACCEPT 规则是否完整iptables -S | grep -A 5 FORWARD。看内核转发开关sysctl net.ipv4.ip_forward如果是 0一切转发都白搭手动改成 1 再持久化到/etc/sysctl.conf。这几个步骤里最容易忽略的是最后一项。有些云镜像默认没开ip_forwardDocker 可能在启动时帮你打开但如果重启了系统、而配置没有持久化就会回到 0容器外网访问和端口映射全部失败偏偏容器本身运行得好好的。4.2 重启防火墙把 Docker 规则弄丢了这是最经典的坑很多使用 firewalld 的主机上会复现这样一个现象容器跑得好好的端口映射也正常。管理员执行了systemctl restart firewalld或者firewall-cmd --reload过一会儿发现容器访问不了了。原因很简单firewalld 重启时会把整个 iptables 规则集重置Docker 之前插入的 DOCKER 链、NAT 规则全部消失而 Docker 进程并不知道防火墙发生了什么它不会主动去重建已经写入过一遍的规则。这时候恢复的手段很简单但很多人不敢做就是重启 Docker 服务systemctl restart dockerDocker 会在启动时重新写入整套 iptables 规则端口映射也随之恢复。注意重启 Docker 服务会让所有运行中的容器全部退出并重新启动这在生产环境里是有代价的。更稳妥的做法是提前做好规则备份或者尽量避免在同一台宿主机上同时依赖 firewalld 和 Docker 的自动规则管理。如果确实两个都要用那就把自定义规则写进 DOCKER-USER 链并在 firewalld 里对 docker 相关端口做好放行。还有一个让我印象深刻的翻车现场有人用systemctl stop docker去停 Docker 服务发现容器还在运行但网络全部不通。这是因为 Docker 服务停止时会把 socket、CI 等组件停掉但容器进程不一定马上被杀而 Docker 写入的 iptables 规则又可能被清理了一部分容器处于“活着但没网”的状态。遇到这种情况别慌systemctl start docker起来后网络会恢复正常但要知道这个现象别误判成内核问题。4.3 宿主机访问自身映射端口失败问题出在 hairpin NAT 上还有一种奇怪的表现外部机器访问宿主机的IP:8080完全正常唯独在宿主机本机上执行curl 127.0.0.1:8080超时或者重置。这本质上是一个 hairpin NAT发夹流问题。数据包从本机回环接口发出目标指向本机 IP正常情况下会走 OUTPUT 链。如果 docker-proxy 被关掉而你依赖纯 DNAT 规则某些内核配置下回环流量不会完整地经过 PREROUTING就会导致该 DNAT 不生效。解决办法是开启route_localnetsysctl -w net.ipv4.conf.docker0.route_localnet1不过对于一个不需要探究内核细节的普通使用者最省事的方案就是不要关 userland-proxy。只有当你明确知道自己在做什么并且在测试环境里验证过所有路径都正常才建议关掉它。4.4 多网卡服务器上端口暴露范围比你以为的大默认执行docker run -p 8080:80时Docker 实际绑定的是0.0.0.0:8080也就是宿主机所有网卡的 IP 上都会监听这个端口。在只有一张公网网卡的服务器上无所谓但在有多块网卡比如内网 IP、管理网 IP、公网 IP的服务器上这意味着你原本只想对内网暴露的服务公网网卡上也同步暴露了。这个问题在安全要求严格的场景里非常致命。一定要养成写地址绑定的习惯# 只绑定到内网网卡 192.168.1.10 docker run -d -p 192.168.1.10:8080:80 --name web nginx这样 Docker 在 PREROUTING 的 DNAT 规则里会额外加一层源地址限制只有访问 192.168.1.10:8080 的数据包才会被转发到容器公网网卡上根本没有监听外部自然无法访问。5. 生产环境的安全硬化建议5.1 容器网络隔离不等于安全隔离默认 bridge 要慎用Docker 默认的 bridge 网络虽然让容器有了独立 IP但只要容器跑在同一个宿主机、同一个 docker0 网段它们之间默认就能互相访问。如果有两个应用一个对外提供 API另一个是数据库你把它们放在同一个默认网络里API 容器被攻破后可以直接对内网段的数据库容器发起攻击。这在微服务初期“先跑起来再说”的阶段很常见但长期是安全隐患。我的习惯是每个应用单独创建自定义网络docker network create app-backend docker network create app-frontend然后把容器挂到对应的网络上只让需要互联的容器共享网络。自定义 bridge 网络还有一个额外好处自带内嵌 DNS 服务容器之间可以通过服务名互相访问而不需要记 IP 地址。这也是 Compose 项目里默认推荐的行为。5.2 在 DOCKER-USER 链里做真正的黑名单白名单前面多次提到 DOCKER-USER 链这个链是生产环境中做访问控制的正确位置。例如我只想让 192.168.1.0/24 网段的机器访问宿主机 8080 端口映射出来的容器服务可以这样写iptables -I DOCKER-USER -i eth0 -p tcp --dport 8080 -s 192.168.1.0/24 -j ACCEPT iptables -I DOCKER-USER -i eth0 -p tcp --dport 8080 -j DROP这两条规则的含义是来自指定内网段的请求放行来自其他源的 8080 请求直接丢弃。规则放在 DOCKER-USER 链里Docker 不会去改它重启容器后依然生效。再配合上一条RETURN规则可以保证其他未被过滤的流量继续走 Docker 逻辑iptables -I DOCKER-USER -j RETURN不要把这种规则放到 FORWARD 链的默认策略里也不要试图用 INPUT 链去管控容器映射端口。INPUT 链管控的是宿主机本机的入站流量容器端口映射的流量走的是 FORWARD管错链是白费功夫。5.3 同时用 nftables、iptables-legacy 和 Docker 时要搞清楚你操作的是哪套规则当下主流 Linux 发行版都已经切换到 nftables 框架但很多命令行的iptables实际是指向 iptables-nft 的兼容层系统里还可能残留 iptables-legacy。Docker 在启动时会探测当前系统使用的是哪一套规则集然后写入对应的一套。如果你经常用iptables -L查看规则却看不到 Docker 写入的规则一定先确认你的 iptables 命令是 legacy 还是 nft 版本再用update-alternatives --config iptables切换到一致的那套。这种“规则互相看不见”的问题比很多规则冲突更隐蔽标志性现象是iptables -S里啥都有但流量行为完全不符合规则逻辑或者容器网络一会儿通一会儿不通。我自己就遇到过 Ubuntu 上同时装了旧版 Docker 和新版 nftables结果 Docker 把规则写到 legacy 表我用iptables -t nat -S看的是 nft 表折腾了两个小时才搞清楚原因。5.4 别随便用“清空防火墙规则”来救场容器网络出问题后网上有很多帖子会教“直接iptables -F清空规则再试试”。这个操作在只有 Docker 的单机环境里确实能临时解决问题但后果很严重清空 FORWARD 链会让 Docker 的规则全部消失容器网络彻底断掉你还要重新启动 Docker 才能重建。如果这台机器上还有其他业务依赖自定义 iptables 规则事故范围会进一步扩大。更安全的方法是用iptables-save 备份文件先备份规则再逐条定位问题规则。定位方式也很简单iptables -t nat -S和iptables -S输出都存在但规则不匹配时可以临时在 DOCKER-USER 链首部加一条LOG规则观察/var/log/kern.log里有没有对应请求的 log就能判断流量有没有走到这一步。测完立刻把 LOG 规则删掉不然日志增长会很难受。6. 一点个人经验与最后提醒我在短暂又漫长的 Docker 排障生涯里花了很多时间在“端口不通”这件事上前后端排查过容器内进程、网络模式、安全组、防火墙策略最后的结论是一切能在应用层找到的原因都没有底层网络规则查得快。哪怕你对 Docker 网络原理再熟悉也不要省略iptables -t nat -S和iptables -S这两个命令它们能让你在五分钟内确认问题是出在 DNAT、FORWARD 还是容器本身的监听不需要靠猜。另外我给自己定了一条规矩凡是需要自定义访问控制一律进 DOCKER-USER 链绝不直接改 DOCKER 链或 FORWARD 链的系统级结构。这个习惯让我在 Docker 版本升级、服务重启之后少了非常多重复配置的工作。最后分享一个调试小技巧检查端口映射时不要只看某一台机器的规则先把数据链路画一遍——数据从哪个网卡进来、有没有过 DNAT、走了哪条 FORWARD 链路径、最后落到哪个接口。只要这条链路里每跳都有对应的规则支撑问题基本就能定位到容器内进程本身了。Docker 网络驱动和 iptables 这套机制并不神秘真正神秘的是那些不愿意动手把规则捞出来看一眼的人。
返回列表