天天看點

處于 establish 狀态的 TCP 連接配接,收到 SYN 包會發生什麼?

大家好,我是小林。

昨晚有位讀者問了我這麼個問題:

處于 establish 狀态的 TCP 連接配接,收到 SYN 包會發生什麼?

大概意思是,一個已經建立的 TCP 連接配接,用戶端中途當機了,而服務端此時也沒有資料要發送,一直處于 establish 狀态,用戶端恢複後,向服務端建立連接配接,此時服務端會怎麼處理?

看過我的圖解網絡的讀者都知道,TCP 連接配接是由「四元組」唯一确認的。

然後這個場景中,用戶端的IP、服務端IP、目的端口并沒有變化,是以這個問題關鍵要看用戶端發送的 SYN 封包中的源端口是否和上一次連接配接的源端口相同。

1. 用戶端的 SYN 封包裡的端口号與曆史連接配接不相同

如果用戶端恢複後發送的 SYN 封包中的源端口号跟上一次連接配接的源端口号不一樣,此時服務端會認為是新的連接配接要建立,于是就會通過三次握手來建立新的連接配接。

那舊連接配接裡處于 establish 狀态的服務端最後會怎麼樣呢?

如果服務端發送了資料包給用戶端,由于用戶端的連接配接已經被關閉了,此時客戶的核心就會回 RST 封包,服務端收到後就會釋放連接配接。

如果服務端一直沒有發送資料包給用戶端,在超過一段時間後, TCP 保活機制就會啟動,檢測到用戶端沒有存活後,接着服務端就會釋放掉該連接配接。

2. 用戶端的 SYN 封包裡的端口号與曆史連接配接相同

如果用戶端恢複後,發送的 SYN 封包中的源端口号跟上一次連接配接的源端口号一樣,也就是處于 establish 狀态的服務端收到了這個 SYN 封包。

大家覺得服務端此時會做什麼處理呢?

丢掉 SYN 封包?

回複 RST 封包?

回複 ACK 封包?

剛開始我看到這個問題的時候,也是沒有思路的,因為之前沒關注過,然後這個問題不能靠猜,是以我就看了 RFC 規範和看了 Linux 核心源碼,最終知道了答案。

我不賣關子,先直接說答案。

處于 establish 狀态的 TCP 連接配接,收到 SYN 包會發生什麼?

處于 establish 狀态的服務端如果收到了用戶端的 SYN 封包(注意此時的 SYN 封包其實是亂序的,因為 SYN 封包的初始化序列号其實是一個随機數),會回複一個攜帶了正确序列号和确認号的 ACK 封包,這個 ACK 被稱之為 Challenge ACK。

接着,用戶端收到這個 Challenge ACK,發現序列号并不是自己期望收到的,于是就會回 RST 封包,服務端收到後,就會釋放掉該連接配接。

rfc793 文檔裡的第 34 頁裡,有說到這個例子。

處于 establish 狀态的 TCP 連接配接,收到 SYN 包會發生什麼?

原文的解釋我也貼出來給大家看看。

When the SYN arrives at line 3, TCP B, being in a synchronized state,

and the incoming segment outside the window, responds with an

acknowledgment indicating what sequence it next expects to hear (ACK

100).

TCP A sees that this segment does not acknowledge anything it

sent and, being unsynchronized, sends a reset (RST) because it has

detected a half-open connection.

TCP B aborts at line 5.

TCP A willcontinue to try to establish the connection;

我就不瞎翻譯了,意思和我在前面用中文說的解釋差不多。

處于 establish 狀态的服務端如果收到了用戶端的 SYN 封包時,核心會調用這些函數:

我們隻關注 tcp_validate_incoming 函數是怎麼處理 SYN 封包的,精簡後的代碼如下:

處于 establish 狀态的 TCP 連接配接,收到 SYN 包會發生什麼?

從上面的代碼實作可以看到,處于 establish 狀态的服務端,在收到封包後,首先會判斷序列号是否在視窗内,如果不在,則看看 RST 标記有沒有被設定,如果有就會丢掉。然後如果沒有 RST 标志,就會判斷是否有 SYN 标記,如果有 SYN 标記就會跳轉到 syn_challenge 标簽,然後執行 tcp_send_challenge_ack 函數。

tcp_send_challenge_ack 函數裡就會調用 tcp_send_ack 函數來回複一個攜帶了正确序列号和确認号的 ACK 封包。

這裡問題大家這麼一個問題,如何關閉一個 TCP 連接配接?

可能大家第一反應是「殺掉程序」不就行了嗎?

是的,這個是最粗暴的方式,殺掉用戶端程序和服務端程序影響的範圍會有所不同:

在用戶端殺掉程序的話,就會發送 FIN 封包,來斷開這個用戶端程序與服務端建立的所有 TCP 連接配接,這種方式影響範圍隻有這個用戶端程序所建立的連接配接,而其他用戶端或程序不會受影響。

而在服務端殺掉程序影響就大了,此時所有的 TCP 連接配接都會被關閉,服務端無法繼續提供通路服務。

是以,關閉程序的方式并不可取,最好的方式要精細到關閉某一條 TCP 連接配接。

有的小夥伴可能會說,僞造一個四元組相同的 RST 封包不就行了?

這個思路很好,但是不要忘了還有個序列号的問題,你僞造的 RST 封包的序列号一定能被對方接受嗎?

如果 RST 封包的序列号不能落在對方的滑動視窗内,這個 RST 封包會被對方丢棄的,就達不到關閉的連接配接的效果。

是以,要僞造一個能關閉 TCP 連接配接的 RST 封包,必須同時滿足「四元組相同」和「序列号正好落在對方的滑動視窗内」這兩個條件。

直接僞造符合預期的序列号是比較困難,因為如果一個正在傳輸資料的 TCP 連接配接,滑動視窗時刻都在變化,是以很難剛好僞造一個剛好落在對方滑動視窗内的序列号的 RST 封包。

辦法還是有的,我們可以僞造一個四元組相同的 SYN 封包,來拿到“合法”的序列号!

正如我們最開始學到的,如果處于 establish 狀态的服務端,收到四元組相同的 SYN 封包後,會回複一個 Challenge ACK,這個 ACK 封包裡的「确認号」,正好是服務端下一次想要接收的序列号,說白了,就是可以通過這一步拿到服務端下一次預期接收的序列号。

然後用這個确認号作為 RST 封包的序列号,發送給服務端,此時服務端會認為這個 RST 封包裡的序列号是合法的,于是就會釋放連接配接!

在 Linux 上有個叫 killcx 的工具,就是基于上面這樣的方式實作的,它會主動發送 SYN 包擷取 SEQ/ACK 号,然後利用 SEQ/ACK 号僞造兩個 RST 封包分别發給用戶端和服務端,這樣雙方的 TCP 連接配接都會被釋放,這種方式活躍和非活躍的 TCP 連接配接都可以殺掉。

使用方式也很簡單,隻需指明用戶端的 IP 和端口号。

killcx 工具的工作原理,如下圖。

處于 establish 狀态的 TCP 連接配接,收到 SYN 包會發生什麼?

它僞造用戶端發送 SYN 封包,服務端收到後就會回複一個攜帶了正确「序列号和确認号」的 ACK 封包(Challenge ACK),然後就可以利用這個 ACK 封包裡面的資訊,僞造兩個 RST 封包:

用 Challenge ACK 裡的确認号僞造 RST 封包發送給服務端,服務端收到 RST 封包後就會釋放連接配接。

用 Challenge ACK 裡的序列号僞造 RST 封包發送給用戶端,用戶端收到 RST 也會釋放連接配接。

正是通過這樣的方式,成功将一個 TCP 連接配接關閉了!

這裡給大家貼一個使用 killcx 工具關閉連接配接的抓包圖,大家多看看序列号和确認号的變化。

處于 establish 狀态的 TCP 連接配接,收到 SYN 包會發生什麼?

是以,以後抓包中,如果莫名奇妙出現一個 SYN 包,有可能對方接下來想要對你發起的 RST 攻擊,直接将你的 TCP 連接配接斷開!

怎麼樣,很巧妙吧!

關注公衆号:「小林coding」 ,回複「我要學習」即可免費獲得「伺服器 Linux C/C++ 」成長路程(書籍資料 + 思維導圖)