讓人迷糊的 socket udp 連接問題
公司內(nèi)部的一個 golang 中間件報 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 協(xié)議既沒有三次握手,又沒有 TCP 那樣的狀態(tài)控制報文,那么如何判定對端的 UDP 端口是否已打開?
通過抓包可以發(fā)現(xiàn),當服務端的端口沒有打開時,服務端的系統(tǒng)向客戶端返回 icmp ECONNREFUSED 報文,表明該連接異常。
通過抓包可以發(fā)現(xiàn)返回的協(xié)議為 ICMP,但含有源端口和目的端口,客戶端系統(tǒng)解析該報文時,通過五元組找到對應的 socket,并 errno 返回異常錯誤,如果客戶端陷入等待,則喚醒起來,設置錯誤狀態(tài).

(上面是 udp 異常下的 icmp,下面是正常 icmp)

當 UDP 連接異常時,可以通過 tcpdump 工具指定 ICMP 協(xié)議來抓取該異常報文,畢竟對方是通過 icmp 返回的 ECONNREFUSED。
使用 tcpdump 抓包
請求命令:
先找到一個可以 ping 通的主機,然后用 nc 模擬 udp 客戶端去請求不存在的端口,出現(xiàn) Connection refused。
[root@ocean?~]#?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.
抓包信息如下:
[root@ocean?~]#?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小結(jié)
當 ip 無法連通時,udp 客戶端連接時,通常會顯示成功。 當 udp 服務端程序關閉,但系統(tǒng)還存在時,對方系統(tǒng)會 `icmp ECONNREFUSE 錯誤。 當對方有操作 iptables udp port drop 時,通??蛻舳艘矔@示成功。
IP 無法聯(lián)通時:
[root@host-46?~?]$?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
[root@host-46?~?]$?nc?-vzu?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 那樣的狀態(tài)報文,所以單純對 UDP 抓包是看不到啥異常信息。
那么當 IP 不通時,為啥 NC UDP 命令顯示成功?
netcat nc udp 的邏輯
為什么當 ip 不連通或者報文被 DROP 時,返回連接成功?
因為 nc 默認的探測邏輯很簡單,只要在 2 秒鐘內(nèi)沒有收到 icmp ECONNREFUSED 異常報文,那么就認為 UDP 連接成功。??
下面是 nc udp 命令執(zhí)行的過程。
setsockopt(3,?SOL_SOCKET,?SO_BROADCAST,?[1],?4)?=?0
connect(3,?{sa_family=AF_INET,?sin_port=htons(30000),?sin_addr=inet_addr("172.16.0.111")},?16)?=?0
select(4,?[3],?[3],?[3],?NULL)??????????=?1?(out?[3])
getsockopt(3,?SOL_SOCKET,?SO_ERROR,?[0],?[4])?=?0
write(2,?"Ncat:?",?6Ncat:?)???????????????????=?6
write(2,?"Connected?to?172.16.0.111:29999."...,?33Connected?to?172.16.0.111:29999.
)?=?33
sendto(3,?"\0",?1,?0,?NULL,?0)??????????=?1
// select 多路復用方法里加入了超時邏輯。
select(4,?[3],?[],?[],?{tv_sec=2,?tv_usec=0})?=?0?(Timeout)
write(2,?"Ncat:?",?6Ncat:?)???????????????????=?6
write(2,?"UDP?packet?sent?successfully\n",?29UDP?packet?sent?successfully
)?=?29
write(2,?"Ncat:?",?6Ncat:?)???????????????????=?6
write(2,?"1?bytes?sent,?0?bytes?received?i"...,?481?bytes?sent,?0?bytes?received?in?2.02?seconds.
)?=?48
close(3)????????????????????????????????=?0
使用 golang/ python 編寫的 UDP 客戶端,給無法連通的地址發(fā) UDP 報文時,其實也不會報錯,這時候通常會認為發(fā)送成功。
還是那句話,UDP 沒有 TCP 那樣的握手步驟,像 TCP 發(fā)送 syn 總得不到回報時,協(xié)議棧會在時間退避下嘗試 6 次,當 6 次還得不到回應,內(nèi)核會給與錯誤的 errno 值。
UDP 連接信息
在客戶端的主機上,通過 ss lsof netstat 可以看到 UDP 五元組連接信息。
[root@host-46?~?]$?netstat?-tunalp|grep?29999
udp????????0??????0?172.16.0.46:44136???????172.16.0.46:29999???????ESTABLISHED?1285966/cccc
通常在服務端上看不到 UDP 連接信息,只可以看到 udp listen 信息!
[root@host-62?~?]#?netstat?-tunalp|grep?29999
udp???????0??????0?:::29999????????????????:::*????????????????????????????????4038720/ss
客戶端重新實例化問題?
當 client 跟 server 已連接,server 端手動重啟后,客戶端無需再次重新實例化連接,可以繼續(xù)發(fā)送數(shù)據(jù),當服務端再次啟動后,照樣可以收到客戶端發(fā)來的報文。
udp 本就無握手的過程,他的 udp connect() 也只是在本地創(chuàng)建 socket 信息。在服務端使用 netstat 是看不到 udp 五元組的 socket。
Golang 測試代碼
服務端代碼:
package?main
import?(
????"fmt"
????"net"
)
//?UDP?服務端
func?main()?{
????listen,?err?:=?net.ListenUDP("udp",?&net.UDPAddr{
????????IP:???net.IPv4(0,?0,?0,?0),
????????Port:?29999,
????})
????if?err?!=?nil?{
????????fmt.Println("Listen?failed,?err:?",?err)
????????return
????}
????defer?listen.Close()
????for?{
????????var?data?[1024]byte
????????n,?addr,?err?:=?listen.ReadFromUDP(data[:])
????????if?err?!=?nil?{
????????????fmt.Println("read?udp?failed,?err:?",?err)
????????????continue
????????}
????????fmt.Printf("data:%v?addr:%v?count:%v\n",?string(data[:n]),?addr,?n)
????}
}
客戶端代碼:
package?main
import?(
????"fmt"
????"net"
????"time"
)
//?UDP?客戶端
func?main()?{
????socket,?err?:=?net.DialUDP("udp",?nil,?&net.UDPAddr{
????????IP:???net.IPv4(172,?16,?0,?46),
????????Port:?29999,
????})
????if?err?!=?nil?{
????????fmt.Println("連接UDP服務器失敗,err:?",?err)
????????return
????}
????defer?socket.Close()
????for?{
????????time.Sleep(1e9?*?2)
????????sendData?:=?[]byte("Hello?Server")
????????_,?err?=?socket.Write(sendData)
????????if?err?!=?nil?{
????????????fmt.Println("發(fā)送數(shù)據(jù)失敗,err:?",?err)
????????????continue
????????}
????????fmt.Println("已發(fā)送")
????}
}
總結(jié)
當 udp 服務端的機器可以連通且無異常時,客戶端通常會顯示成功。但當有異常時,會有以下的情況:
當 ip 地址無法連通時,udp 客戶端連接時,通常會顯示成功。
當 udp 服務端程序關閉,但系統(tǒng)還存在時,對方系統(tǒng)通過
icmp ECONNREFUSE返回錯誤,客戶端會報錯。當對方有操作 iptables udp port drop 時,客戶端也會顯示成功。
客戶端和服務端互通數(shù)據(jù),當服務進程掛了時,UDP 客戶端不能立馬感知關閉狀態(tài),只有當再次發(fā)數(shù)據(jù)時才會被對方系統(tǒng)回應
icmp ECONNREFUSE異常報文,客戶端才能感知對方掛了。
