记一次 Docker 容器网络问题排查

本文记录了一个容器网络问题的排查过程及心得。

背景

实验环境:一个实验环境对应一个容器,有一个 Docker 集群,一个容器在一个集群中的某一个节点上。

网络:每个集群中有 A 和 B 两个网络,分别用于不同的用户群,通过 iptables 做网络控制。

管理服务:运行在 K8S 中,里面有一个脚本执行功能需要访问实验环境。

现象

脚本执行功能偶发性的会失败,报错如下:

time="2023-12-18T14:22:47Z" level=error msg="execute script timeout"
time="2023-12-18T14:31:05Z" level=error msg="execute script timeout"
time="2023-12-18T14:38:18Z" level=error msg="execute script timeout"

追踪得到详细错误信息:

"error to create ssh executor: dial tcp 172.16.55.244:34848: i/o timeout: unavailable"

开始觉得是网络连接问题,但随着问题多次出现,发现偶然中有一种必然,于是进一步排查。

过程

追踪一个执行失败的日志,得到容器节点 IP 和端口信息。新起一个测试 pod ,在 pod 内部尝试 telnet 连接节点IP和端口,发现连接建立不了。

然后又在节点找个另一个容器的端口,发现可以正常连接。

经过多次比对发现,初步得出结论:连接失败的都是连接 A 网络的。

那么,为什么连接 A 网络的会连接失败呢?下面进一步分析。

抓包:

连接失败的端口收到了 SYN 包,但是没有 ACK 包,怀疑报是被 iptables 过滤掉了。

问题来了,为什么 A 网路的包被过滤了,而 B 的没有?

弄懂这个问题前,首先要明白访问容器的网络包到达主机后,在主机内部是怎么流转的。

    包到达主机
    |
    进入防火墙(iptables)
    |
    ——进入 PREROUTING 链,因为访问的是容器映射到主机的端口,在这里会进行 NAT,目的地址及端口转换成容器在 A 网络中的地址和端口
    |
    ——转换完地址,目的地址不是本机,进入 FORWARD 链
    |
    ——匹配 filter 规则,匹配到规则被放行或者被丢弃
    |
    ——进入容器,服务接受到 SYN 包,返回 ACK 包
    |
    ——从 A 网络的 bridge 网关流出,目的地址为管理服务所在节点地址
    |
    ——不是本机,进入 FOWARD 链
    |
    ——匹配 filter 规则,匹配到规则被放行或者被丢弃

查看详细的 iptables 规则:

sudo iptables -nvL --line-number

发现这样一条规则:

76    368K   23M DROP       all  --  br-9b331161a502 !br-9b331161a502  0.0.0.0/0           !172.16.2.250  

br-9b331161a502 正式 A 网络的网关地址。

这条规则的意思是,从 br-9b331161a502 网关出来的,流向非 br-9b331161a502 网关的,目的地址不是

172.16.2.250 的都 DROP 掉。

于是 ACK 包在这里被 DROP 掉了,导致网络连接失败。

修复:

 sudo iptables -I DOCKER-USER 76 -i br-9b331161a502 ! -o br-9b331161a502 -s 0.0.0.0 ! -d 172.16.0.0/16  -j DROP  

允许 k8s 节点网段通过。

但这样会带来一个问题,在容器内也能访问 k8s 网络。

解决思路:丢弃从 A 网络容器内发出的到 k8s 网络的握手包。

sudo iptables -I DOCKER-USER 76 -p tcp -m tcp -i br-9b331161a502 ! -o br-9b331161a502 -d 172.16.0.0/16 --tcp-flags SYN,ACK,FIN,RST,URG,PSH SYN  -j DROP 

至此,问题解决。

另一个问题

在排查上面问题的过程中,发现一个新的问题,本来以为是和上面的问题有关的,后面发现无关。

现象是在主机上直接连接两个端口,发现都是不通的。

$ telnet 172.16.55.244 34818
Trying 172.16.55.244...

$ telnet 172.16.55.244 34848
Trying 172.16.55.244...

在容器上

netstat -nat

查看网络状态:

...
tcp 0 1 172.16.55.244:37116 172.16.55.244:34848 SYN_SENT
...

从输出中可以看到,SYN 包已经发出去了。

在容器内查看:

tcp 0 0 172.18.0.3:22 172.16.55.244:34848 SYN_RECV

发现容器是收到了包的,那问题就出在包返回的途中。

包返回会经过哪里呢?

返回的包经过 iptables,因为目的地址是本地,所以会进入 INPUT 表,而 INPUT 表中有这样一条规则:

DROP    all     --      br-9b331161a502     *       0.0.0.0/0       172.16.55.244

在这里被 DROP 掉了。

这个规则本身没有问题,因为容器是对外开放的,此规则可以防止用户访问到容器所在节点。

总结

容器网络问题,应该遵循此过程:

  1. 在主机抓包,看包有没有到达主机,排除服务到容器主机之间的网络问题;
  2. 在容器内抓包,看包有没有容器,如果到达了主机没有到达容器,说明是在 FORWARD 过程被 DROP 掉;
    如果到达了容器,但是没有返回 ACK 包,说明在返回的 FORWARD 过程中被 DROP 掉了;
  3. 排查 iptables;

docker 的网路控制很多是基于 iptables 的,如果没有搞清楚流量在主机、iptables 和 容器之前是怎么流转的,那么排查起来就会比较费劲。

iptables 本身较为复杂,要熟练允许还需要多多练习。

参考

[1] iptables 及 docker 容器网络分析
[2] docker(7): 网络初探