日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

近期遇到一個問題,簡單點說,主機A上顯示一條ESTABLISHED狀態的TCP連接到主機B,而主機B上卻沒有任何關于主機A的連接信息,經查明,這是由于主機A和主機B的發送/接收緩沖區差異巨大,導致主機B進程退出后,主機A暫時憋住,主機B頻繁發送零窗口探測,FIN_WAIT1狀態超時,進而連接被銷毀,然而主機A并不知情導致。

正好也有人咨詢另外一個類似的問題,寫一篇總結吧。


TCP處處是坑!

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

所以寫一篇文章,趁著這個FIN_WAIT1問題,順便表達一下我是如何學習網絡技術,我是如何解決網絡問題的方法論觀點,都是形而上,個人看法:

  • 設計覆蓋全面的復現實驗
  • 通讀協議標準文檔,理解實現建議
  • 再次實驗,預測并確認問題以外的現象
  • 核對代碼實現,跟蹤代碼的Changelog
  • 寫一個自己的實現或者亂改代

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


首先還是從狀態機入手,看看和FIN_WAIT1相關的狀態機轉換圖:

TCP在FIN_WAIT1狀態能持續多久及TCP假連接問題

 

我們只考慮常規的從ESTABLISHED狀態的轉換,很簡單的一個單一狀態轉換:

  • ESTAB狀態發送FIN即切換到FIN_WAIT1狀態;
  • FIN_WAIT1狀態下收到針對FIN的ACK即可離開FIN_WAIT1到達FIN_WAIT2.

看一下和上述狀態機轉換相關的簡單時序圖:

TCP在FIN_WAIT1狀態能持續多久及TCP假連接問題

 

從狀態圖和時序圖上,我們很明確地可以看到,FIN_WAIT1持續1個RTT左右的時間!這個時間段幾乎不會被肉眼觀察到,轉瞬而即逝。

然而,這是真的嗎?

我們之所以得到FIN_WAIT1持續1個RTT這個結論,基于兩個假設,即:

  1. TCP的對端是一個正常的TCP端;
  2. 兩端TCP之間的鏈路是正常的,可達的。

OK,接下來我們來設計一個實驗模擬異常的情況。準備實驗拓撲如下:

TCP在FIN_WAIT1狀態能持續多久及TCP假連接問題

 

host1和host2的系統內核版本(uname -r獲取):

3.10.0-862.2.3.el7.x86_64

首先,我們看一下如果對端TCP針對FIN發送的ACK丟失,會發生什么。按照上述的時序圖,正常應該是FIN_WAIT1將會永久持續。我們來驗證一下。

  • 實驗1:模擬ACK丟失
    在host1上做以下命令:
nc -l -p 1234

host2上完成以下命令:

cat /dev/zero|nc 1.1.1.1 1234

以上保證了host1和host2之間的TCP建立并且連接之間有持續的數據傳輸。接下來,在host2上執行下列動作:

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

此時在host2上:

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

連續上翻命令,這個FIN_WAIT1均不會消失,暫時符合我們的預期…出去抽根煙,刷會兒微博…回來后,發現這個FIN_WAIT1消失了!

它是如何消失的呢?這個時候,我們提取netstat數據,執行“ netstat -st”,會發現:

TcpExt:
...
    1 connections aborted due to timeout

多了一條timeout連接!


我這里直接說答案吧。

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

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

net.ipv4.tcp_orphan_retries # 默認值是0!這里有坑...

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

所謂的orphan tcp connection,意思就是說,在Linux進程層面,創建該連接的進程已經退出銷毀了,然而在TCP協議層面,它依然在遵循TCP狀態機的轉換規則存在著。

注意,這個參數不是一個時間量,而是一個次數量。我們知道,TCP每一次超時,都會對下一次超時時間進行指數退避,這里的次數量就是要經過幾次退避的時間。舉一個例子,如果RTO是2ms,而tcp_orphan_retries 的值是4,那么所計算出的FIN_WAIT1容忍時間就是:
T=21+22+23+24T=21+22+23+24
還是看看Linux內核文檔怎么說的吧:

tcp_orphan_retries - INTEGER
  This value influences the timeout of a locally closed TCP connection, 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 sockets
  may consume significant resources. Cf. tcp_max_orphans.

讓我們看看tcp_retries2,以獲取數值的含義:

tcp_retries2 - INTEGER
  This value influences the timeout of an alive TCP connection,
  when RTO retransmissions remain unacknowledged.
  Given a value of N, a hypothetical TCP connection following
  exponential backoff with an initial RTO of TCP_RTO_MIN would
  retransmit N times before killing the connection at the (N+1)th RTO.
  The default value of 15 yields a hypothetical timeout of 924.6
  seconds 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.

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

有了這個參數保底,我們知道,即便是ACK永遠不來,FIN_WAIT1狀態也不會一直持續下去的,這有效避免了有針對性截獲ACK或者不發送ACK而導致的DDoS,退一萬步講,即便是沒有DDoS,這種做法也具有資源利用率的容錯性,使得資源使用更加高效。

實驗1的結論如下:

  • 如果主動斷開端調用了close關掉了進程,它會進入FIN_WAIT1狀態,此時如果它再也收不到ACK,無論是針對pending在發送緩沖的數據還是FIN,它都會嘗試重新發送,在收到ACK前會嘗試N次退避,該N由tcp_orphan_retries參數控制。

接下來,我們來看一個更加復雜一點的問題,還是先從實驗說起。

  • 實驗2:模擬對端TCP不收數據,接收窗口憋死
    在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

此時,我們發現host2的TCP連接進入了FIN_WAIT1狀態。然而抓包看的話,數據傳輸依然在進行:

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

這是顯然的,這是因為收發兩端巨大的緩存大小差異造成的,即便是host2發送端進程退出了,在退出前已經有大量數據pending到了TCP的發送緩沖區里面而脫離已經被銷毀的進程了,FIN包當然是排在了緩沖區的末尾了。

TCP的狀態機運行在緩存的上層,即只要把FIN包pending排隊,就切換到了FIN_WAIT1,而不是說實際發送了FIN包才切換。

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

不管怎樣,總是有個頭兒,只要有結束,就不需要擔心。我們可以簡單得出一個結論:

  • 如果主動斷開端調用了close關掉了進程,它會進入FIN_WAIT1狀態,如果接收端的接收窗口呈現打開狀態,此時它的TCP發送隊列中的數據包還是會像正常一樣發往接收端,直到發送完,最后發送FIN包,收到FIN包ACK后進入FIN_WAIT2。

現在,我們進行實驗的下一步,把host1上的接收進程nc的接收邏輯徹底憋死。很簡單,host1上執行下面的命令即可:

killall -STOP nc

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

這個時候,抓包就會發現只剩下指數時間退避的零窗口探測包了:

# 注意觀察探測包發送時間的間隔
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

這個實驗的現象和實驗1的現象,僅有一個區別,那就是實驗1是阻塞了ACK,而本實驗則是FIN根本就還沒有發送出去就進入了FIN_WAIT1,且針對RTO指數時間退避發送的零窗口探測的ACK持續到來,簡單總結就是:
實驗1沒有ACK到來,實驗2有ACK到來。

在實驗結果之前,我們來看一段摘錄,來自RFC 1122: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 closed
   indefinitely. As long as the receiving TCP continues to
  send acknowledgments in response to the probe segments, the
  sending TCP MUST allow the connection to stay open.

緊接著后面是一段注解:

DISCUSSION:
  It is extremely important to remember that ACK
  (acknowledgment) segments that contain no data are not
  reliably transmitted by TCP. If zero window probing is
  not supported, a connection may hang forever when an
  ACK segment that re-opens the window is lost.
.
  The delay in opening a zero window generally occurs
  when the receiving Application stops taking data from
  its TCP. For example, consider a printer daemon
  application, stopped because the printer ran out of
  paper.

只要有ACK到來,連接就要保持,這會帶來什么問題呢?確實會帶來問題,但是在正視這些問題之前,Linux內核協議棧的實現者,也保持了緘默,我們來看一段實驗主機host1和host2所用的標準內核主線版本3.10的內核源碼,來自tcp_probe_timer函數內部的注釋以及一小段代碼:

    /* *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參數,有微調,請詳審。本實驗參數默認值取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,即未應答的probe次數超過探測最大容忍次數后,才會出錯清理連接。
    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);
    }

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


這也許暗示了某種魔咒般的結果,即FIN_WAIT1將會一直持續到終結世界的大決戰之日。然而非也,你會發現大概在發送了9個零窗口探測包之后,連接就消失了。netstat -st的結果中,呈現:

1 connections aborted due to timeout

看來想制造點事端,并非想象般容易!

如上所述,我展示了標準主線的Linux 3.10內核的tcp_probe_timer函數,現在的問題是,為什么下面的條件被滿足了呢?

if (icsk->icsk_probes_out > max_probes) 

只有當這個條件被滿足,tcp_write_err才會被調用,進而:

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

按照注釋和代碼的確認,只要收到ACK,icsk_probes_out 字段就將被清零,這是很明確的啊,我們在tcp_ack函數中便可看到無條件清零icsk_probes_out的動作:

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;
    ...
}

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

預期會永遠僵在FIN_WAIT1狀態的連接在一段時間后竟然銷毀了。沒有符合預期,到底發生了呢?


如果我們看高版本4.14版的Linux內核,同樣是tcp_probe_timer函數,我們會看到一些不一樣的代碼和注釋:

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的連接持續時間超過了TCP_RTO_MAX(這是前提)
        // 如果退避發送探測的次數已經超過了配置參數指定的次數(這是附加條件)
        if (!alive && icsk->icsk_backoff >= max_probes)
            goto abort; // 注意這個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.

然后我們接著看這段注釋,有一個But轉折:

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.

看起來,這段注釋是符合我們實驗的結論的!然而我們實驗的是3.10內核,而這個卻是4.X的內核啊!即Linux在高版本內核上確實進行了優化,這是針對資源利用的優化,并且避免了有針對性的DDoS。


答案揭曉了。

*我們實驗所使用的內核版本不是社區主線版本,而是Redhat的版本!***Redhat顯然會事先回移上游的patch,我們來確認一下我們所所用的實驗版本3.10.0-862.2.3.el7.x86_64的tcp_probe_timer的源碼。

為此,我們到下面的地址去下載Redhat(centos…)專門的源碼,我們看看它和社區同版本源碼是不是在關于probe處理上有所不同:
http://vault.centos.org/7.5.1804/updates/Source/SPackages/

TCP在FIN_WAIT1狀態能持續多久及TCP假連接問題

 

使用下面的命令解壓:

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函數:

TCP在FIN_WAIT1狀態能持續多久及TCP假連接問題

 

看來是Redhat移植了4.X的patch,導致了源碼的邏輯和社區版本的出現差異,這也就解釋了實驗現象!


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

在maillist,我找到了下面的資料:

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.”

該資料最后面給出了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 {
...

簡單說一下這個patch的意義。

在實驗2中,我用kill -STOP信號故意憋死了nc接收進程,以重現現象,然而事實上在現實中,存在下面兩種不太友善情況:

  • 接收端進程出現異常,或者接收端內核存在缺陷,導致進程掛死而軟中斷上下文的協議棧處理正常運行;
  • 接收端就是一個惡意的DDoS進程,故意不接收數據以誘導發送端在FIN_WAIT2狀態(甚至ESTAB狀態)發送數據不成后發送零窗口探測而不休止。

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


現在給出本文的第三個結論:

  • 如果主動斷開端調用了close關掉了進程,它會進入FIN_WAIT1狀態,如果接收端的接收窗口呈現關閉狀態(零窗口),此時它會不斷發送零窗口探測包。發送多少次呢?有兩種實現:
    低版本內核(至少社區3.10及以下):永久嘗試,如果探測ACK每次都返回,則沒完沒了。高版本內核(至少社區4.6及以上):限制嘗試tcp_orphan_retries次,不管是否收到探測ACK。

當然,其實還有關于非探測包的重傳限制,比如關于TCP_USER_TIMEOUT這個socket option的限制:

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

包括關于Keepalive的點點滴滴,本文就不多說了。


在此,先有個必要的總結。我老是說在學習網絡協議的時候讀碼無益并不是說不要去閱讀解析Linux內核源碼,而是一定要先有實驗設計的能力重現問題,然后再去核對RFC或者其它的協議標準,最后再去核對源碼到底是怎么實現的,這樣才能一氣呵成。否則將有可能陷入深淵。

以本文為例,我假設你手頭有3.10的源碼,當你面對“FIN_WAIT1狀態的TCP連接在持續退避的零窗口探測期間并不會如預期那般永久持續下去”這個問題的時候,你讀源碼是沒有任何用的,因為這個時候你只能靜靜地看著那些代碼,然后糾結自己是不是哪里理解錯了,很多人甚至很難能想到去對比不同版本的代碼,因為版本太多了。

源碼只是一種實現的方式,而已,真正重要的是協議的標準以及標準是實現的建議,此外,各個發行版廠商完全有自主的權力對社區源碼做任何的定制和重構,不光是Redhat,即便你去看OpenWRT的代碼,也是一樣,你會發現很多不一樣的東西。

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


本文原本是想解釋完FIN_WAIT1能持續多久就結束的,但是這樣顯得有點遺憾,因為我想本文的這個FIN_WAIT1的論題可以引出一個更大的論題,如果不繼續說一說,那便是不負責任的。

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

所謂的TCP假連接就是TCP的一端已經逃逸出了TCP狀態機,而另一端卻不知道的連接。

我們再看完美的TCP標準RFC793上的TCP狀態圖:

TCP在FIN_WAIT1狀態能持續多久及TCP假連接問題

 

除了TIME_WAIT到CLOSED這唯一的出口,你是找不到其它出口的,也就是說,一個TCP端一旦發起了建立連接請求,暫不考慮同時打開同時關閉的情況,就一定要到其中一方的TIME_WAIT超時而結束

然而,TCP的缺陷在于,TCP是一個端到端的協議,在協議層面上所有的端到端協議是需要底層的傳送協議作為其支撐的,一旦底層永久崩壞,端到端協議將會面臨狀態機僵住的場景,而狀態機僵住意味著對資源的永久消耗,因為連接再也釋放不掉了!

隨便舉一個例子,在兩端ESTAB狀態的時候,把IP動態路由協議停掉并把把網線剪斷,那么TCP兩端將永遠處在ESTAB狀態,直到機器重啟。為了解決這個問題,TCP引入了Keepalive機制,一旦超過一定時間沒有互通有無,那么就會主動銷毀這個連接,事實上,按照純粹的TCP狀態機而言,Keepalive機制是一種對TCP協議的污染。

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

我們執行下面的命令:

[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協議本身的狀態機轉換僵死所引入的控制層Keepalive機制,詳細情況就自己去查閱Linux內核文檔吧。

在具體實現上,防止狀態機僵死的方法分為兩類:

  • ESTABLISHED防止僵死的方法:使用用戶進程設置的Keepalive機制
  • 非ESTABLISHED防止僵死的方法:使用各種retries內核參數設置的timeout機制

分享到:
標簽:TCP
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定