Mininet 使用笔记(二):TCP四次挥手
一、准备
模拟 TCP 四次挥手场景
h1 (服务端 10.0.0.1)、h2 (客户端 10.0.0.2)。四次挥手报文序列:
- 客户端 h2 → h1:FIN(挥手包 1)
- 服务端 h1 → h2:ACK(确认客户端 FIN,挥手包 2)
- 服务端 h1 → h2:FIN(服务端关闭发送,挥手包 3)
- 客户端 h2 → h1:ACK(确认服务端 FIN,挥手包 4)
分析 tcpdump 报文流向
| 丢包阶段 | 丢包流向 | 持续重传方 & 重传报文 | 连接滞留状态 | 观测抓包主机 |
|---|---|---|---|---|
| 客户户端 FIN 丢失(挥手1) | h2→h1 | h2 反复重传【客户端 FIN】 | h2:FIN_WAIT_1 | h2 |
| 服务端 ACK 丢失(挥手2) | h1→h2 | h2 反复重传【客户端 FIN】 | h2:FIN_WAIT_1;h1:CLOSE_WAIT | h1 |
| 服务端 FIN 丢失(挥手3) | h1→h2 | h1 反复重传【服务端 FIN】 | h1:LAST_ACK;h2:FIN_WAIT_2 | h1 |
| 收尾 ACK 丢失(挥手4) | h2→h1 | h1 反复重传【服务端 FIN】 | h1:LAST_ACK;h2:TIME_WAIT | h2 |
tcpdump 抓包策略
# 模拟第 1 次挥手丢包:客户端发往服务端的 FIN (h2→h1 FIN) 丢失
h1 iptables -A INPUT -p tcp --dport 80 --tcp-flags FIN FIN -j DROP
# 模拟第 2 次挥手丢包:服务端回复的 FIN-ACK (h1→h2 ACK) 丢失
h2 iptables -A INPUT -p tcp --sport 80 --tcp-flags ACK,SYN,FIN,PSH ACK -j DROP
# 模拟第 3 次挥手丢包:服务端发送的 FIN (h1→h2 FIN) 丢失
h2 iptables -A INPUT -p tcp --sport 80 --tcp-flags FIN,SYN,PSH FIN -j DROP
# 模拟第 4 次挥手丢包:客户端最后确认 ACK (h2→h1 ACK) 丢失
# 放行握手NEW状态纯ACK,保证连接能建立
h2 iptables -A OUTPUT -p tcp --dport 80 --tcp-flags ACK,SYN,FIN,PSH ACK -m conntrack --ctstate NEW -j ACCEPT
# 拦截所有非NEW状态、发往80的纯ACK
h2 iptables -A OUTPUT -p tcp --dport 80 --tcp-flags ACK,SYN,FIN,PSH ACK -j DROP
1. 【数据传输】模拟第 1 次挥手丢包(客户端主动关闭连接)
方案:
- 终端[1] h1 作为服务端
- 终端[2] 抓包客户端 h2 的 TCP 通讯
- 终端[3] h2 作为客户端
但执行顺序是先启动服务端,再抓包,再启动客户端,最后查看抓包结果。
终端[1] h1 作为服务端
# 查看拓扑
mininet> dump
<Host h1: h1-eth0:10.0.0.1 pid=3418517>
<Host h2: h2-eth0:10.0.0.2 pid=3418519>
<OVSBridge s1: lo:127.0.0.1,s1-eth1:None,s1-eth2:None pid=3418524>
# 查看客户端 h2 的重传策略(仅管控 ESTABLISHED 状态重传)
mininet> h2 cat /proc/sys/net/ipv4/tcp_retries2
15
# 查看客户端 h2 的孤儿连接重传策略(仅管控 FIN_WAIT_1 状态重传)
# tcp_orphan_retries值为0时,内核默认最大重传次数为8次
mininet> h2 cat /proc/sys/net/ipv4/tcp_orphan_retries
0
# 查看客户端 h2 的 FIN 超时时间(仅管控 FIN_WAIT_2 状态重传)
mininet> h2 cat /proc/sys/net/ipv4/tcp_fin_timeout
60
# 配置 # h1 丢弃目的80、带FIN标志的入站包
mininet> h1 iptables -A INPUT -p tcp --dport 80 --tcp-flags FIN FIN -j DROP
python 自带的 http.server 模块是短链接,无法测试客户端主动关闭连接的四次挥手场景。所以丢给 AI 写一个 TCP 长连接服务端。
import socket
import time
from datetime import datetime
server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_sock.bind(("0.0.0.0", 80))
server_sock.listen(1)
print(f"[{datetime.now().strftime('%H:%M:%S')}] TCP长连接服务启动,监听 0.0.0.0:80")
while True:
conn, addr = server_sock.accept()
print(f"\n[{datetime.now().strftime('%H:%M:%S')}] ==== 新客户端接入 {addr} ====")
try:
while True:
data = conn.recv(1024)
if not data:
print(f"[{datetime.now().strftime('%H:%M:%S')}] 客户端 {addr} 主动关闭连接,发送单独ACK确认FIN")
conn.shutdown(socket.SHUT_RD)
# 延迟,让客户端先收到纯ACK,再发服务端FIN
time.sleep(60)
break
print(f"[{datetime.now().strftime('%H:%M:%S')}] 收到客户端原始字节: {data}")
try:
print(f"[{datetime.now().strftime('%H:%M:%S')}] 收到客户端 {addr} 的文本内容: {data.decode('utf-8').strip()}")
except UnicodeDecodeError:
print(f"[{datetime.now().strftime('%H:%M:%S')}] 收到客户端 {addr} 的数据,二进制数据,无法以UTF-8解码")
conn.sendall(data)
print(f"[{datetime.now().strftime('%H:%M:%S')}] 已回复客户端 {addr}\n")
except Exception as e:
print(f"[{datetime.now().strftime('%H:%M:%S')}] 客户端 {addr} 发生连接异常:", e)
finally:
conn.close()
print(f"[{datetime.now().strftime('%H:%M:%S')}] 客户端 {addr} 连接已清理\n")
# 启动 HTTP 服务
mininet> h1 python3 tcp_server.py
TCP长连接服务启动,监听 0.0.0.0:80
终端[2] 监听客户端 h2 eth0 的 TCP 通讯
root@null:/home/null# sudo mnexec -a 3418519 tcpdump -l -i h2-eth0 tcp
终端[3] h2 作为客户端,通过 nc 建立 TCP 请求,1秒后主动关闭连接
root@null:/home/null# sudo mnexec -a 3418519 bash -c 'echo "Hello World!" | nc -w 1 10.0.0.1 80'
回到 终端[2] 查看监听客户端 h2 的 TCP 通讯结果
10:20:07.248079 IP 10.0.0.2.36734 > 10.0.0.1.http: Flags [S], seq 1742861858, win 42340, options [mss 1460,sackOK,TS val 3963837231 ecr 0,nop,wscale 9], length 0
10:20:07.248221 IP 10.0.0.1.http > 10.0.0.2.36734: Flags [S.], seq 2578764832, ack 1742861859, win 43440, options [mss 1460,sackOK,TS val 3553641895 ecr 3963837231,nop,wscale 9], length 0
10:20:07.248228 IP 10.0.0.2.36734 > 10.0.0.1.http: Flags [.], ack 1, win 83, options [nop,nop,TS val 3963837231 ecr 3553641895], length 0
10:20:07.248286 IP 10.0.0.2.36734 > 10.0.0.1.http: Flags [P.], seq 1:14, ack 1, win 83, options [nop,nop,TS val 3963837231 ecr 3553641895], length 13: HTTP
10:20:07.248294 IP 10.0.0.1.http > 10.0.0.2.36734: Flags [.], ack 14, win 85, options [nop,nop,TS val 3553641895 ecr 3963837231], length 0
10:20:07.248461 IP 10.0.0.1.http > 10.0.0.2.36734: Flags [P.], seq 1:14, ack 14, win 85, options [nop,nop,TS val 3553641896 ecr 3963837231], length 13: HTTP
10:20:07.248465 IP 10.0.0.2.36734 > 10.0.0.1.http: Flags [.], ack 14, win 83, options [nop,nop,TS val 3963837232 ecr 3553641896], length 0
10:20:08.249692 IP 10.0.0.2.36734 > 10.0.0.1.http: Flags [F.], seq 14, ack 14, win 83, options [nop,nop,TS val 3963838233 ecr 3553641896], length 0
10:20:08.453435 IP 10.0.0.2.36734 > 10.0.0.1.http: Flags [F.], seq 14, ack 14, win 83, options [nop,nop,TS val 3963838437 ecr 3553641896], length 0
10:20:08.661438 IP 10.0.0.2.36734 > 10.0.0.1.http: Flags [F.], seq 14, ack 14, win 83, options [nop,nop,TS val 3963838645 ecr 3553641896], length 0
10:20:09.069442 IP 10.0.0.2.36734 > 10.0.0.1.http: Flags [F.], seq 14, ack 14, win 83, options [nop,nop,TS val 3963839053 ecr 3553641896], length 0
10:20:09.933433 IP 10.0.0.2.36734 > 10.0.0.1.http: Flags [F.], seq 14, ack 14, win 83, options [nop,nop,TS val 3963839917 ecr 3553641896], length 0
10:20:11.597424 IP 10.0.0.2.36734 > 10.0.0.1.http: Flags [F.], seq 14, ack 14, win 83, options [nop,nop,TS val 3963841581 ecr 3553641896], length 0
10:20:14.861420 IP 10.0.0.2.36734 > 10.0.0.1.http: Flags [F.], seq 14, ack 14, win 83, options [nop,nop,TS val 3963844845 ecr 3553641896], length 0
10:20:21.709423 IP 10.0.0.2.36734 > 10.0.0.1.http: Flags [F.], seq 14, ack 14, win 83, options [nop,nop,TS val 3963851693 ecr 3553641896], length 0
10:20:35.021440 IP 10.0.0.2.36734 > 10.0.0.1.http: Flags [F.], seq 14, ack 14, win 83, options [nop,nop,TS val 3963865005 ecr 3553641896], length 0
10:21:01.133445 IP 10.0.0.2.36734 > 10.0.0.1.http: Flags [F.], seq 14, ack 14, win 83, options [nop,nop,TS val 3963891117 ecr 3553641896], length 0
分析
客户端 10.0.0.2 服务端 10.0.0.1
| |
|---------- 1. SYN ---------->| seq=1742861858
| |
|<-------- 2. SYN+ACK --------| seq=2578764832, ack=1742861859
| |
|---------- 3. ACK ---------->| ack=1,连接建立完成
| |
|---------- 4. PSH DATA ----->| 发送文本数据,seq1~14
| |
|<---------- 5. ACK ----------| ack=14,确认收到客户端数据
| |
|<-------- 6. PSH DATA -------| 服务端原样回包 seq1~14
| |
|---------- 7. ACK ---------->| ack=14,确认服务端回包
| |
| [等待 1s] |
| (客户端已close发FIN, |
| 未收到服务端ACK,超时重传) |
| |
|---------- 8. FIN ---------->| 第一次FIN,seq=14
| |
| [等待 ~0.2s] |
| (无服务端响应,RTO退避重传FIN) |
| |
|---------- 9. FIN ---------->| 重传FIN 1
| |
| [等待 ~0.2s] |
| |
|----------10. FIN ---------->| 重传FIN 2
| |
| [等待 ~0.4s] |
| |
|----------11. FIN ---------->| 重传FIN 3
| |
| [等待 ~0.8s] |
| |
|----------12. FIN ---------->| 重传FIN 4
| |
| [等待 ~1.6s] |
| |
|----------13. FIN ---------->| 重传FIN 5
| |
| [等待 ~3.2s] |
| |
|----------14. FIN ---------->| 重传FIN 6
| |
| [等待 ~6.4s] |
| |
|----------15. FIN ---------->| 重传FIN 7
| |
| [等待 ~12.8s] |
| |
|----------16. FIN ---------->| 重传FIN 8
| |
| [等待 ~25.6s] |
| |
|----------17. FIN ---------->| 重传FIN 9
| |
|=============================|
TCP 状态链路
这里有个疑问:为什么 h2 没有重传 tcp_retries2 次,而是只重传了 9 次?
第 1 次发送(初始 FIN):10:20:08.249692
第 1 次重传:10:20:08.453435 (间隔约 0.2s)
第 2 次重传:10:20:08.661438 (间隔约 0.2s)
第 3 次重传:10:20:09.069442 (间隔约 0.4s)
第 4 次重传:10:20:09.933433 (间隔约 0.8s)
第 5 次重传:10:20:11.597424 (间隔约 1.6s)
第 6 次重传:10:20:14.861420 (间隔约 3.2s)
第 7 次重传:10:20:21.709423 (间隔约 6.8s)
第 8 次重传:10:20:35.021440 (间隔约 13.3s)
第 9 次重传:10:21:01.133445 (间隔约 26.1s)
参数分工(tcp_retries2 和 FIN 重传完全无关)
-
tcp_retries2=15
仅管控 ESTABLISHED 业务数据报文 丢失重传;
一旦客户端调用close()发出 FIN、进入FIN_WAIT_1,挥手 FIN 重传不再走这个参数,因此永远不会重传 15 次。 -
tcp_orphan_retries=0
统一管控所有 FIN_WAIT_1 状态(无论是否孤儿连接)FIN 报文重传上限;
Linux 内核硬编码规则:值为 0 时,最大允许 8 次 FIN 重传。
孤儿连接 是指在TCP连接中,主动关闭连接的一方的进程已经退出,但连接仍然存在于内核中,导致无法正常管理和释放资源。
为什么 tcp_orphan_retries 理论上限是 8,却能跑出 9 次?
retry=0 → 允许 → 发 1,retry=1,注册下一轮
retry=1 → 允许 → 发 2,retry=2,注册下一轮
retry=2 → 允许 → 发 3,retry=3,注册下一轮
retry=3 → 允许 → 发 4,retry=4,注册下一轮
retry=4 → 允许 → 发 5,retry=5,注册下一轮
retry=5 → 允许 → 发 6,retry=6,注册下一轮
retry=6 → 允许 → 发 7,retry=7,注册下一轮
retry=7 → 允许 → 发 8,retry=8,注册下一轮
retry=8 → 不允许新注册,但上一步已经注册好第 9 轮定时器 → 发 9,不再注册新定时器
第 9 次是提前注册遗留的定时器,不会再产生第 10 次。
为什么 9 次之后彻底不再重传?
- 内核不再注册任何新的 RTO 重传定时器;
- TCP 控制块 tcp_sock 标记待销毁;
- 内核资源回收流程执行,连接彻底消失;
总结
tcp_orphan_retries=0 内置最大重传阈值 8 次,观测到 9 次重传是内核定时器提前注册机制导致的一轮溢出,属于 Linux TCP 正常时序表现,不代表参数规则失效,且 9 次之后不再产生新重传。
PS:后续测试把 tcp_orphan_retries 设置为 1,观测到 3 次重传。tcp_orphan_retries=1 代表最多新增 1 次重传调度,内核提前注册定时器带来 2 轮溢出重传,因此抓包一共观测到 3 次 FIN 重传。
2. 【数据传输】模拟第 2 次挥手丢包
方案:
- 终端[1] h1 作为服务端
- 终端[2] 抓包服务端 h1 的 TCP 通讯
- 终端[3] h2 作为客户端
但执行顺序是先启动服务端,再抓包,再启动客户端,最后查看抓包结果。
终端[1] h1 作为服务端
# 服务端回复的 FIN-ACK (h1→h2 ACK) 丢失
mininet> h2 iptables -A INPUT -p tcp --sport 80 --tcp-flags ACK,SYN,FIN,PSH ACK -j DROP
# 启动 TCP 服务
mininet> h1 python3 tcp_server.py
TCP长连接服务启动,监听 0.0.0.0:80
终端[2] 监听服务端 h1 eth0 的 TCP 通讯
root@null:/home/null# sudo mnexec -a 3418517 tcpdump -l -i h1-eth0 tcp
终端[3] h2 作为客户端,通过 nc 建立 TCP 请求
root@null:/home/null# sudo mnexec -a 3418519 bash -c 'echo "Hello World!" | nc -w 1 10.0.0.1 80'
回到 终端[2] 查看监听服务端 h1 的 TCP 通讯结果
15:14:07.859035 IP 10.0.0.2.56070 > 10.0.0.1.http: Flags [S], seq 2025241781, win 42340, options [mss 1460,sackOK,TS val 3981477842 ecr 0,nop,wscale 9], length 0
15:14:07.859045 IP 10.0.0.1.http > 10.0.0.2.56070: Flags [S.], seq 3203169476, ack 2025241782, win 43440, options [mss 1460,sackOK,TS val 3571282506 ecr 3981477842,nop,wscale 9], length 0
15:14:07.859130 IP 10.0.0.2.56070 > 10.0.0.1.http: Flags [.], ack 1, win 83, options [nop,nop,TS val 3981477842 ecr 3571282506], length 0
15:14:07.859194 IP 10.0.0.2.56070 > 10.0.0.1.http: Flags [P.], seq 1:14, ack 1, win 83, options [nop,nop,TS val 3981477842 ecr 3571282506], length 13: HTTP
15:14:07.859198 IP 10.0.0.1.http > 10.0.0.2.56070: Flags [.], ack 14, win 85, options [nop,nop,TS val 3571282506 ecr 3981477842], length 0
15:14:07.859321 IP 10.0.0.1.http > 10.0.0.2.56070: Flags [P.], seq 1:14, ack 14, win 85, options [nop,nop,TS val 3571282507 ecr 3981477842], length 13: HTTP
15:14:07.859331 IP 10.0.0.2.56070 > 10.0.0.1.http: Flags [.], ack 14, win 83, options [nop,nop,TS val 3981477843 ecr 3571282507], length 0
15:14:08.860442 IP 10.0.0.2.56070 > 10.0.0.1.http: Flags [F.], seq 14, ack 14, win 83, options [nop,nop,TS val 3981478844 ecr 3571282507], length 0
15:14:08.901421 IP 10.0.0.1.http > 10.0.0.2.56070: Flags [.], ack 15, win 85, options [nop,nop,TS val 3571283549 ecr 3981478844], length 0
15:14:09.069427 IP 10.0.0.2.56070 > 10.0.0.1.http: Flags [F.], seq 14, ack 14, win 83, options [nop,nop,TS val 3981479053 ecr 3571282507], length 0
15:14:09.069430 IP 10.0.0.1.http > 10.0.0.2.56070: Flags [.], ack 15, win 85, options [nop,nop,TS val 3571283717 ecr 3981479053,nop,nop,sack 1 {14:15}], length 0
15:14:09.277426 IP 10.0.0.2.56070 > 10.0.0.1.http: Flags [F.], seq 14, ack 14, win 83, options [nop,nop,TS val 3981479261 ecr 3571282507], length 0
15:14:09.277429 IP 10.0.0.1.http > 10.0.0.2.56070: Flags [.], ack 15, win 85, options [nop,nop,TS val 3571283925 ecr 3981479261,nop,nop,sack 1 {14:15}], length 0
15:14:09.685443 IP 10.0.0.2.56070 > 10.0.0.1.http: Flags [F.], seq 14, ack 14, win 83, options [nop,nop,TS val 3981479669 ecr 3571282507], length 0
15:14:09.685447 IP 10.0.0.1.http > 10.0.0.2.56070: Flags [.], ack 15, win 85, options [nop,nop,TS val 3571284333 ecr 3981479669,nop,nop,sack 1 {14:15}], length 0
15:14:10.509438 IP 10.0.0.2.56070 > 10.0.0.1.http: Flags [F.], seq 14, ack 14, win 83, options [nop,nop,TS val 3981480493 ecr 3571282507], length 0
15:14:10.509440 IP 10.0.0.1.http > 10.0.0.2.56070: Flags [.], ack 15, win 85, options [nop,nop,TS val 3571285157 ecr 3981480493,nop,nop,sack 1 {14:15}], length 0
15:14:12.173443 IP 10.0.0.2.56070 > 10.0.0.1.http: Flags [F.], seq 14, ack 14, win 83, options [nop,nop,TS val 3981482157 ecr 3571282507], length 0
15:14:12.173447 IP 10.0.0.1.http > 10.0.0.2.56070: Flags [.], ack 15, win 85, options [nop,nop,TS val 3571286821 ecr 3981482157,nop,nop,sack 1 {14:15}], length 0
15:14:15.437428 IP 10.0.0.2.56070 > 10.0.0.1.http: Flags [F.], seq 14, ack 14, win 83, options [nop,nop,TS val 3981485421 ecr 3571282507], length 0
15:14:15.437433 IP 10.0.0.1.http > 10.0.0.2.56070: Flags [.], ack 15, win 85, options [nop,nop,TS val 3571290085 ecr 3981485421,nop,nop,sack 1 {14:15}], length 0
15:14:22.157457 IP 10.0.0.2.56070 > 10.0.0.1.http: Flags [F.], seq 14, ack 14, win 83, options [nop,nop,TS val 3981492141 ecr 3571282507], length 0
15:14:22.157461 IP 10.0.0.1.http > 10.0.0.2.56070: Flags [.], ack 15, win 85, options [nop,nop,TS val 3571296805 ecr 3981492141,nop,nop,sack 1 {14:15}], length 0
15:14:35.469562 IP 10.0.0.2.56070 > 10.0.0.1.http: Flags [F.], seq 14, ack 14, win 83, options [nop,nop,TS val 3981505453 ecr 3571282507], length 0
15:14:35.469567 IP 10.0.0.1.http > 10.0.0.2.56070: Flags [.], ack 15, win 85, options [nop,nop,TS val 3571310117 ecr 3981505453,nop,nop,sack 1 {14:15}], length 0
15:15:01.581592 IP 10.0.0.2.56070 > 10.0.0.1.http: Flags [F.], seq 14, ack 14, win 83, options [nop,nop,TS val 3981531565 ecr 3571282507], length 0
15:15:01.581597 IP 10.0.0.1.http > 10.0.0.2.56070: Flags [.], ack 15, win 85, options [nop,nop,TS val 3571336229 ecr 3981531565,nop,nop,sack 1 {14:15}], length 0
15:15:08.860756 IP 10.0.0.1.http > 10.0.0.2.56070: Flags [F.], seq 14, ack 15, win 85, options [nop,nop,TS val 3571343508 ecr 3981531565], length 0
15:15:08.860775 IP 10.0.0.2.56070 > 10.0.0.1.http: Flags [.], ack 15, win 83, options [nop,nop,TS val 3981538844 ecr 3571343508], length 0
分析
15:14:08.860442 —— 原始 FIN(首次发送,不计入重传计数)
15:14:09.069427 —— 重传 #1
15:14:09.277426 —— 重传 #2
15:14:09.685443 —— 重传 #3
15:14:10.509438 —— 重传 #4
15:14:12.173443 —— 重传 #5
15:14:15.437428 —— 重传 #6
15:14:22.157457 —— 重传 #7
15:14:35.469562 —— 重传 #8
15:15:01.581592 —— 重传 #9
15:15:08.860756 不是重传,为服务端阻塞sleep(60),60秒后执行 close() 发送 FIN。
总结
- h1 收到客户端 FIN,调用 shutdown(SHUT_RD),内核发出纯 ACK [.] ack=15;
- 这条纯 ACK 匹配 iptables 规则,在 h2 INPUT 链被直接丢弃;
- 客户端内核始终收不到对自己 FIN 的确认 ACK,停留在 FIN_WAIT_1;
- RTO 指数退避不断超时,反复重传 FIN。
tcp_orphan_retries=0 等价最大允许调度8 次重传,但 Linux TCP 存在定时器预注册时序溢出:
- 每一次重传发送前校验 retry < 8,满足则发包,同时提前注册下一轮 RTO 定时器;
- 当第 8 次重传调度完成时,已经预先注册了第 9 轮定时器;
- 第 9 轮定时器到时依旧执行发包,因此观测到 9 次重传。
3. 【数据传输】模拟第 3 次挥手丢包
方案:
- 终端[1] h1 作为服务端
- 终端[2] 抓包服务端 h1 的 TCP 通讯
- 终端[3] h2 作为客户端
但执行顺序是先启动服务端,再抓包,再启动客户端,最后查看抓包结果。
终端[1] h1 作为服务端
# 服务端发送的 FIN (h1→h2 FIN) 丢失
mininet> h2 iptables -A INPUT -p tcp --sport 80 --tcp-flags FIN,SYN,PSH FIN -j DROP
# 启动 TCP 服务
mininet> h1 python3 tcp_server.py
TCP长连接服务启动,监听 0.0.0.0:80
终端[2] 监听服务端 h1 eth0 的 TCP 通讯
root@null:/home/null# sudo mnexec -a 3418517 tcpdump -l -i h1-eth0 tcp
终端[3] h2 作为客户端,通过 nc 建立 TCP 请求
root@null:/home/null# sudo mnexec -a 3418519 bash -c 'echo "Hello World!" | nc -w 1 10.0.0.1 80'
回到 终端[2] 查看监听服务端 h1 的 TCP 通讯结果
14:48:17.044398 IP 10.0.0.2.49270 > 10.0.0.1.http: Flags [S], seq 2706151616, win 42340, options [mss 1460,sackOK,TS val 3979927027 ecr 0,nop,wscale 9], length 0
14:48:17.044408 IP 10.0.0.1.http > 10.0.0.2.49270: Flags [S.], seq 1988708177, ack 2706151617, win 43440, options [mss 1460,sackOK,TS val 3569731692 ecr 3979927027,nop,wscale 9], length 0
14:48:17.044508 IP 10.0.0.2.49270 > 10.0.0.1.http: Flags [.], ack 1, win 83, options [nop,nop,TS val 3979927028 ecr 3569731692], length 0
14:48:17.044560 IP 10.0.0.2.49270 > 10.0.0.1.http: Flags [P.], seq 1:14, ack 1, win 83, options [nop,nop,TS val 3979927028 ecr 3569731692], length 13: HTTP
14:48:17.044564 IP 10.0.0.1.http > 10.0.0.2.49270: Flags [.], ack 14, win 85, options [nop,nop,TS val 3569731692 ecr 3979927028], length 0
14:48:17.044742 IP 10.0.0.1.http > 10.0.0.2.49270: Flags [P.], seq 1:14, ack 14, win 85, options [nop,nop,TS val 3569731692 ecr 3979927028], length 13: HTTP
14:48:17.044750 IP 10.0.0.2.49270 > 10.0.0.1.http: Flags [.], ack 14, win 83, options [nop,nop,TS val 3979927028 ecr 3569731692], length 0
14:48:18.045936 IP 10.0.0.2.49270 > 10.0.0.1.http: Flags [F.], seq 14, ack 14, win 83, options [nop,nop,TS val 3979928029 ecr 3569731692], length 0
14:48:18.086435 IP 10.0.0.1.http > 10.0.0.2.49270: Flags [.], ack 15, win 85, options [nop,nop,TS val 3569732734 ecr 3979928029], length 0
14:49:18.046229 IP 10.0.0.1.http > 10.0.0.2.49270: Flags [F.], seq 14, ack 15, win 85, options [nop,nop,TS val 3569792693 ecr 3979928029], length 0
14:49:18.253426 IP 10.0.0.1.http > 10.0.0.2.49270: Flags [F.], seq 14, ack 15, win 85, options [nop,nop,TS val 3569792901 ecr 3979928029], length 0
14:49:18.461449 IP 10.0.0.1.http > 10.0.0.2.49270: Flags [F.], seq 14, ack 15, win 85, options [nop,nop,TS val 3569793109 ecr 3979928029], length 0
14:49:18.869421 IP 10.0.0.1.http > 10.0.0.2.49270: Flags [F.], seq 14, ack 15, win 85, options [nop,nop,TS val 3569793517 ecr 3979928029], length 0
14:49:19.693426 IP 10.0.0.1.http > 10.0.0.2.49270: Flags [F.], seq 14, ack 15, win 85, options [nop,nop,TS val 3569794341 ecr 3979928029], length 0
14:49:21.357421 IP 10.0.0.1.http > 10.0.0.2.49270: Flags [F.], seq 14, ack 15, win 85, options [nop,nop,TS val 3569796005 ecr 3979928029], length 0
14:49:24.621437 IP 10.0.0.1.http > 10.0.0.2.49270: Flags [F.], seq 14, ack 15, win 85, options [nop,nop,TS val 3569799269 ecr 3979928029], length 0
14:49:31.213421 IP 10.0.0.1.http > 10.0.0.2.49270: Flags [F.], seq 14, ack 15, win 85, options [nop,nop,TS val 3569805861 ecr 3979928029], length 0
14:49:44.525428 IP 10.0.0.1.http > 10.0.0.2.49270: Flags [F.], seq 14, ack 15, win 85, options [nop,nop,TS val 3569819173 ecr 3979928029], length 0
14:50:10.637430 IP 10.0.0.1.http > 10.0.0.2.49270: Flags [F.], seq 14, ack 15, win 85, options [nop,nop,TS val 3569845285 ecr 3979928029], length 0
分析
14:49:18.046229 —— 原始 FIN(sleep60 秒结束,首次发送,不计重传)
14:49:18.253426 —— 重传 #1
14:49:18.461449 —— 重传 #2
14:49:18.869421 —— 重传 #3
14:49:19.693426 —— 重传 #4
14:49:21.357421 —— 重传 #5
14:49:24.621437 —— 重传 #6
14:49:31.213421 —— 重传 #7
14:49:44.525428 —— 重传 #8
14:50:10.637430 —— 重传 #9
总结
- 服务端第三次挥手 [F.] 报文总共重传 9 次;
- 根源:iptables 拦截服务端 FIN 报文,客户端收不到第三次挥手包,不会回复第四次挥手 ACK,服务端持续 RTO 超时重传;
- 内核 TCP 定时器预注册机制造成 1 次溢出,理论上限 8 次、实际抓到 9 次重传;
- tcp_orphan_retries 仅控制客户端侧,服务端重传由 tcp_retries2 控制。
4. 【数据传输】模拟第 4 次挥手丢包
方案:
- 终端[1] h1 作为服务端
- 终端[2] 抓包客户端 h2 的 TCP 通讯
- 终端[3] h2 作为客户端
但执行顺序是先启动服务端,再抓包,再启动客户端,最后查看抓包结果。
终端[1] h1 作为服务端
# 客户端最后确认 ACK (h2→h1 ACK) 丢失
mininet> h2 iptables -A OUTPUT -p tcp --dport 80 --tcp-flags ACK,SYN,FIN,PSH ACK -m conntrack --ctstate NEW -j ACCEPT
mininet> h2 iptables -A OUTPUT -p tcp --dport 80 --tcp-flags ACK,SYN,FIN,PSH ACK -j DROP
# 启动 TCP 服务
mininet> h1 python3 tcp_server.py
TCP长连接服务启动,监听 0.0.0.0:80
终端[2] 监听客户端 h2 eth0 的 TCP 通讯
root@null:/home/null# sudo mnexec -a 3418519 tcpdump -l -i h2-eth0 tcp
终端[3] h2 作为客户端,通过 nc 建立 TCP 请求
root@null:/home/null# sudo mnexec -a 3418519 bash -c 'echo "Hello World!" | nc -w 1 10.0.0.1 80'
回到 终端[2] 查看监听客户端 h2 的 TCP 通讯结果
15:53:26.212607 IP 10.0.0.2.34526 > 10.0.0.1.http: Flags [S], seq 1191896553, win 42340, options [mss 1460,sackOK,TS val 3983836196 ecr 0,nop,wscale 9], length 0
15:53:26.212780 IP 10.0.0.1.http > 10.0.0.2.34526: Flags [S.], seq 2634200470, ack 1191896554, win 43440, options [mss 1460,sackOK,TS val 3573640860 ecr 3983836196,nop,wscale 9], length 0
15:53:26.212838 IP 10.0.0.2.34526 > 10.0.0.1.http: Flags [P.], seq 1:14, ack 1, win 83, options [nop,nop,TS val 3983836196 ecr 3573640860], length 13: HTTP
15:53:26.212865 IP 10.0.0.1.http > 10.0.0.2.34526: Flags [.], ack 14, win 85, options [nop,nop,TS val 3573640860 ecr 3983836196], length 0
15:53:26.213079 IP 10.0.0.1.http > 10.0.0.2.34526: Flags [P.], seq 1:14, ack 14, win 85, options [nop,nop,TS val 3573640860 ecr 3983836196], length 13: HTTP
15:53:26.421305 IP 10.0.0.1.http > 10.0.0.2.34526: Flags [P.], seq 1:14, ack 14, win 85, options [nop,nop,TS val 3573641069 ecr 3983836196], length 13: HTTP
15:53:26.629427 IP 10.0.0.1.http > 10.0.0.2.34526: Flags [P.], seq 1:14, ack 14, win 85, options [nop,nop,TS val 3573641277 ecr 3983836196], length 13: HTTP
15:53:27.037437 IP 10.0.0.1.http > 10.0.0.2.34526: Flags [P.], seq 1:14, ack 14, win 85, options [nop,nop,TS val 3573641685 ecr 3983836196], length 13: HTTP
15:53:27.214234 IP 10.0.0.2.34526 > 10.0.0.1.http: Flags [F.], seq 14, ack 14, win 83, options [nop,nop,TS val 3983837197 ecr 3573641685], length 0
15:53:27.254425 IP 10.0.0.1.http > 10.0.0.2.34526: Flags [.], ack 15, win 85, options [nop,nop,TS val 3573641902 ecr 3983837197], length 0
15:54:27.214582 IP 10.0.0.1.http > 10.0.0.2.34526: Flags [F.], seq 14, ack 15, win 85, options [nop,nop,TS val 3573701862 ecr 3983837197], length 0
15:54:27.445426 IP 10.0.0.1.http > 10.0.0.2.34526: Flags [F.], seq 14, ack 15, win 85, options [nop,nop,TS val 3573702093 ecr 3983837197], length 0
15:54:27.669427 IP 10.0.0.1.http > 10.0.0.2.34526: Flags [F.], seq 14, ack 15, win 85, options [nop,nop,TS val 3573702317 ecr 3983837197], length 0
15:54:28.117427 IP 10.0.0.1.http > 10.0.0.2.34526: Flags [F.], seq 14, ack 15, win 85, options [nop,nop,TS val 3573702765 ecr 3983837197], length 0
15:54:29.069426 IP 10.0.0.1.http > 10.0.0.2.34526: Flags [F.], seq 14, ack 15, win 85, options [nop,nop,TS val 3573703717 ecr 3983837197], length 0
15:54:30.861425 IP 10.0.0.1.http > 10.0.0.2.34526: Flags [F.], seq 14, ack 15, win 85, options [nop,nop,TS val 3573705509 ecr 3983837197], length 0
15:54:34.445427 IP 10.0.0.1.http > 10.0.0.2.34526: Flags [F.], seq 14, ack 15, win 85, options [nop,nop,TS val 3573709093 ecr 3983837197], length 0
15:54:41.869446 IP 10.0.0.1.http > 10.0.0.2.34526: Flags [F.], seq 14, ack 15, win 85, options [nop,nop,TS val 3573716517 ecr 3983837197], length 0
15:54:56.205608 IP 10.0.0.1.http > 10.0.0.2.34526: Flags [F.], seq 14, ack 15, win 85, options [nop,nop,TS val 3573730853 ecr 3983837197], length 0
15:55:24.877544 IP 10.0.0.1.http > 10.0.0.2.34526: Flags [F.], seq 14, ack 15, win 85, options [nop,nop,TS val 3573759525 ecr 3983837197], length 0
分析
客户端 10.0.0.2 (Client) 服务端 10.0.0.1 (Server)
| |
|---------- 1. SYN ---------->| 15:53:26.212607 握手第一步
| |
|<-------- 2. SYN+ACK --------| 15:53:26.212780 握手第二步
| |
|----- 3. ACK+DATA ---------->| 15:53:26.212838 握手第三步+发送数据Hello World
| |
|<-------- 4. ACK ------------| 15:53:26.212865 服务端确认客户端数据
| |
|<-------- 5. DATA -----------| 15:53:26.213079 服务端回射数据
| |
|<-------- 6. DATA (重传1) ----| 15:53:26.421305
| |
|<-------- 7. DATA (重传2) ----| 15:53:26.629427
| |
|<-------- 8. DATA (重传3) ----| 15:53:27.037437
| |
|---------- 9. FIN ---------->| 15:53:27.214234 第一次挥手,客户端主动关闭
| |
|<-------- 10. ACK -----------| 15:53:27.254425 第二次挥手,纯ACK确认客户端FIN
| |
| [服务端阻塞sleep 60秒] |
| |
|<------- 11. FIN+ACK --------| 15:54:27.214582 第三次挥手,服务端发送FIN
| |
| (OUTPUT被丢弃,无报文发出) |
| |
|<----- 12. FIN+ACK(重传1)-----| 15:54:27.445426 未收到客户端ACK,RTO超时重传
| |
|<----- 13. FIN+ACK(重传2)-----| 15:54:27.669427
| |
|<----- 14. FIN+ACK(重传3)-----| 15:54:28.117427
| |
|<----- 15. FIN+ACK(重传4)-----| 15:54:29.069426
| |
|<----- 16. FIN+ACK(重传5)-----| 15:54:30.861425
| |
|<----- 17. FIN+ACK(重传6)-----| 15:54:34.445427
| |
|<----- 18. FIN+ACK(重传7)-----| 15:54:41.869446
| |
|<----- 19. FIN+ACK(重传8)-----| 15:54:56.205608
| |
|<----- 20. FIN+ACK(重传9)-----| 15:55:24.877544
| |
|=============================|
这里有个疑问:为什么只重传 9 次,没到上限 tcp_retries2=15 次?
TCP 状态对照表
| 时间戳 | 报文方向 | 标志 | 挥手阶段 | 客户端 h2 状态 | 服务端 h1 状态 | 行为说明 |
|---|---|---|---|---|---|---|
| 15:53:26.212607 | h2→h1 | [S] | 握手 1 | SYN_SENT | LISTEN | 客户端发起 SYN |
| 15:53:26.212780 | h1→h2 | [S.] | 握手 2 | SYN_RCVD | SYN_RCVD | 服务端回复 SYN+ACK |
| 15:53:26.212838 | h2→h1 | [P.] | 握手 3 + 数据 | ESTABLISHED | ESTABLISHED | 客户端合并 ACK + 发送业务数据 |
| 15:53:26.212865 | h1→h2 | [.] | 数据 ACK | ESTABLISHED | ESTABLISHED | 服务端确认客户端数据 |
| 15:53:26.213079 ~ 15:53:27.037437 | h1→h2 | [P.] | 回射数据 | ESTABLISHED | ESTABLISHED | 服务端回传数据,多次重传 |
| 15:53:27.214234 | h2→h1 | [F.] | 第一次挥手 | FIN_WAIT_1 | CLOSE_WAIT | nc 超时退出,客户端发送 FIN 主动关闭 |
| 15:53:27.254425 | h1→h2 | [.] | 第二次挥手 | FIN_WAIT_2 | CLOSE_WAIT | 服务端 shutdown (SHUT_RD),回复纯 ACK 确认 FIN |
| 15:53:27.254425 ~ 15:54:27.214582 | — | — | 等待窗口 | FIN_WAIT_2(60s 超时) | CLOSE_WAIT(sleep 60 阻塞) | 客户端 tcp_fin_timeout=60,满 60s 后客户端 TCB 销毁;服务端 sleep 阻塞不发 FIN |
| 15:54:27.214582 | h1→h2 | [F.] | 第三次挥手 | 已销毁 (无连接) | LAST_ACK | sleep 结束,conn.close () 发送 FIN+ACK;客户端内核已无连接,无法生成第四次 ACK |
| 15:54:27.445426 | h1→h2 | [F.] | FIN 重传 1 | 无 | LAST_ACK | 无第四次挥手 ACK,RTO 超时重传 |
| 15:54:27.669427 | h1→h2 | [F.] | FIN 重传 2 | 无 | LAST_ACK | RTO 指数退避重传 |
| 15:54:28.117427 | h1→h2 | [F.] | FIN 重传 3 | 无 | LAST_ACK | RTO 指数退避重传 |
| 15:54:29.069426 | h1→h2 | [F.] | FIN 重传 4 | 无 | LAST_ACK | RTO 指数退避重传 |
| 15:54:30.861425 | h1→h2 | [F.] | FIN 重传 5 | 无 | LAST_ACK | RTO 指数退避重传 |
| 15:54:34.445427 | h1→h2 | [F.] | FIN 重传 6 | 无 | LAST_ACK | RTO 指数退避重传 |
| 15:54:41.869446 | h1→h2 | [F.] | FIN 重传 7 | 无 | LAST_ACK | RTO 指数退避重传 |
| 15:54:56.205608 | h1→h2 | [F.] | FIN 重传 8 | 无 | LAST_ACK | RTO 指数退避重传 |
| 15:55:24.877544 | h1→h2 | [F.] | FIN 重传 9 | 无 | LAST_ACK | 第 9 次重传;内核判定孤儿连接长期无响应,后续销毁 TCB,不再产生第 10 次重传 |
TCB = Transmission Control Block,TCP 传输控制块,是 Linux 内核里描述一条 TCP 连接的核心内存结构体。
里面完整保存这条连接所有运行信息:
四元组:源 IP / 源端口、目的 IP / 目的端口;
当前 TCP 状态(LISTEN / ESTABLISHED / FIN_WAIT_2 / LAST_ACK 等);
收发序列号、滑动窗口、SACK 信息;
RTO 重传定时器、延时 ACK 定时器、FIN_WAIT_2 超时定时器;
关联的用户进程 socket 句柄、孤儿连接标记等。
简单理解:一条 TCP 连接在内核里的全部 “档案”,档案销毁 = 连接彻底消失。
- FIN_WAIT_2 超时销毁
客户端收到第二次挥手 ACK 后进入 FIN_WAIT_2,内核计时 60s;服务端 sleep 刚好 60s,发送第三次挥手时客户端连接已超时释放,不会生成第四次挥手 ACK。 - 服务端 LAST_ACK + 孤儿连接
服务端执行 conn.close() 后用户态无 socket 句柄,连接变为孤儿连接;内核持续重传 FIN,RTO 间隔成倍拉长,未跑完 tcp_retries2=15 就被内核回收 TCB,重传终止。 - iptables 叠加失效
即便客户端未超时,生成第四次挥手纯 ACK 也会被 h2 OUTPUT 规则丢弃,双重保证服务端永远收不到确认。
总结
- nc 发 FIN → 服务端 shutdown (SHUT_RD) 回纯 ACK(二次挥手);
- 客户端进入 FIN_WAIT_2,等待 60 秒后 tcp_fin_timeout 超时,客户端 TCB 销毁;
- 服务端 sleep (60) 到期,发送 FIN+ACK(第三次挥手);
- 客户端内核已销毁,无法生成第四次挥手 ACK,叠加 iptables 拦截,无任何确认返回;
- 服务端进入 LAST_ACK,无用户态 socket,成为孤儿连接;
- 内核逐轮 RTO 重传 FIN,指数退避间隔持续放大;
- 第 9 次重传发送完毕,注册第 10 轮超长超时定时器;
- 在第 10 轮超时到来前,内核判定孤儿连接长期无响应,回收 TCB、清除全部定时器;
- 不再产生第 10 次及以后重传,无论等待多久都无报文。
重传停止的核心:服务端 FIN 发送后成为孤儿连接,内核提前回收 TCB,未跑完 tcp_retries2=15 全部轮次;
客户端 nc 孤儿、服务端 sleep (60) 只是创造 “第四次挥手永久丢失” 的前置条件,不控制重传终止时机;
指数退避让第 10 轮等待窗口极长,给了内核充足时间提前清理死连接,因此长时间等待也看不到第 10 次重传。
参考资料: 《Linux TCP_RTO_MIN, TCP_RTO_MAX and the tcp_retries2 sysctl》