【在 TIME_WAIT 状态的 TCP 连接,收到 SYN 后会发生什么()】人生难得几回搏,此时不搏待何时。这篇文章主要讲述在 TIME_WAIT 状态的 TCP 连接,收到 SYN 后会发生什么?相关的知识,希望能为你提供帮助。
周末跟朋友讨论了一些 TCP 的问题,在查阅《Linux 服务器高性能编程》这本书的时候,发现书上写了这么一句话:
书上说,处于 TIME_WAIT 状态的连接,在收到相同四元组的 SYN 后,会回 RST 报文,对方收到后就会断开连接。
书中作者只是提了这么一句话,没有给予源码或者抓包图的证据。
起初,我看到也觉得这个逻辑也挺符合常理的,但是当我自己去啃了 TCP 源码后,发现并不是这样的。
所以,今天就来讨论下这个问题,「在 TCP 正常挥手过程中,处于 TIME_WAIT 状态的连接,收到相同四元组的 SYN 后会发生什么?」
问题现象如下图,左边是服务端,右边是客户端:
先说结论在跟大家分析 TCP 源码前,我先跟大家直接说下结论。
针对这个问题,关键是要看 SYN 的「序列号和时间戳」是否合法,因为处于 TIME_WAIT 状态的连接收到 SYN 后,会判断 SYN 的「序列号和时间戳」是否合法,然后根据判断结果的不同做不同的处理。
先跟大家说明下, 什么是「合法」的 SYN?
上面 SYN 合法判断是基于双方都开启了 TCP 时间戳机制的场景,如果双方都没有开启 TCP 时间戳机制,则 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 报文,客户端收到后,发现并不是自己期望收到确认号(ack num),就回 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 报文,重传次数达到最大值后,就会断开连接。
源码分析下面源码分析是基于 Linux 4.2 版本的内核代码。
PS:这里先埋一个疑问,处于 TIME_WAIT 状态的连接,收到 RST 会断开连接吗?
Linux 内核在收到 TCP 报文后,会执行 ??tcp_v4_rcv?
? 函数,在该函数和 TIME_WAIT 状态相关的主要代码如下:
int tcp_v4_rcv(struct sk_buff *skb)
struct sock *sk;
...
//收到报文后,会调用此函数,查找对应的 sock
sk = __inet_lookup_skb(&
tcp_hashinfo, skb, __tcp_hdrlen(th), th->
source,
th->
dest, sdif, &
refcounted);
if (!sk)
goto no_tcp_socket;
process:
//如果连接的状态为 time_wait,会跳转到 do_time_wait
if (sk->
sk_state == TCP_TIME_WAIT)
goto do_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
case TCP_TW_SYN:
struct sock *sk2 = inet_lookup_listener(....);
if (sk2)
....
goto process;
// 如果是TCP_TW_ACK,那么,返回记忆中的ACK
case TCP_TW_ACK:
tcp_v4_timewait_ack(sk, skb);
break;
// 如果是TCP_TW_RST直接发送RESET包
case TCP_TW_RST:
tcp_v4_send_reset(sk, skb);
inet_twsk_deschedule_put(inet_twsk(sk));
goto discard_it;
// 如果是TCP_TW_SUCCESS则直接丢弃此包,不做任何响应
case TCP_TW_SUCCESS:;
goto discard_it;
该代码的过程:
先跟大家说下,如果收到的 SYN 是合法的,??__inet_lookup_skb()?
? 函数查找对应的 sock 结构;?TIME_WAIT?
?,会跳转到 do_time_wait 处理;?tcp_timewait_state_process()?
? 函数来处理收到的报文,处理后根据返回值来做相应的处理。?tcp_timewait_state_process()?
?? 函数就会返回 ??TCP_TW_SYN?
??,然后重用此连接。如果收到的 SYN 是非法的,??tcp_timewait_state_process()?
?? 函数就会返回 ??TCP_TW_ACK?
?,然后会回上次发过的 ACK。
接下来,看 ??tcp_timewait_state_process()?
? 函数是如何判断 SYN 包的。
enum tcp_tw_status
tcp_timewait_state_process(struct inet_timewait_sock *tw, struct sk_buff *skb,
const struct tcphdr *th)
...
//paws_reject 为 false,表示没有发生时间戳回绕
//paws_reject 为 true,表示发生了时间戳回绕
bool paws_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)))//时间戳没有回绕
// 初始化序列号
u32 isn = tcptw->
tw_snd_nxt + 65535 + 2;
if (isn == 0)
isn++;
TCP_SKB_CB(skb)->
tcp_tw_isn = isn;
return TCP_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
return TCP_TW_ACK;
inet_twsk_put(tw);
return TCP_TW_SUCCESS;
如果双方启用了 TCP 时间戳机制,就会通过 ??tcp_paws_reject()?
? 函数来判断时间戳是否发生了回绕,也就是「当前收到的报文的时间戳」是否大于「上一次收到的报文的时间戳」:
从源码可以看到,当收到 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):
源码处理如下:
enum tcp_tw_status
tcp_timewait_state_process(struct inet_timewait_sock *tw, struct sk_buff *skb,
const struct tcphdr *th)
....
//rst报文的时间戳没有发生回绕
if (!paws_reject &
&
(TCP_SKB_CB(skb)->
seq == tcptw->
tw_rcv_nxt &
&
(
推荐阅读
- k8s集群service流量服务代理5大严肃思考()
- Python 工匠(使用数字与字符串的技巧)
- Azure Virtual Desktop 快速上手--AZURE AD JOIN
- Mac配置Java环境变量
- Mac配置maven环境变量
- mac安装虚拟机win10,Mac安装VirtualBox
- 提交代码到gitee
- linux复制目录到另一个目录,另一个目录中已经存在名称一样的目录,会怎么杨(使用ftp操作)
- shell编程语法