“三次握手,四次揮手”你確定你了解么?(之三)
連接隊列
在外部請求到達時,被服務(wù)程序最終感知到前,連接可能處于SYN_RCVD狀態(tài)或是ESTABLISHED狀態(tài),但還未被應(yīng)用程序接受。
對應(yīng)地,服務(wù)器端也會維護兩種隊列,處于SYN_RCVD狀態(tài)的半連接隊列,而處于ESTABLISHED狀態(tài)但仍未被應(yīng)用程序accept的為全連接隊列。如果這兩個隊列滿了之后,就會出現(xiàn)各種丟包的情形。
1 2 | 查看是否有連接溢出 netstat -s | grep LISTEN |
半連接隊列滿了
在三次握手協(xié)議中,服務(wù)器維護一個半連接隊列,該隊列為每個客戶端的SYN包開設(shè)一個條目(服務(wù)端在接收到SYN包的時候,就已經(jīng)創(chuàng)建了request_sock結(jié)構(gòu),存儲在半連接隊列中),該條目表明服務(wù)器已收到SYN包,并向客戶發(fā)出確認(rèn),正在等待客戶的確認(rèn)包。這些條目所標(biāo)識的連接在服務(wù)器處于Syn_RECV狀態(tài),當(dāng)服務(wù)器收到客戶的確認(rèn)包時,刪除該條目,服務(wù)器進入ESTABLISHED狀態(tài)。
目前,Linux下默認(rèn)會進行5次重發(fā)SYN-ACK包,重試的間隔時間從1s開始,下次的重試間隔時間是前一次的雙倍,5次的重試時間間隔為1s, 2s, 4s, 8s, 16s, 總共31s, 稱為
指數(shù)退避,第5次發(fā)出后還要等32s才知道第5次也超時了,所以,總共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 63s, TCP才會把斷開這個連接。由于,SYN超時需要63秒,那么就給攻擊者一個攻擊服務(wù)器的機會,攻擊者在短時間內(nèi)發(fā)送大量的SYN包給Server(俗稱SYN flood攻擊),用于耗盡Server的SYN隊列。對于應(yīng)對SYN 過多的問題,linux提供了幾個TCP參數(shù):tcp_syncookies、tcp_synack_retries、tcp_max_syn_backlog、tcp_abort_on_overflow 來調(diào)整應(yīng)對。
| 參數(shù) | 作用 |
|---|---|
| tcp_syncookies | SYNcookie將連接信息編碼在ISN(initialsequencenumber)中返回給客戶端,這時server不需要將半連接保存在隊列中,而是利用客戶端隨后發(fā)來的ACK帶回的ISN還原連接信息,以完成連接的建立,避免了半連接隊列被攻擊SYN包填滿。 |
| tcp_syncookies | 內(nèi)核放棄建立連接之前發(fā)送SYN包的數(shù)量。 |
| tcp_synack_retries | 內(nèi)核放棄連接之前發(fā)送SYN+ACK包的數(shù)量 |
| tcp_max_syn_backlog | 默認(rèn)為1000. 這表示半連接隊列的長度,如果超過則放棄當(dāng)前連接。 |
| tcp_abort_on_overflow | 如果設(shè)置了此項,則直接reset. 否則,不做任何操作,這樣當(dāng)服務(wù)器半連接隊列有空了之后,會重新接受連接。Linux堅持在能力許可范圍內(nèi)不忽略進入的連接。客戶端在這期間會重復(fù)發(fā)送sys包,當(dāng)重試次數(shù)到達上限之后,會得到connection time out響應(yīng)。 |
全連接隊列滿了
當(dāng)?shù)谌挝帐謺r,當(dāng)server接收到ACK包之后,會進入一個新的叫 accept 的隊列。
當(dāng)accept隊列滿了之后,即使client繼續(xù)向server發(fā)送ACK的包,也會不被響應(yīng),此時ListenOverflows+1,同時server通過tcp_abort_on_overflow來決定如何返回,0表示直接丟棄該ACK,1表示發(fā)送RST通知client;相應(yīng)的,client則會分別返回read timeout?或者?connection reset by peer。另外,tcp_abort_on_overflow是0的話,server過一段時間再次發(fā)送syn+ack給client(也就是重新走握手的第二步),如果client超時等待比較短,就很容易異常了。而客戶端收到多個 SYN ACK 包,則會認(rèn)為之前的 ACK 丟包了。于是促使客戶端再次發(fā)送 ACK ,在 accept隊列有空閑的時候最終完成連接。若 accept隊列始終滿員,則最終客戶端收到 RST 包(此時服務(wù)端發(fā)送syn+ack的次數(shù)超出了tcp_synack_retries)。
服務(wù)端僅僅只是創(chuàng)建一個定時器,以固定間隔重傳syn和ack到服務(wù)端
| 參數(shù) | 作用 |
|---|---|
| tcp_abort_on_overflow | 如果設(shè)置了此項,則直接reset. 否則,不做任何操作,這樣當(dāng)服務(wù)器半連接隊列有空了之后,會重新接受連接。Linux堅持在能力許可范圍內(nèi)不忽略進入的連接。客戶端在這期間會重復(fù)發(fā)送sys包,當(dāng)重試次數(shù)到達上限之后,會得到connection time out響應(yīng)。 |
| min(backlog, somaxconn) | 全連接隊列的長度。 |
命令
netstat -s命令
1 2 3 | [root<a href="http://www.jobbole.com/members/server">@server</a> ~]#??netstat -s | egrep "listen|LISTEN" 667399 times the listen queue of a socket overflowed 667399 SYNs to LISTEN sockets ignored |
上面看到的 667399 times ,表示全連接隊列溢出的次數(shù),隔幾秒鐘執(zhí)行下,如果這個數(shù)字一直在增加的話肯定全連接隊列偶爾滿了。
1 | [root<a href="http://www.jobbole.com/members/server">@server</a> ~]#??netstat -s | grep TCPBacklogDrop |
查看 Accept queue 是否有溢出
ss命令
1 2 3 4 | [root<a href="http://www.jobbole.com/members/server">@server</a> ~]#??ss -lnt State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN???? 0??????128 *:6379 *:* LISTEN???? 0??????128 *:22 *:* |
如果State是listen狀態(tài),Send-Q 表示第三列的listen端口上的全連接隊列最大為50,第一列Recv-Q為全連接隊列當(dāng)前使用了多少。
非 LISTEN 狀態(tài)中 Recv-Q 表示 receive queue 中的 bytes 數(shù)量;Send-Q 表示 send queue 中的 bytes 數(shù)值。
小結(jié)
當(dāng)外部連接請求到來時,TCP模塊會首先查看max_syn_backlog,如果處于SYN_RCVD狀態(tài)的連接數(shù)目超過這一閾值,進入的連接會被拒絕。根據(jù)tcp_abort_on_overflow字段來決定是直接丟棄,還是直接reset.
從服務(wù)端來說,三次握手中,第一步server接受到client的syn后,把相關(guān)信息放到半連接隊列中,同時回復(fù)syn+ack給client. 第三步當(dāng)收到客戶端的ack, 將連接加入到全連接隊列。
一般,全連接隊列比較小,會先滿,此時半連接隊列還沒滿。如果這時收到syn報文,則會進入半連接隊列,沒有問題。但是如果收到了三次握手中的第3步(ACK),則會根據(jù)tcp_abort_on_overflow字段來決定是直接丟棄,還是直接reset.此時,客戶端發(fā)送了ACK, 那么客戶端認(rèn)為三次握手完成,它認(rèn)為服務(wù)端已經(jīng)準(zhǔn)備好了接收數(shù)據(jù)的準(zhǔn)備。但此時服務(wù)端可能因為全連接隊列滿了而無法將連接放入,會重新發(fā)送第2步的syn+ack, 如果這時有數(shù)據(jù)到來,服務(wù)器TCP模塊會將數(shù)據(jù)存入隊列中。一段時間后,client端沒收到回復(fù),超時,連接異常,client會主動關(guān)閉連接。
“三次握手,四次揮手”redis實例分析
我在dev機器上部署redis服務(wù),端口號為6379,
通過tcpdump工具獲取數(shù)據(jù)包,使用如下命令
1 2 | tcpdump -w /tmp/a.cap port 6379 -s0 -w把數(shù)據(jù)寫入文件,-s0設(shè)置每個數(shù)據(jù)包的大小默認(rèn)為68字節(jié),如果用-S 0則會抓到完整數(shù)據(jù)包 |
在dev2機器上用redis-cli訪問dev:6379, 發(fā)送一個ping, 得到回復(fù)pong
停止抓包,用tcpdump讀取捕獲到的數(shù)據(jù)包
1 2 | tcpdump -r /tmp/a.cap -n -nn -A -x| vim - (-x 以16進制形式展示,便于后面分析) |
共收到了7個包。
抓到的是IP數(shù)據(jù)包,IP數(shù)據(jù)包分為IP頭部和IP數(shù)據(jù)部分,IP數(shù)據(jù)部分是TCP頭部加TCP數(shù)據(jù)部分。
IP的數(shù)據(jù)格式為:
它由固定長度20B+可變長度構(gòu)成。
1 2 3 4 5 | 10:55:45.662077 IP dev2.39070 > dev.6379: Flags [S], seq 4133153791, win 29200, options [mss 1460,sackOK,TS val 2959270704 ecr 0,nop,wscale 7], length 0 ????????0x0000:??4500 003c 08cf 4000 3606 14a5 0ab3 b561 ????????0x0010:??0a60 5cd4 989e 18eb f65a ebff 0000 0000 ????????0x0020:??a002 7210 872f 0000 0204 05b4 0402 080a ????????0x0030:??b062 e330 0000 0000 0103 0307 |
對著IP頭部格式,來拆解數(shù)據(jù)包的具體含義。
| 字節(jié)值 | 字節(jié)含義 |
|---|---|
| 0x4 | IP版本為ipv4 |
| 0x5 | 首部長度為5 * 4字節(jié)=20B |
| 0x00 | 服務(wù)類型,現(xiàn)在基本都置為0 |
| 0x003c | 總長度為3*16+12=60字節(jié),上面所有的長度就是60字節(jié) |
| 0x08cf | 標(biāo)識。同一個數(shù)據(jù)報的唯一標(biāo)識。當(dāng)IP數(shù)據(jù)報被拆分時,會復(fù)制到每一個數(shù)據(jù)中。 |
| 0x4000 | 3bit 標(biāo)志 + 13bit 片偏移。3bit 標(biāo)志對應(yīng) R、DF、MF。目前只有后兩位有效,DF位:為1表示不分片,為0表示分片。MF:為1表示“更多的片”,為0表示這是最后一片。13bit 片位移:本分片在原先數(shù)據(jù)報文中相對首位的偏移位。(需要再乘以8 ) |
| 0x36 | 生存時間TTL。IP報文所允許通過的路由器的最大數(shù)量。每經(jīng)過一個路由器,TTL減1,當(dāng)為 0 時,路由器將該數(shù)據(jù)報丟棄。TTL 字段是由發(fā)送端初始設(shè)置一個 8 bit字段.推薦的初始值由分配數(shù)字 RFC 指定。發(fā)送 ICMP 回顯應(yīng)答時經(jīng)常把 TTL 設(shè)為最大值 255。TTL可以防止數(shù)據(jù)報陷入路由循環(huán)。 此處為54. |
| 0x06 | 協(xié)議類型。指出IP報文攜帶的數(shù)據(jù)使用的是哪種協(xié)議,以便目的主機的IP層能知道要將數(shù)據(jù)報上交到哪個進程。TCP 的協(xié)議號為6,UDP 的協(xié)議號為17。ICMP 的協(xié)議號為1,IGMP 的協(xié)議號為2。該 IP 報文攜帶的數(shù)據(jù)使用 TCP 協(xié)議,得到了驗證。 |
| 0x14a5 | 16bitIP首部校驗和。 |
| 0x0ab3 b561 | 32bit源ip地址。 |
| 0x0a60 5cd4 | 32bit目的ip地址。 |
剩余的數(shù)據(jù)部分即為TCP協(xié)議相關(guān)的。TCP也是20B固定長度+可變長度部分。
| 字節(jié)值 | 字節(jié)含義 |
|---|---|
| 0x989e | 16bit源端口。1161616+81616+1416+11=39070 |
| 0x18eb | 16bit目的端口6379 |
| 0xf65a ebff | 32bit序列號。4133153791 |
| 0x0000 0000 | 32bit確認(rèn)號。 |
| 0xa | 4bit首部長度,以4byte為單位。共10*4=40字節(jié)。因此TCP報文的可選長度為40-20=20 |
| 0b000000 | 6bit保留位。目前置為0. |
| 0b000010 | 6bitTCP標(biāo)志位。從左到右依次是緊急 URG、確認(rèn) ACK、推送 PSH、復(fù)位 RST、同步 SYN 、終止 FIN。 |
| 0x7210 | 滑動窗口大小,滑動窗口即tcp接收緩沖區(qū)的大小,用于tcp擁塞控制。29200 |
| 0x872f | 16bit校驗和。 |
| 0x0000 | 緊急指針。僅在 URG = 1時才有意義,它指出本報文段中的緊急數(shù)據(jù)的字節(jié)數(shù)。當(dāng) URG = 1 時,發(fā)送方 TCP 就把緊急數(shù)據(jù)插入到本報文段數(shù)據(jù)的最前面,而在緊急數(shù)據(jù)后面的數(shù)據(jù)仍是普通數(shù)據(jù)。 |
可變長度部分,協(xié)議如下:
| 字節(jié)值 | 字節(jié)含義 |
|---|---|
| 0x0204 05b4 | 最大報文長度為,05b4=1460. 即可接收的最大包長度,通常為MTU減40字節(jié),IP頭和TCP頭各20字節(jié) |
| 0x0402 | 表示支持SACK |
| 0x080a b062 e330 0000 0000 | 時間戳。Ts val=b062 e330=2959270704, ecr=0 |
| 0x01 | 無操作 |
| 0x03 0307 | 窗口擴大因子為7. 移位7, 乘以128 |
這樣第一個包分析完了。dev2向dev發(fā)送SYN請求。也就是三次握手中的第一次了。SYN seq(c)=4133153791
第二個包,dev響應(yīng)連接,ack=4133153792. 表明dev下次準(zhǔn)備接收這個序號的包,用于tcp字節(jié)注的順序控制。dev(也就是server端)的初始序號為seq=4264776963, syn=1.SYN ack=seq(c)+1 seq(s)=4264776963
第三個包,client包確認(rèn),這里使用了相對值應(yīng)答。seq=4133153792, 等于第二個包的ack. ack=4264776964.ack=seq(s)+1, seq=seq(c)+1
至此,三次握手完成。接下來就是發(fā)送ping和pong的數(shù)據(jù)了。
接著第四個包。
1 2 3 4 5 6 | 10:55:48.090073 IP dev2.39070 > dev.6379: Flags [P.], seq 1:15, ack 1, win 229, options [nop,nop,TS val 2959273132 ecr 3132256230], length 14 ????????0x0000:??4500 0042 08d1 4000 3606 149d 0ab3 b561 ????????0x0010:??0a60 5cd4 989e 18eb f65a ec00 fe33 5504 ????????0x0020:??8018 00e5 4b5f 0000 0101 080a b062 ecac ????????0x0030:??bab2 6fe6 2a31 0d0a 2434 0d0a 7069 6e67 ????????0x0040:??0d0a |
tcp首部長度為32B, 可選長度為12B. IP報文的總長度為66B, 首部長度為20B, 因此TCP數(shù)據(jù)部分長度為14B. seq=0xf65a ec00=4133153792
ACK, PSH. 數(shù)據(jù)部分為2a31 0d0a 2434 0d0a 7069 6e67 0d0a
1 2 3 4 5 6 | 0x2a31???????? -> *1 0x0d0a???????? -> rn 0x2434???????? -> $4 0x0d0a???????? -> rn 0x7069 0x6e67??-> ping 0x0d0a???????? -> rn |
dev2向dev發(fā)送了ping數(shù)據(jù),第四個包完畢。
第五個包,dev2向dev發(fā)送ack響應(yīng)。
序列號為0xfe33 5504=4264776964, ack確認(rèn)號為0xf65a ec0e=4133153806=(4133153792+14).
第六個包,dev向dev2響應(yīng)pong消息。序列號fe33 5504,確認(rèn)號f65a ec0e, TCP頭部可選長度為12B, IP數(shù)據(jù)報總長度為59B, 首部長度為20B, 因此TCP數(shù)據(jù)長度為7B.
數(shù)據(jù)部分2b50 4f4e 470d 0a, 翻譯過來就是+PONGrn.
至此,Redis客戶端和Server端的三次握手過程分析完畢。
總結(jié)
“三次握手,四次揮手”看似簡單,但是深究進去,還是可以延伸出很多知識點的。比如半連接隊列、全連接隊列等等。以前關(guān)于TCP建立連接、關(guān)閉連接的過程很容易就會忘記,可能是因為只是死記硬背了幾個過程,沒有深入研究背后的原理。
所以,“三次握手,四次揮手”你真的懂了嗎?
參考資料
【redis】https://segmentfault.com/a/1190000015044878
【tcp option】https://blog.csdn.net/wdscq1234/article/details/52423272
【滑動窗口】https://www.zhihu.com/question/32255109
【全連接隊列】http://jm.taobao.org/2017/05/25/525-1/
【client fooling】 https://github.com/torvalds/linux/commit/5ea8ea2cb7f1d0db15762c9b0bb9e7330425a071
【backlog RECV_Q】http://blog.51cto.com/59090939/1947443
【定時器】https://www.cnblogs.com/menghuanbiao/p/5212131.html
【隊列圖示】https://www.itcodemonkey.com/article/5834.html
【tcp flood攻擊】https://www.cnblogs.com/hubavyn/p/4477883.html
【MSS MTU】https://blog.csdn.net/LoseInVain/article/details/53694265





