网络实验 -- TIME_WAIT状态的连接收到SYN是什么表现

2024-07-13

TIME_WAIT状态的连接收到同四元组的SYN是什么表现

1. 背景

星球实验:连接处于 TIME_WAIT 状态,这时收到了 syn 握手包,并参考4.11 在 TIME_WAIT 状态的 TCP 连接,收到 SYN 后会发生什么?

说明:本博客作为个人学习实践笔记,可供参考但非系统教程,可能存在错误或遗漏,欢迎指正。若需系统学习,建议参考原链接。

2. 问题描述

一个连接如果 Server 主动断开,那么这个连接在Server 上会进入 TIME_WAIT

这个时候如果客户端再次用原来的四元组发起连接请求。首先这个连接 和 已经断开但处于TIME_WAIT的连接是重复的,这个时候Server该怎么处理这个握手包呢?

假设服务端监听端口为8888,客户端请求端口为12345,示意图如下:

示意图

3. 构造场景(场景1)

这里使用CentOS8.5系统,构造常规场景,保持默认TCP参数。内核为4.18.0-348.7.1.el8_5.x86_64.

1
2
3
4
5
6
7
# tcp_tw_timeout 在标准内核上没有,ALinux(`Alibaba Cloud Linux`)单独新增
[root@xdlinux ➜ ~ ]$ sysctl -a|grep -E 'ip_local_port_range|tcp_max_tw_buckets|tcp_tw_reuse|tcp_rfc1337|tcp_timestamps|tcp_tw_timeout'
net.ipv4.ip_local_port_range = 32768	60999
net.ipv4.tcp_max_tw_buckets = 131072
net.ipv4.tcp_rfc1337 = 0
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_tw_reuse = 2

注意:这里tcp_tw_reuse是2,而不是布尔的0或1。包括之前起的ECS也默认2:Alibaba Cloud Linux 3.2104 LTS 64位(内核版本:5.10.134-16.1.al8.x86_64)

在高版本内核中,net.ipv4.tcp_tw_reuse 默认值为 2,表示仅为回环地址开启复用,基本可以粗略的认为没开启复用。 参考

对于网络相关内核参数的说明和取值,可以参考 kernel.org 或者 内核代码里的Documentation/networking/ip-sysctl.rst(如下)。

1
2
3
4
5
6
7
8
9
10
11
12
13
# linux-5.10.10/Documentation/networking/ip-sysctl.rst
tcp_tw_reuse - INTEGER
    Enable reuse of TIME-WAIT sockets for new connections when it is
    safe from protocol viewpoint.

    - 0 - disable
    - 1 - global enable
    - 2 - enable for loopback traffic only

    It should not be changed without advice/request of technical
    experts.

    Default: 2

3.1. 构造方式

1、服务端:192.168.1.150,CentOS8.5,开不同终端分别进行监听、开启抓包、tcpstates观测

代码:accept连接后就close,github noread

1
2
3
4
5
# 终端1
[root@xdlinux ➜ ~ ]$ tcpdump -i any port 8888 -nn -w server150_8888.cap -v

# 终端2
[root@xdlinux ➜ tools ]$ ./tcpstates -L 8888

2、客户端:192.168.1.2,MacOS,开启抓包,并指定端口请求 nc 192.168.1.150 8888 -p 12345,在60s内请求2次

说明:此处客户端机器为MacOS,自己在Linux上用上述nc命令实验会阻塞,若出现可以考虑换成curl并用--local-port指定端口

1
➜  /Users/xd/Downloads tcpdump -i en0 port 8888 -nn -w client2_12345.cap -v

3.2. 实验现象

按时序写一下观察现象。

服务端:

  • 请求前服务端状态
1
2
[root@xdlinux ➜ ~ ]$ ss -antp|grep 8888
LISTEN 0      5            0.0.0.0:8888      0.0.0.0:*     users:(("server",pid=17006,fd=3))
  • 请求一次时的服务端状态,第二次(60s内发起)也是这个状态

由于是发起端,最后是TIME_WAIT状态

1
2
3
[root@xdlinux ➜ ~ ]$ ss -antp|grep 8888
LISTEN    0      5            0.0.0.0:8888      0.0.0.0:*     users:(("server",pid=17006,fd=3))                      
TIME-WAIT 0      0      192.168.1.150:8888  192.168.1.2:12345 

两次打印

1
2
3
[root@xdlinux ➜ tcp_timewait_rcv_syn git:(main)]$ ./server
close, client_ip:192.168.1.2, port:12345
close, client_ip:192.168.1.2, port:12345
  • 最后(2MSL即60s之后)没有TIME_WAIT了
1
2
[root@xdlinux ➜ ~ ]$ ss -antp|grep 8888
LISTEN 0      5            0.0.0.0:8888      0.0.0.0:*     users:(("server",pid=17006,fd=3))
  • 抓包
1
2
3
4
[root@xdlinux ➜ ~ ]$ tcpdump -i any port 8888 -nn -w server150_8888.cap -v
dropped privs to tcpdump
tcpdump: listening on any, link-type LINUX_SLL (Linux cooked v1), capture size 262144 bytes
Got 14
  • tcpstates结果,可看到均为服务端发起FIN关闭
1
2
3
4
5
6
7
8
9
10
11
12
13
[root@xdlinux ➜ tools ]$ ./tcpstates -L 8888
SKADDR           C-PID C-COMM     LADDR           LPORT RADDR           RPORT OLDSTATE    -> NEWSTATE    MS
ffff9f74a152d7c0 0     swapper/9  0.0.0.0         8888  0.0.0.0         0     LISTEN      -> SYN_RECV    0.000
ffff9f74a152d7c0 0     swapper/9  192.168.1.150   8888  192.168.1.2     12345 SYN_RECV    -> ESTABLISHED 0.016
ffff9f74a152d7c0 0     swapper/9  192.168.1.150   8888  192.168.1.2     12345 FIN_WAIT1   -> FIN_WAIT2   1.004
ffff9f74a152d7c0 0     swapper/9  192.168.1.150   8888  192.168.1.2     12345 FIN_WAIT2   -> CLOSE       0.006
ffff9f74a152d7c0 17006 server     192.168.1.150   8888  192.168.1.2     12345 ESTABLISHED -> FIN_WAIT1   0.132
# 这里是第2次
ffff9f74a152d7c0 0     swapper/9  0.0.0.0         8888  0.0.0.0         0     LISTEN      -> SYN_RECV    0.000
ffff9f74a152d7c0 0     swapper/9  192.168.1.150   8888  192.168.1.2     12345 SYN_RECV    -> ESTABLISHED 0.018
ffff9f74a152d7c0 17006 server     192.168.1.150   8888  192.168.1.2     12345 ESTABLISHED -> FIN_WAIT1   0.106
ffff9f74a152d7c0 0     swapper/9  192.168.1.150   8888  192.168.1.2     12345 FIN_WAIT1   -> FIN_WAIT2   2.142
ffff9f74a152d7c0 0     swapper/9  192.168.1.150   8888  192.168.1.2     12345 FIN_WAIT2   -> CLOSE       0.006

客户端:

1
2
➜  /Users/xd nc 192.168.1.150 8888 -p 12345
➜  /Users/xd nc 192.168.1.150 8888 -p 12345
  • 抓包,和服务端的包一样
1
2
3
➜  /Users/xd/Downloads tcpdump -i en0 port 8888 -nn -w client2_12345.cap -v
tcpdump: listening on en0, link-type EN10MB (Ethernet), capture size 262144 bytes
Got 14

3.3. 抓包和结果分析

1、抓包:可看到两次请求均为服务端发起关闭,且连接正常走了三次握手和四次挥手

服务端-客户端抓包

2、(局部)结论

此处实验结果:服务端socket处于TIME_WAIT时,客户端重用原端口组成相同的四元组是可以连接的;

但不能简单得出所有情况下服务端TIME_WAIT都能接收相同四元组的连接,具体见下小节说明。

4. 完整结论说明

为什么有的文章或书籍里会说上述场景客户端是无法连接的,参考文章里给了说明,这里先直接贴下结论,下述的说明和流程图均来自4.11 在 TIME_WAIT 状态的 TCP 连接,收到 SYN 后会发生什么?

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

4.1. SYN是否合法

TIME_WAIT时收到同四元组的SYN是否合法

1、TCP时间戳机制开启情况下(即net.ipv4.tcp_timestamps=1,一般默认开启):

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

2、如果双方都没有开启 TCP 时间戳机制(net.ipv4.tcp_timestamps=0),则 SYN 合法判断如下:

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

上述说的开启tcp_timestamps,是需要客户端和服务端都开启。那么对于一端开启一端关闭的情况,表现如何?(待定 TODO

4.2. 流程图示

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

收到合法的SYN处理过程(双方都启用了 TCP 时间戳机制,TSval是发送报文时的时间戳):

收到合法的 SYN

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

收到合法的SYN处理过程(双方都启用了 TCP 时间戳机制,TSval 是发送报文时的时间戳):

收到非法的 SYN

这里特别注意下:服务端收到非法SYN后回复的是ACK(而不是RST),RST是客户端判断收到的ACK号不符合其预期才发起的。

根据上述说明,构造实验场景的思路:

  1. 情形1:两端都设置net.ipv4.tcp_timestamps=1,构造下次请求的seq比上次的小、或者构造发送时间戳比前面的小
  2. 情形2:两端都设置net.ipv4.tcp_timestamps=0,构造下次请求的seq比上次的小

而构造方式需要再考虑。星球给了一个方式:Server 端调大 net.ipv4.tcp_tw_timeout注意 该参数ALinux特有,可以起Alibaba Cloud Linux的ECS实验) 到600秒,时间长 seq 才有机会回绕

这里暂时仅作分析,先不进行实验。

5. TIME_WAIT 影响说明

1、TIME_WAIT 过多对客户端的影响(大多场景):

端口不够、搜索可用端口导致CPU 飙高、QPS 500 上限等,如果设置好 tw_bucket/reuse 可以解决这些问题

2、TIME_WAIT 过多对Server端的影响(也有Server主动断开的场景):

客户端 syn需要两次、syn 被reset等,主要是导致连接握手异常

但是TIME_WAIT状态是必要的

5.1. 为什么要设计 TIME_WAIT 状态?

参考:tcp_tw_reuse 为什么默认是关闭的?,详情请见原链接。

设计 TIME_WAIT 状态,主要有两个原因:

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

1、原因一:防止历史连接中的数据,被后面相同四元组的连接错误的接收

先了解下:

  • 序列号,是 TCP 一个头部字段(Seq),标识了 TCP 发送端到 TCP 接收端的数据流的一个字节,因为 TCP 是面向字节流的可靠协议,为了保证消息的顺序性和可靠性,TCP 为每个传输方向上的每个字节都赋予了一个编号,以便于传输成功后确认、丢失后重传以及在接收端保证不会乱序。序列号是一个 32 位的无符号数,因此在到达 4G 之后再循环回到 0(该情况称为 回绕)。这意味着无法根据序列号来判断新老数据
  • 初始序列号(Initial Sequence Number,ISN),在 TCP 建立连接的时候,客户端和服务端都会各自生成一个初始序列号,它是基于时钟生成的一个随机数,来保证每个连接都拥有不同的初始序列号。初始化序列号可被视为一个 32 位的计数器,该计数器的数值每 4 微秒加 1,循环一次需要 4.55 小时。

MSL(Maximum Segment Lifetime)是指 TCP 协议中任何报文在网络上最大的生存时间,任何超过这个时间的数据都将被丢弃。虽然 RFC 793 规定 MSL 为 2 分钟,但是在实际实现的时候会有所不同,比如 Linux 默认为 30 秒,那么 2MSL 就是 60 秒。

为了防止历史连接中的数据,被后面相同四元组的连接错误的接收,因此 TCP 设计了 TIME_WAIT 状态,状态会持续 2MSL 时长,这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。(能”保证”一次完整的发送->应答)

2、原因二:保证「被动关闭连接」的一方,能被正确的关闭

如果主动关闭方最后一次 ACK 报文(第四次挥手)在网络中丢失了,那么按照 TCP 可靠性原则,被动关闭方会重发 FIN 报文。

假设主动关闭方没有 TIME_WAIT 状态,而是在发完最后一次回 ACK 报文就直接进入 CLOSED 状态,如果该 ACK 报文丢失了,对端则重传 FIN 报文,而这时主动关闭方已经进入到关闭状态了,在收到对端重传的 FIN 报文后,就会回 RST 报文。

服务端收到这个 RST 并将其解释为一个错误(Connection reset by peer),这对于一个可靠的协议来说不是一个优雅的终止方式。

为了防止这种情况出现,客户端必须等待足够长的时间,确保服务端能够收到 ACK,如果服务端没有收到 ACK,那么就会触发 TCP 重传机制,服务端会重新发送一个 FIN,这样一去一来刚好两个 MSL 的时间。

再说下几个和TIME_WAIT相关的参数:

  • net.ipv4.tcp_tw_reuse:其作用是让客户端快速复用处于 TIME_WAIT 状态的端口,相当于跳过了 TIME_WAIT 状态。
    • 如果开启该选项的话,客户端(连接发起方) 在调用 connect() 函数时,如果内核选择到的端口,已经被相同四元组的连接占用的时候,就会判断该连接是否处于 TIME_WAIT 状态,如果该连接处于 TIME_WAIT 状态并且 TIME_WAIT 状态持续的时间超过了 1 秒,那么就会重用这个连接,然后就可以正常使用该端口了。所以该选项只适用于连接发起方。
    • 新内核中默认值为2,表示仅为回环地址开启复用,基本可以粗略的认为没开启复用
  • net.ipv4.tcp_tw_recycle:如果开启该选项的话,允许处于 TIME_WAIT 状态的连接被快速回收,该参数在 NAT 的网络下是不安全的!
  • net.ipv4.tcp_timestamps:tcp_timestamps 选项开启之后, PAWS(Protect Against Wrapped Sequences) 机制会自动开启,它的作用是防止 TCP 包中的序列号发生回绕。(感觉保护回绕更准确,Seq 4G回绕后借助timestamp判断)
    • 在开启 tcp_timestamps 选项情况下,一台机器发的所有 TCP 包都会带上发送时的时间戳,PAWS 要求连接双方维护最近一次收到的数据包的时间戳(Recent TSval),每收到一个新数据包都会读取数据包中的时间戳值跟 Recent TSval 值做比较,如果发现收到的数据包中时间戳不是递增的,则表示该数据包是过期的,就会直接丢弃这个数据包。

6. 构造场景(场景2)

针对上面说的Seq回绕并判断SYN非法的情况,构造场景复现。

6.1. 环境说明

环境:起两个阿里云抢占式实例,Alibaba Cloud Linux 3.2104 LTS 64位(内核版本:5.10.134-16.1.al8.x86_64)

相关内核网络参数如下:

1
2
3
4
5
6
7
8
9
10
11
[root@iZ2zeegk1auuwxkov67qfmZ ~]# sysctl -a|grep -E 'ip_local_port_range|tcp_max_tw_buckets|tcp_tw_reuse|tcp_rfc1337|tcp_timestamps|tcp_tw_timeout|tcp_tw_timeout'
net.ipv4.ip_local_port_range = 32768    60999
net.ipv4.tcp_max_tw_buckets = 5000
# 为1时丢掉RST,避免因为 TIME_WAIT 状态收到 RST 报文而跳过 2MSL 的时间;为0则收到RST时提前结束 TIME_WAIT 状态,释放连接
net.ipv4.tcp_rfc1337 = 0
net.ipv4.tcp_timestamps = 1
# 仅回环地址开启TIME_WAIT复用,基本可以粗略的认为没开启复用
net.ipv4.tcp_tw_reuse = 2
# Alinux特有,控制TIME_WAIT的持续时间
net.ipv4.tcp_tw_timeout = 60
net.ipv4.tcp_tw_timeout_inherit = 0

做服务端的那台安装需要的工具:yum install g++ bcc -y

6.2. (失败)构造方式

1、服务端:172.23.133.149

TIME_WAIT的持续时间改成10分钟,sysctl -w net.ipv4.tcp_tw_timeout=600(Alinux特有)

开不同终端分别进行监听、开启抓包、tcpstates观测

代码:accept连接后就close,github noreadg++ server.cpp -o server

1
2
3
4
5
# 终端1
[root@iZ2zeegk1auuwxkov67qfmZ ~]# tcpdump -i any port 8888 -nn -w server149_8888.cap -v

# 终端2
[root@iZ2zeegk1auuwxkov67qfmZ ~]# /usr/share/bcc/tools/tcpstates -L 8888

2、客户端:172.23.133.150,开启抓包,并指定端口请求 nc 172.23.133.149 8888 -p 12345

1
[root@iZ2zeegk1auuwxkov67qflZ ~]# tcpdump -i any port 8888 -nn -w client150_12345.cap -v

6.3. (失败)现象结果

客户端一共发起4次请求。前两次tcp_timestamps默认是开启的,后两次两端都关闭:sysctl -w net.ipv4.tcp_timestamps=0

服务端:

1、只观察到FIN-WAIT-2状态,没有TIME_WAIT (为什么?TODO)

1
2
3
[root@iZ2zeegk1auuwxkov67qfmZ ~]# ss -antp|grep 8888
LISTEN     0      5             0.0.0.0:8888          0.0.0.0:*     users:(("server",pid=5107,fd=3))                        
FIN-WAIT-2 0      0      172.23.133.149:8888   172.23.133.150:12345
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
[root@iZ2zeegk1auuwxkov67qfmZ ~]# /usr/share/bcc/tools/tcpstates -L 8888
SKADDR           C-PID C-COMM     LADDR           LPORT RADDR           RPORT OLDSTATE    -> NEWSTATE    MS
ffff9a7306de47c0 4630  server     0.0.0.0         8888  0.0.0.0         0     CLOSE       -> LISTEN      0.000
ffff9a7306de47c0 4630  server     0.0.0.0         8888  0.0.0.0         0     LISTEN      -> CLOSE       82994.442
ffff9a7306de5200 5107  server     0.0.0.0         8888  0.0.0.0         0     CLOSE       -> LISTEN      0.000
# 1
ffff9a7306de3d80 0     swapper/0  0.0.0.0         8888  0.0.0.0         0     LISTEN      -> SYN_RECV    0.000
ffff9a7306de3d80 0     swapper/0  172.23.133.149  8888  172.23.133.150  12345 SYN_RECV    -> ESTABLISHED 0.042
ffff9a7306de3d80 5107  server     172.23.133.149  8888  172.23.133.150  12345 ESTABLISHED -> FIN_WAIT1   0.090
ffff9a7306de3d80 0     swapper/0  172.23.133.149  8888  172.23.133.150  12345 FIN_WAIT1   -> FIN_WAIT2   0.704
ffff9a7306de3d80 0     swapper/0  172.23.133.149  8888  172.23.133.150  12345 FIN_WAIT2   -> CLOSE       0.002
# 2
ffff9a7306de3d80 0     swapper/0  0.0.0.0         8888  0.0.0.0         0     LISTEN      -> SYN_RECV    0.000
ffff9a7306de3d80 0     swapper/0  172.23.133.149  8888  172.23.133.150  12345 SYN_RECV    -> ESTABLISHED 0.010
ffff9a7306de3d80 5107  server     172.23.133.149  8888  172.23.133.150  12345 ESTABLISHED -> FIN_WAIT1   0.050
ffff9a7306de3d80 0     swapper/0  172.23.133.149  8888  172.23.133.150  12345 FIN_WAIT1   -> FIN_WAIT2   0.322
ffff9a7306de3d80 0     swapper/0  172.23.133.149  8888  172.23.133.150  12345 FIN_WAIT2   -> CLOSE       0.005
# 3
ffff9a7306de0a40 0     swapper/0  0.0.0.0         8888  0.0.0.0         0     LISTEN      -> SYN_RECV    0.000
ffff9a7306de0a40 0     swapper/0  172.23.133.149  8888  172.23.133.150  12345 SYN_RECV    -> ESTABLISHED 0.011
ffff9a7306de0a40 5107  server     172.23.133.149  8888  172.23.133.150  12345 ESTABLISHED -> FIN_WAIT1   0.077
ffff9a7306de0a40 0     swapper/0  172.23.133.149  8888  172.23.133.150  12345 FIN_WAIT1   -> FIN_WAIT2   0.597
ffff9a7306de0a40 0     swapper/0  172.23.133.149  8888  172.23.133.150  12345 FIN_WAIT2   -> CLOSE       0.003
# 4
ffff9a7306de2900 0     swapper/0  0.0.0.0         8888  0.0.0.0         0     LISTEN      -> SYN_RECV    0.000
ffff9a7306de2900 0     swapper/0  172.23.133.149  8888  172.23.133.150  12345 SYN_RECV    -> ESTABLISHED 0.008
ffff9a7306de2900 5107  server     172.23.133.149  8888  172.23.133.150  12345 ESTABLISHED -> FIN_WAIT1   0.041
ffff9a7306de2900 0     swapper/0  172.23.133.149  8888  172.23.133.150  12345 FIN_WAIT1   -> FIN_WAIT2   1.109
ffff9a7306de2900 0     swapper/0  172.23.133.149  8888  172.23.133.150  12345 FIN_WAIT2   -> CLOSE       0.003

2、抓包结果,4次都是服务端发送RST(和上面没有TIME_WAIT有关联),4次请求间隔最长差不多有6分钟

ECS上抓包情况

可以观察到客户端第2次和第3次时发SYN时有Seq回绕,但并不是服务端socket为TIME_WAIT时收到的SYN,还是要分析上面为什么没有TIME_WAIT

客户端:

1、现象都是请求后阻塞一段时间,最后输入回车后报错退出。抓包和服务端是一样的,见上面的图。

1
2
[root@iZ2zeegk1auuwxkov67qflZ ~]# nc 172.23.133.149 8888 -p 12345
Ncat: Broken pipe.

6.4. (失败)问题分析

上述几个流都由服务端(本处的主动发起关闭方)发起了RST,这里分析下原因。

再看下上述的抓包,服务端发起FIN后收到对端的ACK,变成FIN_WAIT2状态,本来正常的挥手应该由对端再发送FIN(看上面的流程图示更直观),而后主动发起方才变成TIME_WAIT状态。

但是nc 172.23.133.149 8888 -p 12345执行时阻塞在那,需要多次回车才结束,看过程抓包也是没发送FIN的。所以要查下:为什么nc在ECS上没发FIN呢?

尝试看下:

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
40
[root@iZ2zeegk1auuwxkov67qflZ ~]# strace -yy nc 172.23.133.149 8888 -p 12345
...
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 3<TCP:[60349]>
fcntl(3<TCP:[60349]>, F_GETFL)          = 0x2 (flags O_RDWR)
fcntl(3<TCP:[60349]>, F_SETFL, O_RDWR|O_NONBLOCK) = 0
setsockopt(3<TCP:[60349]>, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(3<TCP:[60349]>, {sa_family=AF_INET, sin_port=htons(12345), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
connect(3<TCP:[60349]>, {sa_family=AF_INET, sin_port=htons(8888), sin_addr=inet_addr("172.23.133.149")}, 16) = -1 EINPROGRESS (Operation now in progress)
select(4, [3<TCP:[172.23.133.150:12345->172.23.133.149:8888]>], [3<TCP:[172.23.133.150:12345->172.23.133.149:8888]>], [3<TCP:[172.23.133.150:12345->172.23.133.149:8888]>], {tv_sec=9, tv_usec=999000}) = 1 (out [3], left {tv_sec=9, tv_usec=998876})
getsockopt(3<TCP:[172.23.133.150:12345->172.23.133.149:8888]>, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
# 贴一下声明:int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
# 此处还有8888对应fd read事件的监控
select(4, [0</dev/pts/0<char 136:0>> 3<TCP:[172.23.133.150:12345->172.23.133.149:8888]>], [], [0</dev/pts/0<char 136:0>> 3<TCP:[172.23.133.150:12345->172.23.133.149:8888]>], NULL) = 1 (in [3])
recvfrom(3<TCP:[172.23.133.150:12345->172.23.133.149:8888]>, "", 8192, 0, 0x7ffc073e1710, [128 => 0]) = 0
close(1</dev/pts/0<char 136:0>>)        = 0
# 阻塞 (此处没有8888对应fd read事件监控了)
select(4, [0</dev/pts/0<char 136:0>>], [], [0</dev/pts/0<char 136:0>> 3<TCP:[172.23.133.150:12345->172.23.133.149:8888]>], NULL

# 第1次回车,已经发出了抓包对应的空数据,抓包里可以看出对端发了RST,本端也收到了
) = 1 (in [0])
recvfrom(0</dev/pts/0<char 136:0>>, 0x7ffc073e1790, 8192, 0, 0x7ffc073e1710, [128]) = -1 ENOTSOCK (Socket operation on non-socket)
read(0</dev/pts/0<char 136:0>>, "\n", 8192) = 1
# 8888对应fd write事件的监控
select(4, [], [3<TCP:[172.23.133.150:12345->172.23.133.149:8888]>], [0</dev/pts/0<char 136:0>> 3<TCP:[172.23.133.150:12345->172.23.133.149:8888]>], NULL) = 1 (out [3])
sendto(3<TCP:[172.23.133.150:12345->172.23.133.149:8888]>, "\n", 1, 0, NULL, 0) = 1
# 阻塞,但上面已经RST了(为什么还阻塞着?read、write事件监控都没了)
select(4, [0</dev/pts/0<char 136:0>>], [], [0</dev/pts/0<char 136:0>> 3<TCP:[172.23.133.150:12345->172.23.133.149:8888]>], NULL

# 第2次回车,无法向对端发任何东西
) = 1 (in [0])
recvfrom(0</dev/pts/0<char 136:0>>, 0x7ffc073e1790, 8192, 0, 0x7ffc073e1710, [128]) = -1 ENOTSOCK (Socket operation on non-socket)
read(0</dev/pts/0<char 136:0>>, "\n", 8192) = 1
select(4, [], [3<TCP:[172.23.133.150:12345->172.23.133.149:8888]>], [0</dev/pts/0<char 136:0>> 3<TCP:[172.23.133.150:12345->172.23.133.149:8888]>], NULL) = 1 (out [3])
sendto(3<TCP:[172.23.133.150:12345->172.23.133.149:8888]>, "\n", 1, 0, NULL, 0) = -1 EPIPE (Broken pipe)
--- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=14093, si_uid=0} ---
write(2</dev/pts/0<char 136:0>>, "Ncat: ", 6Ncat: ) = 6
write(2</dev/pts/0<char 136:0>>, "Broken pipe.\n", 13Broken pipe.
) = 13
exit_group(1)                           = ?
+++ exited with 1 +++

查看nc自身的打印信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@iZ2ze39uwj39zyd1pdvq3xZ ~]#  nc -vv 172.23.133.149 8888 -p 12345
libnsock nsock_iod_new2(): nsock_iod_new (IOD #1)
libnsock nsock_connect_tcp(): TCP connection requested to 172.23.133.149:8888 (IOD #1) EID 8
libnsock mksock_bind_addr(): Binding to 0.0.0.0:12345 (IOD #1)
libnsock nsock_trace_handler_callback(): Callback: CONNECT SUCCESS for EID 8 [172.23.133.149:8888]
Ncat: Connected to 172.23.133.149:8888.
libnsock nsock_iod_new2(): nsock_iod_new (IOD #2)
libnsock nsock_read(): Read request from IOD #1 [172.23.133.149:8888] (timeout: -1ms) EID 18
libnsock nsock_readbytes(): Read request for 0 bytes from IOD #2 [peer unspecified] EID 26
# 阻塞
libnsock nsock_trace_handler_callback(): Callback: READ EOF for EID 18 [172.23.133.149:8888]

# 回车
libnsock nsock_trace_handler_callback(): Callback: READ SUCCESS for EID 26 [peer unspecified] (1 bytes): .
libnsock nsock_write(): Write request for 1 bytes to IOD #1 EID 35 [172.23.133.149:8888]
libnsock nsock_trace_handler_callback(): Callback: WRITE SUCCESS for EID 35 [172.23.133.149:8888]
# 阻塞
libnsock nsock_readbytes(): Read request for 0 bytes from IOD #2 [peer unspecified] EID 42

# 回车
libnsock nsock_trace_handler_callback(): Callback: READ SUCCESS for EID 42 [peer unspecified] (1 bytes): .
libnsock nsock_write(): Write request for 1 bytes to IOD #1 EID 51 [172.23.133.149:8888]
libnsock nsock_trace_handler_callback(): Callback: WRITE ERROR [Broken pipe (32)] for EID 51 [172.23.133.149:8888]
Ncat: Broken pipe.

没直接看出啥问题,试了下服务端先发送部分数据,表现也一样。想回头strace-v跟踪下CentOS8.5上的过程用作对比,客户端和服务端都在这台采集,发现表现跟这里一样,也不发送FIN!!!(之前场景1的客户端机器是MacOS)

场景1的实验里,是由于nc在MacOS上执行的,收到服务端FIN主动关闭后,后面发送了FIN完成挥手,依赖客户端的实现。

涉及nc不同系统的实现机制,先不纠结,换一个客户端工具。

6.5. (成功)客户端切换为curl重新验证

curl--local-port选项,可指定本地端口(--local-port 12345)或端口范围(--local-port 4000-4200

代码:accept连接后read再close,read下curl请求要不curl会请求失败,github withread

下述IP说明:重新拉了2个ECS,服务端172.23.133.151、客户端172.23.133.152。

1)尝试1:手动curl调用,隔个十几秒调用并观察实时抓包的Seq(一个窗口-w保存抓包、一个实时观察),回绕时未观察到RST

2)尝试2:按星球建议的重现技巧,观察到了(差别是请求间隔和次数,为什么?TODO)

  • Server 端调大 net.ipv4.tcp_tw_timeout 到600秒(之前设置过)
  • Server 和 Client 都关闭 net.ipv4.tcp_timestamps(之前设置过)
  • 客户端两次连接间隔150秒左右(这次不请求那么频繁了,sleep后再请求

客户端请求3次,每次间隔150s,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
# 脚本内容
[root@iZ2ze39uwj39zyd1pdvq3xZ ~]# cat test.sh 
curl 172.23.133.151:8888 --local-port 12345
sleep 150
curl 172.23.133.151:8888 --local-port 12345
sleep 150
curl 172.23.133.151:8888 --local-port 12345

# 执行
[root@iZ2ze39uwj39zyd1pdvq3xZ ~]# sh test.sh
curl: (52) Empty reply from server
curl: (52) Empty reply from server
curl: (52) Empty reply from server

服务端收到请求打印形式如下:

1
2
3
4
5
6
7
Client: GET / HTTP/1.1
Host: 172.23.133.151:8888
User-Agent: curl/7.61.1
Accept: */*


close, client_ip:172.23.133.152, port:12345

看下抓包:服务端和客户端抓到的包是一样的。

服务端客户端抓包情况

服务端抓包文件
客户端抓包文件

6.6. (成功)结果和问题分析

发了3个请求,每个请求间隔150s:

  • stream0正常三次握手和四次挥手
  • stream1、stream2,服务端对三次握手时的SYN应答 TCP ACKed unseen segment,客户端于是RST之前的连接;然后客户端发起新的三次握手

下面取stream1的Flow Graph进行分析:

stream1 Flow Graph

分析上述过程,存在如下疑问。TODO

6.6.1. 客户端TCP Port numbers reused问题

  • 1、客户端第2、第3个stream发起的SYN握手,为什么也是 TCP Port numbers reused(上图mark0)? 客户端请求完后netstat/ss看是已经没有任何888812345的连接的,之前端口用完释放了才对?

分析解答:

这里的TCP Port numbers reused提示,是Wireshark的TCP解析器提供的分析功能,可以在Wireshark设置->协议->TCP中,勾选/取消“Analyze TCP sequence numbers”来启用或禁用此功能。(还有些其他场景可参考:TCP Analysis Flags 之 TCP Port numbers reused

针对 SYN 数据包(实际SYN+ACK包也是),如果已经有一个使用相同 IP+Port 的会话,并且这个 SYN 的序列号与已有会话的 ISN 不同时就会设置TCP Port numbers reused标记。

所以这个只是Wireshark侧的辅助信息,可展开抓包看下:

wireshark分析port reused

注意:这里展示的port reused内核的端口重用(REUSEPORT)特性不是一回事,端口重用允许同一机器上的多个进程同时创建不同的socket来bindlisten在相同的端口上,然后在内核层面实现多个用户进程的负载均衡。可通过setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, ...)方式开启该特性,进一步了解可见:深入理解Linux端口重用这一特性

6.6.2. Seq回绕时应答的ACK 和 Challenge ACK 问题

  • 2、对于mark0处对应的发起SYN后收到的ACK(mark1,被标记为 TCP ACKed unseen segment),和处于Established状态收到SYN而收到的 Challenge ACK是一回事吗?

Challenge ACK,相关机制具体参考:4.9 已建立连接的TCP,收到SYN会发生什么?

6.6.3. 服务端TIME_WAIT收到RST的表现问题

  • 3、对于客户端发送的RST(上图mark2),服务端收到后的表现具体如何?

理论上和net.ipv4.tcp_rfc1337有关,为0时(默认值,当前环境也是0)则提前结束 TIME_WAIT 状态,释放连接;若为1,则会丢掉该 RST 报文。参考

但是看后面mark4好像有复用关系?此环境tcp_rfc1337为0,没结束TIME_WAIT释放连接吗?

解答:

这个复用同问题1,只是Wireshark发现前面用了相同IP+Port的辅助信息,不用关注了。

至于tcp_rfc1337为0,此处应该是释放了之前的TIME_WAIT

6.6.4. 重新发起SYN为什么也是TCP Port numbers reused

  • 4、mark3从Seq看是mark0的重传,重新发起SYN三次握手,这里疑问还是和第1个一样,为什么是TCP Port numbers reused

解答:

这个复用也是同问题1,只是Wireshark发现前面用了相同IP+Port的辅助信息,不用关注了。

6.6.5. 服务端端口重用问题 及 为什么被标记重传

  • 5、mark4是服务端复用TIME_WAIT四元组端口?和前面第3个问题一起待定。另外,这里标记的重传,是谁的重传?

分析:客户端重新发SYN握手时,服务端应答SYN+ACK,其中Seq是重新生成的,对应上面说的初始序列号mark1里Seq对应的是老连接)

看起来是正常的三次握手,为什么标记成重传了

1
13	20:17:27.322431	172.23.133.151	172.23.133.152	8888	12345	68	TCP	64	0.000029000	[TCP Retransmission] [TCP Port numbers reused] 8888 → 12345 [SYN, ACK] Seq=301265408 Ack=226707748 Win=64240 Len=0 MSS=1460 SACK_PERM WS=128

没找到其他Seq=301265408的包,待定

6.6.6. 服务端FIN+ACK包为什么被标记重传

  • 6、mark5的重传又是什么鬼?发FIN的同时重传ACK? 印象里ACK不存在重传啊?

分析:类似上面端口复用的提示?应该要看下Wireshark的判断规则,本篇暂不深入分析了

没注意看,确实是重传了上一个包(Seq都是301265409),这次只是多加了FIN(ACK也有重传?)

1
2
16	20:17:27.322773	172.23.133.151	172.23.133.152	8888	12345	56	TCP	64	0.000011000	8888 → 12345 [ACK] Seq=301265409 Ack=226707831 Win=64256 Len=0
17	20:17:27.322815	172.23.133.151	172.23.133.152	8888	12345	56	TCP	64	0.000042000	[TCP Retransmission] 8888 → 12345 [FIN, ACK] Seq=301265409 Ack=226707831 Win=64256 Len=0

6.6.7. 一端开启一端关闭tcp_timestamps表现如何

上面描述”SYN是否合法”的小节留的TODO:时间回绕判断一般需要客户端和服务端都开启tcp_timestamps,那么对于一端开启一端关闭的情况,表现如何?

一般两端都开启(下面代码中有对应判断),本篇暂不深入分析了。

7. TCP接收处理源码简要说明

af_inet.c里初始化:inet_init->inet_add_protocol(&tcp_protocol, IPPROTO_TCP),里面会注册tcp_v4_rcv为TCP层的处理入口函数。下面简要分析TIME_WAIT时的处理。

7.1. tcp_v4_rcv

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// linux-5.10.10/net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
    struct net *net = dev_net(skb->dev);
    struct sk_buff *skb_to_free;
    int sdif = inet_sdif(skb);
    int dif = inet_iif(skb);
    const struct iphdr *iph;
    const struct tcphdr *th;
    bool refcounted;
    struct sock *sk;
    ...
    // TCP协议头
    th = (const struct tcphdr *)skb->data;
    // IP协议头
    iph = ip_hdr(skb);
lookup:
    // 根据四元组查找对应的sock
    sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
                   th->dest, sdif, &refcounted);
process:
    // 如果连接的状态为TIME_WAIT,会跳转到 do_time_wait
    if (sk->sk_state == TCP_TIME_WAIT)
        // 本地为TIME_WAIT状态时,跳到do_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,允许TIM_WAIT状态跃迁到SYN_RECV
    case TCP_TW_SYN: {
        struct sock *sk2 = inet_lookup_listener(dev_net(skb->dev),
                            &tcp_hashinfo, skb,
                            __tcp_hdrlen(th),
                            iph->saddr, th->source,
                            iph->daddr, th->dest,
                            inet_iif(skb),
                            sdif);
        if (sk2) {
            inet_twsk_deschedule_put(inet_twsk(sk));
            sk = sk2;
            tcp_v4_restore_cb(skb);
            refcounted = false;
            goto process;
        }
    }
        /* to ACK */
        fallthrough;
    // 如果是TCP_TW_ACK,返回记忆中的ACK(上一次发送的ACK)
    case TCP_TW_ACK:
        tcp_v4_timewait_ack(sk, skb);
        break;
    // 如果是TCP_TW_RST,直接发送RST包
    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:;
    }
    ...
}

7.2. tcp_timewait_state_process

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// linux-5.10.10/net/ipv4/tcp_minisocks.c
enum tcp_tw_status
tcp_timewait_state_process(struct inet_timewait_sock *tw, struct sk_buff *skb,
               const struct tcphdr *th)
{
    struct tcp_options_received tmp_opt;
    struct tcp_timewait_sock *tcptw = tcp_twsk((struct sock *)tw);
    //paws_reject 为 false,表示没有发生时间戳回绕
    //paws_reject 为 true,表示发生了时间戳回绕
    bool paws_reject = false;
    tmp_opt.saw_tstamp = 0;
    // TCP头中有选项且旧连接开启了时间戳选项
    // th->doff表示数据偏移量,即 TCP 报头的长度。以 32 位字的形式表示,用于确定数据开始的位置。它的值乘以 4 得到报头长度的字节数。
    // tw_ts_recent_stamp 记录最近一次更新 `tw_ts_recent` 的实际时间(通常是系统时间或 jiffies)
    if (th->doff > (sizeof(*th) >> 2) && tcptw->tw_ts_recent_stamp) {
        tcp_parse_options(twsk_net(tw), skb, &tmp_opt, 0, NULL); // 解析TCP选项放入tmp_opt。其中若协商过时间戳选项或者本地开启了tcp_timestamps,则对时间戳进行记录
        // 启用了 TCP 时间戳选项
        if (tmp_opt.saw_tstamp) {
            // rcv_tsecr:接收方将其上次接收到的数据包中的TSval值填入TSecr并返回给发送方
            if (tmp_opt.rcv_tsecr)
                // tw_ts_offset 是时间戳偏移量,用于防止 TIME-WAIT 重用问题,即在新的连接中避免时间戳冲突
                tmp_opt.rcv_tsecr -= tcptw->tw_ts_offset;
            // ts_recent:存储的是接收到的最新时间戳值。这值是对方在其发送的 TCP 包的时间戳选项(TSval字段)中提供的值。
                // 当一个新的 TCP 包被接收并且其中的时间戳值大于当前的 `ts_recent` 时,这个字段就会被更新,并且 `ts_recent_stamp` 也会相应更新
            // tw_ts_recent:存储最近一次接收到的 TCP 时间戳值。该值用于 PAWS 机制,确保 TIME-WAIT 期间的时间戳一致
            tmp_opt.ts_recent	= tcptw->tw_ts_recent;
            // 上次记录的时间戳
            // 每当 `ts_recent` 更新时,相应地,`ts_recent_stamp` 也会更新为内核当前的时间值。
            tmp_opt.ts_recent_stamp	= tcptw->tw_ts_recent_stamp;
            // 检查收到的报文的时间戳是否发生了时间戳回绕
            paws_reject = tcp_paws_reject(&tmp_opt, th->rst);
        }
    }
    ...
    // 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) {
            // 不开启 sysctl_tcp_rfc1337 选项,当收到 RST 时会立即回收tw
            if (twsk_net(tw)->ipv4.sysctl_tcp_rfc1337 == 0) {
kill:
                // 删除tw定时器,并释放tw
                inet_twsk_deschedule_put(tw);
                // 如果是TCP_TW_SUCCESS,则直接丢弃此包,不做任何响应
                return TCP_TW_SUCCESS;
            }
        } else {
            // 将 TIMEWAIT 状态的持续时间重新延长
            inet_twsk_reschedule(tw, TCP_TIMEWAIT_LEN);
        }
        ...
        // 如果是TCP_TW_SUCCESS,则直接丢弃此包,不做任何响应
        return TCP_TW_SUCCESS;
    }

    //是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 (paws_reject)
        __NET_INC_STATS(twsk_net(tw), LINUX_MIB_PAWSESTABREJECTED);

    if (!th->rst) {
        // 如果时间戳回绕,或者报文里包含ack,则将 TIMEWAIT 状态的持续时间重新延长
        if (paws_reject || th->ack)
            inet_twsk_reschedule(tw, TCP_TIMEWAIT_LEN);

        return tcp_timewait_check_oow_rate_limit(
            tw, skb, LINUX_MIB_TCPACKSKIPPEDTIMEWAIT);
    }
    inet_twsk_put(tw);
    return TCP_TW_SUCCESS;
}

7.3. struct tcp_options_received

上面tmp_opt.ts_recent/tmp_opt.ts_recent_stamp等赋值涉及tcp_options_receivedtcp_timewait_sock结构,这里说明下。

  • tcp_options_received 是 Linux 内核中用于处理接收到的 TCP 选项的重要结构。此结构在 TCP 协议的实现中起着至关重要的作用。TCP 选项是 TCP 首部的一部分,用于提供附加的通信控制功能,例如窗口扩展、时间戳等。
  • tcp_timewait_sock 是 Linux 内核中用于管理 TIME-WAIT 状态的结构。

TCP三次握手的过程中会协商各类选项,包括时间戳选项。如果双方都支持时间戳选项,就会使用时间戳进行后续通信。

下面说明下字段含义,有助于后续梳理代码逻辑。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// linux-5.10.10/include/linux/tcp.h
// 提供了一个集中化的地方来保持所有已接收 TCP 选项的状态。处理函数会根据这些选项来调整 TCP 连接的行为。
// 通过解析 TCP 包中的选项字段,然后填充这个结构,TCP 协议可以动态调整其操作。
struct tcp_options_received {
/*	PAWS/RTTM data	*/
    // `ts_recent_stamp` 是一个时间戳,
    // 记录的是最近一次更新 `ts_recent` 的内核时间。这是一个内核时间的标记,通常以系统时间或者定时器滴答值(jiffies)表示,用于衡量时间流逝的。
    // 主要用于计算 TCP 连接中的 RTT(Round-Trip Time),以及进行 PAWS(Protect Against Wrapped Sequence numbers)保护机制中的时间验证。
    // 每当 `ts_recent` 更新时,相应地,`ts_recent_stamp` 也会更新为内核当前的时间值。这样能够帮助内核知道 `ts_recent` 是什么时候最后更新的,从而进行时间相关的验证和计算。
    int	ts_recent_stamp;/* Time we stored ts_recent (for aging) */
    // 存储的是接收到的最新时间戳值。这值是对方在其发送的 TCP 包的时间戳选项(TSval字段)中提供的值。
    // PAWS 通过检查接收的时间戳值是否小于 `ts_recent` 来防止因 TCP 序列号回绕(序列号重新开始)引起的包混乱
    // 当一个新的 TCP 包被接收并且其中的时间戳值大于当前的 `ts_recent` 时,这个字段就会被更新,并且 `ts_recent_stamp` 也会相应更新
    u32	ts_recent;	/* Time stamp to echo next		*/
    // 时间戳值(Timestamp Value, TSval):发送方获取本地时钟的当前值填入TSval
    u32	rcv_tsval;	/* Time stamp value             	*/
    // 回送时间戳回显值(Timestamp Echo Reply field, TSecr):接收方将其上次接收到的数据包中的TSval值填入TSecr并返回给发送方
    u32	rcv_tsecr;	/* Time stamp echo reply        	*/
    // `saw_tstamp` 是一个标志位,表示该连接是否启用了 TCP 时间戳选项。
    // 当一个 TCP 包被接收并且其中包含时间戳选项时,这个标志位就会被设置为1。根据这个标志位,内核可以决定是否需要解析和使用其他时间戳相关的字段。
    // 如果 `saw_tstamp` 是1,表示时间戳功能已经确认,可以信赖这些时间戳数据来进行 RTT 计算和 PAWS 保护
    u16 	saw_tstamp : 1,	/* Saw TIMESTAMP on last packet		*/
    // 一个标志,表示时间戳选项是否已协商通过
        tstamp_ok : 1,	/* TIMESTAMP seen on SYN packet		*/
        dsack : 1,	/* D-SACK is scheduled			*/
        // 一个标志,表示窗口扩大选项是否已协商通过
        wscale_ok : 1,	/* Wscale seen on SYN packet		*/
        // 一个标志,表示选择性确认(Selective ACK)的支持情况。它值为3是为了确保多次协商的结果一致。
        sack_ok : 3,	/* SACK seen on SYN packet		*/
        smc_ok : 1,	/* SMC seen on SYN packet		*/
        // 存储发送方向的窗口扩大因子(scale factor)
        snd_wscale : 4,	/* Window scaling received from sender	*/
        // 存储接收方向的窗口扩大因子(scale factor)
        rcv_wscale : 4;	/* Window scaling to send to receiver	*/
    u8	saw_unknown:1,	/* Received unknown option		*/
        unused:7;
    // 用于统计选项 SACK(Selective Acknowledgment)的数量
    u8	num_sacks;	/* Number of SACK blocks		*/
    // 用户设置的最大报文段大小(MSS, Maximum Segment Size)
    u16	user_mss;	/* mss requested by user in ioctl	*/
    // 代表协商过后使用的最大报文段大小(MSS, Maximum Segment Size)
    u16	mss_clamp;	/* Maximal mss, negotiated at connection setup */
};

// linux-5.10.10/include/linux/tcp.h
struct tcp_timewait_sock {
    // 基类,包含通用的 TIME-WAIT 状态信息,如源、目标地址等。
    struct inet_timewait_sock tw_sk;
    // 下一个期待接收的序列号。辅助确保重传数据包可以被正确识别和处理。
#define tw_rcv_nxt tw_sk.__tw_common.skc_tw_rcv_nxt
    // 下一个要发送的序列号。在 TIME-WAIT 状态期间,这个字段通常不会改变,但仍然需要保留它以应对潜在的重传和处理。
#define tw_snd_nxt tw_sk.__tw_common.skc_tw_snd_nxt
    // 接收窗口大小
    u32			  tw_rcv_wnd;
    // 时间戳偏移量,用于防止 TIME-WAIT 重用问题,即在新的连接中避免时间戳冲突。
    u32			  tw_ts_offset;
    // 存储最近一次接收到的 TCP 时间戳值。该值用于 PAWS 机制,确保 TIME-WAIT 期间的时间戳一致。
    u32			  tw_ts_recent;

    /* The time we sent the last out-of-window ACK: */
    u32			  tw_last_oow_ack_time;

    // 记录最近一次更新 `tw_ts_recent` 的实际时间(通常是系统时间或 jiffies)。这对于时间相关的验证非常重要。
    int			  tw_ts_recent_stamp;
    u32			  tw_tx_delay;
#ifdef CONFIG_TCP_MD5SIG
    struct tcp_md5sig_key	  *tw_md5_key;
#endif
};

三次握手时会协商TCP选项,下面是开启和关闭net.ipv4.tcp_timestamps时握手的抓包对比:

tcp头选项-timestamp对比

8. 小结

TIME_WAIT状态的连接收到同四元组的SYN的表现做了实验分析。踩了一些坑,还有好几个问题待定分析。

9. 参考

1、连接处于 TIME_WAIT 状态,这时收到了 syn 握手包

2、4.11 在 TIME_WAIT 状态的 TCP 连接,收到 SYN 后会发生什么?

3、K8S最佳实践-网络性能调优

4、tcp_tw_reuse 为什么默认是关闭的?

5、4.9 已建立连接的TCP,收到SYN会发生什么?

6、SYN 报文什么时候情况下会被丢弃?

7、深入理解Linux端口重用这一特性

8、TCP Analysis Flags 之 TCP Port numbers reused

9、GPT



Comments