摘要:
在私有化部署 Docker Registry 时,遇到宿主机 curl 容器映射端口超时的诡异问题。经历 iptables、rp_filter、bindv6only、抓包等深入排查后,最终发现是 Docker 卸载残留的 veth 接口扰乱了内核包转发路径。本文完整记录排错过程与根因,供同行参考。


一、问题现象

在宿主机(192.168.0.146)上使用 docker run -d -p 5555:5000 registry:3 启动官方 Registry 容器后,发现:

  • 容器内 wget 127.0.0.1:5000/v2/ 正常返回 200 和 {}

  • 宿主机执行 curl 127.0.0.1:5555/v2/ 超时Connection timed out

  • 宿主机执行 curl 172.17.0.2:5000/v2/ 同样超时

  • 但宿主机 ping 172.17.0.2 却能通,且延迟极低。

二、环境信息

  • OS: CentOS Stream 8 / 9

  • Docker: 24.x

  • Registry 镜像:registry:3

  • 启动命令:docker run -d --name ags-registry -p 5555:5000 -v /data/...:/var/lib/registry registry:3

三、第一阶段:排查 iptables 与 Docker 代理

  1. 检查端口监听
    ss -tlnp | grep 5555 显示 docker-proxy 正在监听所有网络接口的 5555 端口,状态正常。

  2. 检查 NAT 规则
    iptables -t nat -L -v 中发现 DNAT 规则将访问宿主机 5555 端口的数据包转发到 172.17.0.2:5000,看起来没有问题。

  3. 验证 docker-proxy 是否工作
    用 curl 127.0.0.1:5555 和 192.168.0.146:5555 都超时,说明流量进入了代理但未从容器返回。

  4. 排查 filter 表
    FORWARD 链默认 ACCEPTDOCKER 链中有明确允许到容器 5000 端口的规则,未被拦截。

初步结论:流量成功送达容器,但应用层未响应。

四、第二阶段:怀疑内核网络参数

  1. rp_filter 反向路径过滤
    rp_filter 为 1,可能导致容器回包被丢弃。临时关闭 rp_filter 后问题依旧,排除此项。

  2. bridge-nf-call-iptables
    该参数为 1,但关闭后依然超时,且 ping 通 TCP 不通,排除。

  3. conntrack 表
    连接跟踪表占用极低,无 table full 日志,排除。

五、第三阶段:深入应用层与 IPv6 陷阱

  1. 确认容器内服务正常
    docker exec 进容器用 wget 127.0.0.1:5000/v2/ 成功,证明 Registry 进程正常。

  2. 跨容器测试
    docker run --rm --network bridge busybox wget http://172.17.0.2:5000/v2/ 同样超时!这说明问题不是宿主机独有,而是任何非容器本身的访问都失败。

  3. 检查 Registry 监听地址
    日志中出现 listening on [::]:5000,表明 Registry 默认绑定到了 IPv6 通配符地址。虽然容器内 net.ipv6.bindv6only=0,但实际测试发现,来自桥接网络的 IPv4 流量虽然能完成 TCP 握手(抓包可见),但 HTTP 响应包永远不会发出。

  4. 使用 Host 网络模式验证
    改用 --network host 并设置 REGISTRY_HTTP_ADDR=0.0.0.0:5555 后,curl 127.0.0.1:5555/v2/ 立刻成功。由此基本确认:Bridge 网络下 IPv4 到 IPv6 监听套接字的映射存在缺陷,导致应用层无声丢弃连接。

六、转折点:发现残留 veth 接口

尽管 Host 模式解决了问题,但我们注意到 ip neigh show 中仍有旧容器的 IP 172.17.0.2 和 MAC 地址 02:42:ac:11:00:02。进一步检查 /sys/class/net/docker0/brif/ 发现残留的 veth 接口 vethfa82f9f,且没有对应的容器进程。

该残留 veth 曾在之前的 Docker 卸载/重装中未被清理,其 MAC 地址恰好与新创建的 Registry 容器的 IP 相同。当新容器连接到 docker0 时,内核 ARP 缓存可能将流量错误地导向这个僵尸 veth,导致正常数据包被丢弃,而 ICMP 响应由内核直接处理不受影响,从而解释了“ping 通 TCP 不通”的现象。

七、最终修复

  1. 清理残留 veth 接口

    bash

    ip link del vethfa82f9f
    ip neigh flush dev docker0
  2. 彻底重启 Docker(推荐)或重新创建容器

    bash

    systemctl restart docker
  3. 强制 Registry 监听 IPv4 地址
    重新创建容器时添加环境变量:

    bash

    docker run -d --name ags-registry \
      -p 5555:5000 \
      -e REGISTRY_HTTP_ADDR=0.0.0.0:5000 \
      registry:3
  4. 验证

    bash

    curl -v http://127.0.0.1:5555/v2/   # 200 OK
    docker push hub.ags.local:5555/myimage  # 成功

八、总结

现象 真正原因
ping 通但 TCP 超时 残留 veth 导致非 ICMP 流量被导向无效端点
容器内访问正常 容器内走 lo 接口,不受 veth 干扰
Host 网络下正常 绕过了 docker0 桥接,避免残留接口影响
Registry 日志显式 IPv6 双栈绑定在纯净网络下无问题,但与残留 veth 共存时触发内核 bug

经验教训

  • 卸载 Docker 时应使用 yum remove 并手动清理 /var/lib/docker 和 /run/docker,必要时重启。

  • 遇到类似“网络半通”故障时,检查桥接接口下的 veth 残留 往往比反复调整 iptables 更高效。

  • 容器化服务应显式绑定 0.0.0.0 而非依赖通配符,避免 IPv6 兼容性踩坑

更多推荐