TCP 握手丢包分析:客户端连接超时、服务端无日志的场景排查实践

📅 2026/7/13 12:34:37 👁️ 阅读次数 📝 编程学习
TCP 握手丢包分析:客户端连接超时、服务端无日志的场景排查实践

本文是网络排查案例集系列的第 1 篇 叙事框架:现象 → 排查过程 → 根因 → 修复 → 预防

问题现象

生产环境某上游服务频繁上报连接超时。抓包分析:在客户端观察到 SYN 报文正常发出,在服务端确认 SYN 已到达且 SYN-ACK 已回复,但客户端侧未捕获到 SYN-ACK。根因定位为防火墙 ACL 规则丢弃了 SYN-ACK 报文。

排查过程

第一步:确认服务端状态

张工先确认服务进程正常运行。

ss -tlnp | grep 8080确认端口在监听,进程正常运行。但 access 日志中 grep 超时时段的关键字,结果为空——这印证了服务端确实没收到这些请求。

为了看清网络层面发生了什么,张工在服务端启动 tcpdump:

tcpdump-ieth0 port8080-c20-nn-w/tmp/handshake.pcap

等待 15 秒后查看抓包结果,发现了一个关键模式:

09:35:12.345678 IP10.0.1.100.54321>10.0.2.200.8080: Flags[S]← SYN 09:35:12.345679 IP10.0.2.200.8080>10.0.1.100.54321: Flags[S.]← SYN+ACK 09:35:15.345678 IP10.0.1.100.54321>10.0.2.200.8080: Flags[S]← SYN 重传 09:35:15.345679 IP10.0.2.200.8080>10.0.1.100.54321: Flags[S.]← SYN+ACK 重传 09:35:21.345678 IP10.0.1.100.54321>10.0.2.200.8080: Flags[S]← SYN 再次重传 09:35:21.345679 IP10.0.2.200.8080>10.0.1.100.54321: Flags[S.]← SYN+ACK 再次重传

模式是:服务端收到了客户端的 SYN,也回复了 SYN+ACK,但第三次握手的 ACK 始终没有来。客户端不断重传 SYN,服务端每次都回应了 SYN+ACK,但连接始终无法建立。

这是一个典型的半开连接场景——服务端认为连接在进行中,但客户端感知不到。

第二步:检查 TCP 连接状态

张工用ss查看 TCP 半连接队列:

$ ss-tstate syn-recv Recv-Q Send-Q Local Address:Port Peer Address:Port0110.0.2.200:808010.0.1.100.543210110.0.2.200:808010.0.1.100.543220110.0.2.200:808010.0.1.100.543230110.0.2.200:808010.0.1.100.543240110.0.2.200:808010.0.1.100.54325

5 个连接停留在 SYN-RECV 状态。这意味着服务端收到了 SYN、发出了 SYN+ACK,但客户端一直没有回复 ACK 来完成三次握手。这些连接正在消耗服务端的 TCP 资源。

进一步用nstat查看重传统计:

$ nstat-az|grep-E'Retrans|Loss|Syn'TcpExtTCPSynRetrans1560.0TcpExtTCPFastRetrans30.0TcpExtTCPLostRetransmit120.0

156 次 SYN 重传——这是服务端反复回应客户端 SYN 重传所发出的 SYN+ACK 累加数量。

ss -ti查看每个半连接的详细 TCP 参数:

SYN-RECV0110.0.2.200:808010.0.1.100:54321 wscale:7,7 rto:312 rtt:3.5/1.2 ato:40 mss:1460 cwnd:1 bytes_acked:0 segs_out:4 segs_in:4

关键信息:cwnd:1bytes_acked:0segs_out:4——发送了 4 个段但 0 字节被确认,这正是半开连接的典型特征。

第三步:深入分析重传统计

为了确认丢包发生在哪个方向,张工进一步查看了网络层的统计数据:

$netstat-s|grep-E'retrans|Retrans'178segments retransmitted167timesSYN retransmitted

更重要的是ethtool -S eth0的输出:

tx_dropped:167

167 个包在发送时被丢弃,与 167 次 SYN 重传完全吻合。这说明 SYN+ACK 包在服务端本机的网络栈中就被丢弃了,根本没发到网线上。问题不在网络中间设备,而在本机的网络配置。

第四步:用 Wireshark 视角看清 TCP 握手全过程

为了直观地理解 TCP 三次握手在被 iptables 阻断前后的差异,我们用 Wireshark 风格的包分析工具来看:

正常的三次握手:

  1. Packet 1 — SYN:客户端(10.0.1.100:54321)→ 服务端(10.0.2.200:8080),Seq=0,Flags=SYN
  2. Packet 2 — SYN+ACK:服务端 → 客户端,Seq=0,Ack=1,Flags=SYN+ACK
  3. Packet 3 — ACK:客户端 → 服务端,Seq=1,Ack=1,Flags=ACK

三次握手在 0.126ms 内完成,连接建立。

客户端抓包视角——只有 SYN 发出,没有 SYN+ACK 回来:

从客户端角度看:

  1. SYN 发出(0.000s)
  2. 等待 3s 没有收到 SYN+ACK → 重传 SYN(3.000s)
  3. 再等 6s → 重传 SYN(9.000s)
  4. 最后等 12s → 重传 SYN(21.000s)
  5. 最终connect()超时,抛出Connection timed out

客户端每次重传的间隔呈指数退避(3s → 6s → 12s),这是 TCP 协议的重传机制。如果 SYN+ACK 一直不来,客户端的connect()系统调用最终会在超时后返回错误。

服务端抓包视角——SYN+ACK 发出就被丢:

服务端看到的是:客户端的 SYN 每次都到达,服务端每次都回复 SYN+ACK,但回复的包被标识为 “DROPPED by iptables”。服务端的 TCP 栈认为连接还在建立中,所以在 SYN-RECV 队列中维护了这些半连接。

这正是"客户端报超时、服务端说没收到"的完整链路图。

第五步:定位根因——iptables 规则

有了前几步的分析,张工把矛头指向了本机的网络过滤规则。

$ iptables-LOUTPUT-n-v--line-numbers Chain OUTPUT(policy ACCEPT32567packets,12.3M bytes)num pkts bytes target prot optinoutsourcedestination100ACCEPT all -- * *0.0.0.0/00.0.0.0/0 state RELATED,ESTABLISHED215612480DROP tcp -- * *0.0.0.0/00.0.0.0/0 tcp spt:8080 tcpflags: SYN,ACK/SYN,ACK33256712.3M ACCEPT all -- * *0.0.0.0/00.0.0.0/0

第 2 条规则:DROP tcp spt:8080 tcpflags: SYN,ACK/SYN,ACK

这条规则匹配从 8080 端口发出的、设置了 SYN+ACK 标志位的 TCP 包,然后丢弃。pkts=156,已经丢弃了 156 个 SYN+ACK 包——与前面的 156 次 SYN 重传、167 次 tx_dropped 完美对齐。

这条规则来自上周安全加固的脚本,本意是防 SYN flood 攻击,但配置在 OUTPUT 链上,且匹配条件写反了方向。SYN flood 防护应该在 INPUT 链限制 SYN 包的速率,而不是在 OUTPUT 链丢弃 SYN+ACK 回包。

# 正确的做法:在 INPUT 链限制 SYN 速率iptables-AINPUT-ptcp--dport8080--syn-mlimit--limit100/s-jACCEPT iptables-AINPUT-ptcp--dport8080--syn-jDROP

第六步:修复与验证

张工立即删除了有问题的规则:

iptables-DOUTPUT2

删除后,再次检查 SYN-RECV 队列:

$ ss-tstate syn-recv# 空——队列已清空

客户端重试的请求立刻建立连接成功:

$ ss-tnsport=:8080|grepESTAB ESTAB0010.0.2.200:808010.0.1.100:54324 ESTAB0010.0.2.200:808010.0.1.100:54325

access 日志中恢复正常流量:

10.0.1.100 - -[18/Jun/2025:09:42:15 +0800]"POST /api/callback HTTP/1.1"2004510.0.1.100 - -[18/Jun/2025:09:42:16 +0800]"POST /api/callback HTTP/1.1"20045

用 Wireshark 风格工具验证修复后的完整握手:

SYN → SYN+ACK(✅ Delivered)→ ACK(三次握手完成),接着客户端发送 HTTP POST 数据(PSH+ACK),完整的数据传输恢复正常。

根因分析

这次事故的直接原因是 iptables OUTPUT 链上的一条规则错误地丢弃了出站的 SYN+ACK 包。但更深层的问题是:

1. 防 SYN flood 的规则配反了方向

SYN flood 攻击的特征是大量伪造源 IP 的 SYN 包涌入服务端,服务端回复 SYN+ACK 后永远收不到最终的 ACK,导致半连接队列被耗尽。因此 SYN flood 防护应在INPUT 链限制 SYN 包的速率,而非在 OUTPUT 链丢弃 SYN+ACK。

这条有问题的规则做了完全相反的事——它限制了正常回包。服务端处理正常客户端的 SYN 请求,回复 SYN+ACK,但 SYN+ACK 被本机 iptables 丢弃。客户端收不到 SYN+ACK,重传 SYN,服务端再次回复 SYN+ACK——再次被丢弃。恶性循环形成。

2. iptables 规则上线前没有 review

安全加固脚本直接推到了生产环境,没有经过 code review 和测试环境验证。iptables 规则的链选择(INPUT vs OUTPUT)、匹配条件(sport vs dport)、目标动作(ACCEPT vs DROP)都需要仔细核对。

3. 监控没有覆盖 TCP 建连阶段

应用层监控只关注了接口响应时间和错误率,但没有拆解 TCP 连接建连阶段的指标。如果当时有 SYN-RECV 队列深度的监控,这个问题在告警出现前就能被发现。

避坑建议

  1. iptables 规则上线前必须 Review:特别是涉及 DROP 操作的规则,确认链方向、端口匹配条件、协议标志位的组合是否符合预期。建议用iptables -L -n -v检查规则命中数,上线初期频繁观察 pkts 列是否异常增长。

  2. 防 SYN flood 用专业工具而非手写 iptables:Linux 内核的sysctl参数(tcp_syncookiestcp_max_syn_backlogtcp_syn_retries)提供了更成熟的 SYN flood 防护机制。专业场景建议用 nftables、fail2ban 或硬件防火墙。

  3. TCP 建连阶段纳入监控:SYN-RECV 队列长度、TCP 重传率(nstat -az TcpRetransSegs)应该作为基础网络指标监控。一旦 SYN-RECV 增长或重传率超过阈值,立即告警。

  4. 客户端设置合理的连接超时connect()超时应根据业务需求设置,通常 5-10s 即可,不必设到 120s。长超时会导致线程资源被长时间占用,在批量超时场景下可能引发线程池耗尽。

  5. tcpdump 双向抓包对比:排查连接问题时,客户端和服务端同时抓包最能定位丢包点。客户端只看到 SYN 发出没收到 SYN+ACK,服务端看到 SYN 和 SYN+ACK 但没有 ACK——方向一目了然。

  6. 安全加固脚本遵循最小权限原则:iptables 默认策略改为 DROP 前,确认已有 ACCEPT 规则覆盖所有必要的流量。逐条测试后再设置默认策略。

附:完整命令清单

# 检查端口监听ss-tlnp|grep8080# 查看 TCP 半连接队列ss-tstate syn-recv ss-tnsport=:8080# 查看 TCP 详细参数(RTT、cwnd、重传等)ss-tistate syn-recv# 查看 TCP 重传统计nstat-az|grep-E'Retrans|Loss|Syn'netstat-s|grep-iretrans# 查看网卡丢包统计ethtool-Seth0|grep-E'drop|error|retrans'# iptables 规则检查iptables-LOUTPUT-n-v--line-numbers# 删除有问题的规则iptables-DOUTPUT2# 服务端抓包tcpdump-ieth0 port8080-c20-nn-w/tmp/handshake.pcap tcpdump-r/tmp/handshake.pcap-nn# 筛选特定 TCP 标志位的包tcpdump-ieth0'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack != 0'-nntcpdump-ieth0'tcp[tcpflags] & tcp-rst != 0'-nn