客户端报连接超时、服务端说没收到——TCP 握手丢包分析
本文是网络排查案例集系列的第 1 篇 叙事框架:
现象 → 排查过程 → 根因 → 修复 → 预防
问题现象
某日早高峰,运维群突然弹出告警:支付回调(payment-callback)接口超时率超过 2%,当前已达 3.2%。
值班的张工立刻登录服务器查看。服务端口在正常监听,但查看 access 日志却发现——超时时段内一条请求记录都没有。

症状:客户端调用 payment-callback 时抛出 java.net.ConnectException: Connection timed out,而服务端完全没收到这些请求。客户端超时时间设为 120s,每次都是完整超时后才报错。

这不是普通的慢请求——服务端根本没感知到连接的存在。问题出在哪一层?应用层?网络层?还是中间设备?
排查过程
第一步:确认服务端状态
张工先确认服务进程正常运行。

ss -tlnp | grep 8080 确认端口在监听,进程正常运行。但 access 日志中 grep 超时时段的关键字,结果为空——这印证了服务端确实没收到这些请求。
为了看清网络层面发生了什么,张工在服务端启动 tcpdump:
tcpdump -i eth0 port 8080 -c 20 -nn -w /tmp/handshake.pcap
等待 15 秒后查看抓包结果,发现了一个关键模式:
09:35:12.345678 IP 10.0.1.100.54321 > 10.0.2.200.8080: Flags [S] ← SYN
09:35:12.345679 IP 10.0.2.200.8080 > 10.0.1.100.54321: Flags [S.] ← SYN+ACK
09:35:15.345678 IP 10.0.1.100.54321 > 10.0.2.200.8080: Flags [S] ← SYN 重传
09:35:15.345679 IP 10.0.2.200.8080 > 10.0.1.100.54321: Flags [S.] ← SYN+ACK 重传
09:35:21.345678 IP 10.0.1.100.54321 > 10.0.2.200.8080: Flags [S] ← SYN 再次重传
09:35:21.345679 IP 10.0.2.200.8080 > 10.0.1.100.54321: Flags [S.] ← SYN+ACK 再次重传
模式是:服务端收到了客户端的 SYN,也回复了 SYN+ACK,但第三次握手的 ACK 始终没有来。客户端不断重传 SYN,服务端每次都回应了 SYN+ACK,但连接始终无法建立。
这是一个典型的半开连接场景——服务端认为连接在进行中,但客户端感知不到。
第二步:检查 TCP 连接状态
张工用 ss 查看 TCP 半连接队列:

$ ss -t state syn-recv
Recv-Q Send-Q Local Address:Port Peer Address:Port
0 1 10.0.2.200:8080 10.0.1.100.54321
0 1 10.0.2.200:8080 10.0.1.100.54322
0 1 10.0.2.200:8080 10.0.1.100.54323
0 1 10.0.2.200:8080 10.0.1.100.54324
0 1 10.0.2.200:8080 10.0.1.100.54325
5 个连接停留在 SYN-RECV 状态。 这意味着服务端收到了 SYN、发出了 SYN+ACK,但客户端一直没有回复 ACK 来完成三次握手。这些连接正在消耗服务端的 TCP 资源。
进一步用 nstat 查看重传统计:
$ nstat -az | grep -E 'Retrans|Loss|Syn'
TcpExtTCPSynRetrans 156 0.0
TcpExtTCPFastRetrans 3 0.0
TcpExtTCPLostRetransmit 12 0.0
156 次 SYN 重传——这是服务端反复回应客户端 SYN 重传所发出的 SYN+ACK 累加数量。
用 ss -ti 查看每个半连接的详细 TCP 参数:
SYN-RECV 0 1 10.0.2.200:8080 10.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:1、bytes_acked:0、segs_out:4——发送了 4 个段但 0 字节被确认,这正是半开连接的典型特征。
第三步:深入分析重传统计
为了确认丢包发生在哪个方向,张工进一步查看了网络层的统计数据:

$ netstat -s | grep -E 'retrans|Retrans'
178 segments retransmitted
167 times SYN retransmitted
更重要的是 ethtool -S eth0 的输出:
tx_dropped: 167
167 个包在发送时被丢弃,与 167 次 SYN 重传完全吻合。 这说明 SYN+ACK 包在服务端本机的网络栈中就被丢弃了,根本没发到网线上。问题不在网络中间设备,而在本机的网络配置。
第四步:用 Wireshark 视角看清 TCP 握手全过程
为了直观地理解 TCP 三次握手在被 iptables 阻断前后的差异,我们用 Wireshark 风格的包分析工具来看:
正常的三次握手:

- Packet 1 — SYN:客户端(10.0.1.100:54321)→ 服务端(10.0.2.200:8080),Seq=0,Flags=SYN
- Packet 2 — SYN+ACK:服务端 → 客户端,Seq=0,Ack=1,Flags=SYN+ACK
- 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 -L OUTPUT -n -v --line-numbers
Chain OUTPUT (policy ACCEPT 32567 packets, 12.3M bytes)
num pkts bytes target prot opt in out source destination
1 0 0 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
2 156 12480 DROP tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp spt:8080 tcpflags: SYN,ACK/SYN,ACK
3 32567 12.3M ACCEPT all -- * * 0.0.0.0/0 0.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 -A INPUT -p tcp --dport 8080 --syn -m limit --limit 100/s -j ACCEPT
iptables -A INPUT -p tcp --dport 8080 --syn -j DROP
第六步:修复与验证
张工立即删除了有问题的规则:
iptables -D OUTPUT 2
删除后,再次检查 SYN-RECV 队列:
$ ss -t state syn-recv
# 空——队列已清空
客户端重试的请求立刻建立连接成功:
$ ss -tn sport = :8080 | grep ESTAB
ESTAB 0 0 10.0.2.200:8080 10.0.1.100:54324
ESTAB 0 0 10.0.2.200:8080 10.0.1.100:54325
access 日志中恢复正常流量:
10.0.1.100 - - [18/Jun/2025:09:42:15 +0800] "POST /api/callback HTTP/1.1" 200 45
10.0.1.100 - - [18/Jun/2025:09:42:16 +0800] "POST /api/callback HTTP/1.1" 200 45
用 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 队列深度的监控,这个问题在告警出现前就能被发现。
避坑建议
-
iptables 规则上线前必须 Review:特别是涉及 DROP 操作的规则,确认链方向、端口匹配条件、协议标志位的组合是否符合预期。建议用
iptables -L -n -v检查规则命中数,上线初期频繁观察 pkts 列是否异常增长。 -
防 SYN flood 用专业工具而非手写 iptables:Linux 内核的
sysctl参数(tcp_syncookies、tcp_max_syn_backlog、tcp_syn_retries)提供了更成熟的 SYN flood 防护机制。专业场景建议用 nftables、fail2ban 或硬件防火墙。 -
TCP 建连阶段纳入监控:SYN-RECV 队列长度、TCP 重传率(
nstat -az TcpRetransSegs)应该作为基础网络指标监控。一旦 SYN-RECV 增长或重传率超过阈值,立即告警。 -
客户端设置合理的连接超时:
connect()超时应根据业务需求设置,通常 5-10s 即可,不必设到 120s。长超时会导致线程资源被长时间占用,在批量超时场景下可能引发线程池耗尽。 -
tcpdump 双向抓包对比:排查连接问题时,客户端和服务端同时抓包最能定位丢包点。客户端只看到 SYN 发出没收到 SYN+ACK,服务端看到 SYN 和 SYN+ACK 但没有 ACK——方向一目了然。
-
安全加固脚本遵循最小权限原则:iptables 默认策略改为 DROP 前,确认已有 ACCEPT 规则覆盖所有必要的流量。逐条测试后再设置默认策略。
附:完整命令清单
# 检查端口监听
ss -tlnp | grep 8080
# 查看 TCP 半连接队列
ss -t state syn-recv
ss -tn sport = :8080
# 查看 TCP 详细参数(RTT、cwnd、重传等)
ss -ti state syn-recv
# 查看 TCP 重传统计
nstat -az | grep -E 'Retrans|Loss|Syn'
netstat -s | grep -i retrans
# 查看网卡丢包统计
ethtool -S eth0 | grep -E 'drop|error|retrans'
# iptables 规则检查
iptables -L OUTPUT -n -v --line-numbers
# 删除有问题的规则
iptables -D OUTPUT 2
# 服务端抓包
tcpdump -i eth0 port 8080 -c 20 -nn -w /tmp/handshake.pcap
tcpdump -r /tmp/handshake.pcap -nn
# 筛选特定 TCP 标志位的包
tcpdump -i eth0 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack != 0' -nn
tcpdump -i eth0 'tcp[tcpflags] & tcp-rst != 0' -nn