金亚洲技术笔记

凡是过往,皆为序章。

Mininet 使用笔记(二):TCP四次挥手

Posted on   » 网络通信 • 7592 words • 16 minute read
Tags: mininet, tcp

一、准备

模拟 TCP 四次挥手场景

h1 (服务端 10.0.0.1)、h2 (客户端 10.0.0.2)。四次挥手报文序列:

  1. 客户端 h2 → h1:FIN(挥手包 1)
  2. 服务端 h1 → h2:ACK(确认客户端 FIN,挥手包 2)
  3. 服务端 h1 → h2:FIN(服务端关闭发送,挥手包 3)
  4. 客户端 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 重传完全无关)

  1. tcp_retries2=15
    仅管控 ESTABLISHED 业务数据报文 丢失重传;
    一旦客户端调用close()发出 FIN、进入FIN_WAIT_1,挥手 FIN 重传不再走这个参数,因此永远不会重传 15 次。

  2. 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 次之后彻底不再重传?
  1. 内核不再注册任何新的 RTO 重传定时器;
  2. TCP 控制块 tcp_sock 标记待销毁;
  3. 内核资源回收流程执行,连接彻底消失;
总结

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。

总结
  1. h1 收到客户端 FIN,调用 shutdown(SHUT_RD),内核发出纯 ACK [.] ack=15;
  2. 这条纯 ACK 匹配 iptables 规则,在 h2 INPUT 链被直接丢弃;
  3. 客户端内核始终收不到对自己 FIN 的确认 ACK,停留在 FIN_WAIT_1;
  4. 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
总结
  1. 服务端第三次挥手 [F.] 报文总共重传 9 次;
  2. 根源:iptables 拦截服务端 FIN 报文,客户端收不到第三次挥手包,不会回复第四次挥手 ACK,服务端持续 RTO 超时重传;
  3. 内核 TCP 定时器预注册机制造成 1 次溢出,理论上限 8 次、实际抓到 9 次重传;
  4. 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 连接在内核里的全部 “档案”,档案销毁 = 连接彻底消失。

  1. FIN_WAIT_2 超时销毁
    客户端收到第二次挥手 ACK 后进入 FIN_WAIT_2,内核计时 60s;服务端 sleep 刚好 60s,发送第三次挥手时客户端连接已超时释放,不会生成第四次挥手 ACK。
  2. 服务端 LAST_ACK + 孤儿连接
    服务端执行 conn.close() 后用户态无 socket 句柄,连接变为孤儿连接;内核持续重传 FIN,RTO 间隔成倍拉长,未跑完 tcp_retries2=15 就被内核回收 TCB,重传终止。
  3. iptables 叠加失效
    即便客户端未超时,生成第四次挥手纯 ACK 也会被 h2 OUTPUT 规则丢弃,双重保证服务端永远收不到确认。
总结
  1. nc 发 FIN → 服务端 shutdown (SHUT_RD) 回纯 ACK(二次挥手);
  2. 客户端进入 FIN_WAIT_2,等待 60 秒后 tcp_fin_timeout 超时,客户端 TCB 销毁;
  3. 服务端 sleep (60) 到期,发送 FIN+ACK(第三次挥手);
  4. 客户端内核已销毁,无法生成第四次挥手 ACK,叠加 iptables 拦截,无任何确认返回;
  5. 服务端进入 LAST_ACK,无用户态 socket,成为孤儿连接;
  6. 内核逐轮 RTO 重传 FIN,指数退避间隔持续放大;
  7. 第 9 次重传发送完毕,注册第 10 轮超长超时定时器;
  8. 在第 10 轮超时到来前,内核判定孤儿连接长期无响应,回收 TCB、清除全部定时器;
  9. 不再产生第 10 次及以后重传,无论等待多久都无报文。

重传停止的核心:服务端 FIN 发送后成为孤儿连接,内核提前回收 TCB,未跑完 tcp_retries2=15 全部轮次;
客户端 nc 孤儿、服务端 sleep (60) 只是创造 “第四次挥手永久丢失” 的前置条件,不控制重传终止时机
指数退避让第 10 轮等待窗口极长,给了内核充足时间提前清理死连接,因此长时间等待也看不到第 10 次重传。



参考资料: 《Linux TCP_RTO_MIN, TCP_RTO_MAX and the tcp_retries2 sysctl》

×