前言
默认情况下,Wireshark 的 TCP 解析器会跟踪每个 TCP 会话的状态,并在检测到问题或潜在问题时提供额外的信息。在第一次打开捕获文件时,会对每个 TCP 数据包进行一次分析,数据包按照它们在数据包列表中出现的顺序进行处理。可以通过 “Analyze TCP sequence numbers” TCP 解析首选项启用或禁用此功能。
TCP 分析展示
在 TCP 分析中和 TCP Keep-Alive 相关的实际上有两种信息,分别是:TCP Keep-Alive
、TCP Keep-Alive ACK
。实际运行环境中,有时 TCP Keep-Alive
单独出现的,因为有可能对端未确认 ,有时是一起出现的,也就是对端对 TCP Keep-Alive
数据包进行了确认,回复了 TCP Keep-Alive ACK
数据包。
在数据包文件中进行 TCP 分析时,关于 “TCP Keep-Alive”、“TCP Keep-Alive ACK” 一般是如下显示的,包括:
- Packet List 窗口中的 Info 信息列,以 [TCP Keep-Alive]、[TCP Keep-Alive ACK] 黑底红字进行标注;
- Packet Details 窗口中的 TCP 协议树下,在 [SEQ/ACK analysis] -> [TCP Analysis Flags] 中定义该 TCP 数据包的分析说明。
TCP Keep-Alive 定义
实际在 TCP 分析中,关于 TCP Keep-Alive
和 TCP Keep-Alive ACK
相关的定义也相对简单,主要说明如下:
- TCP Keep-Alive
当 TCP 数据段大小为 0 或 1 时设置,当前序列号比下一个期望的序列号小 1 字节,并且没有设置 SYN、FIN 或 RST。
替代 Fast Retransmission
、Out-Of-Order
、Spurious Retransmission
、Retransmission
。
Set when the segment size is zero or one, the current sequence number is one byte less than the next expected sequence number, and none of SYN, FIN, or RST are set.Supersedes “Fast Retransmission”, “Out-Of-Order”, “Spurious Retransmission”, and “Retransmission”.
next expected sequence number,为 nextseq,定义为 highest seen nextseq。
具体的代码如下,总的来说这段代码的作用是检测出 TCP 保活包,并对其进行适当的标记,以便 Wireshark 能够正确识别和显示这种特殊的 TCP 控制数据包,帮助分析长连接的保活状态。这段代码的主要逻辑如下,如果所有下述条件均满足,则认为该数据包是一个保活探测包。
- 检查 TCP 段大小是否为 0 或 1 字节;
- 检查 Seq Num 比同方向之前的下一个期望的 Seq Num 少 1;
- 检查当前数据包是否不是 SYN/FIN/RST 数据包。
/* KEEP ALIVE* a keepalive contains 0 or 1 bytes of data and starts one byte prior* to what should be the next sequence number.* SYN/FIN/RST segments are never keepalives*/if( (seglen==0||seglen==1)&& seq==(tcpd->fwd->tcp_analyze_seq_info->nextseq-1)&& (flags&(TH_SYN|TH_FIN|TH_RST))==0 ) {if(!tcpd->ta) {tcp_analyze_get_acked_struct(pinfo->num, seq, ack, TRUE, tcpd);}tcpd->ta->flags|=TCP_A_KEEP_ALIVE;}
- TCP Keep-Alive ACK
当以下所有条件都为真时设置该标志:
- TCP 段大小为 0
- 窗口大小非零且没有改变
- Seq Num 等于之前下一个期望的 Seq Num
- ACK Num 等于之前的 LastACK Num
- 最近看到的一个反向数据包是 Keep-Alive
- SYN、FIN、RST 均未设置
替代 Dup ACK
和 ZeroWindowProbeAck
。
Set when all of the following are true:The segment size is zero.
The window size is non-zero and hasn’t changed.
The current sequence number is the same as the next expected sequence number.
The current acknowledgment number is the same as the last-seen acknowledgment number.
The most recently seen packet in the reverse direction was a keepalive.
The packet is not a SYN, FIN, or RST.Supersedes “Dup ACK” and “ZeroWindowProbeAck”.
具体的代码如下,总的来说这段代码的作用是检测对 TCP 保活包的 ACK 响应,并对其进行适当的标记,以便 Wireshark 能够正确分析和显示 TCP 长连接的保活状态。这段代码的主要逻辑如下,如果所有下述条件均满足,则认为该数据包是一个保活探测响应包。
- 检查 TCP 段大小是否为 0;
- 检查窗口大小是否不为 0;
- 检查当前窗口大小与同方向之前窗口大小是否相同;
- 检查 Seq Num 是否等于同方向之前下一个期望的 Seq Num;
- 检查 ACK Num 是否等于同方向之前的 LastACK Num;
- 检查反方向上一个数据包是否是 TCP Keep-Alive;
- 检查当前数据包是否不是 SYN/FIN/RST 数据包。
/* KEEP ALIVE ACK* It is a keepalive ack if it repeats the previous ACK and if* the last segment in the reverse direction was a keepalive*/if( seglen==0&& window&& window==tcpd->fwd->window&& seq==tcpd->fwd->tcp_analyze_seq_info->nextseq&& ack==tcpd->fwd->tcp_analyze_seq_info->lastack&& (tcpd->rev->lastsegmentflags&TCP_A_KEEP_ALIVE)&& (flags&(TH_SYN|TH_FIN|TH_RST))==0 ) {if(!tcpd->ta) {tcp_analyze_get_acked_struct(pinfo->num, seq, ack, TRUE, tcpd);}tcpd->ta->flags|=TCP_A_KEEP_ALIVE_ACK;goto finished_fwd;}
- next expected sequence number,为 nextseq,定义为 highest seen nextseq。
- lastack,定义为 Last seen ack for the reverse flow。
Packetdrill 示例
在上述 TCP Keep-Alive
和 TCP Keep-Alive ACK
定义和代码可知,TCP 分析的逻辑相对比较简单,因此通过 packetdrill 比较容易模拟出相关现象。
# cat tcp_keep_alive.pkt
0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 bind(3, ..., ...) = 0
+0 listen(3, 1) = 0+0 < S 0:0(0) win 1000 <mss 1460>
+0 > S. 0:0(0) ack 1 <...>
+0.01 < . 1:1(0) ack 1 win 1000+0 accept(3, ..., ...) = 4
+.1 write(4, ..., 500) = 500
+0 > P. 1:501(500) ack 1
+0.1 < . 1:1(0) ack 501 win 1000+0.1 < . 0:0(0) ack 501 win 1000
#
经 Wireshark 展示如下,可以看到满足判断条件后,No.6 标识 [TCP Keep-Alive]
,No.7 标识 [TCP Keep-Alive ACK]
,此时 No.6 的 TCP Len 长度为 0 字节。
同样,可以验证一下 TCP Keep-Alive 数据包 TCP Len 长度为 1 字节的场景,这在 RFC9293 中也明确说明,0 或 1 字节都可以。
# cat tcp_keep_alive_02.pkt
0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 bind(3, ..., ...) = 0
+0 listen(3, 1) = 0+0 < S 0:0(0) win 1000 <mss 1460>
+0 > S. 0:0(0) ack 1 <...>
+0.01 < . 1:1(0) ack 1 win 1000+0 accept(3, ..., ...) = 4
+.1 write(4, ..., 500) = 500
+0 > P. 1:501(500) ack 1
+0.1 < . 1:1(0) ack 501 win 1000+0.1 < . 0:1(1) ack 501 win 1000
#
经 Wireshark 展示如下,可以看到满足判断条件后,No.6 标识 [TCP Keep-Alive]
,No.7 标识 [TCP Keep-Alive ACK]
,此时 No.6 的 TCP Len 长度为 1 字节。
实例
关于 TCP Keep-Alive
的实例,正常来说日常抓包中应该比较少看到,TCP Keep-Alive
数据包必须仅在没有未完成的发送数据,且在一定时间间隔内没有收到连接的数据或确认数据包时发送,默认的时间间隔为 2 小时,相对来说很长。
- TCP Keep-Alive Len 0 场景
TCP Keep-Alive
数据包 TCP Len 为 0 的场景相对来说更容易碰到。如下 TCP 流在经过 45 秒间隔后,客户端发出了 No.579 [TCP Keep-Alive]
, 此时 TCP Len 为 0,之后服务器端响应 No.580 [TCP Keep-Alive ACK]
。
- TCP Keep-Alive Len 1 场景
TCP Keep-Alive
数据包 TCP Len 为 1 的场景相对来说少见。如下 TCP 流在经过 45 秒间隔后,客户端发出了 No.151 [TCP Keep-Alive]
, 此时 TCP Len 为 1,之后服务器端响应 No.152 [TCP Keep-Alive ACK]
,之后每经过 45 间隔后,就继续保活检测和响应。
此案例有意思的是,No.151 TCP Keep-Alive
数据包 Seq Num 为 1155,TCP Len 为 1 ,相较于 No.124 Next Seq Num 1156 以及 No.125 ACK Num 1156,No.151 实际上回退了 1 字节,即所携带的 1 字节数据段为垃圾数据,而对于 No.152 在收到该数据后,除了响应标记 TCP Keep-Alive ACK
之外,同时会标记 [SLE==1155 SRE==1156]
,说明这 1 个字节的数据之前已收到过,属于重复数据,这属于该 TCP 流建连时均支持 SACK ,其中 D-SACK 的功能实现。
- TCP Keep-Alive 无响应场景
无响应的场景没有什么太多要说的,这也是网上经常讨论的,没有数据传输的时候,对端主机宕机或是什么拔掉网线的场景。一端到了规定的时间之后,即发出 TCP Keep-Alive
数据包,对端如果无响应,则每间隔一段时间继续发送探测,持续规定的一定次数之后,中断该连接。在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:
net.ipv4.tcp_keepalive_time=7200
net.ipv4.tcp_keepalive_intvl=75
net.ipv4.tcp_keepalive_probes=9
- 不是 TCP Keep-Alive 的 TCP Keep-Alive 的场景一
一个特殊的例子,仔细看 No.1-2,再对比下标记成 TCP Keep-Alive
和 TCP Keep-Alive ACK
的 No.134-135,你会发现实际的 Seq Num、ACK Num、TCP Len 以及 SLE SRE 基本都是一模一样的。
No.134-135 是 TCP Keep-Alive
和 TCP Keep-Alive ACK
,为什么 No.1-2 在这不是?真实的答案是,No.1-2 也是一对 TCP Keep-Alive
和 TCP Keep-Alive ACK
数据包。原因自然是关于数据包跟踪文件中上下文的关系,对于 No.1-2 数据包,它们是这个 TCP 流中抓取的前两个数据包,从 Wireshark 代码判断条件下,它是无法匹配 TCP Keep-Alive
和 TCP Keep-Alive ACK
的判断条件的,因此 No.1-2 不会标记,但实际确实是。
对比一下案例2,和案例4取自同一个跟踪文件,只不过案例2的TCP流中抓取到了前置数据包 No.123-125,因此后续的
TCP Keep-Alive
均能正常识别,而案例4缺少前置数据包,No.1-2 因此不会标记,空空如也。
- 不是 TCP Keep-Alive 的 TCP Keep-Alive 的场景二
另一个特殊的例子,其实原因是一样的,均是在 TCP 流中缺少前置数据包,只不过案例 4 中没有任何标记,而在此案例中会被标识为其它,也影响一定判断。
数据包 No.1-2 的类型实际上也是一对 TCP Keep-Alive
和 TCP Keep-Alive ACK
数据包。因为捕获数据包并未抓到之前的包,Wireshark 无法根据上文判断,所以并未标记成 TCP Keep-Alive
数据包,造成之后的数据包 No.2-4 进行 TCP 分析判断时出现偏差,认为是存在丢包情况。
该案例之前在《丢包?不要轻易下结论续》有详细解释,有兴趣的可以瞅瞅。
总结
看似简单的 TCP Keep-Alive
,其实也有大学问,不是嘛。