0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

关于容器网络下使用UDP协议无法通讯问题的分析和处理

Linux爱好者 来源:Linux爱好者 2023-05-19 15:11 次阅读

一、问题背景

工作中遇到一个 docker 容器下 UDP 协议网络不通的问题,困扰了很久,也比较有意思,所以想写下来和大家分享。

我们有个应用是基于 UDP 协议的,部署上去发现无法工作,但是换成 TCP 协议是可以的(应用同时支持 UDP、TCP 协议,切换成 TCP 模式发现一切正常)。

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

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

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

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

这个问题在 docker 上也有 issue 记录,但是目前并没有合理的解决方案。「https://github.com/moby/moby/issues/15127」「https://github.com/iotaledger/iri/issues/961」

这篇文章就分析一下出现这个问题的原因,希望给同样遇到这个问题的读者提供些帮助。

二、重现问题

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

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

第一个报文是能发送出去的,但是以后的报文虽然在网络上能看到,但是对方无法接收。

在宿主机运行 nc UDP 服务器(-u 表示 UDP 协议,-l 表示监听的端口)

$nc-ul56789
$ss-uan|grep56789

$ss-an|grep56789
udpUNCONN00*:56789*:*
udpUNCONN00[::]:56789[::]:*

注:默认没有指定绑定ip,就是监听在0.0.0.0上。

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

$dockerrun-itaplinesh
/#nc-u172.16.13.1356789

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.1172.16.13.13

三、tcpdump抓包分析

遇到这种疑难杂症,第一个想到的抓包。我们需要在 docker0 上抓包,因为这是报文必经过的地方。通过过滤容器的 ip 地址,很容器找到感兴趣的报文:

$sudotcpdump-idocker0-nnhost172.17.0.3

为了模拟多数应用一问一答的通信方式,我们一共发送三个报文,并用 tcpdump 抓取 docker0 接口上的报文:

  • 客户端先向服务器端发送 111 字符串
  • 服务器端回复 222 字符串
  • 客户端继续发送 333 字符串抓包的结果如下,可以发现第一个报文发送出去没有任何问题。

UDP 是没有 ACK 报文的,所以客户端无法知道对方有没有收到,这里说的没有问题没有看到对应的 ICMP 差错报文。

但是第二个报文从服务端发送的报文,对方会返回一个 ICMP 告诉端口 38908 不可达;第三个报文从客户端发送的报文也是如此。以后的报文情况类似,双方再也无法进行通信了。

1143.973286IP172.17.0.3.38908>172.16.13.13.56789:UDP,length6
1150.102018IP172.17.0.1.56789>172.17.0.3.38908:UDP,length6
1150.102129IP172.17.0.3>172.17.0.1:ICMP172.17.0.3udpport38908unreachable,length42
1154.503198IP172.17.0.3.38908>172.16.13.13.56789:UDP,length3
1154.503242IP172.16.13.13>172.17.0.3:ICMP172.16.13.13udpport56789unreachable,length39

而此时主机上 UDP nc 服务器并没有退出,使用 ss -uan | grep 56789 可能看到它仍然在监听着该端口。

四、问题原因分析

从网络报文的分析中可以看到服务端返回的报文源地址不是我们预想的 eth0 地址,而是 docker0 的地址,而客户端直接认为该报文是非法的,返回了 ICMP 的报文给对方。

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

  • 为什么应答报文源地址是错误的?
  • 既然 UDP 是无状态的,内核怎么判断源地址不正确呢?

主机多网络接口 UDP 源地址选择问题第一个问题的关键词是:UDP 和多网络接口。因为如果主机上只有一个网络接口,发出去的报文源地址一定不会有错;而我们也测试过 TCP 协议是能够处理这个问题的。

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

fd435426-f613-11ed-90ce-dac502259ad0.png

这个问题可以归结为一句话:UDP 在多网卡的情况下,可能会发生【服务器端】【源地址】不对的情况,这是内核选路的结果。

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

因为 UDP 是无状态的协议,内核不会保存连接双方的信息,因此每次发送的报文都认为是独立的,socket 层每次发送报文默认情况不会指明要使用的源地址,只是说明对方地址。

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

那么,为什么 dnsmasq 服务没有这个问题呢?于是我使用 strace 工具抓取了 dnsmasq 和出问题应用的网络 socket 系统调用,来查看它们两个到底有什么区别。

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
111fterminal19-05u50163"...,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
201200111fterminal19-05u50163"...,83}],msg_controllen=28,{cmsg_len=28,cmsg_level=SOL_IP,cmsg_type=,...},msg_flags=0},0)=83

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

[pid477]socket(PF_INET6,SOCK_DGRAM,IPPROTO_IP)=124
[pid477]setsockopt(124,SOL_IPV6,IPV6_V6ONLY,[0],4)=0
[pid477]setsockopt(124,SOL_IPV6,IPV6_MULTICAST_HOPS,[1],4)=0
[pid477]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

[pid477]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
[pid477]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

[pid477]recvfrom(124,"j20124502012422413215242321
243160f0
24142222524224"...,2048,0,{sa_family=AF_INET6,sin6_port=htons(38790),inet_pton(AF_INET6,":172.17.0.3",&sin6_addr),sin6_flowinfo=0,sin6_scope_id=0},[28])=168

[pid477]sendto(124,"k20222102022
2403215241321v2435333TDH2442202024032"...,533,0,{sa_family=AF_INET6,sin6_port=htons(38790),inet_pton(AF_INET6,":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 中找到:https://stackoverflow.com/questions/3062205/setting-the-source-ip-for-a-udp-socket

而 man 7 ip 文档中也说明了 IP_PKTINFO 是怎么控制源地址选择的:

IP_PKTINFO(sinceLinux2.2)
PassanIP_PKTINFOancillarymessagethatcontainsapktinfostructurethatsuppliessomeinformationabouttheincomingpacket.Thisonlyworksfordatagramori‐
entedsockets.TheargumentisaflagthattellsthesocketwhethertheIP_PKTINFOmessageshouldbepassedornot.Themessageitselfcanonlybesent/retrievedas
controlmessagewithapacketusingrecvmsg(2)orsendmsg(2).

structin_pktinfo{
unsignedintipi_ifindex;/*Interfaceindex*/
structin_addripi_spec_dst;/*Localaddress*/
structin_addripi_addr;/*HeaderDestination
address*/
};

ipi_ifindexistheuniqueindexoftheinterfacethepacketwasreceivedon.ipi_spec_dstisthelocaladdressofthepacketandipi_addristhedestinationaddress
inthepacketheader.IfIP_PKTINFOispassedtosendmsg(2)andipi_spec_dstisnotzero,thenitisusedasthelocalsourceaddressfortheroutingtablelookup
andforsettingupIPsourcerouteoptions.Whenipi_ifindexisnotzero,theprimarylocaladdressoftheinterfacespecifiedbytheindexoverwritesipi_spec_dst
fortheroutingtablelookup.

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

也就是说,通过设置 IP_PKTINFO socket 选项为 1,然后使用 recvmsg 和 sendmsg 传输数据就能保证源地址选择符合我们的期望。这也是 dnsmasq 使用的方案,而出问题的应用是因为使用了默认的 recvfrom 和 sendto。

为什么内核会把源地址和之前不同的报文丢弃,认为它是非法的?

因为我们前面已经说过,UDP 协议是无连接的,默认情况下 socket 也不会保存双方连接的信息。即使服务端发送报文的源地址有误,只要对方能正常接收并处理,也不会导致网络不通。

但是 conntrack 不是这样,内核的 netfilter 模块会保存连接的状态,并作为防火墙设置的依据。它保存的 UDP 连接,只是简单记录了主机上本地 ip 和端口,和对端 ip 和端口,并不会保存更多的内容。

关于这块可参考 iptables info 网站的文章:http://www.iptables.info/en/connection-state.html#UDPCONNECTIONS

在找到根源之前,我们曾经尝试过用 SNAT 来修改服务端应答报文的源地址,期望能够修复该问题,但是却发现这种方法行不通,为什么呢?

因为 SNAT 是在 netfilter 最后做的,在之前 netfilter 的 conntrack 因为不认识该 connection,直接丢弃了,所以即使添加了 SNAT 也是无法工作的。

那能不能把 conntrack 功能去掉呢?比如解决方案:

iptables-IOUTPUT-traw-pudp--sport5060-jCT--notrack
iptables-IPREROUTING-traw-pudp--dport5060-jCT--notrack

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

五、解决方案

知道了问题的原因,解决方案也就很容易找到。

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

$nc-l56789

2、监听在特定绑定在指定接口上使用 nc 启动一个 udp 服务器,监听在 eth0 上:

$nc-ul172.16.13.1356789

nc 可以跟两个参数,分别代表 ip 和 端口,表示服务端监听在某个特定 ip 上。如果接收到的报文目的地址不是 172.16.13.13,也会被内核直接丢弃,这种情况下,服务端和客户端也能正常通信。

3、改动应用程序实现修改应用程序的逻辑,在 UDP socket 上设置 IP_PKTIFO,并通过 recvmsg 和 sendmsg 函数传输数据。


审核编辑 :李倩


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 服务器
    +关注

    关注

    12

    文章

    8120

    浏览量

    82530
  • UDP
    UDP
    +关注

    关注

    0

    文章

    311

    浏览量

    33622
  • 容器
    +关注

    关注

    0

    文章

    481

    浏览量

    21883

原文标题:关于容器网络下使用UDP协议无法通讯问题的分析和处理

文章出处:【微信号:LinuxHub,微信公众号:Linux爱好者】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    基于UDP协议的ARM与X86平台之间的通讯方案

    本文介绍了通过案例,首先在触摸屏进行画图,使其在液晶屏上显示,同时通过网络传输数据,使其在计算机屏幕上显示,并由计算机控制清除液晶屏上的图形,实现了基于UDP协议的ARM、X86平台之间的通讯
    发表于 04-14 15:46 1054次阅读

    基于UDP协议的ARM、X86平台之间的通讯方案

    本文介绍了通过案例,首先在触摸屏进行画图,使其在液晶屏上显示,同时通过网络传输数据,使其在计算机屏幕上显示,并由计算机控制清除液晶屏上的图形,实现了基于UDP协议的ARM、X86平台之间的通讯
    发表于 07-31 16:06 1447次阅读

    linxu网络协议分析:IP协议、TCP协议UDP协议

    本章节主要介绍linxu网络模型、以及常用的网络协议分析以太网协议、IP协议、TCP
    的头像 发表于 10-28 16:44 3302次阅读
    linxu<b class='flag-5'>网络</b><b class='flag-5'>协议</b><b class='flag-5'>分析</b>:IP<b class='flag-5'>协议</b>、TCP<b class='flag-5'>协议</b>、<b class='flag-5'>UDP</b><b class='flag-5'>协议</b>

    基于adhoc网络,采用UDP协议进行通讯

    上传的附件是自己编的程序,请高手基于下面几点目的,帮忙完善改正。此通信是基于ADHOC网络,所以与普通的UDP通讯不同:1、无客户端与服务器之分2、通信时不知道对方IP,需要代码获取3、采用非阻塞模式4、发送和接收完立即返回因为
    发表于 04-26 09:56

    UDP网络通讯

    UDP基于网络通讯
    发表于 09-21 10:36

    【EVB-335X-II试用体验】UDP网络通讯

    前面已经介绍过编译一个完整的应用程序。今天在此基础上编译一个UDP通讯应用程序。实现的功能:完成一个UDP服务器端程序,实现接收客户端的报文,在串口终端打印出来,同时将报文返回给客户端。1 硬件连接
    发表于 07-24 11:52

    关于MyRIO的Modbus通讯问

    哪位大侠研究过MyRIO和Modbus的通讯问题啊?有好几个硬件设备都是用Modbus通讯的,都调不出来,憋了好久了!哪位大神帮我解答一啊,有例子就更好了,万分感谢,痛苦等待!
    发表于 08-21 00:39

    TCP协议UDP协议的区别有哪些

    计算机网络简答题1、TCP 协议UDP 协议的区别有哪些?(1)TCP 属于面向连接的协议UDP
    发表于 08-06 08:43

    基于UDP协议网络通信应用程序

    基于UDP协议网络通信应用程序(UDP-Socket)前两篇文章介绍了基于TCP/IP协议网络
    发表于 11-05 08:29

    通讯协议TCP和UDP协议使用方法

    通讯协议TCP和UDP协议UDP会把数据一股脑儿地发送出去,并不会在意是否全部收到,适用于广播类型多对多
    发表于 01-21 14:53

    串口与单片机通讯问

    串口与单片机通讯问
    发表于 09-23 23:04 62次下载

    UDP协议,UDP协议是什么意思

    UDP协议,UDP协议是什么意思 UDP 是User Datagram Protocol的简称, 中文名是用户数据包
    发表于 03-29 17:35 1408次阅读

    Linux内核网络数据包发送在UDP协议层的处理

    1. 前言 本文分享了Linux内核网络数据包发送在UDP协议层的处理,主要分析udp_sen
    的头像 发表于 08-04 16:23 3135次阅读
    Linux内核<b class='flag-5'>网络</b>数据包发送在<b class='flag-5'>UDP</b><b class='flag-5'>协议</b>层的<b class='flag-5'>处理</b>

    有线网络通信实验2之UDP协议

    UDP协议是TCP/IP协议栈中传输层协议,是一个简单的面向数据报的协议,在传输层中还有一个TCP协议
    的头像 发表于 03-01 14:29 1510次阅读
    有线<b class='flag-5'>网络</b>通信实验2之<b class='flag-5'>UDP</b><b class='flag-5'>协议</b>

    网络下使用UDP协议无法通讯问题的分析处理

    工作中遇到一个 docker 容器UDP 协议网络不通的问题,困扰了很久,也比较有意思,所以想写下来和大家分享。
    的头像 发表于 05-19 15:11 2386次阅读
    <b class='flag-5'>网络</b>下使用<b class='flag-5'>UDP</b><b class='flag-5'>协议</b><b class='flag-5'>无法</b><b class='flag-5'>通讯问</b>题的<b class='flag-5'>分析</b>和<b class='flag-5'>处理</b>