我们有个应用是基于 UDP 协议的,部署上去发现无法工作,但是换成 TCP 协议是可以的(应用同时支持 UDP、TCP 协议,切换成 TCP 模式发现一切正常)。
虽然换成 TCP 能解决问题,但是我们还是想知道到底 UDP 协议在容器网络模式下为什么会出现这个问题,以防止后面其他 UDP 应用会有异常。
这个问题抽象出来是这样的:如果有 UDP 服务运行在宿主机上(或者运行在网络模型为 host 的容器里),并且监听在 0.0.0.0 地址(也就是所有的 ip 地址),从运行在 docker bridge(网桥为 docker0) 网络的容器运行客户端访问服务,两者通信有问题。
注意以上的的限制条件,通过测试,我们发现下来几种情况都是正常的:
这个问题在 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 表示监听的端口)
注:默认没有指定绑定ip,就是监听在0.0.0.0上。
然后在同一宿主机上,启动一个容器,运行客户端:
nc 的通信是双方的,不管对方输入什么字符,回车后对方就能立即收到。
但是在这个模式下,客户端第一次输入对方能够收到,后续的报文对方都收不到。
在这个实验中,容器使用的是 docker 的默认网络,容器的 ip 是 172.17.0.3,通过 veth pair(图中没有显示)连接到虚拟网桥 docker0(ip 地址为 172.17.0.1),宿主机本身的网络为 eth0,其 ip 地址为 172.16.13.13。
遇到这种疑难杂症,第一个想到的抓包。
我们需要在 docker0 上抓包,因为这是报文必经过的地方。
通过过滤容器的 ip 地址,很容器找到感兴趣的报文:
为了模拟多数应用一问一答的通信方式,我们一共发送三个报文,并用 tcpdump 抓取 docker0 接口上的报文:
抓包的结果如下,可以发现第一个报文发送出去没有任何问题。
UDP 是没有 ACK 报文的,所以客户端无法知道对方有没有收到,这里说的没有问题没有看到对应的 ICMP 差错报文。
但是第二个报文从服务端发送的报文,对方会返回一个 ICMP 告诉端口 38908 不可达;第三个报文从客户端发送的报文也是如此。以后的报文情况类似,双方再也无法进行通信了。
而此时主机上 UDP nc 服务器并没有退出,使用 ss -uan | grep 56789 可能看到它仍然在监听着该端口。
从网络报文的分析中可以看到服务端返回的报文源地址不是我们预想的 eth0 地址,而是 docker0 的地址,而客户端直接认为该报文是非法的,返回了 ICMP 的报文给对方。
那么问题的原因也可以分为两个部分:
第一个问题的关键词是:UDP 和多网络接口。
因为如果主机上只有一个网络接口,发出去的报文源地址一定不会有错;而我们也测试过 TCP 协议是能够处理这个问题的。
通过搜索,发现这确实是个已知的问题。
这个问题可以归结为一句话:UDP 在多网卡的情况下,可能会发生【服务器端】【源地址】不对的情况,这是内核选路的结果。
为什么 UDP 和 TCP 有不同的选路逻辑呢?
因为 UDP 是无状态的协议,内核不会保存连接双方的信息,因此每次发送的报文都认为是独立的,socket 层每次发送报文默认情况不会指明要使用的源地址,只是说明对方地址。
因此,内核会为要发出去的报文选择一个 ip,这通常都是报文路由要经过的设备 ip 地址。
那么,为什么 dnsmasq 服务没有这个问题呢?
于是我使用 strace 工具抓取了 dnsmasq 和出问题应用的网络 socket 系统调用,来查看它们两个到底有什么区别。
dnsmasq 在启动阶段监听了 UDP 和 TCP 的 54 端口
因为是在本地机器上测试的,为了防止和本地 DNS 监听的DNS端口冲突,我选择了 54 而不是标准的 53 端口:
比起 TCP,UDP 部分少了 listen,但是多个 setsockopt(4, SOL_IP, IP_PKTINFO, [1], 4) 这句。
到底这两点和我们的问题是否有关,先暂时放着,继续看传输报文的部分。
dnsmasq 收包和发包的系统调用,直接使用 recvmsg 和 sendmsg 系统调用:
而出问题的 UDP 应用 strace 结果如下:
其对应的逻辑是这样的:使用 ipv6 绑定在 0.0.0.0 和 6088 端口,调用 getsockname 获取当前 socket 绑定的端口信息,数据传输过程使用的是 recvfrom 和 sendto。
对比下来,两者的不同有几点:
因为是在传输数据的时候出错的,因此第一个疑点是 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 是怎么控制源地址选择的:
如果 ipi_spec_dst 和 ipi_ifindex 不为空,它们都能作为源地址选择的依据,而不是让内核通过路由决定。
也就是说,通过设置 IP_PKTINFO socket 选项为 1,然后使用 recvmsg 和 sendmsg 传输数据就能保证源地址选择符合我们的期望。
这也是 dnsmasq 使用的方案,而出问题的应用是因为使用了默认的 recvfrom 和 sendto。
为什么内核会把源地址和之前不同的报文丢弃,认为它是非法的?
因为我们前面已经说过,UDP 协议是无连接的,默认情况下 socket 也不会保存双方连接的信息。即使服务端发送报文的源地址有误,只要对方能正常接收并处理,也不会导致网络不通。
但是 conntrack 不是这样,内核的 netfilter 模块会保存连接的状态,并作为防火墙设置的依据。
它保存的 UDP 连接,只是简单记录了主机上本地 ip 和端口,和对端 ip 和端口,并不会保存更多的内容。
关于 这块可参考 intables info 网站的文章:
http://www.iptables.info/en/connection-state.html#UDPCONNECTIONS
在找到根源之前,我们曾经尝试过用 SNAT 来修改服务端应答报文的源地址,期望能够修复该问题,但是却发现这种方法行不通,为什么呢?
因为 SNAT 是在 netfilter 最后做的,在之前 netfilter 的 conntrack 因为不认识该 connection,直接丢弃了,所以即使添加了 SNAT 也是无法工作的。
那能不能把 conntrack 功能去掉呢?比如解决方案:
答案也是否定的,因为 NAT 需要 conntrack 来做翻译工作,如果去掉 conntrack 等于 SNAT 完全没用。
知道了问题的原因,解决方案也就很容易找到。
nc 可以跟两个参数,分别代表 ip 和 端口,表示服务端监听在某个特定 ip 上。
如果接收到的报文目的地址不是 172.16.13.13,也会被内核直接丢弃,这种情况下,服务端和客户端也能正常通信。
docker 容器网络下 UDP 协议的一个问题
https://cizixs.com/2017/08/21/docker-udp-issue/
Setting the source IP for a UDP socket
https://stackoverflow.com/questions/3062205/setting-the-source-ip-for-a-udp-socket
LinuxC下获取UDP包中的路由目的IP地址和头标识目的地址
https://www.cnblogs.com/kissazi2/p/3158603.html
Source IP address selection
https://www.ibm.com/docs/en/zos/2.1.0?topic=profiletcpip-source-ip-address-selection
UDP recvmsg 返回目的地址和目的接口信息
http://blog.chinaunix.net/uid-16813896-id-4593514.html
告知你不为人知的 UDP:连接性和负载均衡
https://cloud.tencent.com/developer/article/1004554
告知你不为人知的 UDP:疑难杂症和使用
https://cloud.tencent.com/developer/article/1004554
docker添加挂载目录:先在docker容器里创建目录/import
1.关闭docker
2.sudo su切换到root身份,cd /var/lib/docker/containers/容器id/,进入对应容器目录
3.vi hostconfig.json,修改如下,将容器目录/import绑定到主机/data目录:
4.vi config.v2.json,修改如下,添加MountPoints:
5.启动docker
最后docker ecec -it 容器id /bin/bash进入ls -l /就可以看见import目录
添加端口在这个文件hostconfig.json
首先输入
可以看到我当前的名叫mynginx容器只打开了80端口
在给mynginx容器添加上这条命令:
来设置重启docker之后自动启动该容器。设置完成后再修改hostconfig.json文件中的"PortBindings"就行。
然后停止容器systemctl stop docker
然后进入到该容器的hostconfig.json文件中,增加一个8000的端口
保存后退出
再次启动docker容器systemctl start docker
输入docker ps -a查看
发现已经增加了8000端口
若想要增加容器端口,则需要把config.v2.json中的ExposedPorts也加上你想添加的端口号
客户机获取DHCP服务器主要分为4个步骤:1.IP租用请求:DHCP客户机初始化TCP/IP,通过UDP端口67向网络中发送一个DHCPDISCOVER广播包,请求租用IP地址。该广播包中的源IP地址为0.0.0.0,目标IP地址为255.255.255.255;包中还包含客户机的MAC地址和计算机名。
2.IP租用提供:
任何接收到DHCPDISCOVER广播包并且能够提供IP地址的DHCP服务器,都会通过UDP端口68给客户机回应一个DHCPOFFER广播包,提供一个IP地址。该广播包的源IP地址为DCHP服务器IP,目标IP地址为255.255.255.255;包中还包含提供的IP地址、子网掩码及租期等信息。
3.IP租用选择:
客户机从不止一台DHCP服务器接收到提供之后,会选择第一个收到的DHCPOFFER包,并向网络中广播一个DHCPREQUEST消息包,表明自己已经接受了一个DHCP服务器提供的IP地址。该广播包中包含所接受的IP地址和服务器的IP地址。
所有其他的DHCP服务器撤消它们的提供以便将IP地址提供给下一次IP租用请求。
4.IP租用确认:
被客户机选择的DHCP服务器在收DHCPREQUEST广播后,会广播返回给客户机一个DHCPACK消息包,表明已经接受客户机的选择,并将这一IP地址的合法租用以及其他的配置信息都放入该广播包发给客户机。
客户机在收到DHCPACK包,会使用该广播包中的信息来配置自己的TCP/IP,则租用过程完成,客户机可以在网络中通信。
DHCP客户机在发出IP租用请求的DHCPDISCOVER广播包后,将花费1秒钟的时间等待DHCP服务器的回应,如果1秒钟没有服务器的回应,它会将这一广播包重新广播四次(以2,4,8和16秒为间隔,加上1~1000毫秒之间随机长度的时间)。四次之后,如果仍未能收到服务器的回应,则运行Windows 2000的DHCP客户机将从169.254.0.0/16这个自动保留的私有IP地址(APIPA)中选用一个IP地址,而运行其他 *** 作系统的DHCP客户机将无法获得IP地址。DHCP客户机仍然每隔5分钟重新广播一次,如果收到某个服务器的回应,则继续IP租用过程。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)