每日一句英語學習,每天進步一點點: 前言 為了讓大家更容易「看得見」 TCP,我搭建不少測試環境,並且數據包抓很多次,花費了不少時間,才抓到比較容易分析的數據包。 接下來丟包、亂序、超時重傳、快速重傳、選擇性確認、流量控制等等 TCP 的特性,都能「一覽無雲」。 沒錯,我把 TCP 的"衣服扒光"了 ...
每日一句英語學習,每天進步一點點:

前言
為了讓大家更容易「看得見」 TCP,我搭建不少測試環境,並且數據包抓很多次,花費了不少時間,才抓到比較容易分析的數據包。
接下來丟包、亂序、超時重傳、快速重傳、選擇性確認、流量控制等等 TCP 的特性,都能「一覽無雲」。
沒錯,我把 TCP 的"衣服扒光"了,就為了給大家看的清楚,嘻嘻。
提綱
正文
顯形“不可見”的網路包
網路世界中的數據包交互我們肉眼是看不見的,它們就好像隱形了一樣,我們對著課本學習電腦網路的時候就會覺得非常的抽象,加大了學習的難度。
還別說,我自己在大學的時候,也是如此。
直到工作後,認識了兩大分析網路的利器:tcpdump 和 Wireshark,這兩大利器把我們“看不見”的數據包,呈現在我們眼前,一目瞭然。
唉,當初大學學習計網的時候,要是能知道這兩個工具,就不會學的一臉懵逼。
tcpdump 和 Wireshark 有什麼區別?
tcpdump 和 Wireshark 就是最常用的網路抓包和分析工具,更是分析網路性能必不可少的利器。
- tcpdump 僅支持命令行格式使用,常用在 Linux 伺服器中抓取和分析網路包。
- Wireshark 除了可以抓包外,還提供了可視化分析網路包的圖形頁面。
所以,這兩者實際上是搭配使用的,先用 tcpdump 命令在 Linux 伺服器上抓包,接著把抓包的文件拖出到 Windows 電腦後,用 Wireshark 可視化分析。
當然,如果你是在 Windows 上抓包,只需要用 Wireshark 工具就可以。
tcpdump 在 Linux 下如何抓包?
tcpdump 提供了大量的選項以及各式各樣的過濾表達式,來幫助你抓取指定的數據包,不過不要擔心,只需要掌握一些常用選項和過濾表達式,就可以滿足大部分場景的需要了。
假設我們要抓取下麵的 ping 的數據包:

要抓取上面的 ping 命令數據包,首先我們要知道 ping 的數據包是 icmp
協議,接著在使用 tcpdump 抓包的時候,就可以指定只抓 icmp 協議的數據包:

那麼當 tcpdump 抓取到 icmp 數據包後, 輸出格式如下:


從 tcpdump 抓取的 icmp 數據包,我們很清楚的看到 icmp echo
的交互過程了,首先發送方發起了 ICMP echo request
請求報文,接收方收到後回了一個 ICMP echo reply
響應報文,之後 seq
是遞增的。
我在這裡也幫你整理了一些最常見的用法,並且繪製成了表格,你可以參考使用。
首先,先來看看常用的選項類,在上面的 ping 例子中,我們用過 -i
選項指定網口,用過 -nn
選項不對 IP 地址和埠名稱解析。其他常用的選項,如下表格:

接下來,我們再來看看常用的過濾表用法,在上面的 ping 例子中,我們用過的是 icmp and host 183.232.231.174
,表示抓取 icmp 協議的數據包,以及源地址或目標地址為 183.232.231.174 的包。其他常用的過濾選項,我也整理成了下麵這個表格。

說了這麼多,你應該也發現了,tcpdump 雖然功能強大,但是輸出的格式並不直觀。
所以,在工作中 tcpdump 只是用來抓取數據包,不用來分析數據包,而是把 tcpdump 抓取的數據包保存成 pcap 尾碼的文件,接著用 Wireshark 工具進行數據包分析。
Wireshark 工具如何分析數據包?
Wireshark 除了可以抓包外,還提供了可視化分析網路包的圖形頁面,同時,還內置了一系列的彙總分析工具。
比如,拿上面的 ping 例子來說,我們可以使用下麵的命令,把抓取的數據包保存到 ping.pcap 文件

接著把 ping.pcap 文件拖到電腦,再用 Wireshark 打開它。打開後,你就可以看到下麵這個界面:

是吧?在 Wireshark 的頁面里,可以更加直觀的分析數據包,不僅展示各個網路包的頭部信息,還會用不同的顏色來區分不同的協議,由於這次抓包只有 ICMP 協議,所以只有紫色的條目。
接著,在網路包列表中選擇某一個網路包後,在其下麵的網路包詳情中,可以更清楚的看到,這個網路包在協議棧各層的詳細信息。比如,以編號 1 的網路包為例子:

- 可以在數據鏈路層,看到 MAC 包頭信息,如源 MAC 地址和目標 MAC 地址等欄位;
- 可以在 IP 層,看到 IP 包頭信息,如源 IP 地址和目標 IP 地址、TTL、IP 包長度、協議等 IP 協議各個欄位的數值和含義;
- 可以在 ICMP 層,看到 ICMP 包頭信息,比如 Type、Code 等 ICMP 協議各個欄位的數值和含義;
Wireshark 用了分層的方式,展示了各個層的包頭信息,把“不可見”的數據包,清清楚楚的展示了給我們,還有理由學不好電腦網路嗎?是不是相見恨晚?
從 ping 的例子中,我們可以看到網路分層就像有序的分工,每一層都有自己的責任範圍和信息,上層協議完成工作後就交給下一層,最終形成一個完整的網路包。

解密 TCP 三次握手和四次揮手
既然學會了 tcpdump 和 Wireshark 兩大網路分析利器,那我們快馬加鞭,接下用它倆抓取和分析 HTTP 協議網路包,並理解 TCP 三次握手和四次揮手的工作原理。
本次例子,我們將要訪問的 http://192.168.3.200 服務端。在終端一用 tcpdump 命令抓取數據包:

接著,在終端二執行下麵的 curl 命令:

最後,回到終端一,按下 Ctrl+C 停止 tcpdump,並把得到的 http.pcap 取出到電腦。
使用 Wireshark 打開 http.pcap 後,你就可以在 Wireshark 中,看到如下的界面:

我們都知道 HTTP 是基於 TCP 協議進行傳輸的,那麼:
- 最開始的 3 個包就是 TCP 三次握手建立連接的包
- 中間是 HTTP 請求和響應的包
- 而最後的 3 個包則是 TCP 斷開連接的揮手包
Wireshark 可以用時序圖的方式顯示數據包交互的過程,從菜單欄中,點擊 統計 (Statistics) -> 流量圖 (Flow Graph),然後,在彈出的界面中的「流量類型」選擇 「TCP Flows」,你可以更清晰的看到,整個過程中 TCP 流的執行過程:

你可能會好奇,為什麼三次握手連接過程的 Seq 是 0 ?
實際上是因為 Wireshark 工具幫我們做了優化,它預設顯示的是序列號 seq 是相對值,而不是真實值。
如果你想看到實際的序列號的值,可以右鍵菜單, 然後找到「協議首選項」,接著找到「Relative Seq」後,把它給取消,操作如下:

取消後,Seq 顯示的就是真實值了:

可見,客戶端和服務端的序列號實際上是不同的,序列號是一個隨機值。
這其實跟我們書上看到的 TCP 三次握手和四次揮手很類似,作為對比,你通常看到的 TCP 三次握手和四次揮手的流程,基本是這樣的:

為什麼抓到的 TCP 揮手是三次,而不是書上說的四次?
因為伺服器端收到客戶端的 FIN
後,伺服器端同時也要關閉連接,這樣就可以把 ACK
和 FIN
合併到一起發送,節省了一個包,變成了“三次揮手”。
而通常情況下,伺服器端收到客戶端的 FIN
後,很可能還沒發送完數據,所以就會先回覆客戶端一個 ACK
包,稍等一會兒,完成所有數據包的發送後,才會發送 FIN
包,這也就是四次揮手了。
如下圖,就是四次揮手的過程:

TCP 三次握手異常情況實戰分析
TCP 三次握手的過程相信大家都背的滾瓜爛熟,那麼你有沒有想過這三個異常情況:
- TCP 第一次握手的 SYN 丟包了,會發生了什麼?
- TCP 第二次握手的 SYN、ACK 丟包了,會發生什麼?
- TCP 第三次握手的 ACK 包丟了,會發生什麼?
有的小伙伴可能說:“很簡單呀,包丟了就會重傳嘛。”
那我在繼續問你:
- 那會重傳幾次?
- 超時重傳的時間 RTO 會如何變化?
- 在 Linux 下如何設置重傳次數?
- ….
是不是啞口無言,無法回答?
不知道沒關係,接下里我用三個實驗案例,帶大家一起探究探究這三種異常。
實驗場景
本次實驗用了兩台虛擬機,一臺作為服務端,一臺作為客戶端,它們的關係如下:

- 客戶端和服務端都是 CentOs 6.5 Linux,Linux 內核版本 2.6.32
- 服務端 192.168.12.36,apache web 服務
- 客戶端 192.168.12.37
實驗一:TCP 第一次握手 SYN 丟包
為了模擬 TCP 第一次握手 SYN 丟包的情況,我是在拔掉伺服器的網線後,立刻在客戶端執行 curl 命令:

其間 tcpdump 抓包的命令如下:

過了一會, curl 返回了超時連接的錯誤:

從 date
返回的時間,可以發現在超時接近 1 分鐘的時間後,curl 返回了錯誤。
接著,把 tcp_sys_timeout.pcap 文件用 Wireshark 打開分析,顯示如下圖:

從上圖可以發現, 客戶端發起了 SYN 包後,一直沒有收到服務端的 ACK ,所以一直超時重傳了 5 次,並且每次 RTO 超時時間是不同的:
- 第一次是在 1 秒超時重傳
- 第二次是在 3 秒超時重傳
- 第三次是在 7 秒超時重傳
- 第四次是在 15 秒超時重傳
- 第五次是在 31 秒超時重傳
可以發現,每次超時時間 RTO 是指數(翻倍)上漲的,當超過最大重傳次數後,客戶端不再發送 SYN 包。
在 Linux 中,第一次握手的 SYN
超時重傳次數,是如下內核參數指定的:
$ cat /proc/sys/net/ipv4/tcp_syn_retries
5
tcp_syn_retries
預設值為 5,也就是 SYN 最大重傳次數是 5 次。
接下來,我們繼續做實驗,把 tcp_syn_retries
設置為 2 次:
$ echo 2 > /proc/sys/net/ipv4/tcp_syn_retries
重傳抓包後,用 Wireshark 打開分析,顯示如下圖:

實驗一的實驗小結
通過實驗一的實驗結果,我們可以得知,當客戶端發起的 TCP 第一次握手 SYN 包,在超時時間內沒收到服務端的 ACK,就會在超時重傳 SYN 數據包,每次超時重傳的 RTO 是翻倍上漲的,直到 SYN 包的重傳次數到達 tcp_syn_retries
值後,客戶端不再發送 SYN 包。

實驗二:TCP 第二次握手 SYN、ACK 丟包
為了模擬客戶端收不到服務端第二次握手 SYN、ACK 包,我的做法是在客戶端加上防火牆限制,直接粗暴的把來自服務端的數據都丟棄,防火牆的配置如下:

接著,在客戶端執行 curl 命令:

從 date
返回的時間前後,可以算出大概 1 分鐘後,curl 報錯退出了。
客戶端在這其間抓取的數據包,用 Wireshark 打開分析,顯示的時序圖如下:

從圖中可以發現:
- 客戶端發起 SYN 後,由於防火牆屏蔽了服務端的所有數據包,所以 curl 是無法收到服務端的 SYN、ACK 包,當發生超時後,就會重傳 SYN 包
- 服務端收到客戶的 SYN 包後,就會回 SYN、ACK 包,但是客戶端一直沒有回 ACK,服務端在超時後,重傳了 SYN、ACK 包,接著一會,客戶端超時重傳的 SYN 包又抵達了服務端,服務端收到後,超時定時器就重新計時,然後回了 SYN、ACK 包,所以相當於服務端的超時定時器只觸發了一次,又被重置了。
- 最後,客戶端 SYN 超時重傳次數達到了 5 次(tcp_syn_retries 預設值 5 次),就不再繼續發送 SYN 包了。
所以,我們可以發現,當第二次握手的 SYN、ACK 丟包時,客戶端會超時重發 SYN 包,服務端也會超時重傳 SYN、ACK 包。
咦?客戶端設置了防火牆,屏蔽了服務端的網路包,為什麼 tcpdump 還能抓到服務端的網路包?
添加 iptables 限制後, tcpdump 是否能抓到包 ,這要看添加的 iptables 限制條件:
- 如果添加的是
INPUT
規則,則可以抓得到包 - 如果添加的是
OUTPUT
規則,則抓不到包
網路包進入主機後的順序如下:
- 進來的順序 Wire -> NIC -> tcpdump -> netfilter/iptables
- 出去的順序 iptables -> tcpdump -> NIC -> Wire
tcp_syn_retries 是限制 SYN 重傳次數,那第二次握手 SYN、ACK 限制最大重傳次數是多少?
TCP 第二次握手 SYN、ACK 包的最大重傳次數是通過 tcp_synack_retries
內核參數限制的,其預設值如下:
$ cat /proc/sys/net/ipv4/tcp_synack_retries
5
是的,TCP 第二次握手 SYN、ACK 包的最大重傳次數預設值是 5
次。
為了驗證 SYN、ACK 包最大重傳次數是 5 次,我們繼續做下實驗,我們先把客戶端的 tcp_syn_retries
設置為 1,表示客戶端 SYN 最大超時次數是 1 次,目的是為了防止多次重傳 SYN,把服務端 SYN、ACK 超時定時器重置。
接著,還是如上面的步驟:
- 客戶端配置防火牆屏蔽服務端的數據包
- 客戶端 tcpdump 抓取 curl 執行時的數據包
把抓取的數據包,用 Wireshark 打開分析,顯示的時序圖如下:

從上圖,我們可以分析出:
- 客戶端的 SYN 只超時重傳了 1 次,因為
tcp_syn_retries
值為 1 - 服務端應答了客戶端超時重傳的 SYN 包後,由於一直收不到客戶端的 ACK 包,所以服務端一直在超時重傳 SYN、ACK 包,每次的 RTO 也是指數上漲的,一共超時重傳了 5 次,因為
tcp_synack_retries
值為 5
接著,我把 tcp_synack_retries 設置為 2,tcp_syn_retries
依然設置為 1:
$ echo 2 > /proc/sys/net/ipv4/tcp_synack_retries
$ echo 1 > /proc/sys/net/ipv4/tcp_syn_retries
依然保持一樣的實驗步驟進行操作,接著把抓取的數據包,用 Wireshark 打開分析,顯示的時序圖如下:

可見:
- 客戶端的 SYN 包只超時重傳了 1 次,符合 tcp_syn_retries 設置的值;
- 服務端的 SYN、ACK 超時重傳了 2 次,符合 tcp_synack_retries 設置的值
實驗二的實驗小結
通過實驗二的實驗結果,我們可以得知,當 TCP 第二次握手 SYN、ACK 包丟了後,客戶端 SYN 包會發生超時重傳,服務端 SYN、ACK 也會發生超時重傳。
客戶端 SYN 包超時重傳的最大次數,是由 tcp_syn_retries 決定的,預設值是 5 次;服務端 SYN、ACK 包時重傳的最大次數,是由 tcp_synack_retries 決定的,預設值是 5 次。
實驗三:TCP 第三次握手 ACK 丟包
為了模擬 TCP 第三次握手 ACK 包丟,我的實驗方法是在服務端配置防火牆,屏蔽客戶端 TCP 報文中標誌位是 ACK 的包,也就是當服務端收到客戶端的 TCP ACK 的報文時就會丟棄,iptables 配置命令如下:

接著,在客戶端執行如下 tcpdump 命令:

然後,客戶端向服務端發起 telnet,因為 telnet 命令是會發起 TCP 連接,所以用此命令做測試:

此時,由於服務端收不到第三次握手的 ACK 包,所以一直處於 SYN_RECV
狀態:

而客戶端是已完成 TCP 連接建立,處於 ESTABLISHED
狀態:

過了 1 分鐘後,觀察發現服務端的 TCP 連接不見了:

過了 30 分別,客戶端依然還是處於 ESTABLISHED
狀態:

接著,在剛纔客戶端建立的 telnet 會話,輸入 123456 字元,進行發送:

持續「好長」一段時間,客戶端的 telnet 才斷開連接:

以上就是本次的實現三的現象,這裡存在兩個疑點:
- 為什麼服務端原本處於
SYN_RECV
狀態的連接,過 1 分鐘後就消失了? - 為什麼客戶端 telnet 輸入 123456 字元後,過了好長一段時間,telnet 才斷開連接?
不著急,我們把剛抓的數據包,用 Wireshark 打