您现在的位置是:首页 > 文章详情

当TIME_WAIT状态的TCP正常挥手,收到SYN后…

日期:2022-03-03点击:350
摘要:今天就来讨论下这个问题,在TCP正常挥手过程中,处于TIME_WAIT状态的连接,收到相同四元组的SYN后会发生什么?

本文分享自华为云社区《在TIME_WAIT状态的TCP连接,收到SYN后会发生什么?》,作者:小林coding。

周末跟朋友讨论了一些TCP的问题,在查阅《Linux服务器高性能编程》这本书的时候,发现书上写了这么一句话:

书上说,处于TIME_WAIT状态的连接,在收到相同四元组的SYN后,会回RST报文,对方收到后就会断开连接。

书中作者只是提了这么一句话,没有给予源码或者抓包图的证据。

起初,我看到也觉得这个逻辑也挺符合常理的,但是当我自己去啃了TCP源码后,发现并不是这样的。

所以,今天就来讨论下这个问题,「在TCP正常挥手过程中,处于TIME_WAIT状态的连接,收到相同四元组的SYN后会发生什么?

问题现象如下图,左边是服务端,右边是客户端:

先说结论

在跟大家分析TCP源码前,我先跟大家直接说下结论。

针对这个问题,关键是要看SYN的「序列号和时间戳」是否合法,因为处于TIME_WAIT状态的连接收到SYN后,会判断SYN的「序列号和时间戳」是否合法,然后根据判断结果的不同做不同的处理。

先跟大家说明下,什么是「合法」的SYN?

  • 合法SYN:客户端的SYN的「序列号」比服务端「期望下一个收到的序列号」要并且SYN的「时间戳」比服务端「最后收到的报文的时间戳」要
  • 非法SYN:客户端的SYN的「序列号」比服务端「期望下一个收到的序列号」要或者SYN的「时间戳」比服务端「最后收到的报文的时间戳」要

上面SYN合法判断是基于双方都开启了TCP时间戳机制的场景,如果双方都没有开启TCP时间戳机制,则SYN合法判断如下:

  • 合法SYN:客户端的SYN的「序列号」比服务端「期望下一个收到的序列号」要
  • 非法SYN:客户端的SYN的「序列号」比服务端「期望下一个收到的序列号」要

收到合法SYN

如果处于TIME_WAIT状态的连接收到「合法的SYN」后,就会重用此四元组连接,跳过2MSL而转变为SYN_RECV状态,接着就能进行建立连接过程

用下图作为例子,双方都启用了TCP时间戳机制,TSval是发送报文时的时间戳:

上图中,在收到第三次挥手的FIN报文时,会记录该报文的TSval(21),用ts_recent变量保存。然后会计算下一次期望收到的序列号,本次例子下一次期望收到的序列号就是301,用rcv_nxt变量保存。

处于TIME_WAIT状态的连接收到SYN后,因为SYN的seq(400)大于rcv_nxt(301),并且SYN的TSval(30)大于ts_recent(21),所以是一个「合法的SYN」,于是就会重用此四元组连接,跳过2MSL而转变为SYN_RECV状态,接着就能进行建立连接过程。

收到非法的SYN

如果处于TIME_WAIT状态的连接收到「非法的SYN」后,就会再回复一个第四次挥手的ACK报文,客户端收到后,发现并不是自己期望收到确认号(acknum),就回RST报文给服务端

用下图作为例子,双方都启用了TCP时间戳机制,TSval是发送报文时的时间戳:

上图中,在收到第三次挥手的FIN报文时,会记录该报文的TSval(21),用ts_recent变量保存。然后会计算下一次期望收到的序列号,本次例子下一次期望收到的序列号就是301,用rcv_nxt变量保存。

处于TIME_WAIT状态的连接收到SYN后,因为SYN的seq(200)小于rcv_nxt(301),所以是一个「非法的SYN」,就会再回复一个与第四次挥手一样的ACK报文,客户端收到后,发现并不是自己期望收到确认号,就回RST报文给服务端

客户端等待一段时间还是没收到SYN+ACK后,就会超时重传SYN报文,重传次数达到最大值后,就会断开连接。

PS:这里先埋一个疑问,处于TIME_WAIT状态的连接,收到RST会断开连接吗?

源码分析

下面源码分析是基于Linux4.2版本的内核代码。

Linux内核在收到TCP报文后,会执行tcp_v4_rcv函数,在该函数和TIME_WAIT状态相关的主要代码如下:

inttcp_v4_rcv(structsk_buff*skb) { structsock*sk; ... //收到报文后,会调用此函数,查找对应的sock sk=__inet_lookup_skb(&tcp_hashinfo,skb,__tcp_hdrlen(th),th->source, th->dest,sdif,&refcounted); if(!sk) gotono_tcp_socket; process: //如果连接的状态为time_wait,会跳转到do_time_wait if(sk->sk_state==TCP_TIME_WAIT) gotodo_time_wait; ... do_time_wait: ... //由tcp_timewait_state_process函数处理在time_wait状态收到的报文 switch(tcp_timewait_state_process(inet_twsk(sk),skb,th)){ //如果是TCP_TW_SYN,那么允许此SYN重建连接 //即允许TIM_WAIT状态跃迁到SYN_RECV caseTCP_TW_SYN:{ structsock*sk2=inet_lookup_listener(....); if(sk2){ .... gotoprocess; } } //如果是TCP_TW_ACK,那么,返回记忆中的ACK caseTCP_TW_ACK: tcp_v4_timewait_ack(sk,skb); break; //如果是TCP_TW_RST直接发送RESET包 caseTCP_TW_RST: tcp_v4_send_reset(sk,skb); inet_twsk_deschedule_put(inet_twsk(sk)); gotodiscard_it; //如果是TCP_TW_SUCCESS则直接丢弃此包,不做任何响应 caseTCP_TW_SUCCESS:; } gotodiscard_it; }

该代码的过程:

  1. 接收到报文后,会调用__inet_lookup_skb()函数查找对应的sock结构;
  2. 如果连接的状态是TIME_WAIT,会跳转到do_time_wait处理;
  3. 由tcp_timewait_state_process()函数来处理收到的报文,处理后根据返回值来做相应的处理。

先跟大家说下,如果收到的SYN是合法的,tcp_timewait_state_process()函数就会返回TCP_TW_SYN,然后重用此连接。如果收到的SYN是非法的,tcp_timewait_state_process()函数就会返回TCP_TW_ACK,然后会回上次发过的ACK。

接下来,看tcp_timewait_state_process()函数是如何判断SYN包的。

enumtcp_tw_status tcp_timewait_state_process(structinet_timewait_sock*tw,structsk_buff*skb, conststructtcphdr*th) { ... //paws_reject为false,表示没有发生时间戳回绕 //paws_reject为true,表示发生了时间戳回绕 boolpaws_reject=false; tmp_opt.saw_tstamp=0; //TCP头中有选项且旧连接开启了时间戳选项 if(th->doff>(sizeof(*th)>>2)&&tcptw->tw_ts_recent_stamp){ //解析选项 tcp_parse_options(twsk_net(tw),skb,&tmp_opt,0,NULL); if(tmp_opt.saw_tstamp){ ... //检查收到的报文的时间戳是否发生了时间戳回绕 paws_reject=tcp_paws_reject(&tmp_opt,th->rst); } } .... //是SYN包、没有RST、没有ACK、时间戳没有回绕,并且序列号也没有回绕, if(th->syn&&!th->rst&&!th->ack&&!paws_reject&& (after(TCP_SKB_CB(skb)->seq,tcptw->tw_rcv_nxt)|| (tmp_opt.saw_tstamp&&//新连接开启了时间戳 (s32)(tcptw->tw_ts_recent-tmp_opt.rcv_tsval)<0))){//时间戳没有回绕 //初始化序列号 u32isn=tcptw->tw_snd_nxt+65535+2; if(isn==0) isn++; TCP_SKB_CB(skb)->tcp_tw_isn=isn; returnTCP_TW_SYN;//允许重用TIME_WAIT四元组重新建立连接 } if(!th->rst){ //如果时间戳回绕,或者报文里包含ack,则将TIMEWAIT状态的持续时间重新延长 if(paws_reject||th->ack) inet_twsk_schedule(tw,&tcp_death_row,TCP_TIMEWAIT_LEN, TCP_TIMEWAIT_LEN); //返回TCP_TW_ACK,发送上一次的ACK returnTCP_TW_ACK; } inet_twsk_put(tw); returnTCP_TW_SUCCESS; }

如果双方启用了TCP时间戳机制,就会通过tcp_paws_reject()函数来判断时间戳是否发生了回绕,也就是「当前收到的报文的时间戳」是否大于「上一次收到的报文的时间戳」:

  • 如果大于,就说明没有发生时间戳绕回,函数返回false。
  • 如果小于,就说明发生了时间戳回绕,函数返回true。

从源码可以看到,当收到SYN包后,如果该SYN包的时间戳没有发生回绕,也就是时间戳是递增的,并且SYN包的序列号也没有发生回绕,也就是SYN的序列号「大于」下一次期望收到的序列号。就会初始化一个序列号,然后返回TCP_TW_SYN,接着就重用该连接,也就跳过2MSL而转变为SYN_RECV状态,接着就能进行建立连接过程。

如果双方都没有启用TCP时间戳机制,就只需要判断SYN包的序列号有没有发生回绕,如果SYN的序列号大于下一次期望收到的序列号,就可以跳过2MSL,重用该连接。

如果SYN包是非法的,就会返回TCP_TW_ACK,接着就会发送与上一次一样的ACK给对方。

在TIME_WAIT状态,收到RST会断开连接吗?

在前面我留了一个疑问,处于TIME_WAIT状态的连接,收到RST会断开连接吗?

会不会断开,关键看net.ipv4.tcp_rfc1337这个内核参数(默认情况是为0):

  • 如果这个参数设置为0,收到RST报文会提前结束TIME_WAIT状态,释放连接。
  • 如果这个参数设置为1,就会丢掉RST报文。

源码处理如下:

enumtcp_tw_status tcp_timewait_state_process(structinet_timewait_sock*tw,structsk_buff*skb, conststructtcphdr*th) { .... //rst报文的时间戳没有发生回绕 if(!paws_reject&& (TCP_SKB_CB(skb)->seq==tcptw->tw_rcv_nxt&& (TCP_SKB_CB(skb)->seq==TCP_SKB_CB(skb)->end_seq||th->rst))){ //处理rst报文 if(th->rst){ //不开启这个选项,当收到RST时会立即回收tw,但这样做是有风险的 if(twsk_net(tw)->ipv4.sysctl_tcp_rfc1337==0){ kill: //删除tw定时器,并释放tw inet_twsk_deschedule_put(tw); returnTCP_TW_SUCCESS; } }else{ //将TIMEWAIT状态的持续时间重新延长 inet_twsk_reschedule(tw,TCP_TIMEWAIT_LEN); } ... returnTCP_TW_SUCCESS; } }

TIME_WAIT状态收到RST报文而释放连接,这样等于跳过2MSL时间,这么做还是有风险。

sysctl_tcp_rfc1337这个参数是在rfc1337文档提出来的,目的是避免因为TIME_WAIT状态收到RST报文而跳过2MSL的时间,文档里也给出跳过2MSL时间会有什么潜在问题。

TIME_WAIT状态之所以要持续2MSL时间,主要有两个目的:

  • 防止历史连接中的数据,被后面相同四元组的连接错误的接收;
  • 保证「被动关闭连接」的一方,能被正确的关闭;

详细的为什么要设计TIME_WAIT状态,我在这篇有详细说明:如果 TIME_WAIT 状态持续时间过短或者没有,会有什么问题?

虽然TIME_WAIT状态持续的时间是有一点长,显得很不友好,但是它被设计来就是用来避免发生乱七八糟的事情。

《UNIX网络编程》一书中却说道:TIME_WAIT是我们的朋友,它是有助于我们的,不要试图避免这个状态,而是应该弄清楚它

所以,我个人觉得将net.ipv4.tcp_rfc1337设置为1会比较安全。

总结

在TCP正常挥手过程中,处于TIME_WAIT状态的连接,收到相同四元组的SYN后会发生什么?

如果双方开启了时间戳机制:

  • 如果客户端的SYN的「序列号」比服务端「期望下一个收到的序列号」要并且SYN的「时间戳」比服务端「最后收到的报文的时间戳」要。那么就会重用该四元组连接,跳过2MSL而转变为SYN_RECV状态,接着就能进行建立连接过程。
  • 如果客户端的SYN的「序列号」比服务端「期望下一个收到的序列号」要或者SYN的「时间戳」比服务端「最后收到的报文的时间戳」要。那么就会再回复一个第四次挥手的ACK报文,客户端收到后,发现并不是自己期望收到确认号,就回RST报文给服务端

在TIME_WAIT状态,收到RST会断开连接吗?

  • 如果net.ipv4.tcp_rfc1337参数为0,则提前结束TIME_WAIT状态,释放连接。
  • 如果net.ipv4.tcp_rfc1337参数为1,则会丢掉该RST报文。

 

点击关注,第一时间了解华为云新鲜技术~

原文链接:https://my.oschina.net/u/4526289/blog/5472893
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章