当前位置:网站首页>当TIME_WAIT状态的TCP正常挥手,收到SYN后…
当TIME_WAIT状态的TCP正常挥手,收到SYN后…
2022-08-02 19:15:00 【linux大本营】
周末跟朋友讨论了一些 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 状态,接着就能进行建立连接过程。
相关视频推荐
tcpip,accept,11个状态,细枝末节的秘密,还有哪些你不知道
100行代码开启自己的协议栈,《tcp/ip详解》的代码注解
需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
收到非法的 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;...//收到报文后,会调用此函数,查找对应的socksk=__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_waitif(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_RECVcaseTCP_TW_SYN:{structsock*sk2=inet_lookup_listener(....);if(sk2){....gotoprocess;}}//如果是TCP_TW_ACK,那么,返回记忆中的ACKcaseTCP_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;}
该代码的过程:
- 接收到报文后,会调用__inet_lookup_skb () 函数查找对应的 sock 结构;
- 如果连接的状态是 TIME_WAIT,会跳转到 do_time_wait 处理;
- 由 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_statustcp_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,发送上一次的ACKreturnTCP_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_statustcp_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定时器,并释放twinet_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 报文。
边栏推荐
- Gradle系列——Gradle的build.gradle文件详情,项目发布(基于Gradle文档7.5)day3-3
- E. Add Modulo 10(规律)
- Sentienl【动态数据源架构设计理念与改造实践】
- 十六进制文本的字节序问题
- NC | 土壤微生物组的结构和功能揭示全球湿地N2O释放
- Boyun Selected as Gartner China DevOps Representative Vendor
- 如何获取EasyCVR平台设备通道的RTMP视频流地址?
- SQL server有什么认证吗?
- 线程池原理与实践|从入门到放弃,深度解析
- 86.(cesium之家)cesium叠加面接收阴影效果(gltf模型)
猜你喜欢
随机推荐
动态生成不同类型的订单,请问如何存放到Mongodb数据库?
golang刷leetcode 动态规划(13) 最长公共子序列
安装Mac版Mysql卡在Installation阶段,彻底清理mysql并重装mysql
cache2go-源码阅读
openlayers version update difference
治疗 | 如何识别和处理消极想法
golang刷leetcode 经典(11) 朋友圈
阿里35+老测试员生涯回顾,自动化测试真的有这么吃香吗?
技术分享 | Apache Linkis 快速集成网页IDE工具 Scriptis
JVM内存和垃圾回收-06.本地方法栈
Go----Go 语言快速体验之开发环境搭建及第一个项目HelloWord
深度学习-学习笔记(持续更新)
小姐姐面试蚂蚁金服被虐经历,心疼...
MySQL主从搭建(问题大聚集,告别部署烦恼)
分享一个 web 应用版本监测 (更新) 的工具库
86.(cesium之家)cesium叠加面接收阴影效果(gltf模型)
Compose主题切换——让你的APP也能一键换肤
连续三次 | 灵雀云入选Gartner中国ICT技术成熟度曲线报告
微服务-gateway【服务网关入门】
MaxCompute 的SQL 引擎参数化视图具体有哪些增强功能?