知乎千贊的 TCP 文章,我寫錯(cuò)了一個(gè)點(diǎn)。。。
- 實(shí)驗(yàn)一:模擬 TCP 第一次握手的 SYN 丟包;
- 實(shí)驗(yàn)二:模擬 TCP 第二次握手的 SYN、ACK 丟包;
- 實(shí)驗(yàn)三:模擬 TCP 第三次握手的 ACK 包丟;
我也把 TCP 三次握手模擬異常情況的實(shí)驗(yàn)過程整理了下,大家有興趣可以模仿我這篇文章的實(shí)驗(yàn)步驟來做實(shí)驗(yàn)。5000字的車,坐穩(wěn)了!
TCP 三次握手異常情況實(shí)戰(zhàn)分析
TCP 三次握手的過程相信大家都背的滾瓜爛熟,那么你有沒有想過這三個(gè)異常情況:- TCP 第一次握手的 SYN 丟包了,會(huì)發(fā)生了什么?
- TCP 第二次握手的 SYN、ACK 丟包了,會(huì)發(fā)生什么?
- TCP 第三次握手的 ACK 包丟了,會(huì)發(fā)生什么?
- 那會(huì)重傳幾次?
- 超時(shí)重傳的時(shí)間 RTO 會(huì)如何變化?
- 在 Linux 下如何設(shè)置重傳次數(shù)?
- ….
實(shí)驗(yàn)場(chǎng)景
本次實(shí)驗(yàn)用了兩臺(tái)虛擬機(jī),一臺(tái)作為服務(wù)端,一臺(tái)作為客戶端,它們的關(guān)系如下:- 客戶端和服務(wù)端都是 CentOs 6.5 Linux,Linux 內(nèi)核版本 2.6.32
- 服務(wù)端 192.168.12.36,apache web 服務(wù)
- 客戶端 192.168.12.37
實(shí)驗(yàn)一:TCP 第一次握手 SYN 丟包
為了模擬 TCP 第一次握手 SYN 丟包的情況,我是在拔掉服務(wù)器的網(wǎng)線后,立刻在客戶端執(zhí)行 curl 命令:date
返回的時(shí)間,可以發(fā)現(xiàn)在超時(shí)接近 1 分鐘的時(shí)間后,curl 返回了錯(cuò)誤。接著,把 tcp_sys_timeout.pcap 文件用 Wireshark 打開分析,顯示如下圖:- 第一次是在 1 秒超時(shí)重傳
- 第二次是在 3 秒超時(shí)重傳
- 第三次是在 7 秒超時(shí)重傳
- 第四次是在 15 秒超時(shí)重傳
- 第五次是在 31 秒超時(shí)重傳
SYN
超時(shí)重傳次數(shù),是如下內(nèi)核參數(shù)指定的:$?cat?/proc/sys/net/ipv4/tcp_syn_retries5
tcp_syn_retries
默認(rèn)值為 5,也就是 SYN 最大重傳次數(shù)是 5 次。接下來,我們繼續(xù)做實(shí)驗(yàn),把 tcp_syn_retries
設(shè)置為 2 次:$?echo?2?>?/proc/sys/net/ipv4/tcp_syn_retries重傳抓包后,用 Wireshark 打開分析,顯示如下圖:
實(shí)驗(yàn)一的實(shí)驗(yàn)小結(jié)通過實(shí)驗(yàn)一的實(shí)驗(yàn)結(jié)果,我們可以得知,當(dāng)客戶端發(fā)起的 TCP 第一次握手 SYN 包,在超時(shí)時(shí)間內(nèi)沒收到服務(wù)端的 ACK,就會(huì)在超時(shí)重傳 SYN 數(shù)據(jù)包,每次超時(shí)重傳的 RTO 是翻倍上漲的,直到 SYN 包的重傳次數(shù)到達(dá)
tcp_syn_retries
值后,客戶端不再發(fā)送 SYN 包。實(shí)驗(yàn)二:TCP 第二次握手 SYN、ACK 丟包
為了模擬客戶端收不到服務(wù)端第二次握手 SYN、ACK 包,我的做法是在客戶端加上防火墻限制,直接粗暴的把來自服務(wù)端的數(shù)據(jù)都丟棄,防火墻的配置如下:date
返回的時(shí)間前后,可以算出大概 1 分鐘后,curl 報(bào)錯(cuò)退出了??蛻舳嗽谶@其間抓取的數(shù)據(jù)包,用 Wireshark 打開分析,顯示的時(shí)序圖如下:- 客戶端發(fā)起 SYN 后,由于防火墻屏蔽了服務(wù)端的所有數(shù)據(jù)包,所以 curl 是無法收到服務(wù)端的 SYN、ACK 包,當(dāng)發(fā)生超時(shí)后,就會(huì)重傳 SYN 包
- 服務(wù)端收到客戶的 SYN 包后,就會(huì)回 SYN、ACK 包,但是客戶端一直沒有回 ACK,服務(wù)端在超時(shí)后,重傳了 SYN、ACK 包,接著一會(huì),客戶端超時(shí)重傳的 SYN 包又抵達(dá)了服務(wù)端,服務(wù)端收到后,然后回了 SYN、ACK 包,但是SYN、ACK包的重傳定時(shí)器并沒有重置,還持續(xù)在重傳,因?yàn)榈诙挝帐衷跊]收到第三次握手的 ACK 確認(rèn)報(bào)文時(shí),就會(huì)重傳到最大次數(shù)。
- 最后,客戶端 SYN 超時(shí)重傳次數(shù)達(dá)到了 5 次(tcp_syn_retries 默認(rèn)值 5 次),就不再繼續(xù)發(fā)送 SYN 包了。
咦?客戶端設(shè)置了防火墻,屏蔽了服務(wù)端的網(wǎng)絡(luò)包,為什么 tcpdump 還能抓到服務(wù)端的網(wǎng)絡(luò)包?添加 iptables 限制后, tcpdump 是否能抓到包 ,這要看添加的 iptables 限制條件:
- 如果添加的是
INPUT
規(guī)則,則可以抓得到包 - 如果添加的是
OUTPUT
規(guī)則,則抓不到包
- 進(jìn)來的順序 Wire -> NIC -> tcpdump -> netfilter/iptables
- 出去的順序 iptables -> tcpdump -> NIC -> Wire
tcp_syn_retries 是限制 SYN 重傳次數(shù),那第二次握手 SYN、ACK 限制最大重傳次數(shù)是多少?TCP 第二次握手 SYN、ACK 包的最大重傳次數(shù)是通過
tcp_synack_retries
內(nèi)核參數(shù)限制的,其默認(rèn)值如下:$?cat?/proc/sys/net/ipv4/tcp_synack_retries5
是的,TCP 第二次握手 SYN、ACK 包的最大重傳次數(shù)默認(rèn)值是
5
次。為了驗(yàn)證 SYN、ACK 包最大重傳次數(shù)是 5 次,我們繼續(xù)做下實(shí)驗(yàn),我們先把客戶端的 tcp_syn_retries
設(shè)置為 1,表示客戶端 SYN 最大超時(shí)次數(shù)是 1 次,目的是為了防止多次重傳 SYN,把服務(wù)端 SYN、ACK 超時(shí)定時(shí)器重置。接著,還是如上面的步驟:- 客戶端配置防火墻屏蔽服務(wù)端的數(shù)據(jù)包
- 客戶端 tcpdump 抓取 curl 執(zhí)行時(shí)的數(shù)據(jù)包
- 客戶端的 SYN 只超時(shí)重傳了 1 次,因?yàn)?
tcp_syn_retries
值為 1 - 服務(wù)端應(yīng)答了客戶端超時(shí)重傳的 SYN 包后,由于一直收不到客戶端的 ACK 包,所以服務(wù)端一直在超時(shí)重傳 SYN、ACK 包,每次的 RTO 也是指數(shù)上漲的,一共超時(shí)重傳了 5 次,因?yàn)?
tcp_synack_retries
值為 5
tcp_syn_retries
依然設(shè)置為 1:$?echo?2?>?/proc/sys/net/ipv4/tcp_synack_retries$?echo?1?>?/proc/sys/net/ipv4/tcp_syn_retries
依然保持一樣的實(shí)驗(yàn)步驟進(jìn)行操作,接著把抓取的數(shù)據(jù)包,用 Wireshark 打開分析,顯示的時(shí)序圖如下:
- 客戶端的 SYN 包只超時(shí)重傳了 1 次,符合 tcp_syn_retries 設(shè)置的值;
- 服務(wù)端的 SYN、ACK 超時(shí)重傳了 2 次,符合 tcp_synack_retries 設(shè)置的值
實(shí)驗(yàn)二的實(shí)驗(yàn)小結(jié)通過實(shí)驗(yàn)二的實(shí)驗(yàn)結(jié)果,我們可以得知,當(dāng) TCP 第二次握手 SYN、ACK 包丟了后,客戶端 SYN 包會(huì)發(fā)生超時(shí)重傳,服務(wù)端 SYN、ACK 也會(huì)發(fā)生超時(shí)重傳??蛻舳?SYN 包超時(shí)重傳的最大次數(shù),是由 tcp_syn_retries 決定的,默認(rèn)值是 5 次;服務(wù)端 SYN、ACK 包時(shí)重傳的最大次數(shù),是由 tcp_synack_retries 決定的,默認(rèn)值是 5 次。
實(shí)驗(yàn)三:TCP 第三次握手 ACK 丟包
為了模擬 TCP 第三次握手 ACK 包丟,我的實(shí)驗(yàn)方法是在服務(wù)端配置防火墻,屏蔽客戶端 TCP 報(bào)文中標(biāo)志位是 ACK 的包,也就是當(dāng)服務(wù)端收到客戶端的 TCP ACK 的報(bào)文時(shí)就會(huì)丟棄,iptables 配置命令如下:SYN_RECV
狀態(tài):ESTABLISHED
狀態(tài):ESTABLISHED
狀態(tài):- 為什么服務(wù)端原本處于
SYN_RECV
狀態(tài)的連接,過 1 分鐘后就消失了? - 為什么客戶端 telnet 輸入 123456 字符后,過了好長(zhǎng)一段時(shí)間,telnet 才斷開連接?
- 客戶端發(fā)送 SYN 包給服務(wù)端,服務(wù)端收到后,回了個(gè) SYN、ACK 包給客戶端,此時(shí)服務(wù)端的 TCP 連接處于
SYN_RECV
狀態(tài); - 客戶端收到服務(wù)端的 ?SYN、ACK 包后,給服務(wù)端回了個(gè) ACK 包,此時(shí)客戶端的 TCP 連接處于
ESTABLISHED
狀態(tài); - 由于服務(wù)端配置了防火墻,屏蔽了客戶端的 ACK 包,所以服務(wù)端一直處于
SYN_RECV
狀態(tài),沒有進(jìn)入 ?ESTABLISHED
狀態(tài),tcpdump 之所以能抓到客戶端的 ACK 包,是因?yàn)閿?shù)據(jù)包進(jìn)入系統(tǒng)的順序是先進(jìn)入 tcpudmp,后經(jīng)過 iptables; - 接著,服務(wù)端超時(shí)重傳了 SYN、ACK 包,重傳了 5 次后,也就是超過 tcp_synack_retries 的值(默認(rèn)值是 5),然后就沒有繼續(xù)重傳了,此時(shí)服務(wù)端的 TCP 連接主動(dòng)中止了,所以剛才處于 SYN_RECV 狀態(tài)的 TCP 連接斷開了,而客戶端依然處于
ESTABLISHED
狀態(tài); - 雖然服務(wù)端 TCP 斷開了,但過了一段時(shí)間,發(fā)現(xiàn)客戶端依然處于
ESTABLISHED
狀態(tài),于是就在客戶端的 telnet 會(huì)話輸入了 123456 字符; - 此時(shí)由于服務(wù)端已經(jīng)斷開連接,客戶端發(fā)送的數(shù)據(jù)報(bào)文,一直在超時(shí)重傳,每一次重傳,RTO 的值是指數(shù)增長(zhǎng)的,所以持續(xù)了好長(zhǎng)一段時(shí)間,客戶端的 telnet 才報(bào)錯(cuò)退出了,此時(shí)共重傳了 15 次。
- 服務(wù)端在重傳 SYN、ACK 包時(shí),超過了最大重傳次數(shù)
tcp_synack_retries
,于是服務(wù)端的 TCP 連接主動(dòng)斷開了。 - 客戶端向服務(wù)端發(fā)送數(shù)據(jù)包時(shí),由于服務(wù)端的 TCP 連接已經(jīng)退出了,所以數(shù)據(jù)包一直在超時(shí)重傳,共重傳了 15 次, telnet 就斷開了連接。
TCP 第一次握手的 SYN 包超時(shí)重傳最大次數(shù)是由 tcp_syn_retries 指定,TCP 第二次握手的 SYN、ACK 包超時(shí)重傳最大次數(shù)是由 tcp_synack_retries 指定,那 TCP 建立連接后的數(shù)據(jù)包最大超時(shí)重傳次數(shù)是由什么參數(shù)指定呢?TCP 建立連接后的數(shù)據(jù)包傳輸,最大超時(shí)重傳次數(shù)是由
tcp_retries2
指定,默認(rèn)值是 15 次,如下:$?cat?/proc/sys/net/ipv4/tcp_retries215
如果 15 次重傳都做完了,TCP 就會(huì)告訴應(yīng)用層說:“搞不定了,包怎么都傳不過去!”
那如果客戶端不發(fā)送數(shù)據(jù),什么時(shí)候才會(huì)斷開處于 ESTABLISHED 狀態(tài)的連接?這里就需要提到 TCP 的 ?;顧C(jī)制。這個(gè)機(jī)制的原理是這樣的:定義一個(gè)時(shí)間段,在這個(gè)時(shí)間段內(nèi),如果沒有任何連接相關(guān)的活動(dòng),TCP 保活機(jī)制會(huì)開始作用,每隔一個(gè)時(shí)間間隔,發(fā)送一個(gè)「探測(cè)報(bào)文」,該探測(cè)報(bào)文包含的數(shù)據(jù)非常少,如果連續(xù)幾個(gè)探測(cè)報(bào)文都沒有得到響應(yīng),則認(rèn)為當(dāng)前的 TCP 連接已經(jīng)死亡,系統(tǒng)內(nèi)核將錯(cuò)誤信息通知給上層應(yīng)用程序。在 Linux 內(nèi)核可以有對(duì)應(yīng)的參數(shù)可以設(shè)置?;顣r(shí)間、?;钐綔y(cè)的次數(shù)、?;钐綔y(cè)的時(shí)間間隔,以下都為默認(rèn)值:net.ipv4.tcp_keepalive_time=7200
net.ipv4.tcp_keepalive_intvl=75??
net.ipv4.tcp_keepalive_probes=9
- tcp_keepalive_time=7200:表示?;顣r(shí)間是 7200 秒(2小時(shí)),也就 2 小時(shí)內(nèi)如果沒有任何連接相關(guān)的活動(dòng),則會(huì)啟動(dòng)?;顧C(jī)制
- tcp_keepalive_intvl=75:表示每次檢測(cè)間隔 75 秒;
- tcp_keepalive_probes=9:表示檢測(cè) 9 次無響應(yīng),認(rèn)為對(duì)方是不可達(dá)的,從而中斷本次的連接。
實(shí)驗(yàn)三的實(shí)驗(yàn)小結(jié)在建立 TCP 連接時(shí),如果第三次握手的 ACK,服務(wù)端無法收到,則服務(wù)端就會(huì)短暫處于
SYN_RECV
狀態(tài),而客戶端會(huì)處于 ESTABLISHED
狀態(tài)。由于服務(wù)端一直收不到 TCP 第三次握手的 ACK,則會(huì)一直重傳 SYN、ACK 包,直到重傳次數(shù)超過 tcp_synack_retries
值(默認(rèn)值 5 次)后,服務(wù)端就會(huì)斷開 TCP 連接。而客戶端則會(huì)有兩種情況:- 如果客戶端沒發(fā)送數(shù)據(jù)包,一直處于
ESTABLISHED
狀態(tài),然后經(jīng)過 2 小時(shí) 11 分 15 秒才可以發(fā)現(xiàn)一個(gè)「死亡」連接,于是客戶端連接就會(huì)斷開連接。 - 如果客戶端發(fā)送了數(shù)據(jù)包,一直沒有收到服務(wù)端對(duì)該數(shù)據(jù)包的確認(rèn)報(bào)文,則會(huì)一直重傳該數(shù)據(jù)包,直到重傳次數(shù)超過
tcp_retries2
值(默認(rèn)值 15 次)后,客戶端就會(huì)斷開 TCP 連接。