TCP在FIN_WAIT1状态到底能持续多久以及TCP假连接问题

<span style="color:#000000"><code class="language-c">;

        <span style="color:#000088">if</span> (tcp_out_of_resources(sk, alive || icsk->icsk_probes_out <= max_probes))
            <span style="color:#000088">return</span>;
    }
    <span style="color:#880000">// 只有在icsk_probes_out,即未应答的probe次数超过探测最大容忍次数后,才会出错清理连接。</span>
    <span style="color:#000088">if</span> (icsk->icsk_probes_out > max_probes) {
        tcp_write_err(sk);
    } <span style="color:#000088">else</span> {
        <span style="color:#880000">/* Only send another probe if we didn't close things up. */</span>
        tcp_send_probe0(sk);
    }</code></span>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

是的,从上面那一段注释,我们看出了抱怨,一个FIN_WAIT1的连接可能会等到世界终结日之后,然而我们却只能“in full accordance with RFCs”


这也许暗示了某种魔咒般的结果,即FIN_WAIT1将会一直持续到终结世界的大决战之日。然而非也,你会发现大概在发送了9个零窗口探测包之后,连接就消失了。netstat -st的结果中,呈现:

<span style="color:#000000"><code class="language-bash"><span style="color:#006666">1</span> connections aborted due to timeout</code></span>
  • 1

看来想制造点事端,并非想象般容易!

如上所述,我展示了标准主线的Linux 3.10内核的tcp_probe_timer函数,现在的问题是,为什么下面的条件被满足了呢?

<span style="color:#000000"><code class="language-c"><span style="color:#000088">if</span> (icsk->icsk_probes_out > max_probes) </code></span>
  • 1

只有当这个条件被满足,tcp_write_err才会被调用,进而:

<span style="color:#000000"><code class="language-c">tcp_done(sk);
<span style="color:#880000">// 递增计数,即netstat -st中的那条“1 connections aborted due to timeout”</span>
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPABORTONTIMEOUT);</code></span>
  • 1
  • 2
  • 3

按照注释和代码的确认,只要收到ACK,icsk_probes_out 字段就将被清零,这是很明确的啊,我们在tcp_ack函数中便可看到无条件清零icsk_probes_out的动作:

<span style="color:#000000"><code class="language-c"><span style="color:#000088">static</span> <span style="color:#000088">int</span> tcp_ack(<span style="color:#000088">struct</span> sock *sk, <span style="color:#000088">const</span> <span style="color:#000088">struct</span> sk_buff *skb, <span style="color:#000088">int</span> flag)
{
    ...
    sk->sk_err_soft = <span style="color:#006666">0</span>;
    icsk->icsk_probes_out = <span style="color:#006666">0</span>;
    tp->rcv_tstamp = tcp_time_stamp;
    ...
}</code></span>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

从代码上看,只要零窗口探测持续发送,不管退避到多久(最大TCP_RTO_MAX),只要对端会有ACK回来,icsk_probes_out 就会被清零,上述的条件就不会被满足,连接就会一直在FIN_WAIT1状态,而从我们抓包看,确实是零窗口探测有去必有回的!

预期会永远僵在FIN_WAIT1状态的连接在一段时间后竟然销毁了。没有符合预期,到底发生了呢?


如果我们看高版本4.14版的Linux内核,同样是tcp_probe_timer函数,我们会看到一些不一样的代码和注释:

<span style="color:#000000"><code class="language-c"><span style="color:#000088">static</span> <span style="color:#000088">void</span> tcp_probe_timer(<span style="color:#000088">struct</span> sock *sk)
{
    ...
    <span style="color:#880000">/* 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().
     */</span>
    start_ts = tcp_skb_timestamp(tcp_send_head(sk));
    <span style="color:#000088">if</span> (!start_ts)
        tcp_send_head(sk)->skb_mstamp = tp->tcp_mstamp;
    <span style="color:#000088">else</span> <span style="color:#000088">if</span> (icsk->icsk_user_timeout &&
         (s32)(tcp_time_stamp(tp) - start_ts) >
         jiffies_to_msecs(icsk->icsk_user_timeout))
        <span style="color:#000088">goto</span> <span style="color:#4f4f4f">abort</span>; 

    max_probes = sock_net(sk)->ipv4.sysctl_tcp_retries2;
    <span style="color:#000088">if</span> (sock_flag(sk, SOCK_DEAD)) {
        <span style="color:#000088">const</span> <span style="color:#000088">bool</span> alive = inet_csk_rto_backoff(icsk, TCP_RTO_MAX) < TCP_RTO_MAX;

        max_probes = tcp_orphan_retries(sk, alive);
        <span style="color:#880000">// 如果处在FIN_WAIT1的连接持续时间超过了TCP_RTO_MAX(这是前提)</span>
        <span style="color:#880000">// 如果退避发送探测的次数已经超过了配置参数指定的次数(这是附加条件)</span>
        <span style="color:#000088">if</span> (!alive && icsk->icsk_backoff >= max_probes)
            <span style="color:#000088">goto</span> <span style="color:#4f4f4f">abort</span>; <span style="color:#880000">// 注意这个goto!直接销毁连接。</span>
        <span style="color:#000088">if</span> (tcp_out_of_resources(sk, <span style="color:#000088">true</span>))
            <span style="color:#000088">return</span>;
    }

    <span style="color:#000088">if</span> (icsk->icsk_probes_out > max_probes) {
<span style="color:#4f4f4f">abort</span>:      tcp_write_err(sk);
    } <span style="color:#000088">else</span> {
        <span style="color:#880000">/* Only send another probe if we didn't close things up. */</span>
        tcp_send_probe0(sk);
    }
}</code></span>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

我们来看这段代码的注释,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/

这里写图片描述

使用下面的命令解压:

<span style="color:#000000"><code class="language-bash">rpm2cpio ../kernel-<span style="color:#006666">3.10</span>.<span style="color:#006666">0</span>-<span style="color:#006666">862.2</span>.<span style="color:#006666">3</span>.el7.src.rpm | cpio -idmv
xz linux-<span style="color:#006666">3.10</span>.<span style="color:#006666">0</span>-<span style="color:#006666">862.2</span>.<span style="color:#006666">3</span>.el7.tar.xz -d
tar xvf linux-<span style="color:#006666">3.10</span>.<span style="color:#006666">0</span>-<span style="color:#006666">862.2</span>.<span style="color:#006666">3</span>.el7.tar </code></span>
  • 1
  • 2
  • 3

查看net/ipv4/tcp_timer.c文件,找到tcp_probe_timer函数: 
这里写图片描述

看来是Redhat移植了4.X的patch,导致了源码的逻辑和社区版本的出现差异,这也就解释了实验现象!


那么这个针对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:

<span style="color:#000000"><code class="language-diff">...
<span style="color:#009900">+   max_probes = sysctl_tcp_retries2;</span>
    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);
-
<span style="color:#009900">+       if (!alive && icsk->icsk_backoff >= max_probes)</span>
<span style="color:#009900">+           goto abort;</span>
        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);
<span style="color:#009900">+abort:     tcp_write_err(sk);</span>
    } else {
...</code></span>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

简单说一下这个patch的意义。

在实验2中,我用kill -STOP信号故意憋死了nc接收进程,以重现现象,然而事实上在现实中,存在下面两种不太友善情况:

  • 接收端进程出现异常,或者接收端内核存在缺陷,导致进程挂死而软中断上下文的协议栈处理正常运行;
  • 接收端就是一个恶意的DDoS进程,故意不接收数据以诱导发送端在FIN_WAIT2状态(甚至ESTAB状态)发送数据不成后发送零窗口探测而不休止。

无论哪种情况,最主动断开的发送端来讲,其后果都是消耗大量的资源,而orphan连接则占着茅坑不拉屎。这比较悲哀。


现在给出本文的第三个结论:

  • 如果主动断开端调用了close关掉了进程,它会进入FIN_WAIT1状态,如果接收端的接收窗口呈现关闭状态(零窗口),此时它会不断发送零窗口探测包。发送多少次呢?有两种实现: 
    1. 低版本内核(至少社区3.10及以下):永久尝试,如果探测ACK每次都返回,则没完没了。
    2. 高版本内核(至少社区4.6及以上):限制尝试tcp_orphan_retries次,不管是否收到探测ACK。 

当然,其实还有关于非探测包的重传限制,比如关于TCP_USER_TIMEOUT这个socket option的限制:

<span style="color:#000000"><code class="language-c"><span style="color:#000088">else</span> <span style="color:#000088">if</span> (icsk->icsk_user_timeout &&
     (s32)(tcp_time_stamp(tp) - start_ts) >
     jiffies_to_msecs(icsk->icsk_user_timeout))
    <span style="color:#000088">goto</span> <span style="color:#4f4f4f">abort</span>;</code></span>
  • 1
  • 2
  • 3
  • 4

包括关于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状态图: 
这里写图片描述

除了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呢??这时怎么办?

我们执行下面的命令:

<span style="color:#000000"><code class="language-bash">[root@localhost ~]<span style="color:#880000"># sysctl -a|grep retries</span>
net.ipv4.tcp_orphan_retries = <span style="color:#006666">0</span>
net.ipv4.tcp_retries1 = <span style="color:#006666">3</span>
net.ipv4.tcp_retries2 = <span style="color:#006666">15</span>
net.ipv4.tcp_syn_retries = <span style="color:#006666">6</span>
net.ipv4.tcp_synack_retries = <span style="color:#006666">5</span>
net.ipv6.idgen_retries = <span style="color:#006666">3</span></code></span>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

嗯,这些就是避免TCP协议本身的状态机转换僵死所引入的控制层Keepalive机制,详细情况就自己去查阅Linux内核文档吧。

在具体实现上,防止状态机僵死的方法分为两类:

  • ESTABLISHED防止僵死的方法:使用用户进程设置的Keepalive机制
  • 非ESTABLISHED防止僵死的方法:使用各种retries内核参数设置的timeout机制

转自dog250的CSDN博客,如有侵权,请联系告知!

FIN_WAIT1 是 TCP 连接的一种状态,它表示连接关闭的阶段之一。当一方发送了 FIN(终止连接)报文后,进入 FIN_WAIT1 状态,等待对方的确认。 在 FIN_WAIT1 状态下,发送方仍然可以接收对方发送的数据。它等待对方发送 ACK(确认)报文作为对 FIN 报文的确认。如果接收到对方的 ACK 报文,连接将进入 FIN_WAIT2 状态。如果在一定时间内没有收到对方的 ACK 报文,或者收到了对方的 RST(复位)报文,则连接会直接关闭。 FIN_WAIT1 状态通常是一个短暂的状态,在正常情况下不会停留太久。如果连接长时间停留FIN_WAIT1 状态,可能是由于以下原因之一: 1. 对方没有发送 ACK 报文:对方可能由于某种原因未正确处理并发送 ACK 报文,导致连接无法继续进入 FIN_WAIT2 状态。可以通过网络抓包或日志来确认是否有 ACK 报文被丢失或未发送。 2. 对方在接收到 FIN 报文后长时间未响应:如果对方在接收到 FIN 报文后长时间没有响应,可能是由于对方应用程序的问题,导致无法及时发送 ACK 报文。 请注意,如果应用程序频繁地打开和关闭连接,并且使用相同的本地 IP 地址和端口号,可能会导致连接在 TIME_WAIT 状态下保持较长时间,进而导致 FIN_WAIT1 状态持续时间延长。在这种情况下,可以尝试使用不同的本地 IP 地址和端口号来避免连接复用。 如果长时间停留FIN_WAIT1 状态引起了问题,可以考虑调整操作系统的参数或检查应用程序的代码逻辑,以确保连接能够正常关闭。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值