
1. 现象还原容器能启动却连不通外网症状典型到什么程度最近在处理一台蓝易云云服务器上的Docker环境时撞上了一个特别典型的网络故障容器能启动、能正常跑进程但怎么都连不通外网。进容器里执行apt-get update卡在连接阶段一直超时用curl访问外网地址没有任何响应可回到宿主机上一看服务、端口、进程全部正常docker ps里容器也老老实实挂着UP状态。排查到最后问题竟出在一个再普通不过的内核参数上net.ipv4.ip_forward0。这个故障在社区里被反复问到尤其是刚上手Docker的朋友或者在服务器上做过安全加固、手动改过内核参数的运维同学最容易碰上。它表面上是网络不通本质上却是Linux内核的IP转发开关被关闭导致容器发出的数据包只走到了docker0网桥后续的路由转发动作被内核直接丢弃。这篇文章我会把数据包到底死在哪个环节、如何一步步确认、怎么修复并持久化以及修复后仍然不通时还能查什么一次说清楚。无论是正在排查问题的开发还是负责云服务器运维的同学都可以照着这里的链路走一遍。1.1 你大概率会看到的报错长什么样先说结论这类故障最大的迷惑性在于——表面上看一切正常只有实际发包时才露馅。很多人在这一步浪费了大量时间反复重建容器、重启Docker服务甚至把整个环境推倒重装结果问题依旧。我在那台服务器上用docker run -it --name test ubuntu:22.04进入容器后依次敲了几个命令现象非常有代表性# 容器内测试 root容器:/# ping 223.5.5.5 PING 223.5.5.5 (223.5.5.5) 56(84) bytes of data. # 一直卡住没有任何回包 root容器:/# apt-get update Err:1 http://archive.ubuntu.com/ubuntu jammy InRelease # 连接超时宿主机上同时观察到的现象是docker ps显示容器状态为UP且已运行了一段时间没有异常退出宿主机能正常访问外网curl百度、ping公网IP都没问题宿主机能ping通容器的IP默认172.17.0.x说明容器网络接口本身是活的。把这些现象汇总成一张表会看得更清楚观察位置测试动作表现能排除的方向容器内ping 172.17.0.1docker0网关通二层链路和容器自身协议栈正常容器内ping 公网IP如223.5.5.5不通暂时无法排除路由/转发问题容器内ping 域名如www.baidu.com不通先不判断DNS转发层卡住时DNS也一样超时宿主机curl外网、ping公网IP正常宿主机自己的外网出口正常宿主机ping容器IP正常容器和宿主机之间的桥接链路正常归纳成一句话就是能进不能出外部数据进来、宿主机的本地通信都还正常但容器作为发起方去访问外部网络时所有数据包都石沉大海。1.2 哪些场景最容易踩中这个坑根据我的经验遇到net.ipv4.ip_forward0导致容器断网的情况通常逃不出下面这几类来源刚开通的云服务器系统镜像本身或初始化脚本把ip_forward设成了0而Docker是在这个状态下被启动的为了安全做过系统加固比如执行过某些安全基线检查脚本或系统优化脚本里面有一条通用规则就是关闭IP转发防止服务器被当作跳板手动修改过/etc/sysctl.conf或/etc/sysctl.d/下的内核参数文件把net.ipv4.ip_forward写成了0然后执行了sysctl --system或sysctl -p重启过网络服务或整机重启后原本临时开启的转发开关失效恢复了系统配置里的默认值。这里有一个容易误导人的细节很多加固脚本为了看起来严谨会在sysctl配置文件里主动加上net.ipv4.ip_forward0但这套配置在普通服务器上没问题一旦跑Docker就会立刻引爆这个坑。所以排查时别光看当前值还要把配置文件里的值找出来否则修好一次重启后又复发。2. 数据路径拆解ip_forward0到底卡死了哪一段网络光知道症状还不够得明白数据包是怎么死的。只有搞懂了这条链路的每个环节遇到变种问题时才能举一反三。2.1 一个数据包从容器出发到外网的完整旅程Docker默认使用bridge网络模式。这个模式下每个容器通过一对veth虚拟网卡接入宿主机的docker0网桥。数据包从容器内进程发出后走的路径是这样的容器进程 → 容器eth0 → veth对端 → docker0网桥 → 内核路由决策 → 物理网卡eth0 → 外网关键就在内核路由决策这一步。内核收到一个目的地址不是本机的数据包时会先查路由表决定这个包应该从哪个接口发出去。如果目标地址超出了本机所在网段比如容器里的进程要访问223.5.5.5路由表会告诉内核这个包得交给物理网卡eth0发送。但内核并不会无条件执行这个转发动作。它要先检查一个总开关net.ipv4.ip_forward。这个参数为1时内核允许数据包从一个网络接口流入、再从另一个网络接口流出为0时内核会直接丢弃这种需要跨接口转发的数据包。于是你容器里发出的包走到docker0网桥这一层之后就被扔进了黑洞物理网卡根本看不到它。可以这样理解docker0相当于你所在楼栋的单元门物理网卡是小区大门。你在自己家里发快递没问题但从单元门走到小区大门这段路必须由门卫放行。ip_forward就是门卫手里的开关开关处于关闭状态所有要出小区大门的包裹都会被拦下。这里顺带说清楚一个很多人混淆的点ip_forward影响的是被转发的数据包也就是源地址和目的地址都不在本机的包。宿主机自己访问外部网络的流量走的是本机发出OUTPUT路径不受这个开关影响。这就是为什么宿主机一切正常、容器却断网因为容器发出的包对宿主机内核来说就是需要转发的包。2.2 为什么端口映射也一起失灵还有一类流量同样被这个开关卡住就是外部访问容器发布端口。很多人在Docker里用-p 8080:80把容器端口发布到宿主机发现ip_forward0时外部也访问不了。原因是这样的外部流量到达宿主机物理网卡eth0后目的地址是宿主机IP:8080。内核先做DNAT目的地址转换把目的地址改成容器的172.17.0.x:80改写完之后内核发现目标地址不再是本机地址于是又需要把数据包从eth0转发到docker0再进入容器的veth网卡。这个过程同样属于跨接口转发也得经过ip_forward这道闸门。所以ip_forward0时容器主动访问外网和外部通过端口映射访问容器这两类流量都会挂掉。只有宿主机直接访问容器IP、容器访问宿主机这两种本地流量不受影响。这个能进不能出、能本地不能跨网段的特征本身就是一条非常关键的排查线索。2.3 一个反常识的点部分流量其实不受影响排查时最容易误导人的地方恰恰是那些还正常的流量。比如容器里ping宿主机IP是通的容器A访问容器B也是通的于是下意识排除网络问题开始怀疑Docker配置或者云厂商的安全策略。实际上容器访问宿主机时数据包的目的地址就是docker0所在网段内的本机地址内核把它当成发往本机的包处理走的是INPUT路径根本不需要转发。容器A访问容器B时数据包从A的veth进到docker0又从docker0直接到B的veth虽然看起来也是跨接口但这两个接口都挂在同一个网桥上属于桥接内部的二层转发也不需要ip_forward参与。换句话说桥内流量和本机流量都绕过了这个总开关唯独容器与外界之间的流量必须经过它。理解了这条边界你就能解释为什么故障现象这么挑食有的通、有的不通。通的全是绕开转发的路径不通的全是需要转发的路径。3. 三步定位法从容器内部到内核参数的一次完整追查知道了原理定位就快了。我习惯按容器内自检 → 宿主机iptables → 内核参数三步走每一步都有明确的判断依据不会瞎猜。3.1 第一步容器内自检先确认网络边界进入容器后先看网络基本情况ip addr # 确认容器IP、网卡状态 ip route # 确认默认路由指向docker0网关 cat /etc/resolv.conf # 确认DNS配置然后做几个分层的连通性测试ping 172.17.0.1docker0网关通的话说明二层链路和容器自身协议栈没问题ping 223.5.5.5直连公网IP不通的话说明问题出在路由或转发层而不是DNSping www.baidu.com如果IP通、域名不通才需要怀疑DNS。在ip_forward0的场景下测试结果通常是网关能ping通、公网IP ping不通。这一步就能把问题从容器网络配置和DNS这两个方向上排除掉把怀疑对象收敛到转发链路。需要提醒的是不要在容器内轻易修改网络配置也不要急着重启docker先记录现象。容器内的路由和网卡都是Docker自动创建的手动改完之后Docker可能又会重置徒增干扰变量。3.2 第二步宿主机上检查iptables FORWARD链容器流量要跨接口转发必然经过iptables的FORWARD链。在宿主机上执行iptables -L FORWARD -n -v这条命令会列出FORWARD链的规则和每条规则的匹配计数。如果看到DOCKER相关规则存在且匹配计数没有增长说明数据包根本没走到这条链或者更早就被丢弃了如果看到Policy为DROP且没有对应的放行规则那也可能出现断网但这种情况下通常还伴随其他特征比如docker服务自身规则不完整。ip_forward0时数据包是在内核路由阶段就被丢弃的根本进不到FORWARD链的匹配流程所以FORWARD链的计数基本纹丝不动。这一点可以用来区分内核转发被关和iptables规则拦截两类原因。3.3 第三步确认内核实时转发开关在宿主机上直接执行sysctl net.ipv4.ip_forward cat /proc/sys/net/ipv4/ip_forward这两个命令读到的都是内核的实时值正常情况下会输出net.ipv4.ip_forward 0/proc下那个文件内容为0。看到0基本上就能下结论了容器断网的直接原因就是内核不允许跨接口转发。这里顺带说一下/proc和sysctl的关系/proc/sys目录下的文件是内核参数在内核态的实时映射sysctl命令本质上是读写这套映射的封装。两个命令互为验证避免某个命令因为权限或路径问题给出误导信息。在某些容器或最小化系统里sysctl这个命令可能没装直接cat /proc/sys/net/ipv4/ip_forward更稳。4. 修复与持久化一条sysctl命令背后还有两个隐藏坑定位到根因后修复本身很简单难的是修得干净、重启不复发。这里有两个坑我必须单独拎出来讲。4.1 正确修复写法与验证临时生效只需一条命令sysctl -w net.ipv4.ip_forward1这条命令会立即把内核参数改成1不需要重启任何服务Docker容器马上就能恢复外网通信。想验证的话回到容器里再ping一次223.5.5.5或者直接curl外网地址通了就是好了。但临时生效有个致命问题服务器重启后参数会恢复成配置文件里的值。所以必须做持久化。正确写法是在/etc/sysctl.d/下新建一个独立配置文件而不是直接改/etc/sysctl.confecho net.ipv4.ip_forward 1 /etc/sysctl.d/99-docker-forward.conf sysctl --system执行sysctl --system后系统会按顺序加载/etc/sysctl.d/*.conf和/etc/sysctl.conf然后再次用sysctl net.ipv4.ip_forward确认输出为1。我的习惯是最后必须亲眼看到输出是1才放心因为配置文件的键冲突问题非常隐蔽下面会讲。4.2 隐藏坑一Docker本来会自动开启转发为什么还会是0这里有一个很多人不知道的细节Docker守护进程默认启动时会主动把net.ipv4.ip_forward设为1这是docker daemon的--ip-forwardtrue默认行为。也就是说只要Docker在运行正常情况下ip_forward应该是1才对。那为什么还会遇到0结合真实场景看通常两种可能Docker启动之后有别的进程把参数改回了0。这类进程最常见的就是安全加固脚本、系统巡检脚本它们会周期性地校对sysctl配置并强制应用如果你在某处写了ip_forward0它就会把Docker设好的1覆盖掉服务器重启后某些初始化脚本或网络管理服务在Docker启动之后又应用了一遍sysctl配置把文件里的0重新写回内核。所以排查时不要只改内核值一定要回头翻配置文件。如果你用的是现成的安全基线模板请检查模板里是否有net.ipv4.ip_forward0这一行有的话要么删掉要么改成1否则脚本一跑又会带回去。4.3 隐藏坑二sysctl配置文件的键冲突与加载顺序sysctl的配置加载是有顺序的/etc/sysctl.d/下的文件按文件名排序后面的文件会覆盖前面文件里的同名键。很多系统里还存在/etc/sysctl.conf它通常最后加载但也可能被某个发行版的顺序规则影响。实际操作中我遇到过这样的场景/etc/sysctl.d/99-security.conf里写着net.ipv4.ip_forward0而我又在/etc/sysctl.d/99-docker.conf里写了net.ipv4.ip_forward1。两个文件都是99开头加载顺序取决于具体文件名排序稍不注意就是我改了但没生效。所以建议命名时拉开差距比如把Docker相关的文件命名为99-docker-forward.conf同时把自己写的安全配置统一收敛到一个目录里避免两个文件都叫99-xxx。另外在检查为什么没生效时用grep在/etc/sysctl.d/和/etc/sysctl.conf里查一遍所有出现ip_forward的行确保没有残留的0grep -r ip_forward /etc/sysctl.d/ /etc/sysctl.conf这条命令能把所有配置来源一次性列出来比逐个文件翻省事得多。5. 打开转发后仍不通这几处排查完才算收工ip_forward1之后大多数情况下容器马上就能上网了。但我遇到过几次例外转发开好了依然不通说明还有其他环节在拦截。为了不让大家卡在最后一步我把后续的排查清单也整理出来。5.1 FORWARD链默认策略与Docker规则是否完整如果sysctl已经确认是1容器依然断网第一个要看的就是宿主机上iptables的FORWARD链。执行iptables -L FORWARD -n -v --line-numbers重点看两点默认策略是否为DROP以及DOCKER相关链和规则是否存在。正常情况下Docker会在FORWARD链里插入自己的规则放行容器相关的转发流量。如果之前手动清过iptables规则、或者Docker是以--iptablesfalse方式启动的这些规则可能缺失即使转发开关开了包也会被默认DROP策略拦掉。遇到这种情况最简单的办法是让Docker重新生成一遍链和规则。先确认没有手动维护的自定义规则然后执行systemctl restart dockerDocker启动时会重建iptables规则。注意这一步会短暂中断所有容器的网络生产环境操作前务必评估影响。另外如果你自己写了一些FORWARD链的放行规则务必把DOCKER-USER链考虑进去Docker把用户自定义规则都收敛在这个链里别直接往FORWARD链里乱插否则Docker重启后规则可能被清掉。5.2 firewalld、安全组和DNS这些邻居也要过一遍第二个常见拦截者是firewalld。它在某些发行版上是默认防火墙和Docker的iptables规则存在兼容性问题。典型表现是刚设置完ip_forward1时通了重启或reload防火墙后又不通。排查方法很简单先临时停掉firewalld看容器是否恢复systemctl stop firewalld如果停止后容器网络恢复说明是firewalld的规则和Docker冲突。长期方案不是关防火墙而是把docker0网段和容器端口在firewalld里显式放行或者考虑把firewalld和Docker的规则统一管理。第三个要确认的是云控制台的安全组。有些云服务器在安全组层面做了出入方向限制这种情况下可能宿主机到外网都通但具体某个端口被拦截。你会发现容器内curl任何外网都不通但宿主机curl也未必通那就不是Docker的问题而是上层安全策略。到云控制台核对安全组规则确认出站方向没有误拦截。DNS也要顺手查一下。如果容器内ping公网IP通、ping域名不通说明转发已经没问题问题在DNS。可以在宿主机上看Docker默认DNS配置或者在docker run时加--dns 223.5.5.5指定一个可用的DNS服务器。别把这个和ip_forward的故障混在一起两者的定位路径完全不同。5.3 收工前的端到端验证清单我每次处理完这类问题都会按下面这个清单做一轮完整验证避免修好了又说不通的反复拉扯容器内ping宿主机docker0网关确认链路正常容器内ping公网IP确认转发已生效容器内通过nslookup或ping一个域名确认DNS可用容器内执行一次真正的业务请求比如curl外网接口并确认返回数据宿主机通过-p映射端口访问容器内服务确认入方向正常重启一次服务器或至少执行一次sysctl --system重启后再次重复上面的验证确认持久化配置没有丢。这套验证跑完基本可以确定问题彻底解决。后面如果再出网络故障就可以排除ip_forward这个因素把精力放在防火墙规则、安全组、路由表这些方向上。实际处理中我最深的体会是这类问题最值钱的不是那条修复命令而是理解数据包在转发路径上的走向。把docker0、veth、FORWARD链、ip_forward这几个概念串起来之后再遇到容器网络问题你就能像看地图一样知道包死在哪一段而不是靠重启大法碰运气。