日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 人工智能 > ChatGpt >内容正文

ChatGpt

TCP的FIN_WAIT1状态理解|深入理解TCP

發(fā)布時(shí)間:2024/4/11 ChatGpt 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 TCP的FIN_WAIT1状态理解|深入理解TCP 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.


原文鏈接:

https://blog.csdn.net/dog250/article/details/81697403

近期遇到一個(gè)問題,簡(jiǎn)單點(diǎn)說,主機(jī)A上顯示一條ESTABLISHED狀態(tài)的TCP連接到主機(jī)B,而主機(jī)B上卻沒有任何關(guān)于主機(jī)A的連接信息,經(jīng)查明,這是由于主機(jī)A和主機(jī)B的發(fā)送/接收緩沖區(qū)差異巨大,導(dǎo)致主機(jī)B進(jìn)程退出后,主機(jī)A暫時(shí)憋住,主機(jī)B頻繁發(fā)送零窗口探測(cè),FIN_WAIT1狀態(tài)超時(shí),進(jìn)而連接被銷毀,然而主機(jī)A并不知情導(dǎo)致。

正好昨天也有人咨詢另外一個(gè)類似的問題,那么就抽昨晚和今天早上的時(shí)間,寫一篇總結(jié)吧。


TCP處處是坑!

不要覺得你對(duì)TCP的實(shí)現(xiàn)的代碼爛熟于心了就能把控它的所有行為!不知道你有沒有發(fā)現(xiàn),目前市面上新上市的關(guān)于Linux內(nèi)核協(xié)議棧的書可謂是汗牛充棟,然而無論作者是國內(nèi)的還是國外,幾乎都是碰到TCP就草草略過,反而對(duì)IP,ARP,DNS這些大書特書,Why?因?yàn)長(zhǎng)inux內(nèi)核里TCP的代碼太亂太復(fù)雜了,很少有人能看明白80%以上的,即便真的有看過的,其中還包括只懂代碼而不懂網(wǎng)絡(luò)技術(shù)的,我就發(fā)現(xiàn)很多聲稱自己精通Linux內(nèi)核TCP/IP源碼,結(jié)果竟然不知道什么是默認(rèn)路由…

所以我打算寫一篇文章,趁著這個(gè)FIN_WAIT1問題,順便表達(dá)一下我是如何學(xué)習(xí)網(wǎng)絡(luò)技術(shù),我是如何解決網(wǎng)絡(luò)問題的方法論觀點(diǎn),都是形而上,個(gè)人看法:

  • 設(shè)計(jì)覆蓋全面的復(fù)現(xiàn)實(shí)驗(yàn)

  • 通讀協(xié)議標(biāo)準(zhǔn)文檔,理解實(shí)現(xiàn)建議

  • 再次實(shí)驗(yàn),預(yù)測(cè)并確認(rèn)問題以外的現(xiàn)象

  • 核對(duì)代碼實(shí)現(xiàn),跟蹤代碼的Changelog

  • 寫一個(gè)自己的實(shí)現(xiàn)或者亂改代碼

本文聊聊TCP的FIN_WAIT1以及TCP假連接(死連接)問題。先看FIN_WAIT1


首先還是從狀態(tài)機(jī)入手,看看和FIN_WAIT1相關(guān)的狀態(tài)機(jī)轉(zhuǎn)換圖:

我們只考慮常規(guī)的從ESTABLISHED狀態(tài)的轉(zhuǎn)換,很簡(jiǎn)單的一個(gè)單一狀態(tài)轉(zhuǎn)換:

  • ESTAB狀態(tài)發(fā)送FIN即切換到FIN_WAIT1狀態(tài);

  • FIN_WAIT1狀態(tài)下收到針對(duì)FIN的ACK即可離開FIN_WAIT1到達(dá)FIN_WAIT2.

看一下和上述狀態(tài)機(jī)轉(zhuǎn)換相關(guān)的簡(jiǎn)單時(shí)序圖:

從狀態(tài)圖和時(shí)序圖上,我們很明確地可以看到,FIN_WAIT1持續(xù)1個(gè)RTT左右的時(shí)間!這個(gè)時(shí)間段幾乎不會(huì)被肉眼觀察到,轉(zhuǎn)瞬而即逝。

然而,這是真的嗎?

我們之所以得到FIN_WAIT1持續(xù)1個(gè)RTT這個(gè)結(jié)論,基于兩個(gè)假設(shè),即:

TCP的對(duì)端是一個(gè)正常的TCP端;

兩端TCP之間的鏈路是正常的,可達(dá)的。

OK,接下來我們來設(shè)計(jì)一個(gè)實(shí)驗(yàn)?zāi)M異常的情況。準(zhǔn)備實(shí)驗(yàn)拓?fù)淙缦?#xff1a;

host1和host2的系統(tǒng)內(nèi)核版本(uname -r獲取):

3.10.0-862.2.3.el7.x86_64

首先,我們看一下如果對(duì)端TCP針對(duì)FIN發(fā)送的ACK丟失,會(huì)發(fā)生什么。按照上述的時(shí)序圖,正常應(yīng)該是FIN_WAIT1將會(huì)永久持續(xù)。我們來驗(yàn)證一下。

實(shí)驗(yàn)1:模擬ACK丟失

在host1上做以下命令:

nc -l -p 1234

host2上完成以下命令:

cat /dev/zero|nc 1.1.1.1 1234

以上保證了host1和host2之間的TCP建立并且連接之間有持續(xù)的數(shù)據(jù)傳輸。接下來,在host2上執(zhí)行下列動(dòng)作:

iptables -A INPUT -p tcp --tcp-flags ACK,FIN ACK killall nc

此時(shí)在host2上:

[root@localhost ~]# netstat -antp|grep 1234 tcp 0 1229 1.1.1.2:39318 1.1.1.1:1234 FIN_WAIT1 -

連續(xù)上翻命令,這個(gè)FIN_WAIT1均不會(huì)消失,暫時(shí)符合我們的預(yù)期…出去抽根煙,刷會(huì)兒微博…回來后,發(fā)現(xiàn)這個(gè)FIN_WAIT1消失了!

它是如何消失的呢?這個(gè)時(shí)候,我們提取netstat數(shù)據(jù),執(zhí)行“ netstat -st”,會(huì)發(fā)現(xiàn):

TcpExt: ...1 connections aborted due to timeout

多了一條timeout連接!


我這里直接說答案吧。

雖然說在協(xié)議上規(guī)范上看,TCP沒有必要為鏈路或者說對(duì)端的不合常規(guī)的行為而買單,但是從現(xiàn)實(shí)角度,TCP的實(shí)現(xiàn)必須處理異常情況,TCP的實(shí)現(xiàn)必然要有所限制!

我們知道,計(jì)算機(jī)是無法處理無限,無窮這種抽線的數(shù)學(xué)概念的,所有如果針對(duì)FIN的ACK遲遲不來,那么必然要有一個(gè)等待的極限,這個(gè)極限在Linux內(nèi)核協(xié)議棧中由以下參數(shù)控制:

net.ipv4.tcp_orphan_retries # 默認(rèn)值是0!這里有坑...

這個(gè)參數(shù)表示如果一直都收不到針對(duì)FIN的ACK,那么在徹底銷毀這個(gè)FIN_WAIT1的連接前,等待幾輪RTO退避

所謂的orphan tcp connection,意思就是說,在Linux進(jìn)程層面,創(chuàng)建該連接的進(jìn)程已經(jīng)退出銷毀了,然而在TCP協(xié)議層面,它依然在遵循TCP狀態(tài)機(jī)的轉(zhuǎn)換規(guī)則存在著。

注意,這個(gè)參數(shù)不是一個(gè)時(shí)間量,而是一個(gè)次數(shù)量。我們知道,TCP每一次超時(shí),都會(huì)對(duì)下一次超時(shí)時(shí)間進(jìn)行指數(shù)退避,這里的次數(shù)量就是要經(jīng)過幾次退避的時(shí)間。舉一個(gè)例子,如果RTO是2ms,而tcp_orphan_retries 的值是4,那么所計(jì)算出的FIN_WAIT1容忍時(shí)間就是:

T=21+22+23+24T=21+22+23+24

還是看看Linux內(nèi)核文檔怎么說的吧:

tcp_orphan_retries - INTEGERThis value influences the timeout of a locally closed TCP connec tion, when RTO retransmissions remain unacknowledged.See tcp_retries2 for more details.The default value is 8.If your machine is a loaded WEB server,you should think about lowering this value, such socketsmay consume significant resources. Cf. tcp_max_orphans.

讓我們看看tcp_retries2,以獲取數(shù)值的含義:

tcp_retries2 - INTEGERThis value influences the timeout of an alive TCP connection,when RTO retransmissions remain unacknowledged.Given a value of N, a hypothetical TCP connection followingexponential backoff with an initial RTO of TCP_RTO_MIN wouldretransmit N times before killing the connection at the (N+1)th RTO.The default value of 15 yields a hypothetical timeout of 924.6seconds and is a lower bound for the effective timeout.TCP will effectively time out at the first RTO which exceeds the hypothetical timeout.RFC 1122 recommends at least 100 seconds for the timeout,which corresponds to a value of at least 8.

雖然說文檔上默認(rèn)值的建議是8,但是大多數(shù)的Linux發(fā)行版上其默認(rèn)值都是0。更多詳情,就自己看RFC和Linux源碼吧。

有了這個(gè)參數(shù)保底,我們知道,即便是ACK永遠(yuǎn)不來,FIN_WAIT1狀態(tài)也不會(huì)一直持續(xù)下去的,這有效避免了有針對(duì)性截獲ACK或者不發(fā)送ACK而導(dǎo)致的DDoS,退一萬步講,即便是沒有DDoS,這種做法也具有資源利用率的容錯(cuò)性,使得資源使用更加高效。

實(shí)驗(yàn)1的結(jié)論如下:

  • 如果主動(dòng)斷開端調(diào)用了close關(guān)掉了進(jìn)程,它會(huì)進(jìn)入FIN_WAIT1狀態(tài),此時(shí)如果它再也收不到ACK,無論是針對(duì)pending在發(fā)送緩沖的數(shù)據(jù)還是FIN,它都會(huì)嘗試重新發(fā)送,在收到ACK前會(huì)嘗試N次退避,該N由tcp_orphan_retries參數(shù)控制。

接下來,我們來看一個(gè)更加復(fù)雜一點(diǎn)的問題,還是先從實(shí)驗(yàn)說起。

實(shí)驗(yàn)2:模擬對(duì)端TCP不收數(shù)據(jù),接收窗口憋死

在host1上做以下命令:

# 模擬小接收緩存,使得憋住接收窗口更加容易

sysctl -w net.ipv4.tcp_rmem="16 32 32" nc?-l?-p?1234

host2上完成以下命令:

cat /dev/zero|nc 1.1.1.1 1234 sleep 5 # 稍微等一下 killall nc

此時(shí),我們發(fā)現(xiàn)host2的TCP連接進(jìn)入了FIN_WAIT1狀態(tài)。然而抓包看的話,數(shù)據(jù)傳輸依然在進(jìn)行:

05:15:51.674630 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [P.], seq 305:321, ack 1, win 5840, options [nop,nop,TS val 1210945 ecr 238593370], length 16 05:15:51.674690 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 321, win 0, options [nop,nop,TS val 238593471 ecr 1210945], length 0 05:15:51.674759 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 321, win 16, options [nop,nop,TS val 238593471 ecr 1210945], length 0 05:15:51.777774 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [P.], seq 321:325, ack 1, win 5840, options [nop,nop,TS val 1211048 ecr 238593471], length 4 05:15:51.777874 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 325, win 16, options [nop,nop,TS val 238593497 ecr 1211048], length 0 05:15:52.182918 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [P.], seq 325:341, ack 1, win 5840, options [nop,nop,TS val 1211453 ecr 238593497], length 16 05:15:52.182970 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 341, win 0, options [nop,nop,TS val 238593599 ecr 1211453], length 0 05:15:52.183055 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 341, win 16, options [nop,nop,TS val 238593599 ecr 1211453], length 0 05:15:52.592759 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [P.], seq 341:357, ack 1, win 5840, options [nop,nop,TS val 1211863 ecr 238593599], length 16 05:15:52.592813 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 357, win 0, options [nop,nop,TS val 238593701 ecr 1211863], length 0 05:15:52.592871 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 357, win 16, options [nop,nop,TS val 238593701 ecr 1211863], length 0 05:15:52.695160 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [P.], seq 357:361, ack 1, win 5840, options [nop,nop,TS val 1211965 ecr 238593701], length 4 05:15:52.695276 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 361, win 16, options [nop,nop,TS val 238593727 ecr 1211965], length 0 05:15:53.099612 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [P.], seq 361:377, ack 1, win 5840, options [nop,nop,TS val 1212370 ecr 238593727], length 16 05:15:53.099641 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 377, win 0, options [nop,nop,TS val 238593828 ecr 1212370], length 0 05:15:53.099671 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 377, win 16, options [nop,nop,TS val 238593828 ecr 1212370], length 0 05:15:53.505028 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [P.], seq 377:393, ack 1, win 5840, options [nop,nop,TS val 1212775 ecr 238593828], length 16 05:15:53.505081 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 393, win 0, options [nop,nop,TS val 238593929 ecr 1212775], length 0 05:15:53.505138 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 393, win 16, options [nop,nop,TS val 238593929 ecr 1212775], length 0 05:15:53.605923 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [P.], seq 393:397, ack 1, win 5840, options [nop,nop,TS val 1212876 ecr 238593929], length 4

這是顯然的,這是因?yàn)槭瞻l(fā)兩端巨大的緩存大小差異造成的,即便是host2發(fā)送端進(jìn)程退出了,在退出前已經(jīng)有大量數(shù)據(jù)pending到了TCP的發(fā)送緩沖區(qū)里面而脫離已經(jīng)被銷毀的進(jìn)程了,FIN包當(dāng)然是排在了緩沖區(qū)的末尾了。

TCP的狀態(tài)機(jī)運(yùn)行在緩存的上層,即只要把FIN包pending排隊(duì),就切換到了FIN_WAIT1,而不是說實(shí)際發(fā)送了FIN包才切換。

因此,我們可有的等了,數(shù)據(jù)傳輸依然在正常有序進(jìn)行,針對(duì)小包的ACK源源不斷從host1回來,這進(jìn)一步促進(jìn)host2發(fā)送未竟的數(shù)據(jù)包,直到所有緩沖區(qū)的數(shù)據(jù)全部發(fā)送完畢…

不管怎樣,總是有個(gè)頭兒,只要有結(jié)束,就不需要擔(dān)心。我們可以簡(jiǎn)單得出一個(gè)結(jié)論:

  • 如果主動(dòng)斷開端調(diào)用了close關(guān)掉了進(jìn)程,它會(huì)進(jìn)入FIN_WAIT1狀態(tài),如果接收端的接收窗口呈現(xiàn)打開狀態(tài),此時(shí)它的TCP發(fā)送隊(duì)列中的數(shù)據(jù)包還是會(huì)像正常一樣發(fā)往接收端,直到發(fā)送完,最后發(fā)送FIN包,收到FIN包ACK后進(jìn)入FIN_WAIT2。

現(xiàn)在,我們進(jìn)行實(shí)驗(yàn)的下一步,把host1上的接收進(jìn)程nc的接收邏輯徹底憋死。很簡(jiǎn)單,host1上執(zhí)行下面的命令即可:

killall -STOP nc

進(jìn)程并沒有退出,只是暫停了,nc進(jìn)程上下文的recv不再執(zhí)行,然而軟中斷上下文的TCP協(xié)議的處理依然在進(jìn)行。

這個(gè)時(shí)候,抓包就會(huì)發(fā)現(xiàn)只剩下指數(shù)時(shí)間退避的零窗口探測(cè)包了:

# 注意觀察探測(cè)包發(fā)送時(shí)間的間隔 05:15:56.444570 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [.], ack 1, win 5840, options [nop,nop,TS val 1215715 ecr 238594487], length 0 05:15:56.444602 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 465, win 0, options [nop,nop,TS val 238594664 ecr 1214601], length 0 05:15:57.757217 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [.], ack 1, win 5840, options [nop,nop,TS val 1217027 ecr 238594664], length 0 05:15:57.757248 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 465, win 0, options [nop,nop,TS val 238594992 ecr 1214601], length 0 05:16:00.283259 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [.], ack 1, win 5840, options [nop,nop,TS val 1219552 ecr 238594992], length 0 05:16:00.283483 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 465, win 0, options [nop,nop,TS val 238595624 ecr 1214601], length 0 05:16:05.234277 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [.], ack 1, win 5840, options [nop,nop,TS val 1224503 ecr 238595624], length 0 05:16:05.234305 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 465, win 0, options [nop,nop,TS val 238596861 ecr 1214601], length 0 05:16:15.032486 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [.], ack 1, win 5840, options [nop,nop,TS val 1234301 ecr 238596861], length 0 05:16:15.032532 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 465, win 0, options [nop,nop,TS val 238599311 ecr 1214601], length 0 05:16:34.629137 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [.], ack 1, win 5840, options [nop,nop,TS val 1253794 ecr 238599311], length 0 05:16:34.629164 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 465, win 0, options [nop,nop,TS val 238604210 ecr 1214601], length 0 05:17:13.757815 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [.], ack 1, win 5840, options [nop,nop,TS val 1292784 ecr 238604210], length 0 05:17:13.757863?IP?1.1.1.1.1234?>?1.1.1.2.39318:?Flags?[.],?ack?465,?win?0,?options?[nop,nop,TS?val?238613992?ecr?1214601],?length?0

這個(gè)實(shí)驗(yàn)的現(xiàn)象和實(shí)驗(yàn)1的現(xiàn)象,僅有一個(gè)區(qū)別,那就是實(shí)驗(yàn)1是阻塞了ACK,而本實(shí)驗(yàn)則是FIN根本就還沒有發(fā)送出去就進(jìn)入了FIN_WAIT1,且針對(duì)RTO指數(shù)時(shí)間退避發(fā)送的零窗口探測(cè)的ACK持續(xù)到來,簡(jiǎn)單總結(jié)就是:

實(shí)驗(yàn)1沒有ACK到來,實(shí)驗(yàn)2有ACK到來。

在實(shí)驗(yàn)結(jié)果之前,我們來看一段摘錄,來自RFC1122:

https://tools.ietf.org/html/rfc1122#page-92

4.2.2.17 Probing Zero Windows: RFC-793 Section 3.7, page 42 .Probing of zero (offered) windows MUST be supported. .A TCP MAY keep its offered receive window closedindefinitely. As long as the receiving TCP continues tosend acknowledgments in response to the probe segments, thesending TCP MUST allow the connection to stay open.

緊接著后面是一段注解:

DISCUSSION:It is extremely important to remember that ACK(acknowledgment) segments that contain no data are notreliably transmitted by TCP. If zero window probing isnot supported, a connection may hang forever when anACK segment that re-opens the window is lost. .The delay in opening a zero window generally occurswhen the receiving application stops taking data fromits TCP. For example, consider a printer daemonapplication, stopped because the printer ran out ofpaper.

只要有ACK到來,連接就要保持,這會(huì)帶來什么問題呢?確實(shí)會(huì)帶來問題,但是在正視這些問題之前,Linux內(nèi)核協(xié)議棧的實(shí)現(xiàn)者,也保持了緘默,我們來看一段實(shí)驗(yàn)主機(jī)host1和host2所用的標(biāo)準(zhǔn)內(nèi)核主線版本3.10的內(nèi)核源碼,來自tcp_probe_timer函數(shù)內(nèi)部的注釋以及一小段代碼:

/* *WARNING* RFC 1122 forbids this** It doesn't AFAIK, because we kill the retransmit timer -AK** FIXME: We ought not to do it, Solaris 2.5 actually has fixing* this behaviour in Solaris down as a bug fix. [AC]** Let me to explain. icsk_probes_out is zeroed by incoming ACKs* even if they advertise zero window. Hence, connection is killed only* if we received no ACKs for normal connection timeout. It is not killed* only because window stays zero for some time, window may be zero* until armageddon and even later. We are in full accordance* with RFCs, only probe timer combines both retransmission timeout* and probe timeout in one bottle. --ANK*/...max_probes = sysctl_tcp_retries2;if (sock_flag(sk, SOCK_DEAD)) { // 如果是orphan連接的話 const int alive = ((icsk->icsk_rto << icsk->icsk_backoff) < TCP_RTO_MAX); // 即獲取tcp_orphan_retries參數(shù),有微調(diào),請(qǐng)?jiān)攲彙1緦?shí)驗(yàn)參數(shù)默認(rèn)值取0!max_probes = tcp_orphan_retries(sk, alive);if (tcp_out_of_resources(sk, alive || icsk->icsk_probes_out <= max_probes)) return;} // 只有在icsk_probes_out,即未應(yīng)答的probe次數(shù)超過探測(cè)最大容忍次數(shù)后,才會(huì)出錯(cuò)清理連接。 if (icsk->icsk_probes_out > max_probes) {tcp_write_err(sk);} else { /* Only send another probe if we didn't close things up. */tcp_send_probe0(sk);}

是的,從上面那一段注釋,我們看出了抱怨,一個(gè)FIN_WAIT1的連接可能會(huì)等到世界終結(jié)日之后,然而我們卻只能“in full accordance with RFCs”!

這也許暗示了某種魔咒般的結(jié)果,即FIN_WAIT1將會(huì)一直持續(xù)到終結(jié)世界的大決戰(zhàn)之日。然而非也,你會(huì)發(fā)現(xiàn)大概在發(fā)送了9個(gè)零窗口探測(cè)包之后,連接就消失了。netstat -st的結(jié)果中,呈現(xiàn):

connections aborted due to timeout

看來想制造點(diǎn)事端,并非想象般容易!

如上所述,我展示了標(biāo)準(zhǔn)主線的Linux 3.10內(nèi)核的tcp_probe_timer函數(shù),現(xiàn)在的問題是,為什么下面的條件被滿足了呢?

if (icsk->icsk_probes_out > max_probes)

只有當(dāng)這個(gè)條件被滿足,tcp_write_err才會(huì)被調(diào)用,進(jìn)而:

tcp_done(sk); // 遞增計(jì)數(shù),即netstat -st中的那條“1 connections aborted due to timeout” NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPABORTONTIMEOUT);

按照注釋和代碼的確認(rèn),只要收到ACK,icsk_probes_out 字段就將被清零,這是很明確的啊,我們?cè)趖cp_ack函數(shù)中便可看到無條件清零icsk_probes_out的動(dòng)作:

static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag) {...sk->sk_err_soft = 0;icsk->icsk_probes_out = 0;tp->rcv_tstamp = tcp_time_stamp;... }

從代碼上看,只要零窗口探測(cè)持續(xù)發(fā)送,不管退避到多久(最大TCP_RTO_MAX),只要對(duì)端會(huì)有ACK回來,icsk_probes_out 就會(huì)被清零,上述的條件就不會(huì)被滿足,連接就會(huì)一直在FIN_WAIT1狀態(tài),而從我們抓包看,確實(shí)是零窗口探測(cè)有去必有回的!

預(yù)期會(huì)永遠(yuǎn)僵在FIN_WAIT1狀態(tài)的連接在一段時(shí)間后竟然銷毀了。沒有符合預(yù)期,到底發(fā)生了呢?

如果我們看高版本4.14版的Linux內(nèi)核,同樣是tcp_probe_timer函數(shù),我們會(huì)看到一些不一樣的代碼和注釋:

static?void?tcp_probe_timer(struct?sock?*sk) {.../* RFC 1122 4.2.2.17 requires the sender to stay open indefinitely as* long as the receiver continues to respond probes. We support this by* default and reset icsk_probes_out with incoming ACKs. But if the* socket is orphaned or the user specifies TCP_USER_TIMEOUT, we* kill the socket when the retry count and the time exceeds the* corresponding system limit. We also implement similar policy when* we use RTO to probe window in tcp_retransmit_timer().*/start_ts = tcp_skb_timestamp(tcp_send_head(sk));if (!start_ts)tcp_send_head(sk)->skb_mstamp = tp->tcp_mstamp;else if (icsk->icsk_user_timeout &&(s32)(tcp_time_stamp(tp) - start_ts) >jiffies_to_msecs(icsk->icsk_user_timeout))goto abort; max_probes = sock_net(sk)->ipv4.sysctl_tcp_retries2;if (sock_flag(sk, SOCK_DEAD)) {const bool alive = inet_csk_rto_backoff(icsk, TCP_RTO_MAX) < TCP_RTO_MAX;max_probes = tcp_orphan_retries(sk, alive);// 如果處在FIN_WAIT1的連接持續(xù)時(shí)間超過了TCP_RTO_MAX(這是前提)// 如果退避發(fā)送探測(cè)的次數(shù)已經(jīng)超過了配置參數(shù)指定的次數(shù)(這是附加條件)if (!alive && icsk->icsk_backoff >= max_probes)goto abort; // 注意這個(gè)goto!直接銷毀連接。if (tcp_out_of_resources(sk, true))return;}if (icsk->icsk_probes_out > max_probes) { abort: tcp_write_err(sk);} else {/* Only send another probe if we didn't close things up. */tcp_send_probe0(sk);} }

我們來看這段代碼的注釋,RFC1122的要求:

RFC 1122 4.2.2.17 requires the sender to stay open indefinitely as long as the receiver continues to respond probes. We support this by default and reset icsk_probes_out with incoming ACKs.

然后我們接著看這段注釋,有一個(gè)But轉(zhuǎn)折:

But if the socket is orphaned or the user specifies TCP_USER_TIMEOUT, we kill the socket when the retry count and the time exceeds the corresponding system limit.

看起來,這段注釋是符合我們實(shí)驗(yàn)的結(jié)論的!然而我們實(shí)驗(yàn)的是3.10內(nèi)核,而這個(gè)卻是4.X的內(nèi)核啊!即Linux在高版本內(nèi)核上確實(shí)進(jìn)行了優(yōu)化,這是針對(duì)資源利用的優(yōu)化,并且避免了有針對(duì)性的DDoS。

答案揭曉了。

*我們實(shí)驗(yàn)所使用的內(nèi)核版本不是社區(qū)主線版本,而是Redhat的版本!***Redhat顯然會(huì)事先回移上游的patch,我們來確認(rèn)一下我們所所用的實(shí)驗(yàn)版本3.10.0-862.2.3.el7.x86_64的tcp_probe_timer的源碼。

為此,我們到下面的地址去下載Redhat(Centos…)專門的源碼,我們看看它和社區(qū)同版本源碼是不是在關(guān)于probe處理上有所不同:

http://vault.centos.org/7.5.1804/updates/Source/SPackages/

使用下面的命令解壓:

rpm2cpio ../kernel-3.10.0-862.2.3.el7.src.rpm | cpio -idmv xz linux-3.10.0-862.2.3.el7.tar.xz -d tar xvf linux-3.10.0-862.2.3.el7.tar

查看net/ipv4/tcp_timer.c文件,找到tcp_probe_timer函數(shù):

看來是Redhat移植了4.X的patch,導(dǎo)致了源碼的邏輯和社區(qū)版本的出現(xiàn)差異,這也就解釋了實(shí)驗(yàn)現(xiàn)象!


那么這個(gè)針對(duì)orphan connection的patch最初是來自何方呢?我們不得不去patchwork去溯源,以便得到更深入的Why。

在maillist,我找到了下面的鏈接:

http://lists.openwall.net/netdev/2014/09/23/8

Date: Mon, 22 Sep 2014 20:52:13 -0700 From: Yuchung Cheng ycheng@...gle.com To: davem@…emloft.net Cc: edumazet@…gle.com, andrey.dmitrov@…etlabs.ru,ncardwell@…gle.com, netdev@…r.kernel.org,Yuchung Cheng ycheng@...gle.com Subject: [PATCH net-next] tcp: abort orphan sockets stalling on zero window probes

摘錄一段描述吧:

Currently we have two different policies for orphan sockets that repeatedly stall on zero window ACKs. If a socket gets a zero window ACK when it is transmitting data, the RTO is used to probe the window. The socket is aborted after roughly tcp_orphan_retries() retries (as in tcp_write_timeout()). . But if the socket was idle when it received the zero window ACK, and later wants to send more data, we use the probe timer to probe the window. If the receiver always returns zero window ACKs, icsk_probes keeps getting reset in tcp_ack() and the orphan socket can stall forever until the system reaches the orphan limit (as commented in tcp_probe_timer()). This opens up a simple attack to create lots of hanging orphan sockets to burn the memory and the CPU, as demonstrated in the recent netdev post “TCP connection will hang in FIN_WAIT1 after closing if zero window is advertised.” http://www.spinics.net/lists/netdev/msg296539.html

該鏈接最后面給出了patch:

... + max_probes = sysctl_tcp_retries2; if (sock_flag(sk, SOCK_DEAD)) { const int alive = inet_csk_rto_backoff(icsk, TCP_RTO_MAX) < TCP_RTO_MAX;max_probes = tcp_orphan_retries(sk, alive); - + if (!alive && icsk->icsk_backoff >= max_probes) + goto abort; if (tcp_out_of_resources(sk, alive || icsk->icsk_probes_out <= max_probes)) return;} if (icsk->icsk_probes_out > max_probes) { - tcp_write_err(sk); +abort: tcp_write_err(sk);} else { ...

簡(jiǎn)單說一下這個(gè)patch的意義。

在實(shí)驗(yàn)2中,我用kill -STOP信號(hào)故意憋死了nc接收進(jìn)程,以重現(xiàn)現(xiàn)象,然而事實(shí)上在現(xiàn)實(shí)中,存在下面兩種不太友善情況:

  • 接收端進(jìn)程出現(xiàn)異常,或者接收端內(nèi)核存在缺陷,導(dǎo)致進(jìn)程掛死而軟中斷上下文的協(xié)議棧處理正常運(yùn)行;

  • 接收端就是一個(gè)惡意的DDoS進(jìn)程,故意不接收數(shù)據(jù)以誘導(dǎo)發(fā)送端在FIN_WAIT2狀態(tài)(甚至ESTAB狀態(tài))發(fā)送數(shù)據(jù)不成后發(fā)送零窗口探測(cè)而不休止。

無論哪種情況,最主動(dòng)斷開的發(fā)送端來講,其后果都是消耗大量的資源,而orphan連接則占著茅坑不拉屎。這比較悲哀。

現(xiàn)在給出本文的第三個(gè)結(jié)論:

  • 如果主動(dòng)斷開端調(diào)用了close關(guān)掉了進(jìn)程,它會(huì)進(jìn)入FIN_WAIT1狀態(tài),如果接收端的接收窗口呈現(xiàn)關(guān)閉狀態(tài)(零窗口),此時(shí)它會(huì)不斷發(fā)送零窗口探測(cè)包。發(fā)送多少次呢?有兩種實(shí)現(xiàn):

????

低版本內(nèi)核(至少社區(qū)3.10及以下):永久嘗試,如果探測(cè)ACK每次都返回,則沒完沒了。

高版本內(nèi)核(至少社區(qū)4.6及以上):限制嘗試tcp_orphan_retries次,不管是否收到探測(cè)ACK。

當(dāng)然,其實(shí)還有關(guān)于非探測(cè)包的重傳限制,比如關(guān)于TCP_USER_TIMEOUT這個(gè)socket option的限制:

else if (icsk->icsk_user_timeout &&(s32)(tcp_time_stamp(tp) - start_ts) >jiffies_to_msecs(icsk->icsk_user_timeout)) goto?abort;

包括關(guān)于Keepalive的點(diǎn)點(diǎn)滴滴,本文就不多說了。

在此,先有個(gè)必要的總結(jié)。我老是說在學(xué)習(xí)網(wǎng)絡(luò)協(xié)議的時(shí)候讀碼無益并不是說不要去閱讀解析Linux內(nèi)核源碼,而是一定要先有實(shí)驗(yàn)設(shè)計(jì)的能力重現(xiàn)問題,然后再去核對(duì)RFC或者其它的協(xié)議標(biāo)準(zhǔn),最后再去核對(duì)源碼到底是怎么實(shí)現(xiàn)的,這樣才能一氣呵成。否則將有可能陷入深淵。

以本文為例,我假設(shè)你手頭有3.10的源碼,當(dāng)你面對(duì)“FIN_WAIT1狀態(tài)的TCP連接在持續(xù)退避的零窗口探測(cè)期間并不會(huì)如預(yù)期那般永久持續(xù)下去”這個(gè)問題的時(shí)候,你讀源碼是沒有任何用的,因?yàn)檫@個(gè)時(shí)候你只能靜靜地看著那些代碼,然后糾結(jié)自己是不是哪里理解錯(cuò)了,很多人甚至很難能想到去對(duì)比不同版本的代碼,因?yàn)榘姹咎嗔恕?/p>

源碼只是一種實(shí)現(xiàn)的方式,而已,真正重要的是協(xié)議的標(biāo)準(zhǔn)以及標(biāo)準(zhǔn)是實(shí)現(xiàn)的建議,此外,各個(gè)發(fā)行版廠商完全有自主的權(quán)力對(duì)社區(qū)源碼做任何的定制和重構(gòu),不光是Redhat,即便你去看OpenWRT的代碼,也是一樣,你會(huì)發(fā)現(xiàn)很多不一樣的東西。

我并不贊同幾乎每一個(gè)程序員都擁護(hù)的那種任何情況下源碼至上,the whole world is cheap,show me the code的觀點(diǎn),當(dāng)一個(gè)邏輯流程擺在那里沒有源碼的時(shí)候,當(dāng)然那絕對(duì)是源碼至上,否則就是紙上談兵,邏輯至少要跑起來,而只有源碼編譯后才能跑起來,流程圖和設(shè)計(jì)圖是無法運(yùn)行的,這個(gè)時(shí)候,你需要放棄討論,潛心編碼。然而,當(dāng)一個(gè)網(wǎng)絡(luò)協(xié)議已經(jīng)被以各種方式實(shí)現(xiàn)了而你只是為了排查一個(gè)問題或者確認(rèn)一個(gè)邏輯的時(shí)候,代碼就退居二三線了,這時(shí)候,請(qǐng)“show me the standard!”。

本文原本是想解釋完FIN_WAIT1能持續(xù)多久就結(jié)束的,但是這樣顯得有點(diǎn)遺憾,因?yàn)槲蚁氡疚牡倪@個(gè)FIN_WAIT1的論題可以引出一個(gè)更大的論題,如果不繼續(xù)說一說,那便是不負(fù)責(zé)任的。

是什么的?嗯,是TCP假連接的問題。那么何謂TCP假連接?

所謂的TCP假連接就是TCP的一端已經(jīng)逃逸出了TCP狀態(tài)機(jī),而另一端卻不知道的連接

我們?cè)倏赐昝赖腡CP標(biāo)準(zhǔn)RFC793上的TCP狀態(tài)圖:

除了TIME_WAIT到CLOSED這唯一的出口,你是找不到其它出口的,也就是說,一個(gè)TCP端一旦發(fā)起了建立連接請(qǐng)求,暫不考慮同時(shí)打開同時(shí)關(guān)閉的情況,就一定要到其中一方的TIME_WAIT超時(shí)而結(jié)束。

然而,TCP的缺陷在于,TCP是一個(gè)端到端的協(xié)議,在協(xié)議層面上所有的端到端協(xié)議是需要底層的傳送協(xié)議作為其支撐的,一旦底層永久崩壞,端到端協(xié)議將會(huì)面臨狀態(tài)機(jī)僵住的場(chǎng)景,而狀態(tài)機(jī)僵住意味著對(duì)資源的永久消耗,因?yàn)檫B接再也釋放不掉了!

隨便舉一個(gè)例子,在兩端ESTAB狀態(tài)的時(shí)候,把IP動(dòng)態(tài)路由協(xié)議停掉并把把網(wǎng)線剪斷,那么TCP兩端將永遠(yuǎn)處在ESTAB狀態(tài),直到機(jī)器重啟。為了解決這個(gè)問題,TCP引入了Keepalive機(jī)制,一旦超過一定時(shí)間沒有互通有無,那么就會(huì)主動(dòng)銷毀這個(gè)連接,事實(shí)上,按照純粹的TCP狀態(tài)機(jī)而言,Keepalive機(jī)制是一種對(duì)TCP協(xié)議的污染。

是不是Keepalive就能完全避免假連接,死連接存在了呢?非也,Keepalive只是一種用戶態(tài)按照自己的業(yè)務(wù)邏輯去檢測(cè)并避免假連接的手段,而我們仔細(xì)觀察TCP狀態(tài)機(jī),很多的步驟遠(yuǎn)不是用戶態(tài)進(jìn)程可是touch的,比如本文講的FIN_WAIT1,一旦連接成為orphan的,將沒有任何進(jìn)程與之關(guān)聯(lián),雖然用戶態(tài)設(shè)置的Keepalive也可以繼續(xù)起作用,但萬一用戶態(tài)沒有設(shè)置Keepalive呢??這時(shí)怎么辦?

我們執(zhí)行下面的命令:

[root@localhost ~]# sysctl -a|grep retries net.ipv4.tcp_orphan_retries = 0 net.ipv4.tcp_retries1 = 3 net.ipv4.tcp_retries2 = 15 net.ipv4.tcp_syn_retries = 6 net.ipv4.tcp_synack_retries = 5 net.ipv6.idgen_retries = 3

嗯,這些就是避免TCP協(xié)議本身的狀態(tài)機(jī)轉(zhuǎn)換僵死所引入的控制層Keepalive機(jī)制,詳細(xì)情況就自己去查閱Linux內(nèi)核文檔吧。

在具體實(shí)現(xiàn)上,防止?fàn)顟B(tài)機(jī)僵死的方法分為兩類:

  • ESTABLISHED防止僵死的方法:使用用戶進(jìn)程設(shè)置的Keepalive機(jī)制

  • 非ESTABLISHED防止僵死的方法:使用各種retries內(nèi)核參數(shù)設(shè)置的timeout機(jī)制

- END -


看完一鍵三連在看轉(zhuǎn)發(fā),點(diǎn)贊

是對(duì)文章最大的贊賞,極客重生感謝你

推薦閱讀

從流量控制算法談網(wǎng)絡(luò)優(yōu)化-TCP核心原理理解

TCP協(xié)議疑難雜癥全景解析|硬核

一個(gè)奇葩的網(wǎng)絡(luò)問題

總結(jié)

以上是生活随笔為你收集整理的TCP的FIN_WAIT1状态理解|深入理解TCP的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。