TCP實戰二(半連接隊列、全連接隊列)

TCP實驗一我們利用了tcpdump以及Wireshark對TCP三次握手、四次揮手、流量控制做了深入的分析,今天就讓我們一同深入理解TCP三次握手中兩個重要的結構:半連接隊列、全連接隊列。

參考文獻://zhuanlan.zhihu.com/p/144785626

目錄

1.TCP半連接隊列與全連接隊列概念

2.TCP全連接隊列溢出

  • 如何查看全連接隊列大小?
  • 如何模擬全連接隊列溢出的場景?
  • 全連接隊列溢出會發生什麼?
  • 如何增大全連接隊列呢?

3.TCP半連接隊列溢出

  • 如何查看半連接隊列大小?
  • 如何模擬半連接隊列溢出場景?
  • 網上都說tcp_max_syn_backlog是指定半連接隊列的大小,是真的嗎?
  • 源碼分析半連接隊列的最大值是如何決定的
  • 如果SYN半連接隊列已經滿了,只能丟棄連接嗎?
  • 如何防禦SYN攻擊?

1.TCP半連接隊列與全連接隊列概念

在 TCP 三次握手的時候,Linux 內核會維護兩個隊列,分別是:

  • 半連接隊列,也稱 SYN 隊列;
  • 全連接隊列,也稱 accepet 隊列;

服務端收到客戶端發起的 SYN 請求後,內核會把該連接存儲到半連接隊列,並向客戶端響應 SYN+ACK,接著客戶端會返回 ACK,服務端收到第三次握手的 ACK 後,內核會把連接從半連接隊列移除,然後創建新的完全的連接,並將其添加到 accept 隊列,等待進程調用 accept 函數時把連接取出來。

不管是半連接隊列還是全連接隊列,都有最大長度限制,超過限制時,內核會直接丟棄,或返回 RST 包。

2.TCP全連接隊列溢出

(1) 如何查看全連接隊列大小?

在服務端可以使用 ss 命令,來查看 TCP 全連接隊列的情況:

ss是Socket Statistics的縮寫。顧名思義,ss命令可以用來獲取socket統計資訊,它可以顯示和netstat類似的內容。但ss的優勢在於它能夠顯示更多更詳細的有關TCP和連接狀態的資訊,而且比netstat更快速更高效。

netstat命令用來列印Linux中網路系統的狀態資訊,可讓你得知整個Linux系統的網路情況。

但需要注意的是 ss 命令獲取的 Recv-Q/Send-Q 在「LISTEN 狀態」和「非 LISTEN 狀態」所表達的含義是不同的。從下面的內核程式碼可以看出區別:

 

在「LISTEN 狀態」時,利用 ss -lnt 命令,Recv-Q/Send-Q 表示的含義如下:

  • -l:–listening 顯示監聽狀態的套接字(sockets)
  • -n:–numeric 不解析服務名稱
  • -t:–tcp 僅顯示 TCP套接字(sockets)

 

  • Recv-Q:當前全連接隊列的大小,也就是當前已完成三次握手並等待服務端 accept() 的 TCP 連接;
  • Send-Q:當前全連接最大隊列長度,上面的輸出結果說明監聽 8088 埠的 TCP 服務,最大全連接長度為 128;

在「非 LISTEN 狀態」時,利用 ss -nt 命令Recv-Q/Send-Q 表示的含義如下:

  • Recv-Q:已收到但未被應用進程讀取的位元組數;
  • Send-Q:已發送但未收到確認的位元組數;

(2) 如何模擬全連接隊列溢出的場景?

實驗環境:

  • 客戶端和服務端都是 CentOs 6.5 (Linux 內核版本 2.6.32)
  • 服務端 IP 192.168.127.150,客戶端 IP 192.168.127.151
  • 服務端是 Nginx 服務,埠為 8088
  • 客戶端利用wrk工具

wrk 工具,它是一款簡單的 HTTP 壓測工具,它能夠在單機多核 CPU 的條件下,使用系統自帶的高性能 I/O 機制,通過多執行緒和事件模式,對目標機器產生大量的負載。

本次模擬實驗就使用 wrk 工具來壓力測試服務端,發起大量的請求,一起看看服務端 TCP 全連接隊列滿了會發生什麼?有什麼觀察指標?

(3) 全連接隊列溢出會發生什麼?

客戶端執行 wrk 命令對服務端發起壓力測試,並發 3 萬個連接:

  • -t 6:表示6個執行緒
  • -c 30000:表示3萬個連接
  • -d 60s:表示持續壓測60s

在服務端使用 ss 命令,來查看當前 TCP 全連接隊列的情況:

其間共執行了兩次 ss 命令,從上面的輸出結果,可以發現當前 TCP 全連接隊列上升到了 129 大小,超過了最大 TCP 全連接隊列的值128。

當超過了 TCP 最大全連接隊列,服務端則會丟掉後續進來的 TCP 連接,丟掉的 TCP 連接的個數會被統計起來,我們在服務端可以使用 netstat -s 命令來查看:

上面看到的 1750、2287….times ,表示全連接隊列溢出的次數,注意這個是累計值。可以隔幾秒鐘執行下,如果這個數字一直在增加的話肯定全連接隊列偶爾滿了。

客戶端執行wrk命令最後的結果:

圖中各個參數的解釋見:HTTP壓測工具之wrk

從上面的模擬結果,可以得知,當服務端並發處理大量請求時,如果 TCP 全連接隊列過小,就容易溢出。發生 TCP 全連接隊溢出的時候,後續的請求就會被丟棄。

Linux 有個參數可以指定當 TCP 全連接隊列滿了會使用什麼策略來回應客戶端。

tcp_abort_on_overflow 共有兩個值分別是 0 和 1,其分別表示:

  • 0 :如果全連接隊列滿了,那麼 server 扔掉 client 發過來的 ack ;
  • 1 :如果全連接隊列滿了,server 發送一個 reset 包給 client,表示廢掉這個握手過程和這個連接;

如果要想知道客戶端連接不上服務端,是不是服務端 TCP 全連接隊列滿的原因,那麼可以把 tcp_abort_on_overflow 設置為 1,這時如果在客戶端異常中可以看到很多 connection reset by peer 的錯誤,那麼就可以證明是由於服務端 TCP 全連接隊列溢出的問題。

通常情況下,應當把 tcp_abort_on_overflow 設置為 0,因為這樣更有利於應對突發流量。

舉個例子,當 TCP 全連接隊列滿導致伺服器丟掉了 ACK,與此同時,客戶端的連接狀態卻是 ESTABLISHED,進程就在建立好的連接上發送請求。只要伺服器沒有為請求回復 ACK,請求就會被多次重發。如果伺服器上的進程只是短暫的繁忙造成 accept 隊列滿,那麼當 TCP 全連接隊列有空位時,再次接收到的請求報文由於含有 ACK,仍然會觸發伺服器端成功建立連接。

所以,tcp_abort_on_overflow 設為 0 可以提高連接建立的成功率,只有你非常肯定 TCP 全連接隊列會長期溢出時,才能設置為 1 以儘快通知客戶端。

我們把服務端 tcp_abort_on_overflow 的值設為 1後,重複上述實驗。

在客戶端繼續執行3W次壓測。

可以明顯看到Socket errors中 read錯誤 和 write錯誤 與 tcp_abort_on_overflow 設為 0之前大幅度增加!

(4) 如何增大全連接隊列呢?

當發現 TCP 全連接隊列發生溢出的時候,我們就需要增大該隊列的大小,以便可以應對客戶端大量的請求。

TCP 全連接隊列足最大值取決於 somaxconn 和 backlog 之間的最小值,也就是 min(somaxconn, backlog)。從下面的 Linux 內核程式碼可以得知:

  • somaxconn 是 Linux 內核的參數,默認值是 128,可以通過 /proc/sys/net/core/somaxconn 來設置其值;
  • backlog 是 listen(int sockfd, int backlog) 函數中的 backlog 大小,Nginx 默認值是 511,可以通過修改配置文件設置其長度;

前面模擬測試中,我的測試環境:

  • somaxconn 是默認值 128;
  • Nginx 的 backlog 是默認值 511

現在我們重新壓測,把 TCP 全連接隊列搞大,把 somaxconn 設置成 5000:

接著把 Nginx 的 backlog 也同樣設置成 5000:

設置完畢後進入nginx下的sbin目錄執行以下命令即可:

[root@localhost sbin]# ./nginx -s reload

服務端執行 ss 命令,查看 TCP 全連接隊列大小:

從執行結果,可以發現 TCP 全連接最大值為 5000。

緊接著在客戶端以 3 萬個連接並發發送請求給服務端,繼續壓測:

服務端執行 ss 命令,查看 TCP 全連接隊列使用情況:

從上面的執行結果,可以發現全連接隊列使用增長的很快,但是一直都沒有超過最大值,所以就不會溢出,那麼 netstat -s 的值就不會改變: 

說明 TCP 全連接隊列最大值從 128 增大到 5000 後,服務端抗住了 3 萬連接並發請求,也沒有發生全連接隊列溢出的現象了。

如果持續不斷地有連接因為 TCP 全連接隊列溢出被丟棄,就應該調大 backlog 以及 somaxconn 參數。

3.TCP半連接隊列溢出

(1) 如何查看半連接隊列大小?

很遺憾,TCP 半連接隊列長度的長度,沒有像全連接隊列那樣可以用 ss 命令查看。

但是我們可以抓住 TCP 半連接的特點,就是服務端處於 SYN_RECV 狀態的 TCP 連接,就是在 TCP 半連接隊列。

(2) 如何模擬半連接隊列溢出場景?

模擬 TCP 半連接溢出場景不難,實際上就是對服務端一直發送 TCP SYN 包,但是不回第三次握手 ACK,這樣就會使得服務端有大量的處於 SYN_RECV 狀態的 TCP 連接。

這其實也就是所謂的 SYN 洪泛、SYN 攻擊、DDos 攻擊。

 實驗環境

  • 客戶端和服務端都是 CentOs 6.5 ,Linux 內核版本 2.6.32
  • 服務端 IP 192.168.127.153,客戶端 IP 192.168.127.152(由於採用的是DHCP動態分配IP地址,所以和上一個實驗相比,服務端和客戶端的IP地址都改變了,建議使用靜態地址!!)
  • 服務端是 Nginx 服務,埠為 8088
  • 客戶端利用hping3工具模擬SYN攻擊

注意:本次模擬實驗是沒有開啟 tcp_syncookies,關於 tcp_syncookies 的作用,後續會說明。centos6.5是默認開啟tcp_syncookies的,必須主動關閉。

 

本次實驗使用 hping3 工具模擬 SYN 攻擊:

  • -S:表示發生SYN數據包
  • -p:表示攻擊的埠
  • –flood:和洪水一樣不停的攻擊
  • –rand-source:隨機構造發送方的IP地址

當服務端受到 SYN 攻擊後,我們在服務端主機上執行查看當前 TCP 半連接隊列大小:

可以發現最大值到256就不再變化,說明當前TCP半連接隊列的最大值為256。

同時,如果半連接隊列滿了且tcp_syncookies未開啟,則客戶端發送至服務端的正常請求連接數據包將會被丟棄,利用curl命令證明了這一點。

(3) 網上都說tcp_max_syn_backlog是指定半連接隊列的大小,是真的嗎?

先說結論,在centos6.5(linux內核2.6.32)環境下,半連接隊列最大值不是單單由 max_syn_backlog 決定,還跟 somaxconn 和 backlog 有關係。

上面模擬 SYN 攻擊場景時,服務端的 tcp_max_syn_backlog 的默認值如下:

但是在測試的時候發現,服務端最多只有 256 個半連接隊列,而不是 512,所以半連接隊列的最大長度不一定由 tcp_max_syn_backlog 值決定的。

(4) 源碼分析半連接隊列的最大值是如何決定的

先說結論:

  • 當 tcp_max_syn_backlog > min(somaxconn, backlog) 時, 半連接隊列最大值 max_qlen_log = min(somaxconn, backlog) * 2;
  • 當 tcp_max_syn_backlog < min(somaxconn, backlog) 時, 半連接隊列最大值 max_qlen_log = tcp_max_syn_backlog * 2;

TCP 第一次握手(收到 SYN 包)的 Linux 內核程式碼如下,其中縮減了大量的程式碼,只需要重點關注 TCP 半連接隊列溢出的處理邏輯:

從源碼中,我可以得出共有三個條件因隊列長度的關係而被丟棄的:

  • 如果半連接隊列滿了,並且沒有開啟 tcp_syncookies,則會丟棄;
  • 若全連接隊列滿了,且沒有重傳 SYN+ACK 包的連接請求多於 1 個,則會丟棄;
  • 如果沒有開啟 tcp_syncookies,並且 max_syn_backlog 減去 當前半連接隊列長度小於 (max_syn_backlog >> 2),則會丟棄;

關於 tcp_syncookies 的設置,後面在詳細說明,可以先給大家說一下,開啟 tcp_syncookies 是緩解 SYN 攻擊其中一個手段。

接下來,我們繼續跟一下檢測半連接隊列是否滿的函數 inet_csk_reqsk_queue_is_full 和 檢測全連接隊列是否滿的函數 sk_acceptq_is_full

從上面源碼,可以得知:

  • 全連接隊列的最大值是 sk_max_ack_backlog 變數,sk_max_ack_backlog 實際上是在 listen() 源碼里指定的,也就是 min(somaxconn, backlog);
  • 半連接隊列的最大值是 max_qlen_log 變數,max_qlen_log 是在哪指定的呢?現在暫時還不知道,我們繼續跟進;

我們繼續跟進程式碼,看一下是哪裡初始化了半連接隊列的最大值 max_qlen_log:

從上面的程式碼中,我們可以算出 max_qlen_log 是 8,於是代入到 檢測半連接隊列是否滿的函數 reqsk_queue_is_full :

也就是 qlen >> 8 什麼時候為 1 就代表半連接隊列滿了。這計算這不難,很明顯是當 qlen 為 256 時,256 >> 8 = 1

至此,總算知道為什麼上面模擬測試 SYN 攻擊的時候,服務端處於 SYN_RECV 連接最大只有 256 個。

可見,半連接隊列最大值不是單單由 max_syn_backlog 決定,還跟 somaxconn 和 backlog 有關係。

在 Linux 2.6.32 內核版本,它們之間的關係,總體可以概況為:

綜上所述,結論如下:

  • 當 tcp_max_syn_backlog > min(somaxconn, backlog) 時, 半連接隊列最大值 max_qlen_log = min(somaxconn, backlog) * 2;
  • 當 tcp_max_syn_backlog < min(somaxconn, backlog) 時, 半連接隊列最大值 max_qlen_log = tcp_max_syn_backlog * 2;

(5) 半連接隊列最大值 max_qlen_log 就表示服務端處於 SYN_REVC 狀態的最大個數嗎?

首先需要明白每個 Linux 內核版本「理論」半連接最大值計算方式會不同。不談linux內核版本介紹就是扯淡。本文是基於Centos6.5(linux內核2.6.32)

答案是否定的,max_qlen_log 是理論半連接隊列最大值,並不一定代表服務端處於 SYN_REVC 狀態的最大個數。

如果「當前半連接隊列」沒超過「理論半連接隊列最大值」,但是超過 max_syn_backlog - (max_syn_backlog >> 2),那麼處於 SYN_RECV 狀態的最大個數就是 max_syn_backlog - (max_syn_backlog >> 2)+1;
如果「當前半連接隊列」超過「理論半連接隊列最大值」,那麼處於 SYN_RECV 狀態的最大個數就是「理論半連接隊列最大值」;

在前面我們在分析 TCP 第一次握手(收到 SYN 包)時會被丟棄的三種條件:

  • 如果半連接隊列滿了,並且沒有開啟 tcp_syncookies,則會丟棄;
  • 若全連接隊列滿了,且沒有重傳 SYN+ACK 包的連接請求多於 1 個,則會丟棄;
  • 如果沒有開啟 tcp_syncookies,並且 tcp_max_syn_backlog 減去 當前半連接隊列長度小於 (tcp_max_syn_backlog >> 2),則會丟棄;

假設條件 1 當前半連接隊列的長度 「沒有超過」理論的半連接隊列最大值 max_qlen_log,那麼如果條件 3 成立,則依然會丟棄 SYN 包,也就會使得服務端處於 SYN_REVC 狀態的最大個數不會是理論值 max_qlen_log。

接下來我用一個實驗來證明這個結論:

服務端的相關變數值如下:

根據上文的結論可以求出在這種情況下,半連接隊列理論最大值為:max_qlen_log = tcp_max_syn_backlog * 2 = 64 * 2 = 128.

客戶端執行hping3發起SYN攻擊

服務端執行如下命令,查看處於 SYN_RECV 狀態的最大個數:

可以發現,服務端處於 SYN_RECV 狀態的最大個數(49)並不是半連接隊列理論最大值(128).

這就是前面所說的原因:如果當前半連接隊列的長度 「沒有超過」理論半連接隊列最大值 max_qlen_log,那麼如果條件 3 成立,則依然會丟棄 SYN 包,也就會使得服務端處於 SYN_REVC 狀態的最大個數不會是理論值 max_qlen_log。

那49是如何計算出來的呢?

tcp_max_syn_backlog 減去 當前半連接隊列長度小於 (tcp_max_syn_backlog >> 2),則會丟棄.

64 - 當前半連接隊列長度 < 64 / 4
當前半連接隊列長度 > 64 - 16 = 48

因為處於 SYN_RECV 狀態的個數還沒到「理論半連接隊列最大值 128」,所以如果當前半連接隊列長度 > 48,則會丟棄SYN包。

(6) 如果SYN半連接隊列已經滿了,只能丟棄連接嗎?

結論:答案是否定的,在前面我們源碼分析也可以看到這點,當開啟了 syncookies 功能就可以在不使用 SYN 半連接隊列的情況下成功建立連接。

tcp_syncookies 參數主要有以下三個值,可以在 /proc/sys/net/ipv4/tcp_syncookies 修改該值。

  • 0 值,表示關閉該功能;
  • 1 值,表示僅當 SYN 半連接隊列放不下時,再啟用它;
  • 2 值,表示無條件開啟功能;

上文也說過了,centos6.5(linun內核2.6.32)默認開啟syncookies功能。

(7) 如何防禦SYN攻擊?(當半連接隊列已滿,如何調整?)

這裡給出幾種方法:

  • 增大半連接隊列;
  • 開啟 tcp_syncookies 功能
  • 減少 SYN+ACK 重傳次數(減小tcp_synack_retries的值)

方式一:增大半連接隊列

在前面源碼和實驗中,得知要想增大半連接隊列,我們得知不能只單純增大 tcp_max_syn_backlog 的值,還需一同增大 somaxconn 和 backlog,也就是增大全連接隊列。否則,只單純增大tcp_max_syn_backlog 是無效的。

增大 tcp_max_syn_backlog 和 somaxconn 的方法是修改 Linux 內核參數。

增大 backlog 的方式,每個 Web 服務都不同,比如 Nginx 增大 backlog 的方法如下:

方式三:減少 SYN+ACK 重傳次數

當服務端受到 SYN 攻擊時,就會有大量處於 SYN_REVC 狀態的 TCP 連接,處於這個狀態的 TCP 會重傳 SYN+ACK ,當重傳超過次數達到上限後,就會斷開連接。

那麼針對 SYN 攻擊的場景,我們可以減少 SYN+ACK 的重傳次數,也就是修改linux內核參數 tcp_synack_retries 以加快處於 SYN_REVC 狀態的 TCP 連接斷開。