連接一個 IP 不存在的主機時,握手過程是怎樣的?
本文轉(zhuǎn)載自微信公眾號「小白debug」,作者小白。轉(zhuǎn)載本文請聯(lián)系小白debug公眾號。
鴿了好長時間了,最近很忙。以前工作忙完,就抽空寫文章。
現(xiàn)在忙完工作,還要一三五學駕照,二四六看家具。有同感的老鐵們不要舉手,拉到右下角點個"在看"就好了。
真的,全怪某音。
扯遠了,回到今天的主題。
方兄最近寫了篇很贊的文章 寫給想去字節(jié)寫 Go 的你 ,里面提到了兩個問題。
連接一個 IP 不存在的主機時,握手過程是怎樣的?
連接一個 IP 地址存在但端口號不存在的主機時,握手過程又是怎樣的呢?
讓我回想起曾經(jīng)也被面試官問過類似的問題,意識到應該很多朋友會對這個問題感興趣。
所以來給大家嘮嘮。
這兩個問題可以延伸出非常多的點。
看完了,說不定能加分!
正常情況的握手過程是怎么樣的
上面提到的問題,其實是指TCP的三次握手流程。這絕對是面試八股文里的老股了。
我們簡單回顧下基礎知識點。
正常情況下的TCP三次握手
在服務端啟動好后會調(diào)用 listen() 方法,進入到 LISTEN 狀態(tài),然后靜靜等待客戶端的連接請求到來。
而此時客戶端主動調(diào)用 connect(IP地址) ,就會向某個IP地址發(fā)起第一次握手,發(fā)送SYN 到目的服務器。
服務器在收到第一次握手后就會響應客戶端,這是第二次握手。
客戶端在收到第二次握手的消息后,響應服務的一個ACK,這算第三次握手,此時客戶端 就會進入 ESTABLISHED狀態(tài),認為連接已經(jīng)建立完成。
通過抓包可以直觀看出三次握手的流程。
正常三次握手抓包
連一個 IP 不存在的主機時,握手過程是怎樣的
那不存在的IP,分兩種,局域網(wǎng)內(nèi)和局域網(wǎng)外的。
家用路由器局域網(wǎng)互聯(lián)
我以我家里的情況舉例。
家里有一臺家用路由器。本質(zhì)上它的功能已經(jīng)集成了我們常說的路由器,交換機和無線接入點的功能了。
其中路由器和交換機在之前寫過的 《硬核圖解!30張圖帶你搞懂!路由器,集線器,交換機,網(wǎng)橋,光貓有啥區(qū)別?》里已經(jīng)詳細介紹過了,就不再說一遍了。無線接入點基本可以認為就是個放出 wifi 信號的組件。
家用路由器下,連著我的N臺設備,包括手機和電腦,他們的IP都有個共同點。都是 192.168.31.xx 形式的。其中,我的電腦的IP是192.168.31.6 ,這個可以通過 ifconfig查到。
符合這個形式的這些個設備,本質(zhì)上就是通過各種設備(wifi或交換機等)接入到上圖路由器的e2端口,他們共同構成一個局域網(wǎng)。
因此,在我家,我們可以粗暴點認為只要是 192.168.31.xx 形式的IP,就是局域網(wǎng)內(nèi)的IP。否則就是局域網(wǎng)外的IP,比如 192.0.2.2 。
目的IP在局域網(wǎng)內(nèi)
因為通過 ifconfig 可以查到我的局域網(wǎng)內(nèi)IP是192.168.31.6 ,這里盲猜末尾+1是不存在的 IP 。試了下,192.168.31.7 還真不存在。
- $ ping 192.168.31.7
- PING 192.168.31.7 (192.168.31.7): 56 data bytes
- Request timeout for icmp_seq 0
- Request timeout for icmp_seq 1
- Request timeout for icmp_seq 2
- Request timeout for icmp_seq 3
- ^C
- --- 192.168.31.7 ping statistics ---
- 5 packets transmitted, 0 packets received, 100.0% packet loss
于是寫個程序嘗試連這個IP 。下面的代碼是 golang 寫的,大家不看代碼也沒關系,放出來只是方便大家自己復現(xiàn)的時候用的。
- // tcp客戶端
- package main
- import (
- "fmt"
- "io"
- "net"
- "os"
- )
- func main() {
- client, err := net.Dial("tcp", "192.168.31.7:8081")
- if err != nil {
- fmt.Println("err:", err)
- return
- }
- defer client.Close()
- go func() {
- input := make([]byte, 1024)
- for {
- n, err := os.Stdin.Read(input)
- if err != nil {
- fmt.Println("input err:", err)
- continue
- }
- client.Write([]byte(input[:n]))
- }
- }()
- buf := make([]byte, 1024)
- for {
- n, err := client.Read(buf)
- if err != nil {
- if err == io.EOF {
- return
- }
- fmt.Println("read err:", err)
- continue
- }
- fmt.Println(string(buf[:n]))
- }
- }
然后嘗試抓包。
連一個不存在的IP(局域網(wǎng)內(nèi))抓包
可以發(fā)現(xiàn)根本沒有三次握手的包,只有一些 ARP 包,在詢問“誰是 192.168.31.7,告訴一下 192.168.31.6” 。
這里有三個問題
- 為什么會發(fā)ARP請求?
- 為什么沒有TCP握手包?
- ARP本身是沒有重試機制的,為什么ARP請求會發(fā)那么多遍?
首先我們看下正常情況下執(zhí)行connect,也就是第一次握手 的流程。
正常connect的流程
應用層執(zhí)行connect過后,會通過socket層,操作系統(tǒng)接口,進程會從用戶態(tài)進入到內(nèi)核態(tài),此時進入 傳輸層,因為是TCP第一次握手,會加入TCP頭,且置SYN標志。
tcp報頭的SYN
然后進入網(wǎng)絡層,我想要連的是 192.168.31.7 ,雖然它是我瞎編的,但IP頭還是得老老實實把它加進去。
此時需要重點介紹的是鄰居子系統(tǒng),它在網(wǎng)絡層和數(shù)據(jù)鏈路層之間。可以通過ARP協(xié)議將目的IP轉(zhuǎn)為對應的MAC地址,然后數(shù)據(jù)鏈路層就可以用這個MAC地址組裝幀頭。
我們看下那么ARP協(xié)議的流程是
ARP流程
1.先到本地ARP表查一下有沒有 192.168.31.7 對應的 mac地址,有的話就返回,這里顯然是不可能會有的。
可以通過 arp -a 命令查看本機的 arp表都記錄了哪些信息
- $ arp -a
- ? (192.168.31.1) at 88:c1:97:59:d1:c3 on en0 ifscope [ethernet]
- ? (224.0.0.251) at 1:0:4e:0:1:fb on en0 ifscope permanent [ethernet]
- ? (239.255.255.250) at 1:0:3e:7f:ff:fb on en0 ifscope permanent [ethernet]
2.看下 192.168.31.7 跟本機IP 192.168.31.6在不在一個局域網(wǎng)下。如果在的話,就在局域網(wǎng)內(nèi)發(fā)一個 arp 廣播,內(nèi)容就是 前面提到的 “誰是 192.168.31.7,告訴一下 192.168.31.6”。
3.如果目的IP跟本機IP不在同一個局域網(wǎng)下,那么會去獲取默認網(wǎng)關的MAC地址,這里就是指獲取家用路由器的MAC地址。然后把消息發(fā)給家用路由器,讓路由器發(fā)到互聯(lián)網(wǎng),找到下一跳路由器,一跳一跳的發(fā)送數(shù)據(jù),直到把消息發(fā)到目的IP上,又或者找不到目的地最終被丟棄。
4.第2和第3點都是本地沒有查到 ARP 緩存記錄的情況,這時候會把SYN報文放進一個隊列(叫unresolved_queue)里暫存起來,然后發(fā)起ARP請求;等ARP層收到ARP回應報文之后,會再從緩存中取出 SYN 報文,組裝 MAC 幀頭,完成剛剛沒完成的發(fā)送流程。
如果經(jīng)過 ARP 流程能正常返回 MAC 地址,那皆大歡喜,直接給數(shù)據(jù)鏈路層,經(jīng)過 ring buffer 后傳到網(wǎng)卡,發(fā)出去。
但因為現(xiàn)在這個IP是瞎編的,因此不可能得到目的地址 MAC ,所以消息也一直沒法到數(shù)據(jù)鏈路層。整個流程卡在了ARP流程中。
而抓包是在數(shù)據(jù)鏈路層之后進行的,因此 TCP 第一次握手的包一直沒能抓到,只能抓到為了獲得 192.168.31.7 的MAC地址的ARP請求。
發(fā)送數(shù)據(jù)時,是在經(jīng)過數(shù)據(jù)鏈路層之后的 dev_queue_xmit_nit 方法執(zhí)行抓包操作的,這是屬于網(wǎng)卡驅(qū)動層的方法了。
順帶一提,接收端抓包是在 __netif_receive_skb_core 方法里執(zhí)行的,也屬于網(wǎng)卡驅(qū)動層。感興趣的朋友們可以以這個為關鍵詞搜索相關知識點哈
此時 因為 TCP 協(xié)議是可靠的協(xié)議,對于 TCP 層來說,第一次握手的消息,已經(jīng)發(fā)出去了,但是一直沒有收到 ACK。也不知道消息是出去后是遇到什么事了。為了保證可靠性,它會不斷重發(fā)。
而每一次重發(fā),都會因為同樣的原因(沒有目的 MAC 地址)而尬在了 ARP 那個流程里。因此,才看到好幾次重復的 ARP 消息。
那回到剛剛的三個問題
- 為什么會發(fā) ARP 請求?
因為目的地址是瞎編的,本地ARP表沒有目的機器的MAC地址,因此發(fā)出ARP消息。
- 為什么沒有 TCP 握手包?
因為協(xié)議棧的數(shù)據(jù)到了網(wǎng)絡層后,在數(shù)據(jù)鏈路層前,就因為沒有目的MAC地址,沒法發(fā)出。因此抓包軟件抓不到相關數(shù)據(jù)。
- 為什么 ARP 請求會發(fā)那么多遍?
因為 TCP 協(xié)議的可靠性,會重發(fā)第一次握手的消息,但每一次都因為沒有目的 MAC 地址而失敗,每次都會發(fā)出ARP請求。
小結
連一個 IP 不存在的主機時,如果目的IP在局域網(wǎng)內(nèi),則第一次握手會失敗,接著不斷嘗試重發(fā)握手的請求。同時,本機會不斷發(fā)出ARP請求,企圖獲得目的機器的 MAC 地址。并且,因為沒能獲得目的 MAC 地址,這些 TCP 握手請求最終都發(fā)不出去,
目的IP在局域網(wǎng)外
上面提到的是,目的 IP 在局域網(wǎng)內(nèi)的情況,下面討論目的IP在局域網(wǎng)外的情況。
瞎編一個不是 192.168.31.xx 形式的 IP 作為這次要用的局域網(wǎng)外IP, 比如 10.225.31.11。
先抓包看一下。
連一個不存在的IP(局域網(wǎng)外)抓包
這次的現(xiàn)象是能發(fā)出 TCP 第一次握手的 SYN包。
這里有兩個問題
- 為什么連局域網(wǎng)外的 IP 現(xiàn)象跟連局域網(wǎng)內(nèi)不一致?
- TCP 第一次握手的重試規(guī)律好像不太對?
為什么連局域網(wǎng)外的IP現(xiàn)象跟連局域網(wǎng)內(nèi)不一致?
這個問題的答案其實在上面 ARP 的流程里已經(jīng)提到過了,如果目的 IP 跟本機 IP 不在同一個局域網(wǎng)下,那么會去獲取默認網(wǎng)關的 MAC 地址,這里就是指獲取家用路由器的MAC地址。
此時ARP流程成功返回家用路由器的 MAC 地址,數(shù)據(jù)鏈路層加入幀頭,消息通過網(wǎng)卡發(fā)到了家用路由器上。
消息會通過互聯(lián)網(wǎng)一直傳遞到某個局域網(wǎng)為 10.225.31.xx 的路由器上,那個路由器 發(fā)出ARP 請求,詢問他們局域網(wǎng)內(nèi)的機器有沒有叫 10.225.31.11的 (結果當然沒有)。
最終沒能發(fā)送成功,發(fā)送端也就遲遲收不到目的機的第二次握手響應。
因此觸發(fā)TCP重傳。
TCP第一次握手的重試規(guī)律好像不太對?
在 Linux 中,第一次握手的 SYN 重傳次數(shù),是通過 tcp_syn_retries 參數(shù)控制的。可以通過下面的方式查看
- $cat /proc/sys/net/ipv4/tcp_syn_retries
- 6
這里的含義是指 syn重傳 會發(fā)生6次。
而每次重試都會間隔一定的時間,這里的間隔一般是 1s,2s,4s,8s, 16s, 32s .
SYN重傳
而事實上,看我的截圖,是先重試4次,每次都是1s,之后才是 1s,2s,4s,8s, 16s, 32s 的重試。
這跟我們知道的不太一樣。
這個是因為我用的是macOS抓的包,跟linux就不是一個系統(tǒng),各自的TCP協(xié)議棧在sync重傳方面的實現(xiàn)都可能會有一定的差異。
我還聽說 oppo 和 vivo 的 syn重傳 是0.5s起步的。而 windows 的 syn重傳 還有自己的專利。
這些冷知識大家可以不用在意。面試的時候知道linux的就夠了,剩下的可以用來裝逼。畢竟面試官不在意"茴"字到底有幾種寫法。
連IP 地址存在但端口號不存在的主機的握手過程
前面提到的是IP地址壓根就不存在的情況。假如IP地址存在但端口號是瞎編的呢?
目的IP是回環(huán)地址
連回環(huán)地址,端口不存在抓包
現(xiàn)象也比較簡單,已經(jīng)IP地址是存在的,也就是在互聯(lián)網(wǎng)中這個機器是存在的。
那么我們可以正常發(fā)消息到目的IP,因為對應的MAC地址和IP都是正確的,所以,數(shù)據(jù)從數(shù)據(jù)鏈路層到網(wǎng)絡層都很OK。
直到傳輸層,TCP協(xié)議在識別到這個端口號對應的進程根本不存在時,就會把數(shù)據(jù)丟棄,響應一個RST消息給發(fā)送端。
連回環(huán)地址時端口不存在
RST是什么?
我們都是到TCP正常情況下斷開連接是用四次揮手,那是正常時候的優(yōu)雅做法。
但異常情況下,收發(fā)雙方都不一定正常,連揮手這件事本身都可能做不到,所以就需要一個機制去強行關閉連接。
RST 就是用于這種情況,一般用來異常地關閉一個連接。它在TCP包頭中,在收到置了這個標志位的數(shù)據(jù)包后,連接就會被關閉,此時接收到 RST的一方,一般會看到一個 connection reset 或 connection refused 的報錯。
TCP報頭RST位
目的IP在局域網(wǎng)內(nèi)
剛剛提到我的本機IP是 192.168.31.6 ,局域網(wǎng)內(nèi)有臺 192.168.31.1 。同樣嘗試連一個不存在的端口。
連存在的局域網(wǎng)內(nèi)IP,端口不存在抓包
此時現(xiàn)象跟前者一致。
唯一不同的是,前者是回環(huán)地址,RST數(shù)據(jù)是從本機的傳輸層返回的。而這次的情況,RST數(shù)據(jù)是從目的機器的傳輸層返回的。
連外網(wǎng)地址時端口不存在
目的IP在局域網(wǎng)外
找一個存在的外網(wǎng)ip,這里我拿了最近剛白嫖的阿里云服務器地址 47.102.221.141 。(炫耀)
進行連接連接,發(fā)現(xiàn)與前面兩種情況是一致的,目的機器在收到我的請求后,立馬就通過 RST標志位 斷開了這次的連接。
連存在的局域網(wǎng)外IP,端口不存在抓包
這一點跟前面兩種情況一致。
熟悉小白的朋友們都知道,每次搞事情做測試,都會用 baidu.com 。
這次也不例外,ping 一下 baidu.com ,獲得它的 IP: 220.181.38.148 。
- $ ping baidu.com
- PING baidu.com (220.181.38.148): 56 data bytes
- 64 bytes from 220.181.38.148: icmp_seq=0 ttl=48 time=35.728 ms
- 64 bytes from 220.181.38.148: icmp_seq=1 ttl=48 time=38.052 ms
- 64 bytes from 220.181.38.148: icmp_seq=2 ttl=48 time=37.845 ms
- 64 bytes from 220.181.38.148: icmp_seq=3 ttl=48 time=37.210 ms
- 64 bytes from 220.181.38.148: icmp_seq=4 ttl=48 time=38.402 ms
- 64 bytes from 220.181.38.148: icmp_seq=5 ttl=48 time=37.692 ms
- ^C
- --- baidu.com ping statistics ---
- 6 packets transmitted, 6 packets received, 0.0% packet loss
- round-trip min/avg/max/stddev = 35.728/37.488/38.402/0.866 ms
發(fā)消息到給百度域名背后的 IP,且瞎隨機指定一個端口 8080, 抓包。
連baidu,端口不存在抓包
現(xiàn)象卻不一致。沒有 RST 。而且觸發(fā)了第一次握手的重試消息。這是為什么?
這是因為baidu的機器,作為線上生產(chǎn)的機器,會設置一系列安全策略,比如只對外暴露某些端口,除此之外的端口,都一律拒絕。
所以很多發(fā)到 8080端口的消息都在防火墻這一層就被拒絕掉了,根本到不了目的主機里,而RST是在目的主機的TCP/IP協(xié)議棧里發(fā)出的,都還沒到這一層,就更不可能發(fā)RST了。因此發(fā)送端發(fā)現(xiàn)消息沒有回應(因為被防火墻丟了),就會重傳。所以才會出現(xiàn)上述抓包里的現(xiàn)象。
防火墻安全策略
總結
連一個 IP 不存在的主機時
- 如果IP在局域網(wǎng)內(nèi),會發(fā)送N次ARP請求獲得目的主機的MAC地址,同時不能發(fā)出TCP握手消息。
- 如果IP在局域網(wǎng)外,會將消息通過路由器發(fā)出,但因為最終找不到目的地,觸發(fā)TCP重試流程。
連IP 地址存在但端口號不存在的主機時
- 不管目的IP是回環(huán)地址還是局域網(wǎng)內(nèi)外的IP地址,目的主機的傳輸層都會在收到握手消息后,發(fā)現(xiàn)端口不正確,發(fā)出RST消息斷開連接。
- 當然如果目的機器設置了防火墻策略,限制他人將消息發(fā)到不對外暴露的端口,那么這種情況,發(fā)送端就會不斷重試第一次握手。
最后留個問題,連一個 不存在的局域網(wǎng)外IP的主機時,我們可以看到TCP的重發(fā)規(guī)律是:開始時,每隔1s重發(fā)五次 TCP SYN消息,接著2s,4s,8s,16s,32s都重發(fā)一次;
對比連一個 不存在的局域網(wǎng)內(nèi)IP的主機時,卻是每隔1s重發(fā)了4次ARP請求,接著過了32s后才再發(fā)出一次ARP請求。已知ARP請求是沒有重傳機制的,它的重試就是TCP重試觸發(fā)的,但兩者規(guī)律不一致,是為什么?