【udp】關于docker 容器網絡下使用 UDP 協(xié)議無法通訊問題的分析和處理

一、問題背景

工作中遇到一個 docker 容器下 UDP 協(xié)議網絡不通的問題,困擾了很久嚣鄙,也比較有意思,所以想寫下來和大家分享串结。

我們有個應用是基于 UDP 協(xié)議的哑子,部署上去發(fā)現(xiàn)無法工作,但是換成 TCP 協(xié)議是可以的(應用同時支持 UDP肌割、TCP 協(xié)議卧蜓,切換成 TCP 模式發(fā)現(xiàn)一切正常)。

雖然換成 TCP 能解決問題声功,但是我們還是想知道到底 UDP 協(xié)議在容器網絡模式下為什么會出現(xiàn)這個問題烦却,以防止后面其他 UDP 應用會有異常。

這個問題抽象出來是這樣的:如果有 UDP 服務運行在宿主機上(或者運行在網絡模型為 host 的容器里)先巴,并且監(jiān)聽在 0.0.0.0 地址(也就是所有的 ip 地址)其爵,從運行在 docker bridge(網橋為 docker0) 網絡的容器運行客戶端訪問服務冒冬,兩者通信有問題。

注意以上的的限制條件摩渺,通過測試简烤,我們發(fā)現(xiàn)下來幾種情況都是正常的:

  1. 使用 TCP 協(xié)議沒有這個問題
  2. 如果 UDP 服務器監(jiān)聽在 eth0 ip ,而不是0.0.0.0上摇幻,也不會出現(xiàn)這個問題
  3. 并不是所有的應用都有這個問題横侦,我們的 DNS(dnsmasq + kubeDNS) 也是同樣的部署方式,但是功能都正常

這個問題在 docker 上也有 issue 記錄绰姻,但是目前并沒有合理的解決方案枉侧。
https://github.com/moby/moby/issues/15127
https://github.com/iotaledger/iri/issues/961

這篇文章就分析一下出現(xiàn)這個問題的原因,希望給同樣遇到這個問題的讀者提供些幫助狂芋。

二榨馁、重現(xiàn)問題

這個問題很容易重現(xiàn),我的實驗是在 ubuntu16.04 下用 netcat 命令完成的帜矾,其他系統(tǒng)應該類似翼虫。

在宿主機上通過 nc 監(jiān)聽 56789 端口,然后使用 bridge 網絡模式屡萤,run 一個容器珍剑,在容器里面使用 nc 發(fā)數(shù)據(jù)。

第一個報文是能發(fā)送出去的死陆,但是以后的報文雖然在網絡上能看到招拙,但是對方無法接收。

在宿主機運行 nc UDP 服務器(-u 表示 UDP 協(xié)議翔曲,-l 表示監(jiān)聽的端口)

$ nc -ul 56789
$  ss  -uan | grep 56789

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


注:默認沒有指定綁定ip迫像,就是監(jiān)聽在0.0.0.0上劈愚。

然后在同一宿主機上瞳遍,啟動一個容器,運行客戶端:

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

nc 的通信是雙方的菌羽,不管對方輸入什么字符掠械,回車后對方就能立即收到。
但是在這個模式下注祖,客戶端第一次輸入對方能夠收到猾蒂,后續(xù)的報文對方都收不到。

在這個實驗中是晨,容器使用的是 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

為了模擬多數(shù)應用一問一答的通信方式,我們一共發(fā)送三個報文昼伴,并用 tcpdump 抓取 docker0 接口上的報文:

  1. 客戶端先向服務器端發(fā)送 111 字符串
  2. 服務器端回復 222 字符串
  3. 客戶端繼續(xù)發(fā)送 333 字符串

抓包的結果如下匾旭,可以發(fā)現(xiàn)第一個報文發(fā)送出去沒有任何問題。

UDP 是沒有 ACK 報文的圃郊,所以客戶端無法知道對方有沒有收到季率,這里說的沒有問題沒有看到對應的 ICMP 差錯報文。

但是第二個報文從服務端發(fā)送的報文描沟,對方會返回一個 ICMP 告訴端口 38908 不可達飒泻;第三個報文從客戶端發(fā)送的報文也是如此。以后的報文情況類似吏廉,雙方再也無法進行通信了泞遗。

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 服務器并沒有退出,使用 ss -uan | grep 56789 可能看到它仍然在監(jiān)聽著該端口席覆。

四史辙、 問題原因分析

從網絡報文的分析中可以看到服務端返回的報文源地址不是我們預想的 eth0 地址,而是 docker0 的地址佩伤,而客戶端直接認為該報文是非法的聊倔,返回了 ICMP 的報文給對方。

那么問題的原因也可以分為兩個部分:

  1. 為什么應答報文源地址是錯誤的生巡?
  2. 既然 UDP 是無狀態(tài)的耙蔑,內核怎么判斷源地址不正確呢?

主機多網絡接口 UDP 源地址選擇問題

第一個問題的關鍵詞是:UDP 和多網絡接口孤荣。
因為如果主機上只有一個網絡接口甸陌,發(fā)出去的報文源地址一定不會有錯;而我們也測試過 TCP 協(xié)議是能夠處理這個問題的盐股。

通過搜索钱豁,發(fā)現(xiàn)這確實是個已知的問題。

image.png

這個問題可以歸結為一句話:UDP 在多網卡的情況下疯汁,可能會發(fā)生【服務器端】【源地址】不對的情況牲尺,這是內核選路的結果。

為什么 UDP 和 TCP 有不同的選路邏輯呢幌蚊?

因為 UDP 是無狀態(tài)的協(xié)議谤碳,內核不會保存連接雙方的信息凛澎,因此每次發(fā)送的報文都認為是獨立的,socket 層每次發(fā)送報文默認情況不會指明要使用的源地址估蹄,只是說明對方地址塑煎。

因此,內核會為要發(fā)出去的報文選擇一個 ip臭蚁,這通常都是報文路由要經過的設備 ip 地址最铁。

那么,為什么 dnsmasq 服務沒有這個問題呢垮兑?
于是我使用 strace 工具抓取了 dnsmasq 和出問題應用的網絡 socket 系統(tǒng)調用冷尉,來查看它們兩個到底有什么區(qū)別。

dnsmasq 在啟動階段監(jiān)聽了 UDP 和 TCP 的 54 端口

因為是在本地機器上測試的系枪,為了防止和本地 DNS 監(jiān)聽的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) 這句雾棺。
到底這兩點和我們的問題是否有關,先暫時放著衬浑,繼續(xù)看傳輸報文的部分捌浩。

dnsmasq 收包和發(fā)包的系統(tǒng)調用,直接使用 recvmsg 和 sendmsg 系統(tǒng)調用:

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 綁定的端口信息尸饺,數(shù)據(jù)傳輸過程使用的是 recvfrom 和 sendto。

對比下來助币,兩者的不同有幾點:

  1. 后者使用的是 ipv6浪听,而前者是 ipv4
  2. 后者使用 recvfrom 和 sendto 傳輸數(shù)據(jù),而前者是 sendmsg 和 recvmsg
  3. 前者有調用 setsockopt 設置 IP_PKTINFO 的值眉菱,而后者沒有

因為是在傳輸數(shù)據(jù)的時候出錯的迹栓,因此第一個疑點是 sendmsg 和 sendto 的某些區(qū)別導致選擇源地址有不同,通過 man sendto 可以知道 sendmsg 包含了更多的控制信息在 msghdr倍谜。
一個合理的猜測是 msghdr 中包含了內核選擇源地址的信息迈螟!

通過查找,發(fā)現(xiàn) IP_PKTINFO 這個選項就是讓內核在 socket 中保存 IP 報文的信息尔崔,當然也包括了報文的源地址和目的地址。IP_PKTINFOmsghdr 的關系可以在這個 stackoverflow 中找到:
https://stackoverflow.com/questions/3062205/setting-the-source-ip-for-a-udp-socket

而 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 不為空褥民,它們都能作為源地址選擇的依據(jù)季春,而不是讓內核通過路由決定。

也就是說消返,通過設置 IP_PKTINFO socket 選項為 1载弄,然后使用 recvmsg 和 sendmsg 傳輸數(shù)據(jù)就能保證源地址選擇符合我們的期望耘拇。
這也是 dnsmasq 使用的方案,而出問題的應用是因為使用了默認的 recvfrom 和 sendto宇攻。

為什么內核會把源地址和之前不同的報文丟棄惫叛,認為它是非法的?

因為我們前面已經說過逞刷,UDP 協(xié)議是無連接的嘉涌,默認情況下 socket 也不會保存雙方連接的信息。即使服務端發(fā)送報文的源地址有誤夸浅,只要對方能正常接收并處理仑最,也不會導致網絡不通。

但是 conntrack 不是這樣帆喇,內核的 netfilter 模塊會保存連接的狀態(tài)警医,并作為防火墻設置的依據(jù)。
它保存的 UDP 連接坯钦,只是簡單記錄了主機上本地 ip 和端口预皇,和對端 ip 和端口,并不會保存更多的內容婉刀。

關于 這塊可參考 intables info 網站的文章:
http://www.iptables.info/en/connection-state.html#UDPCONNECTIONS

在找到根源之前深啤,我們曾經嘗試過用 SNAT 來修改服務端應答報文的源地址,期望能夠修復該問題路星,但是卻發(fā)現(xiàn)這種方法行不通溯街,為什么呢?

因為 SNAT 是在 netfilter 最后做的洋丐,在之前 netfilter 的 conntrack 因為不認識該 connection呈昔,直接丟棄了,所以即使添加了 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 完全沒用郭宝。

五、解決方案

知道了問題的原因掷漱,解決方案也就很容易找到粘室。

  1. 使用 TCP 協(xié)議
    如果服務端和客戶端使用 TCP 協(xié)議進行通信,它們之間的網絡是正常的卜范。
$ nc -l 56789
  1. 監(jiān)聽在特定綁定在指定接口上
    使用 nc 啟動一個 udp 服務器衔统,監(jiān)聽在 eth0 上:
$  nc -ul   172.16.13.13   56789

nc 可以跟兩個參數(shù),分別代表 ip 和 端口,表示服務端監(jiān)聽在某個特定 ip 上锦爵。
如果接收到的報文目的地址不是 172.16.13.13舱殿,也會被內核直接丟棄,這種情況下险掀,服務端和客戶端也能正常通信沪袭。

  1. 改動應用程序實現(xiàn)
    修改應用程序的邏輯,在 UDP socket 上設置 IP_PKTIFO樟氢,并通過 recvmsg 和 sendmsg 函數(shù)傳輸數(shù)據(jù)冈绊。

六 、參考

docker 容器網絡下 UDP 協(xié)議的一個問題
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

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末嗡害,一起剝皮案震驚了整個濱河市焚碌,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌霸妹,老刑警劉巖十电,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異叹螟,居然都是意外死亡鹃骂,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門罢绽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來畏线,“玉大人,你說我怎么就攤上這事良价∏夼梗” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵明垢,是天一觀的道長蚣常。 經常有香客問我,道長痊银,這世上最難降的妖魔是什么抵蚊? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮溯革,結果婚禮上贞绳,老公的妹妹穿的比我還像新娘。我一直安慰自己致稀,他們只是感情好冈闭,可當我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著豺裆,像睡著了一般拒秘。 火紅的嫁衣襯著肌膚如雪号显。 梳的紋絲不亂的頭發(fā)上臭猜,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天躺酒,我揣著相機與錄音,去河邊找鬼蔑歌。 笑死羹应,一個胖子當著我的面吹牛,可吹牛的內容都是我干的次屠。 我是一名探鬼主播园匹,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼劫灶!你這毒婦竟也來了裸违?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤本昏,失蹤者是張志新(化名)和其女友劉穎供汛,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體涌穆,經...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡怔昨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了宿稀。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片趁舀。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖祝沸,靈堂內的尸體忽然破棺而出矮烹,到底是詐尸還是另有隱情,我是刑警寧澤罩锐,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布奉狈,位于F島的核電站,受9級特大地震影響唯欣,放射性物質發(fā)生泄漏嘹吨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一境氢、第九天 我趴在偏房一處隱蔽的房頂上張望蟀拷。 院中可真熱鬧,春花似錦萍聊、人聲如沸问芬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽此衅。三九已至强戴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間挡鞍,已是汗流浹背骑歹。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留墨微,地道東北人道媚。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像翘县,于是被迫代替她去往敵國和親最域。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內容