連接一個 IP 不存在的主機時,握手過程是怎樣的?

連接一個 IP 地址存在但端口號不存在的主機時,握手過程又是怎樣的呢?

讓我回想起曾經也被面試官問過類似的問題,意識到應該很多朋友會對這個問題感興趣。

所以來給大家嘮嘮。

這兩個問題可以延伸出非常多的點。

看完了,說不定能加分!

正常情況的握手過程是怎麼樣的

上面提到的問題,其實是指 TCP 的三次握手流程。這絕對是面試八股文裏的老股了。

我們簡單回顧下基礎知識點。

正常情況下的 TCP 三次握手

服務端啓動好後會調用 listen() 方法,進入到 LISTEN 狀態,然後靜靜等待客戶端的連接請求到來。

而此時客戶端主動調用 connect(IP地址) ,就會向某個 IP 地址發起第一次握手,發送SYN 到目的服務器。

服務器在收到第一次握手後就會響應客戶端,這是第二次握手

客戶端在收到第二次握手的消息後,響應服務的一個ACK,這算第三次握手,此時客戶端 就會進入 ESTABLISHED狀態,認爲連接已經建立完成。

通過抓包可以直觀看出三次握手的流程。

正常三次握手抓包

連一個 IP 不存在的主機時,握手過程是怎樣的

那不存在的 IP,分兩種,局域網內和局域網外的。

家用路由器局域網互聯

我以我家裏的情況舉例。

家裏有一臺家用路由器。本質上它的功能已經集成了我們常說的路由器,交換機和無線接入點的功能了。

其中路由器和交換機在之前寫過的 《硬核圖解!30 張圖帶你搞懂!路由器,集線器,交換機,網橋,光貓有啥區別?》裏已經詳細介紹過了,就不再說一遍了。無線接入點基本可以認爲就是個放出 wifi 信號的組件。

家用路由器下,連着我的 N 臺設備,包括手機和電腦,他們的 IP 都有個共同點。都是 192.168.31.xx  形式的。其中,我的電腦的 IP 是192.168.31.6 ,這個可以通過 ifconfig查到。

符合這個形式的這些個設備,本質上就是通過各種設備(wifi 或交換機等)接入到上圖路由器的 e2 端口,他們共同構成一個局域網

因此,在我家,我們可以粗暴點認爲只要是  192.168.31.xx  形式的 IP,就是局域網內的 IP。否則就是局域網外的 IP,比如 192.0.2.2

目的 IP 在局域網內

因爲通過 ifconfig 可以查到我的局域網內 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 寫的,大家不看代碼也沒關係,放出來只是方便大家自己復現的時候用的。

// 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(局域網內) 抓包

可以發現根本沒有三次握手的包,只有一些 ARP 包,在詢問 “誰是 192.168.31.7,告訴一下 192.168.31.6” 。

這裏有三個問題

首先我們看下正常情況下執行connect,也就是第一次握手 的流程。

正常 connect 的流程

應用層執行connect過後,會通過 socket 層,操作系統接口,進程會從用戶態進入到內核態,此時進入 傳輸層,因爲是 TCP 第一次握手,會加入 TCP 頭,且置 SYN 標誌。

tcp 報頭的 SYN

然後進入網絡層,我想要連的是 192.168.31.7 ,雖然它是我瞎編的,但 IP 頭還是得老老實實把它加進去。

此時需要重點介紹的是鄰居子系統,它在網絡層和數據鏈路層之間。可以通過 ARP 協議將目的 IP 轉爲對應的 MAC 地址,然後數據鏈路層就可以用這個 MAC 地址組裝幀頭

我們看下那麼 ARP 協議的流程

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]
  1. 看下 192.168.31.7  跟本機 IP  192.168.31.6在不在一個局域網下。如果在的話,就在局域網內發一個 arp 廣播,內容就是 前面提到的 “誰是 192.168.31.7,告訴一下 192.168.31.6”。

  2. 如果目的 IP 跟本機 IP 不在同一個局域網下,那麼會去獲取默認網關的 MAC 地址,這裏就是指獲取家用路由器的 MAC 地址。然後把消息發給家用路由器,讓路由器發到互聯網,找到下一跳路由器,一跳一跳的發送數據,直到把消息發到目的 IP 上,又或者找不到目的地最終被丟棄。

  3. 第 2 和第 3 點都是本地沒有查到 ARP 緩存記錄的情況,這時候會把 SYN 報文放進一個隊列(叫 unresolved_queue)裏暫存起來,然後發起 ARP 請求;等 ARP 層收到 ARP 迴應報文之後,會再從緩存中取出 SYN 報文,組裝 MAC 幀頭,完成剛剛沒完成的發送流程。

如果經過 ARP 流程能正常返回 MAC 地址,那皆大歡喜,直接給數據鏈路層,經過 ring buffer 後傳到網卡,發出去。

但因爲現在這個 IP 是瞎編的,因此不可能得到目的地址 MAC ,所以消息也一直沒法到數據鏈路層。整個流程卡在了 ARP 流程中。

抓包是在數據鏈路層之後進行的,因此 TCP 第一次握手的包一直沒能抓到,只能抓到爲了獲得  192.168.31.7 的 MAC 地址的 ARP 請求。

發送數據時,是在經過數據鏈路層之後的  dev_queue_xmit_nit 方法執行抓包操作的,這是屬於網卡驅動層的方法了。

順帶一提,接收端抓包是在  __netif_receive_skb_core 方法裏執行的,也屬於網卡驅動層。感興趣的朋友們可以以這個爲關鍵詞搜索相關知識點哈

此時 因爲 TCP 協議是可靠的協議,對於 TCP 層來說,第一次握手的消息,已經發出去了,但是一直沒有收到 ACK。也不知道消息是出去後是遇到什麼事了。爲了保證可靠性,它會不斷重發。

而每一次重發,都會因爲同樣的原因(沒有目的 MAC 地址)而尬在了 ARP 那個流程裏。因此,纔看到好幾次重複的 ARP 消息。

那回到剛剛的三個問題

小結

連一個 IP 不存在的主機時,如果目的 IP 在局域網內,則第一次握手會失敗,接着不斷嘗試重發握手的請求。同時,本機會不斷髮出 ARP 請求,企圖獲得目的機器的 MAC 地址。並且,因爲沒能獲得目的 MAC 地址,這些 TCP 握手請求最終都發不出去,

目的 IP 在局域網外

上面提到的是,目的 IP 在局域網內的情況,下面討論目的 IP 在局域網外的情況。

瞎編一個不是  192.168.31.xx 形式的 IP 作爲這次要用的局域網外 IP, 比如 10.225.31.11

先抓包看一下。

連一個不存在的 IP(局域網外) 抓包

這次的現象是能發出 TCP 第一次握手的 SYN包

這裏有兩個問題

爲什麼連局域網外的 IP 現象跟連局域網內不一致?

這個問題的答案其實在上面 ARP 的流程裏已經提到過了,如果目的 IP 跟本機 IP 不在同一個局域網下,那麼會去獲取默認網關的 MAC 地址,這裏就是指獲取家用路由器的 MAC 地址

此時 ARP 流程成功返回家用路由器的 MAC 地址,數據鏈路層加入幀頭,消息通過網卡發到了家用路由器上

消息會通過互聯網一直傳遞到某個局域網爲  10.225.31.xx 的路由器上,那個路由器 發出 ARP 請求,詢問他們局域網內的機器有沒有叫 10.225.31.11的 (結果當然沒有)。

最終沒能發送成功,發送端也就遲遲收不到目的機的第二次握手響應。

因此觸發 TCP 重傳。

TCP 第一次握手的重試規律好像不太對?

在 Linux 中,第一次握手的 SYN 重傳次數,是通過 tcp_syn_retries 參數控制的。可以通過下面的方式查看

$cat /proc/sys/net/ipv4/tcp_syn_retries
6

這裏的含義是指 syn重傳 會發生 6 次。

而每次重試都會間隔一定的時間,這裏的間隔一般是 1s,2s,4s,8s, 16s, 32s .

SYN 重傳

而事實上,看我的截圖,是先重試 4 次,每次都是 1s,之後纔是 1s,2s,4s,8s, 16s, 32s 的重試。

這跟我們知道的不太一樣。

這個是因爲我用的是 macOS 抓的包,跟 linux 就不是一個系統,各自的 TCP 協議棧在 sync 重傳方面的實現都可能會有一定的差異。

我還聽說 oppovivo 的 syn 重傳 是 0.5s 起步的。而 windows 的 syn 重傳 還有自己的專利。

這些冷知識大家可以不用在意。面試的時候知道 linux 的就夠了,剩下的可以用來裝逼。畢竟面試官不在意 "茴" 字到底有幾種寫法。

連 IP 地址存在但端口號不存在的主機的握手過程

前面提到的是 IP 地址壓根就不存在的情況。假如 IP 地址存在但端口號是瞎編的呢?

目的 IP 是迴環地址

連回環地址,端口不存在抓包

現象也比較簡單,已經 IP 地址是存在的,也就是在互聯網中這個機器是存在的。

那麼我們可以正常發消息到目的 IP,因爲對應的 MAC 地址和 IP 都是正確的,所以,數據從數據鏈路層到網絡層都很 OK。

直到傳輸層,TCP 協議在識別到這個端口號對應的進程根本不存在時,就會把數據丟棄,響應一個 RST 消息給發送端。

連回環地址時端口不存在

RST 是什麼?

我們都是到 TCP 正常情況下斷開連接是用四次揮手,那是正常時候的優雅做法。

異常情況下,收發雙方都不一定正常,連揮手這件事本身都可能做不到,所以就需要一個機制去強行關閉連接。

RST 就是用於這種情況,一般用來異常地關閉一個連接。它在 TCP 包頭中,在收到置了這個標誌位的數據包後,連接就會被關閉,此時接收到 RST 的一方,一般會看到一個 connection reset 或  connection refused 的報錯。

TCP 報頭 RST 位

目的 IP 在局域網內

剛剛提到我的本機 IP 是 192.168.31.6 ,局域網內有臺 192.168.31.1 。同樣嘗試連一個不存在的端口。

連存在的局域網內 IP,端口不存在抓包

此時現象跟前者一致。

唯一不同的是,前者是迴環地址,RST 數據是從本機的傳輸層返回的。而這次的情況,RST 數據是從目的機器的傳輸層返回的。

連外網地址時端口不存在

目的 IP 在局域網外

找一個存在的外網 ip,這裏我拿了最近剛白嫖的阿里雲服務器地址 47.102.221.141 。(炫耀)

進行連接連接,發現與前面兩種情況是一致的,目的機器在收到我的請求後,立馬就通過 RST 標誌位 斷開了這次的連接。

連存在的局域網外 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=ttl=48 time=35.728 ms
64 bytes from 220.181.38.148: icmp_seq=ttl=48 time=38.052 ms
64 bytes from 220.181.38.148: icmp_seq=ttl=48 time=37.845 ms
64 bytes from 220.181.38.148: icmp_seq=ttl=48 time=37.210 ms
64 bytes from 220.181.38.148: icmp_seq=ttl=48 time=38.402 ms
64 bytes from 220.181.38.148: icmp_seq=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

發消息到給百度域名背後的 IP,且瞎隨機指定一個端口 8080, 抓包。

連 baidu,端口不存在抓包

現象卻不一致。沒有 RST 。而且觸發了第一次握手的重試消息。這是爲什麼?

這是因爲 baidu 的機器,作爲線上生產的機器,會設置一系列安全策略,比如只對外暴露某些端口,除此之外的端口,都一律拒絕。

所以很多發到 8080 端口的消息都在防火牆這一層就被拒絕掉了,根本到不了目的主機裏,而 RST 是在目的主機的 TCP/IP 協議棧裏發出的,都還沒到這一層,就更不可能發 RST 了。因此發送端發現消息沒有迴應(因爲被防火牆丟了),就會重傳。所以纔會出現上述抓包裏的現象。

防火牆安全策略

總結

連一個 IP 不存在的主機時

連 IP 地址存在但端口號不存在的主機時

最後留個問題,連一個 不存在的局域網外 IP 的主機時,我們可以看到 TCP 的重發規律是:開始時,每隔 1s 重發五次 TCP SYN消息,接着 2s,4s,8s,16s,32s 都重發一次;

對比連一個 不存在的局域網內 IP 的主機時,卻是每隔 1s 重發了 4 次ARP請求,接着過了 32s 後纔再發出一次 ARP 請求。已知 ARP 請求是沒有重傳機制的,它的重試就是 TCP 重試觸發的,但兩者規律不一致,是爲什麼?

最後

我是小白,我們下期見。

別說了,一起在知識的海洋裏嗆水吧

關注公衆號:【小白 debug】

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Tg-D9I2xi2ZqKZXOJ_Vbxw