http://daviswang.iteye.com/blog/819176
首先我們需要弄清楚SYN_RCVD狀態是怎樣產生的,通過TCP狀態轉換圖(如下圖)我們可以清楚的看到,SYN_RCVD是TCP三次握手的中間狀態,是服務端口(監聽端口,如應用服務器的80端口)收到SYN包並發送[SYN,ACK]包后所處的狀態。這時如果再收到ACK的包,就完成了三次握手,建立起TCP連接。
如果服務器上出現大量的SYN_RCVD狀態的TCP連接說明這些連接一直沒有收到ACK包,這主要有兩種可能,一種是對方(請求方或客戶端)沒有收到服務器發送的[SYN,ACK]包,另一種可能是對方收到了[SYN,ACK]包卻沒有ACK。
對於第一種情況一般是由於網絡結構或安全規則限制導致(SYN,ACK)包無法發送到對方,這種情況比較容易判斷:只要在服務器上能夠ping通互聯網的任意主機,基本可以排除這種情況。
對於第二種情況要稍微復雜一些,這種情況還有兩種可能:一種是對方根本就不打算ACK,一般在對方程序有意為之才會出現,如SYN Flood類型的DOS/DDOS攻擊;另一種可能是對方收到的[SYN,ACK]包不合法,常見的是SYN包的目的地址(服務地址)和應答[SYN,ACK]包的源地址不同。這種情況在只配置了DNAT而不進行SNAT的服務網絡環境下容易出現,主要是由於inbound(SYN包)和outbound([SYN,ACK]包)的包穿越了不同的網關/防火牆/負載均衡器,從而導致[SYN,ACK]路由到互聯網的源地址(一般是防火牆的出口地址)與SYN包的目的地址(服務的虛擬IP)不同,這時客戶機無法將SYN包和[SYN,ACK]包關聯在一起,從而會認為已發出的SYN包還沒有被應答,於是繼續等待應答包。這樣服務器端的連接一直保持在SYN_RCVD狀態(半開連接)直到超時。
==============
SNAT是指在數據包從網卡發送出去的時候,把數據包中的源地址部分替換為指定的IP,這樣,接收方就認為數據包的來源是被替換的那個IP的主機
DNAT,就是指數據包從網卡發送出去的時候,修改數據包中的目的IP,表現為如果你想訪問A,可是因為網關做了DNAT,把所有訪問A的數據包的目的IP全部修改為B,那么,你實際上訪問的是B
==========================================
SYN攻擊是最常見又最容易被利用的一種攻擊手法。相信很多人還記得2000年YAHOO網站遭受的攻擊事例,當 時黑客利用的就是簡單而有效的SYN攻擊,有些網絡蠕蟲病毒配合SYN攻擊造成更大的破壞。本文介紹SYN攻擊的基本原理、工具及檢測方法,並全面探討 SYN攻擊防范技術。
一、TCP握手協議 在TCP/IP協議中,TCP協議提供可靠的連接服務,采用三次握手建立一個連接。
第一次握手:建立連接時,客戶端發送syn包(syn=j)到服務器,並進入SYN_SEND狀態,等待服務器確認;
第二次握手:服務器收到syn包,必須確認客戶的SYN(ack=j+1),同時自己也發送一個SYN包(syn=k),即SYN+ACK包,此時服務器進入SYN_RECV狀態;
第 三次握手:客戶端收到服務器的SYN+ACK包,向服務器發送確認包ACK(ack=k+1),此包發送完畢,客戶端和服務器進入ESTABLISHED 狀態,完成三次握手。
完成三次握手,客戶端與服務器開始傳送數據,在上述過程中,還有一些重要的概念:
未連接隊列:在三次握手協議中,服務器維護一個未連接隊列,該隊列為每個 客戶端的SYN包(syn=j)開設一個條目,該條目表明服務器已收到SYN包,並向客戶發出確認,正在等待客戶的確認包。這些條目所標識的連接在服務器 處於Syn_RECV狀態,當服務器收到客戶的確認包時,刪除該條目,服務器進入 ESTABLISHED狀態。
已完成連接隊列(completed connection queue),每個已完成TCP三路握手過程的客戶對應其中一項。這些套接口處於ESTABLISHED狀態。
Backlog參數:表示未連接隊列的最大容納數目。
SYN-ACK 重傳次數 服務器發送完SYN-ACK包,如果未收到客戶確認包,服務器進行首次重傳,等待一段時間仍未收到客戶確認包,進行第二次重傳,如果重傳次數超 過系統規定的最大重傳次數,系統將該連接信息從半連接隊列中刪除。注意,每次重傳等待的時間不一定相同。
半連接存活時間:是指半連接隊列的條目存活的最長 時間,也即服務從收到SYN包到確認這個報文無效的最長時間,該時間值是所有重傳請求包的最長等待時間總和。有時我們也稱半連接存活時間為Timeout 時間、SYN_RECV存活時間。
=================
http://bbs.chinaunix.net/thread-4058395-1-1.html
telnet 或 nc 配合調試
於是,通過在服務器端tcpdump抓包,發現在TCP的三次握手過程中出現了靈異現象。
服務器在收到來自客戶端的ACK報文后,並沒有建立最終連接ESTABLISHED,而是再次發送SYN+ACK報文,要求客戶端響應,並且是每隔4秒,7秒,13秒給客戶端發SYN+ACK報文。
而客戶端每次也給予了ACK回復,但服務器似乎沒有收到ACK一樣,一直這么發呀發(最多發5次就斷開)
1) 首先,SYN_RECV狀態的連接是怎么來的?
當服務器端收到來自客戶端的ACK報文,並正確解析后,會新生成一個連接,並初始化此連接的狀態為SYN_RECV。 然后這個連接被放在服務器端的已完成連接隊列里,等待服務器用戶程序accept來取走。
2) 那什么時候,這個新SYN_RECV狀態的連接會被置狀態為ESTABLISHED?
當這個新生成的連接,繼續往下走,一切順利的話會置ESTABLISHED(請原諒我用這么屌絲的描述,因為我實在搞不清這個流程)
關於服務器端處於syn_recv的狀態,但是client端處於established的狀態,原因就是服務器端應用層accpet太忙,導致acceptd隊列滿所致。
====================================================
環境:
Client 通過tcp 連接server,server端只是listen,但是不調用accept。通過netstat –ant查看兩端的連接情況。
server端listen,不調用accept。
client一直去connect server。
問題:
運行一段時間后,為什么server端的ESTABLISHED連接的個數基本是固定的129個,但是client端的ESTABLISHED連接的個數卻在不斷增加?
分析
Linux內核協議棧為一個tcp連接管理使用兩個隊列,一個是半鏈接隊列(用來保存處於SYN_SENT和SYN_RECV狀態的請求),一個是accpetd隊列(用來保存處於established狀態,但是應用層沒有調用accept取走的請求)。
第一個隊列的長度是/proc/sys/net/ipv4/tcp_max_syn_backlog,默認是1024。如果開啟了syncookies,那么基本上沒有限制。
第二個隊列的長度是/proc/sys/net/core/somaxconn,默認是128,表示最多有129個established鏈接等待accept。(為什么是129?詳見下面的附錄1)。【最多有129個established客戶端連接等待服務端accept】
somaxconn
is the number of complete connections waiting.
tcp_max_syn_backlog
is the number of incomplete connections waiting.
現在假設acceptd隊列已經達到129的情況:
client發送syn到server。client(SYN_SENT),server(SYN_RECV)
server端處理流程:tcp_v4_do_rcv--->tcp_rcv_state_process--->tcp_v4_conn_request
if(sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_yong(sk)>1)
goto drop;
inet_csk_reqsk_queue_yong(sk)的含義是請求隊列中有多少個握手過程中沒有重傳過的段。
在第一次的時候,之前的握手過程都沒有重傳過,所以這個syn包server端會直接drop掉,之后client會重傳syn,當inet_csk_reqsk_queue_yong(sk) < 1,那么這個syn被server端接受。server會回復synack給client。這樣一來兩邊的狀態就變為client(ESTABLISHED), server(SYN_SENT)
Client收到synack后回復ack給server。
server端處理流程: tcp_check_req--->syn_recv_sock-->tcp_v4_syn_recv_sock
if(sk_acceptq_is_full(sk)
goto exit_overflow;
如果server端設置了sysctl_tcp_abort_on_overflow,那么server會發送rst給client,並刪除掉這個鏈接;否則server端只是記錄一下LINUX_MIB_LISTENOVERFLOWS(詳見附錄2),然后返回。默認情況下是不會設置的,server端只是標記連接請求塊的acked標志,之后連接建立定時器,會遍歷半連接表,重新發送synack,重復上面的過程(具體的函數是inet_csk_reqsk_queue_prune),如果重傳次數超過synack重傳的閥值(/proc/sys/net/ipv4/tcp_synack_retries),會把該連接從半連接鏈表中刪除。
一次異常問題分析
Nginx通過FASTCGI協議連接cgi程序,出現cgi程序read讀取socket內容的時候永遠block。通過netstat查看,cgi程序所在的服務器上顯示連接存在,但是nginx所在的服務器上顯示不存在該連接。
下面是原始數據圖:
我們從上面的數據流來分析一下:
出現問題的時候,cgi程序(tcp server端)處理非常慢,導致大量的連接請求放到accept隊列,把accept隊列阻塞。
148021 nginx(tcp client端) 連接cgi程序,發送syn
此時server端accpet隊列已滿,並且inet_csk_reqsk_queue_yong(sk) > 1,server端直接丟棄該數據包
148840 client端等待3秒后,重傳SYN
此時server端狀態與之前送變化,仍然丟棄該數據包
150163 client端又等待6秒后,重傳SYN
此時server端accept隊列仍然是滿的,但是存在了重傳握手的連接請求,server端接受連接請求,並發送synack給client端(150164)
150166 client端收到synack,標記本地連接為ESTABLISHED狀態,給server端應答ack,connect系統調用完成。
Server收到ack后,嘗試將連接放到accept隊列,但是因為accept隊列已滿,所以只是標記連接為acked,並不會將連接移動到accept隊列中,也不會為連接分配sendbuf和recvbuf等資源。
150167 client端的應用程序,檢測到connect系統調用完成,開始向該連接發送數據。
Server端收到數據包,由於acept隊列仍然是滿的,所以server端處理也只是標記acked,然后返回。
150225 client端由於沒有收到剛才發送數據的ack,所以會重傳剛才的數據包
150296 同上
150496 同上
150920 同上
151112 server端連接建立定時器生效,遍歷半連接鏈表,發現剛才acked的連接,重新發送synack給client端。
151113 client端收到synack后,根據ack值,使用SACK算法,只重傳最后一個ack內容。
Server端收到數據包,由於accept隊列仍然是滿的,所以server端處理也只是標記acked,然后返回。
151896 client端等待3秒后,沒有收到對應的ack,認為之前的數據包也丟失,所以重傳之前的內容數據包。
152579 server端連接建立定時器生效,遍歷半連接鏈表,發現剛才acked的連接,synack重傳次數在閥值以內,重新發送synack給client端。
152581 cient端收到synack后,根據ack值,使用SACK算法,只重傳最后一個ack內容。
Server端收到數據包,由於accept隊列仍然是滿的,所以server端處理也只是標記acked,然后返回
153455 client端等待3秒后,沒有收到對應的ack,認為之前的數據包也丟失,所以重傳之前的內容數據包。
155399 server端連接建立定時器生效,遍歷半連接鏈表,發現剛才acked的連接,synack重傳次數在閥值以內,重新發送synack給client端。
155400 cient端收到synack后,根據ack值,使用SACK算法,只重傳最后一個ack內容。
Server端收到數據包,由於accept隊列仍然是滿的,所以server端處理也只是標記acked,然后返回。
156468 client端等待幾秒后,沒有收到對應的ack,認為之前的數據包也丟失,所以重傳之前的內容數據包。
161309 server端連接建立定時器生效,遍歷半連接鏈表,發現剛才acked的連接,synack重傳次數在閥值以內,重新發送synack給client端。
161310 cient端收到synack后,根據ack值,使用SACK算法,只重傳最后一個ack內容。
Server端收到數據包,由於accept隊列仍然是滿的,所以server端處理也只是標記acked,然后返回。
162884 client端等待幾秒后,沒有收到對應的ack,認為之前的數據包也丟失,所以重傳之前的內容數據包。
Server端收到數據包,由於accept隊列仍然是滿的,所以server端處理也只是標記acked,然后返回。
164828 client端等待一段時間后,認為連接不可用,於是發送FIN、ACK給server端。Client端的狀態變為FIN_WAIT1,等待一段時間后,client端將看不到該鏈接。
164829 server端收到ACK后,此時cgi程序處理完一個請求,從accept隊列中取走一個連接,此時accept隊列中有了空閑,server端將請求的連接放到accept隊列中。
這樣cgi所在的服務器上顯示該鏈接是established的,但是nginx(client端)所在的服務器上已經沒有該鏈接了。
之后,當cgi程序從accept隊列中取到該連接后,調用read去讀取sock中的內容,但是由於client端早就退出了,所以read就會block那里了。
問題解決
或許你會認為在164829中,server端不應該建立連接,這是內核的bug。但是內核是按照RFC來實現的,在3次握手的過程中,是不會判斷FIN標志位的,只會處理SYN、ACK、RST這三種標志位。
從應用層的角度來考慮解決問題的方法,那就是使用非阻塞的方式read,或者使用select超時方式read;亦或者nginx中關閉連接的時候使用RST方式,而不是FIN方式。
附錄1
when I use linux TCP socket, and find there is a bug in function sk_acceptq_is_full():
When a new SYN comes, TCP module first checks its validation. If valid,send SYN,ACK to the client and add the sock
to the syn hash table.
Next time if received the valid ACK for SYN,ACK from the client. server will accept this connection and increase the
sk->sk_ack_backlog -- which is done in function tcp_check_req().
We check wether acceptq is full in function tcp_v4_syn_recv_sock().
Consider an example:
After listen(sockfd, 1) system call, sk->sk_max_ack_backlog is set to 1
As we know, sk->sk_ack_backlog is initialized to 0. Assuming accept() system call is not invoked now
1. 1st connection comes. invoke sk_acceptq_is_full(). sk->sk_ack_backlog=0 sk->sk_max_ack_backlog=1, function return 0 accept this connection. Increase the sk->sk_ack_backlog
2. 2nd connection comes. invoke sk_acceptq_is_full(). sk->sk_ack_backlog=1 sk->sk_max_ack_backlog=1, function return 0 accept this connection. Increase the sk->sk_ack_backlog
3. 3rd connection comes. invoke sk_acceptq_is_full(). sk->sk_ack_backlog=2 sk->sk_max_ack_backlog=1, function return 1. Refuse this connection.I think it has bugs. after listen system call. sk->sk_max_ack_backlog=1
but now it can accept 2 connections.
附錄2
netstat -s
cat /proc/net/netstat
最后感謝Tiger提供的測試數據