Theory · Network · TCP
一条连接的全生命周期:SYN/SYN-ACK/ACK · FIN 交换 · 11 状态 · TIME_WAIT —— 连接管理(RFC 9293 Connection Management)
双向同步 ISN 的最小可靠方案;SYN 消耗一个序号
全双工两方向独立关闭;被动方延迟关 → ACK/FIN 分离
主动关闭方等 2×MSL(Linux 固定 60s)做可靠收尾
Big Picture
Big Picture · Reading Guide
被动打开:listen() 进 LISTEN → 收 SYN 进 SYN-RECV → 收 ACK 进 ESTABLISHED。
主动打开:connect() 进 SYN-SENT → 收 SYN+ACK 进 ESTABLISHED。
主动方:close() → FIN-WAIT-1 → FIN-WAIT-2 → TIME-WAIT(2MSL)→ CLOSED。
被动方:收 FIN 进 CLOSE-WAIT → close() 进 LAST-ACK → 收 ACK 直接 CLOSED。
TIME-WAIT 只属于主动关闭方;LAST-ACK 直接关闭、不等 2MSL。CLOSING 是同时关闭的罕见分支(虚线路径)。
图例:实线 = 正常路径;虚线 = 同时打开 / 同时关闭;蓝色填充 = ESTABLISHED 数据态。TIME-WAIT 与 SYN flood 的细节分别见后文对应页。
Handshake · Sequence
Handshake · Details
下次发送从 x+1 开始;FIN 同理占一个序号,纯 ACK 不占(RFC 9293 §3.3)
MSS、窗口缩放(WScale)、SACK-Permitted、时间戳,只在 SYN / SYN+ACK 中交换
标准允许 ACK 捎带应用数据(TFO 更进一步,RFC 7413);服务端收到即完成建连
Linux 上 ss 看到的 SYN-RECV 就是半连接状态——半连接队列与 SYN flood 见后文「内核双队列」页。第三次 ACK 丢失时客户端已 ESTABLISHED、服务端仍 SYN-RECV,重发的是 SYN+ACK。
Handshake · Why 3
C → S:服务端收到 SYN ⇒ 确认「客户端发送能力 OK」
S → C:客户端收到 SYN+ACK ⇒ 确认「服务端收、发能力都 OK」
C → S:服务端收到 ACK ⇒ 确认「客户端接收能力 OK」——缺这一步,服务端不知道自己发的包客户端收没收到
RFC 9293 §3.4 / §3.5:三次握手解决的核心问题是"旧重复连接请求"(old duplicate connection initiations)。并非所有协议都强制三次(TCP Fast Open 借助 cookie 省一次 RTT),但标准握手 3 段是不引入额外机制时的最小可靠方案。
Handshake · ISN
// ISN = 单调时钟 + 伪随机偏移 ISN = M + F(localip, localport, remoteip, remoteport, secret) // M:32 位时钟计数,每 4μs +1(RFC 793 传统) // F:密钥参与的安全伪随机函数 // Linux 实现:net/ipv4/secure_seq.c // 对四元组+密钥做哈希,再叠加时间计数
相同四元组的旧连接还有报文在网络中游荡时,随机化 + 时钟推进让新连接的序号窗口错开,旧数据不会被误认为新数据。固定从 0 开始则新旧连接序号空间完全重叠。
ISN 可预测 ⇒ 不在网络路径上的攻击者(off-path)能猜中序号,对已建立连接伪造 RST 或注入数据(1996 年 Mitnick 攻击)。RFC 1948 首次给出时钟+随机偏移方案,RFC 6528 升级为密钥化 PRF。
序号按模 232 回绕,靠"序号比较 + 时间戳选项(PAWS)"丢弃属于上一轮的旧段。这一块属于可靠传输主题,展开见 TCP 可靠传输与拥塞控制(待沉淀)。
Teardown · Sequence
Teardown · Why 4
TCP 两个方向独立关闭。对方发 FIN 只说明"它不再发了",自己可能还有数据要发——ACK 先回,FIN 等应用层调 close() 再发,一来一回就是 4 段。
被动方无待发数据且应用立刻 close()(延迟 ACK 期间被合并)⇒ ACK 与 FIN 合成一段 FIN+ACK。Linux 下抓包最常见的正是"三次挥手"。
考官如果说"我抓包只有三次",答案是合并,不是四次挥手错了——四次挥手描述的是状态机语义(两个方向各关一次),不是固定报文数。
shutdown(fd, SHUT_WR) 只关写方向:发 FIN 后仍可读对方数据。典型用法:客户端发完请求就 FIN("我发完了"),服务端继续算、把响应全部写完再 FIN。四次挥手时序图中间的虚线数据段就是这个窗口。
close():fd 引用计数减 1,减到 0 才发 FIN;同时关闭读写两个方向shutdown():无视引用计数,立即触发 FIN;可只关一个方向Teardown · TIME_WAIT
最后的 ACK 是不可靠交付的——若它丢了,被动方停在 LAST-ACK 并会重传 FIN。留在 TIME-WAIT 的一端才能重发 ACK,把连接体面收尾。若直接关闭并复用端口,重传的 FIN 会打到新连接上。
保证本连接的报文在网络中全部过期(去程 + 回程各一个 MSL),之后相同四元组的新连接不会收到旧幽灵报文,序号也不串扰。MSL = 报文最大生存时间,RFC 9293 建议 2 分钟,实现可权衡调小。
tcp_tw_reuse=1 + tcp_timestamps=1:基于时间戳安全复用,仅对主动发起方向生效;内核默认 2(仅回环地址)ip_local_port_range;监听端配 SO_REUSEADDRtcp_max_tw_buckets 超上限直接销毁并告警——是兜底不是解法TCP_TIMEWAIT_LEN,include/net/tcp.h),并不读 MSL 配置tcp_tw_recycle 依赖按源 IP 排序时间戳,NAT 下误杀,Linux 4.12 已移除——老博客建议开它的一律过时Implementation · Linux
丢弃新 SYN;tcp_syncookies=1(默认开)时改发 SYN cookie,无状态完成握手
第三次 ACK 被静默丢弃(客户端已 ESTABLISHED、服务端仍 SYN-RECV);tcp_abort_on_overflow=1 则直接回 RST
ss -lnt 的 Recv-Q / Send-Q = 当前全连接队列长度 / 容量;日志 "possible SYN flooding on port X" 常常是 accept 队列满
Attack & Defense
tcp_synack_retries 次、约 1 分钟),期间占满 SYN 队列tcp_syncookies=1(默认):队列满自动启用,正常流量零影响tcp_max_syn_backlog(单纯调大只是提高攻击者成本)tcp_synack_retries 调小Edge Cases
| 丢失的报文 | 双方行为 | 关键参数 / 结果 |
|---|---|---|
| SYN | 客户端重传 SYN | tcp_syn_retries=6,指数退避,约 2 分钟(~131s)后放弃,connect 报超时 |
| SYN+ACK | 双方各自重传:客户端重发 SYN,服务端重发 SYN+ACK | 服务端 tcp_synack_retries=5(约 1 分钟放弃);客户端 syn_retries 内重试 |
| 第三次 ACK | 客户端已 ESTABLISHED;服务端停在 SYN-RECV 重传 SYN+ACK | 客户端后续报文(数据本身带 ACK)到达可补完成握手;服务端放弃后发数据会收到 RST |
| FIN | 发送方重传 FIN | 重传由既有超时机制驱动,应用层无感 |
| 最后 ACK | 被动方停在 LAST-ACK 重传 FIN;主动方(TIME-WAIT)重发 ACK | 这正是 TIME_WAIT 存在的理由一;FIN-WAIT-2 孤儿超时 tcp_fin_timeout=60s |
tcp_abort_on_overflow=1SO_LINGER(on, 0s)强制 RST 关闭tcp_syn_retries=6 · tcp_synack_retries=5 · tcp_fin_timeout=60sTCP_TIMEWAIT_LEN=60s(硬编码)· somaxconn=4096(≥5.4)tcp_abort_on_overflow=0(静默丢弃)· tcp_tw_reuse=2(仅回环)Practice
# 状态总览(各状态连接数) ss -s # 按状态过滤:TIME_WAIT / CLOSE-WAIT / SYN-RECV ss -ant state time-wait | wc -l ss -ant state close-wait | wc -l # 监听端口:Recv-Q=当前 accept 队列, Send-Q=容量 ss -lnt # 抓握手/挥手/RST(过滤标志位) tcpdump -i any -nn 'tcp[tcpflags] & (tcp-syn|tcp-fin|tcp-rst) != 0' port 8080
TIME_WAIT 多在主动关闭方;CLOSE_WAIT 多在被动方且语义完全不同。
resp.Body.Close();连接池里的死连接不清理;错误分支漏关 fd对比:TIME_WAIT 堆积是协议行为(正常),CLOSE_WAIT 堆积是应用 Bug(必须修)。前者治"复用",后者治"关闭路径"。
Go 服务:用 http.Transport 连接池复用长连接(MaxIdleConnsPerHost);未读完 body 就 close 会触发 RST
Interview QA · 1/2
每个方向的 ISN 都需要"提出+确认"共 4 个信息,SYN 与 ACK 可合并,3 段是最小承载。两次握手下服务端无法确认自己的 SYN+ACK 被收到,更致命的是旧重复 SYN 会直接建立死连接;三次握手中客户端校验 ack 发现不是自己的连接,回 RST 拒绝(RFC 9293 §3.4)。
服务端停在 SYN-RECV,重传 SYN+ACK 共 5 次(约 1 分钟)后释放半连接。客户端已 ESTABLISHED,之后发的数据报文本身携带 ACK,到达即可补完成握手;若服务端已释放,客户端发数据会收到 RST。若开了 TFO/数据捎带场景另说,标准答案按上面三层展开。
两个方向独立关闭:被动方收到 FIN 后可能还有数据要发,ACK 先回、FIN 等应用 close 时再发,共 4 段。被动方无数据且立即 close 时 ACK 与 FIN 合并为一段(延迟 ACK 下常见),Linux 抓包常见三次。语义上永远是"两个方向各关一次",报文数只是实现细节。
最后的 ACK 不可靠交付:主动方必须停留一段时间,在被动方(LAST-ACK)重传 FIN 时重发 ACK——理由一;同时等本连接报文全部过期,避免污染相同四元组的新连接——理由二。在主动方是因为它持有"最后一个 ACK"的责任。RFC 建议 2×MSL,Linux 固定 60 秒。
Interview QA · 2/2
根治是复用连接(HTTP keep-alive、连接池、gRPC 长连接)。内核侧:tcp_tw_reuse=1 + tcp_timestamps=1(默认 2 仅回环;仅对主动发起方向生效,NAT 安全);扩 ip_local_port_range;监听端 SO_REUSEADDR。tcp_max_tw_buckets 是兜底上限,超限直接销毁并告警,别当正常手段。tcp_tw_recycle 已于 Linux 4.12 移除,别再提。
对端已关闭、本端应用始终不调 close ⇒ fd 泄漏,最终 fd 耗尽。常见根因:HTTP 响应 body 忘 Close、连接池死链不回收、异常分支漏走关闭逻辑。排查:ss -ant state close-wait 定位对端与端口 → lsof 找 fd 归属 → 代码审"读对端 EOF 后的关闭路径"。与 TIME_WAIT 相反,这是必须修的 Bug 而非协议现象。
伪造源地址海量 SYN 且不完成握手,半连接靠 SYN+ACK 重传自然老化(约 1 分钟),队列被占满后正常连接进不来。防御:tcp_syncookies(状态编码进 SYN+ACK 序号,收到 ACK 验证后重建,代价是选项协商丢失)、加大 backlog、缩短 synack_retries、上游清洗。注意"possible SYN flooding"日志可能实为 accept 队列满。
close 只减 fd 引用计数,到 0 才发 FIN 且双向关闭;shutdown 无视计数立即发 FIN,可用 SHUT_WR 半关闭只关写方向,服务端"发完响应再 FIN"靠它。RST 常见触发:端口无监听、half-open(对端崩溃)、accept 队列溢出且 abort_on_overflow=1、close 时收缓冲有未读数据、SO_LINGER(1,0) 强制终止。
References & Related
本 deck 覆盖"连接管理"边界:建立(握手/ISN/选项)、终止(挥手/TIME_WAIT/RST)、内核队列与攻击面。数据传输过程不在本篇。
键盘操作:←→ 翻页 · T 换主题 · S 演讲者模式 · O 总览。