一、 udp問題抓包分析
公司內部的一個 中間件報 UDP 連接異常的日志,問題很明顯,對端的服務掛了,自然重啟下就可以了。
讓人疑惑的問題是 udp 是如何檢測對端掛了?
err: write udp 172.16.44.62:62651->172.16.0.46:29999: write: connection refused
err: write udp 172.16.44.62:62651->172.16.0.46:29999: write: connection refused
err: write udp 172.16.44.62:62651->172.16.0.46:29999: write: connection refused
UDP 協議既沒有三次握手,又沒有 TCP 那樣的狀態控制報文,那么如何判定對端的 UDP 端口是否已打開?
通過抓包可以發現,當服務端的端口沒有打開時,服務端的系統向客戶端返回 icmp ECONNREFUSED 報文,表明該連接異常。
通過抓包可以發現返回的協議為 ICMP,但含有源端口和目的端口,客戶端系統解析該報文時,通過五元組找到對應的 socket,并 errno 返回異常錯誤,如果客戶端陷入等待,則喚醒起來,設置錯誤狀態。
(上面是 udp 異常情況下的 icmp,下面是udp 正常 情況下的icmp)
寫UDP socket程序的時候,在調用sendto或者recvfrom的時候,會發現有Connection refused錯誤返回,錯誤碼是ECONNREFUSED。
對于懂得socket接口但是不很很懂網絡的人,可能這根本就不是個問題,他會根據錯誤碼知道遠端沒有這個服務端口,正如socket api的man手冊中描述的那樣:
ECONNREFUSED
A remote host refused to allow the network connection (typically because it is not running the requested service).
如果你十分精通TCP/IP棧,那么就想不通了,UDP既然無連接,怎么知道遠端的情況呢?
UDP不正如協議標準描述的那樣,發出去就不管了嗎?
對于接收,沒有數據就一直等,如果設置了NOWAIT,則直接返回EAGAIN,表示稍后再試。不管怎么說,也不會有ECONNREFUSED這么詳細的信息返回才對啊。
既然UDP不會從對端返回任何錯誤信息,那么一定有別的什么返回了,這就涉及到了網絡協議設計中的數據平面和控制平面了,對于控制平面的消息,可以是帶內傳輸,也可以是帶外傳輸。
數據分為兩種,一種是帶內數據,一種是帶外數據。
帶內數據就是我們平常傳輸或者說是口頭叫的數據,帶外數據就是我們接下來講的內容。
許多的傳輸層都具有帶外數據(也稱為 經加速數據 )的概念,想法就是連接的某段發生了重要的事情,希望迅速的通知給對端。這里的迅速是指這種通知應該在已經排隊了的帶內數據之前發送,也就是說,帶外數據擁有更高的優先級。帶外數據可以使用一條獨立的傳輸層連接,也可以映射到傳輸普通數據的連接中。其中,UDP沒有實現帶外數據。
TCP中telnet、rlogin和ftp等,除了這樣的遠程非活躍應用之外,幾乎很少有使用到帶外數據的地方。
TCP利用其頭部中的緊急指針標志以及緊急指針字段,給應用程序提供里一種緊急方式,所以TCP是利用傳輸普通數據的連接來傳輸帶外數據。
對于TCP而言,無疑是帶內傳輸的,因為它本身就是有連接的協議,協議本身會處理任何的錯誤和異常,然而對于UDP而言,因為其設計目的就是保持簡單性,故不再附帶有任何帶內的控制消息邏輯,互聯網上為了彌補這一類協議的控制邏輯的缺失,ICMP協議才顯得尤為重要!
實際上,ICMP,根據名稱就可以看出它是一種專門的控制協議,控制和指示IP層發生的事件。
ECONNREFUSED正是ICMP返回的,然而并不是所有的UDP socket都可以享用ICMP帶來的錯誤提示,畢竟帶外控制消息和協議本身的關聯太松散了。
UDP socket必須顯式的connect對端才可以。
現在問題又來了,既然UDP根本就是一個無連接的協議,connect的意義何在呢?
這其實是socket接口設計的范疇,和協議本身沒有任何關系,當一個UDP socket去 connect一個遠端時,并沒有發送任何的數據包,其效果僅僅是在本地建立了一個五元組映射,對應到一個對端,該映射的作用正是為了和UDP帶外的ICMP控制通道捆綁在一起,使得UDP socket的接口含義更加豐滿。
我們知道,ICMP錯誤信息返回時,ICMP的包內容就是出錯的那個原始數據包,根據這個原始數據包可以找出一個五元組,根據該五元組就可以對應到一個本地的connect過的UDP socket,進而把錯誤消息傳輸給該socket,應用程序在調用socket接口函數的時候,就可以得到該錯誤消息。
如果一個UDP socket沒有調用過connect,那么即使有ICMP數據包返回,由于socket保持了UDP的完整語義,協議棧也就不保存關于該socket和對端關聯的任何信息,因此也就無法找到一個特定的五元組將錯誤碼傳給它。
你不能太指望這個Connection refused以及一切帶外返回的錯誤信息,因為你不能保證一定能收到遠端發送的ICMP包,如果中間的某個節點或者本機禁掉了ICMP,socket api調用就無法捕獲這些錯誤了。
當 UDP 連接異常時,可以通過 tcpdmp 工具指定 ICMP 協議來抓取該異常報文,畢竟對方是通過 icmp 返回的 ECONNREFUSED。
我們使用 tcpdump 抓包,先找到一個可以 ping 通的目標主機, 然后用 nc 模擬 udp 客戶端去請求不存在的端口,出現 Connection refused.
# yum -y install nc
# nc -vzu 172.16.0.46 8888
Ncat: Version 7.50 ( https://nmap.org/ncat )
Ncat: Connected to 172.16.0.46:8888.
Ncat: Connection refused.
# tcpdump -i any icmp -nn
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
17:01:14.075617 IP 172.16.0.46 > 172.16.0.62: ICMP 172.16.0.46 udp port 8888 unreachable, length 37
17:01:17.326145 IP 172.16.0.46 > 172.16.0.62: ICMP 172.16.0.46 udp port 8888 unreachable, length 37
17:01:17.927480 IP 172.16.0.46 > 172.16.0.62: ICMP 172.16.0.46 udp port 8888 unreachable, length 37
17:01:18.489560 IP 172.16.0.46 > 172.16.0.62: ICMP 172.16.0.46 udp port 8888 unreachable, length 37
注意: telnet 不支持 udp,,只支持 tcp,建議使用 nc 來探測 udp。
二、各種case的測試
case小結:
- 當 ip 無法連通時, udp 客戶端連接時,通常會顯示成功
- 當 udp 服務端程序關閉, 但系統還存在時, 對方系統會 icmp ECONNREFUSE 錯誤
- 當對方有操作 iptables udp port drop 時,通常客戶端也會顯示成功.
# ping 172.16.0.65
PING 172.16.0.65 (172.16.0.65) 56(84) bytes of data.
From 172.16.0.46 icmp_seq=1 Destination Host Unreachable
From 172.16.0.46 icmp_seq=2 Destination Host Unreachable
From 172.16.0.46 icmp_seq=3 Destination Host Unreachable
From 172.16.0.46 icmp_seq=4 Destination Host Unreachable
From 172.16.0.46 icmp_seq=5 Destination Host Unreachable
From 172.16.0.46 icmp_seq=6 Destination Host Unreachable
^C
--- 172.16.0.65 ping statistics ---
6 packets transmitted, 0 received, +6 errors, 100% packet loss, time 4999ms
pipe 4
# nc -zuv 172.16.0.65 8888
Ncat: Version 7.50 ( https://nmap.org/ncat )
Ncat: Connected to 172.16.0.65:8888.
Ncat: UDP packet sent successfully
Ncat: 1 bytes sent, 0 bytes received in 2.02 seconds.
再次明確一點 udp 沒有類似 tcp 那樣的狀態報文, 所以單純對 UDP 抓包是看不到啥異常信息.
那么當 IP 不通時, 為啥nc udp 命令顯示成功 ?
netcat nc udp 的邏輯
為什么當 ip 不連通或者報文被 drop 時,返回連接成功 ?
因為 nc 默認的探測邏輯很簡單,只要在 2 秒鐘內沒有收到 icmp ECONNREFUSED 異常報文, 那么就認為 UDP 連接成功。
所以, UDP 客戶端,給無法連通的地址發 UDP 報文時,其實也不會報錯, 這時候通常會認為發送成功。
還是那句話 UDP 沒有 TCP 那樣的握手步驟,像 TCP 發送 syn 總得不到回報時, 協議棧會在時間退避下嘗試 6 次,當 6 次還得不到回應,內核會給與錯誤的 errno 值。
UDP 連接信息
在客戶端的主機上, 通過 ss lsof netstat 可以看到 UDP 五元組連接信息。
$ netstat -tunalp | grep 29999
udp 0 0 172.16.0.46:44136 172.16.0.46:29999 ESTABLISHED 1285966/client
通常在服務端上看不到 UDP 連接信息, 只可以看到 udp listen 信息 !
# netstat -tunalp|grep 29999
udp 0 0 :::29999 :::* 4038720/server
客戶端重新實例化問題 ?
當 client 跟 server 已連接,server 端手動重啟后,客戶端無需再次重新實例化連接,可以繼續發送數據。當服務端再次啟動后,照樣可以收到客戶端發來的報文。
udp 本就無握手的過程,他的 udp connect() 也只是在本地創建 socket 信息. 在服務端使用 netstat 是看不到 udp 五元組的 socket。
總結
當 udp 服務端的機器可以連通且無異常時,客戶端通常會顯示成功。
但當有異常時,會有以下的情況:
當 ip 地址無法連通時, udp 客戶端連接時,通常會顯示成功
當 udp 服務端程序關閉, 但系統還存在時, 對方系統通過 icmp ECONNREFUSE 返回錯誤,客戶端會報錯
當對方有操作 iptables udp port drop 時,客戶端也會顯示成功
4 客戶端和服務端互通數據,當服務進程掛了時,UDP 客戶端不能立馬感知關閉狀態,只有當再次發數據時才會被對方系統回應 icmp ECONNREFUSE 異常報文, 客戶端才能感知對方掛了。
三、參考
讓人迷糊的 socket udp 連接問題
https://xiaorui.cc/archives/7255
TCP/IP 某些最常見的錯誤原因碼 (errno)列表
https://www.cnblogs.com/jiu0821/p/5895723.html
技術分享之網絡編程的那些事兒
https://xiaorui.cc/archives/7271
TCP 帶外數據(即緊急模式的發送和接受)
https://blog.csdn.net/liushengxi_root/article/details/82563181
TCP帶外數據
https://www.cnblogs.com/c-slmax/p/5553857.html
TCP-帶外數據(緊急數據)
http://www.lxweimin.com/p/65a4b8c059d4
什么是帶外管理和帶內管理?
https://zhuanlan.zhihu.com/p/341264872
FAQ-什么是帶外管理和帶內管理?它們的區別是什么?
https://support.huawei.com/enterprise/zh/knowledge/EKB1000055297