尚经起名网

您现在的位置是:首页 >

企业资讯

免费领3000名片赞 - 抖音刷粉全网+最低价啊免费

时间:2024-05-17 13:14 评论
网络的容器运行客户端访问服务,两者通信有问题。以后的报文情况类似,双方再也无法进行通信了。在多网卡的情况下,可能会发生【服务器端】【源地址】不对的情况,这是内核选路的结果。即使服务端发送报文的源地址有误,只要对方能正常接收并处理,也不会导致网络不通。协议进行通信,它们之间的网络是正常的。13,也会被内核直接丢弃,这种情况下,服务端和客户端也能正常通信。...

自助下单地址(拼多多砍价,ks/qq/dy赞等业务):点我进入

一、问题背景

工作中遇到docker容器下UDP协议网络故障的问题。 困扰了很久,还蛮有意思​​的,所以想写下来分享给大家。

我们有一个基于 UDP 协议的应用程序。 部署后发现不能用,但是切换到TCP协议是可以的(应用同时支持UDP和TCP协议,切换到TCP模式后一切正常)。

虽然切换到TCP可以解决问题,但是我们还是想知道容器网络模式下UDP协议为什么会出现这个问题,以免后面其他UDP应用出现异常。

这个问题抽象成这样:如果有一个UDP服务运行在宿主机上(或者网络模型为宿主机的容器中),监听0.0.0.0地址(也就是所有ip地址),从运行在docker上bridge(网桥为docker0)网络上的容器运行客户端访问服务,两者通信出现问题。

注意以上限制。 通过测试,我们发现以下几种情况是正常的:

使用TCP协议就没有这个问题。 如果UDP服务器监听eth0 ip而不是0.0.0.0,就不会出现这个问题。 并非所有应用程序都有此问题。 我们的DNS(dnsmasq + kubeDNS)也是这样部署的,但是功能都正常

这个问题在docker上也有issue记录,但是目前没有合理的解决方案。

本文分析出现该问题的原因,希望能为同样遇到该问题的读者提供一些帮助。

2.重现问题

这个问题很容易重现。 我的实验是在ubuntu16.04下用netcat命令完成的。 其他系统应该类似。

在宿主机上通过nc监听56789端口,然后使用bridge网络模式运行一个容器,在容器中使用nc发送数据。

第一条消息可以发送出去,但是后面的消息虽然可以在网络上看到,但是对方是收不到的。

在主机上运行nc UDP server(-u表示UDP协议,-l表示监听端口)

$ nc -ul 56789
$  ss  -uan | grep 56789
$ ss -an | grep 56789
udp    UNCONN     0      0         *:56789                 *:*
udp    UNCONN     0      0      [::]:56789              [::]:*

注意:默认不指定绑定ip,即监听0.0.0.0。

然后在同一台主机上,启动一个容器并运行客户端:

$ docker run -it apline sh
/ # nc  -u  172.16.13.13  56789

nc的通信是双向的,无论对方输入什么字符,对方回车后都能立即收到。

但是在这种模式下,客户端可以接收到第一个输入,但是对方无法接收到后续的消息。

本次实验中容器使用docker默认网络,容器ip为172.17.0.3,通过veth pair(图中未显示)连接到虚拟网桥docker0(ip地址为172.17.0.1) ), 而主机自己的网络是eth0,它的ip地址是172.16.13.13。

 172.17.0.3
+----------+
|   eth0   |
+----+-----+
     |
     |
     |
     |
+----+-----+          +----------+
| docker0  |          |  eth0    |
+----------+          +----------+
172.17.0.1            172.16.13.13

三、tcpdump抓包分析

遇到这样的疑难杂症,第一个想到的就是抓包。

我们需要在 docker0 上捕获数据包,因为这是数据包必须去的地方。

通过过滤容器的IP地址,容器很容易找到感兴趣的数据包:

$  sudo tcpdump -i docker0 -nn host 172.17.0.3

为了模拟大部分应用的一问一答的通信模式,我们一共发送了3条消息火拼qq堂游戏大厅和服务器udp通讯不通,在docker0接口上使用tcpdump抓取消息:

客户端先向服务器发送111的字符串,服务器回复222的字符串,客户端继续发送333的字符串

抓包结果如下,可以发现发送第一个包没有问题。

UDP没有ACK报文,客户端无法知道对方是否收到。 这里没有问题,也没有看到相应的ICMP报错信息。

但是当服务器发送第二个报文时,对方会返回一个ICMP报文,告知38908端口不可达; 客户端发送的第三条消息也是如此。 后续消息的情况类似,双方无法再通信。

11:20:43.973286 IP 172.17.0.3.38908 > 172.16.13.13.56789: UDP, length 6
11:20:50.102018 IP 172.17.0.1.56789 > 172.17.0.3.38908: UDP, length 6
11:20:50.102129 IP 172.17.0.3 > 172.17.0.1: ICMP 172.17.0.3 udp port 38908 unreachable, length 42
11:20:54.503198 IP 172.17.0.3.38908 > 172.16.13.13.56789: UDP, length 3
11:20:54.503242 IP 172.16.13.13 > 172.17.0.3: ICMP 172.16.13.13 udp port 56789 unreachable, length 39

此时宿主机上的UDP nc server并没有退出,可以看到使用ss -uan | 仍然在监听端口 grep 56789。

四、问题原因分析

从网络报文分析可以看出,服务端返回的报文源地址并不是我们期望的eth0地址火拼qq堂游戏大厅和服务器udp通讯不通,而是docker0的地址,客户端直接认为报文不合法,返回发送给另一方的 ICMP 消息。

那么问题的原因也可以分为两部分:

为什么回复包的源地址不对? 由于UDP是无状态的,内核如何判断源地址不正确?主机多网络接口UDP源地址选择问题

第一个问题的关键字是:UDP 和多个网络接口。

因为如果主机上只有一个网络接口,发送报文的源地址一定不会错; 而且我们也测试过TCP协议可以处理这个问题。

通过搜索,发现这确实是一个已知问题。

图片.png

这个问题可以用一句话来概括:在多网卡的UDP情况下,[服务器端][源地址]可能不正确,这是内核路由的结果。

为什么UDP和TCP有不同的路由逻辑?

因为UDP是无状态协议,内核不会保存连接双方的信息,所以发送的每条消息都被认为是独立的,socket层不会每次发送消息都指定默认使用的源地址,只是为了说明对方的地址。

因此,内核会为要发送出去的消息选择一个ip,通常是消息路由经过的设备的ip地址。

那为什么dnsmasq服务没有这个问题呢?

于是我使用strace工具抓取了问题应用的dnsmasq和网络套接字系统调用,看看它们之间的区别。

dnsmasq 启动时监听UDP和TCP的54端口

因为是在本地机器上测试的,为了防止和本地DNS监听的DNS端口冲突,我选择了54,而不是标准的53端口:

socket(PF_INET, SOCK_DGRAM, IPPROTO_IP) = 4
setsockopt(4, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(4, {sa_family=AF_INET, sin_port=htons(54), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
setsockopt(4, SOL_IP, IP_PKTINFO, [1], 4) = 0
##############################################
socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 5
setsockopt(5, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(5, {sa_family=AF_INET, sin_port=htons(54), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(5, 5)                            = 0

相比TCP,UDP部分listen少,但是有多个setsockopt(4, SOL_IP, IP_PKTINFO, [1], 4)语句。

这两点和我们的问题有没有关系,先放一边,继续看发送消息的部分。

dnsmasq 系统调用接收和发送数据包直接使用 recvmsg 和 sendmsg 系统调用:

recvmsg(4, {msg_name(16)={sa_family=AF_INET, sin_port=htons(52072), sin_addr=inet_addr("10.111.59.4")}, msg_iov(1)=[{"\315\n\1 \0\1\0\0\0\0\0\1\fterminal19-0\5u5016\3"..., 4096}], msg_controllen=32, {cmsg_len=28, cmsg_level=SOL_IP, cmsg_type=, ...}, msg_flags=0}, 0) = 67
sendmsg(4, {msg_name(16)={sa_family=AF_INET, sin_port=htons(52072), sin_addr=inet_addr("10.111.59.4")}, msg_iov(1)=[{"\315\n\201\200\0\1\0\1\0\0\0\1\fterminal19-0\5u5016\3"..., 83}], msg_controllen=28, {cmsg_len=28, cmsg_level=SOL_IP, cmsg_type=, ...}, msg_flags=0}, 0) = 83

有问题的UDP应用的strace结果如下:

[pid   477] socket(PF_INET6, SOCK_DGRAM, IPPROTO_IP) = 124
[pid   477] setsockopt(124, SOL_IPV6, IPV6_V6ONLY, [0], 4) = 0
[pid   477] setsockopt(124, SOL_IPV6, IPV6_MULTICAST_HOPS, [1], 4) = 0
[pid   477] bind(124, {sa_family=AF_INET6, sin6_port=htons(6088), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, 28) = 0
[pid   477] getsockname(124, {sa_family=AF_INET6, sin6_port=htons(6088), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28]) = 0
[pid   477] getsockname(124, {sa_family=AF_INET6, sin6_port=htons(6088), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28]) = 0
[pid   477] recvfrom(124, "j\201\2450\201\242\241\3\2\1\5\242\3\2\1\n\243\0160\f0\n\241\4\2\2\0\225\242\2\4\0"..., 2048, 0, {sa_family=AF_INET6, sin6_port=htons(38790), inet_pton(AF_INET6, "::ffff:172.17.0.3", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28]) = 168
[pid   477] sendto(124, "k\202\2\0210\202\2\r\240\3\2\1\5\241\3\2\1\v\243\5\33\3TDH\244\0220\20\240\3\2"..., 533, 0, {sa_family=AF_INET6, sin6_port=htons(38790), inet_pton(AF_INET6, "::ffff:172.17.0.3", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, 28) = 533

对应逻辑如下:使用ipv6绑定在0.0.0.0和6088端口,调用getsockname获取当前socket绑定的端口信息,数据传输时使用recvfrom和sendto。

相比之下,两者之间有几个区别:

后者使用ipv6,而前者是ipv4。 后者使用recvfrom和sendto传输数据,而前者使用sendmsg和recvmsg。 前者调用setsockopt设置IP_PKTINFO的值,后者则不

因为在数据传输过程中出现了错误,第一个疑点是sendmsg和sendto有些不同导致源地址的选择不同。 从man sendto可以知道sendmsg在msghdr中包含了更多的控制信息。

一个合理的猜测是msghdr包含了内核选择源地址的信息!

通过查找,发现IP_PKTINFO选项是让内核在socket中保存IP报文的信息,当然还包括报文的源地址和目的地址。 IP_PKTINFO和msghdr的关系可以在这个stackoverflow中找到:

man 7 ip文档也解释了IP_PKTINFO是如何控制源地址选择的:

IP_PKTINFO (since Linux 2.2)
              Pass  an  IP_PKTINFO  ancillary message that contains a pktinfo structure that supplies some information about the incoming packet.  This only works for datagram ori‐
              ented sockets.  The argument is a flag that tells the socket whether the IP_PKTINFO message should be passed or not.  The message itself can only be sent/retrieved as
              control message with a packet using recvmsg(2) or sendmsg(2).
                  struct in_pktinfo {
                      unsigned int   ipi_ifindex;  /* Interface index */
                      struct in_addr ipi_spec_dst; /* Local address */
                      struct in_addr ipi_addr;     /* Header Destination
                                                      address */
                  };
              ipi_ifindex  is the unique index of the interface the packet was received on.  ipi_spec_dst is the local address of the packet and ipi_addr is the destination address
              in the packet header.  If IP_PKTINFO is passed to sendmsg(2) and ipi_spec_dst is not zero, then it is used as the local source address for the  routing  table  lookup
              and  for  setting up IP source route options.  When ipi_ifindex is not zero, the primary local address of the interface specified by the index overwrites ipi_spec_dst
              for the routing table lookup.

如果ipi_spec_dst和ipi_ifindex不为空,可以作为源地址选择的依据,而不是让内核通过路由来决定。

也就是说,通过将IP_PKTINFO套接字选项设置为1,然后使用recvmsg和sendmsg来传输数据,我们可以确保源地址选择符合我们的预期。

这也是 dnsmasq 使用的方案,有问题的应用程序使用默认的 recvfrom 和 sendto。

为什么内核认为它是非法的而丢弃具有与以前不同的源地址的数据包?

因为我们之前说过UDP协议是无连接的,socket默认不会保存双方的连接信息。 即使服务器发送的报文源地址错误,只要对方能正常接收和处理,就不会造成网络故障。

但是conntrack就不是这样了,内核的netfilter模块会保存连接的状态,作为防火墙设置的依据。

它保存的UDP连接只是简单记录本机ip和端口,以及对端ip和端口,并没有保存更多的内容。

这篇文章可以参考intables info网站上的文章:

#UDP连接

在查找根本原因之前,我们尝试使用SNAT修改服务器响应报文的源地址,希望能够修复问题,但发现这种方法不起作用。 为什么?

因为SNAT是在netfilter的最后做的,之前netfilter的conntrack不知道这个连接,就直接丢弃了,所以即使加了SNAT也无法工作。

conntrack 功能可以去掉吗? 例如解决方案:

iptables -I OUTPUT -t raw -p udp --sport 5060 -j CT --notrack
iptables -I PREROUTING -t raw -p udp --dport 5060 -j CT --notrack

答案也是否定的,因为NAT需要conntrack做翻译工作,如果去掉conntrack,SNAT就完全没用了。

5.解决方案

知道了问题的原因,就很容易找到解决办法。

使用TCP协议

如果服务器和客户端使用TCP协议进行通信,那么它们之间的网络是正常的。

$ nc -l 56789

侦听指定接口上的特定绑定

使用 nc 启动一个 udp 服务器,监听 eth0:

$  nc -ul   172.16.13.13   56789

nc后面可以跟两个参数,分别代表ip和port,表示服务器在监听特定的ip。

如果收到报文的目的地址不是172.16.13.13,则直接被内核丢弃。 这样的话,服务端和客户端也可以正常通信了。

更改应用程序实现

修改应用逻辑,在UDP socket上设置IP_PKTIFO,通过recvmsg和sendmsg函数发送数据。 6. 参考

docker容器网络下UDP协议的一个问题

为 UDP 套接字设置源 IP

LinuxC下获取UDP包中的路由目的IP地址和头标识目的地址

源IP地址选择

UDP recvmsg 返回目的地址和目的接口信息

说说UDP:连通性和负载均衡

告知您未知的 UDP:陷阱和用途