6

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

 2 years ago
source link: https://blog.51cto.com/u_15214399/5068978
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

原创

华为云开发者社区 2022-03-03 14:53:32 博主文章分类:程序员之家 ©著作权

文章标签 时间戳 序列号 服务端 TCP 报文 文章分类 软件设计 软件研发 阅读数687

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

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

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

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

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

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

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

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

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

当TIME_WAIT状态的TCP正常挥手,收到SYN后…_序列号_02

在跟大家分析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是发送报文时的时间戳:

当TIME_WAIT状态的TCP正常挥手,收到SYN后…_序列号_03

上图中,在收到第三次挥手的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是发送报文时的时间戳:

当TIME_WAIT状态的TCP正常挥手,收到SYN后…_时间戳_04

上图中,在收到第三次挥手的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报文。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK