一次诡异的 TCP 连接重置排查

一次诡异的 TCP 连接重置排查:握手能成功,一发数据就被 RST

关键词:TCP RST、非对称路由、子网掩码不一致、状态防火墙、conntrack、ICMP Redirect、MikroTik RouterOS、Wireshark 两端抓包

引子:先回答一个问题

把环境抛开,先问一个纯技术问题:

两台主机,一台认为自己和对方在同一个子网、无需经过网关;另一台认为自己不和对方在同一个子网、需要经过网关。会发生什么?

先说明:这种"各算各的"完全合法。"是否直连"是由每台主机按自己的掩码单方面算出来的,两端不需要达成一致——所以谁都不觉得自己配错了,配置界面上一切正常。

多数人的直觉是:"顶多绕一下,路径长一点,能通就行。"

现实给出的答案远比这诡异:

ping 完全正常,TCP 三次握手也能成功建立,但只要发出第一个带业务数据的包,连接立刻被 RST,永远建不起来。

更让人困惑的是:两端主机的防火墙都是关的,服务本身也活得好好的——责任到底在谁?

这是我们前不久在生产环境里遇到的一次真实故障。下面按实际排查顺序还原:现象 → 定位 RST 的归属 → 两端抓包 → 根因 → 原理 → 解决。

一、故障现象

一台 PC(下称 PC-A)访问一台 Windows 服务器(下称 SRV-B)的两个服务:

  • SRV-B:3000 上的 Web 服务
  • SRV-B:3389 的远程桌面

具体表现:

  • 浏览器/客户端报"连接被重置",重试无限循环,全天不恢复;
  • ICMP ping 完全正常;
  • TCP 三次握手可以成功建立(抓包里 SYN / SYN-ACK / ACK 齐全);
  • 但只要客户端发出带业务数据的包(HTTP GET、RDP 的 X.224 协商请求),连接立刻被 RST 打断,反复重试永远失败。

两端主机的 Windows 防火墙都已关闭,服务器上的服务也确认正常启动。这就排除了最直觉的两类怀疑(ACL 拦截、应用故障)。"能 ping、能握手,就是发不了数据"——三个看似矛盾的结论同时成立,正是这类故障最迷惑人的地方。

二、第一步:先搞清"RST 到底是谁发的"

拿到一份抓包后,很多人的第一反应是"看 RST 的源 IP 是谁"——这是不够的,因为 RST 是可伪造的(IP 层源地址随便写)。要判断 RST 的真实来源,必须看下面四个维度:

判据 说明
以太网源 MAC 最有价值的一列。RST 帧的 eth.src 是不是主机真实网卡?还是网关设备?
IP TTL 对端直连应为默认值(Windows=128、Linux=64);少 1 跳说明包经过了路由
TCP 标志位组合 内核发的 RST 通常符合规范;网络设备伪造的 RST 常带异常组合(如同时带 PSH)
Seq/Ack 是否合理 例如 RST 的 Seq 恰好"确认"了刚被丢弃的那个包的字节数 → 它是对那个包生成的

实操技巧:在 Wireshark 里加三列自定义列 —— eth.src、ip.ttl、tcp.flags.str,肉眼扫一遍就能筛掉大部分误判。

本例的第一手线索:RST 的 TTL 与服务端 SYN-ACK 的 TTL 一模一样(都是 127),而同网段直连本应是 128。这说明两个方向都走了路由,路径上有一跳设备。

三、决定性证据:两端同时抓包

单端抓包只能看到"现象",两端同时抓包才能看到"差异"。在 PC-A 和 SRV-B 上同时抓包后(地址已匿名化):

角色 地址 抓到的关键包
PC-A 10.10.103.37/23 SYN、SYN-ACK、ACK、请求、重传、最后收到 RST
SRV-B 10.10.102.200/24 SYN、SYN-ACK、请求、响应、被伪造的 RST、最终 RST
网关 10.10.102.1 持续发出 ICMP Redirect

先看清这组地址为什么是关键(本例的整个前提就在这两个掩码上):

  • PC-A 是 /23,子网为 10.10.102.0/23(覆盖 10.10.102.1 ~ 10.10.103.254),它把 10.10.102.200 判定为同网段 → 直接 ARP、二层送达,不经过网关;
  • SRV-B 是 /24,子网为 10.10.102.0/24,它认为 10.10.103.37 是异网段 → 所有回包交给默认网关 10.10.102.1。

两端掩码不一致,各自按自己的掩码做判断,于是同一条会话的两个方向走了不同的路——这就是非对称路由。

对比结论:

  1. PC-A 发出的所有包都是二层直连(以太网源 = PC-A 网卡,目的 = SRV-B 网卡);
  2. SRV-B 发出的所有回包都交给了网关(以太网目的 = 网关 MAC);
  3. SRV-B 明确发出了响应报文(RDP Negotiate Response),但 PC-A 侧从未收到——被网关吞掉了;
  4. 紧接着 SRV-B 收到一个 RST:tcp.srcport = 客户端端口、tcp.dstport = 3389、Seq 恰好确认了那个被吞掉的响应包;
  5. 但 PC-A 侧抓包里,PC-A 从未发送过这个 RST;
  6. 该 RST 帧的以太网源 MAC 是网关的,而不是 PC-A 的网卡。

结论:这个 RST 是网关伪造的(源 IP/端口伪装成客户端)。这是 reject-with=tcp-reset 这类行为的标志性特征:被拒绝的包不回,反而把 RST 按"反向地址"发回给原发送方。

至此完整的死亡时序还原(以 RDP 为例):

客户端 SYN ──── 二层直连 ────► 服务端          (网关看不到)
客户端 ◄──── 经网关 ──── SYN,ACK                 (网关据此建了个半开表项)
客户端 ACK / Negotiate Request ── 二层直连 ────► (网关看不到,表项永远到不了 established)
服务端 Negotiate Response ─────► 网关:状态不符,丢弃 ✗
服务端 ◄──── 网关伪造的 RST(伪装成客户端)      (服务端单方面销毁会话)
客户端 重传 Negotiate Request ──► 服务端         (撞上已销毁的会话)
客户端 ◄──── 服务端内核真实 RST(被网关正常转发) (两端全断)

四、根因:一个掩码写错,引发全链路连环坑

现在可以回答引子里的那个问题了。答案是:两端各算各的,谁都没算错,但同一条会话的两个方向走上了两条不同的路。

PC-A 掩码 /23   → 认为 SRV-B 同网段 → 直接二层发包(绕过网关)
SRV-B 掩码 /24  → 认为 PC-A 异网段 → 所有回包发给默认网关
                        ↓
              流量路径不对称(非对称路由)
                        ↓
     网关只能看到"服务端 → 客户端"的单向流量
                        ↓
     状态防火墙连接跟踪里的会话永远是"半开"状态
                        ↓
  ① 丢弃服务端带业务数据的响应
  ② 用 tcp-reset 拒绝,并伪造 RST 打回服务端
                        ↓
     客户端收不到响应 → 无限重传;服务端收到伪 RST → 断链

是的,整条链路的所有异常,源头只是"两台机器的子网掩码不一致"。

五、原理追问:四个"为什么"

1. 为什么 ping 能通?

ICMP 是无连接协议,不参与 TCP 状态机。路由器只需查路由表转发,不检查会话状态。所以路径虽不对称,也没有任何设备会拦它。

2. 为什么三次握手能成功?

状态防火墙的会话表就是靠 SYN 建立的。SYN / SYN-ACK 在状态机里是"连接的合法开端",任何厂商的策略都必须放行,否则所有连接都建不起来。

本例的微妙之处:网关只看到服务端的 SYN-ACK(回程经它),于是建立了一个半开(SYN_RECV)表项——表项建起来了,所以 SYN-ACK 能过。

3. 为什么带业务数据的包被 RST?

因为客户端的 ACK 和请求全部走了二层直连,网关看不到,该表项永远无法迁移到 established。当服务端发出带 payload 的响应时,网关看到的是"一条还没建立的会话里突然出现了数据包" → 判定为 invalid / out-of-state → 命中拒绝规则。

此外还有两重叠加因素:

  • 很多设备对"无 SYN 却携带数据"的包有专门防护逻辑(防伪造、防序列号攻击);
  • 应用层识别(IPS/DPI)需要 payload 才能做判断。握手阶段没有数据,设备无从下手;一旦出现 GET / HTTP/1.1、X.224 协商这类特征,才命中策略。这正是"RST 总出现在请求发出之后"的原因。

4. 为什么客户端也能收到 RST?为什么这个 RST 反而被放行?

网关只看得见回程流量,它只能朝服务端方向伪造 RST。客户端收到的 RST 其实是服务端内核发出来的,遵循两条 TCP 铁律:

  • 无监听端口收到 SYN → 回 RST, ACK(所以 3000 端口在服务未启动时会立刻被拒);
  • 收到不属于任何已存在连接的数据包 → 回 RST(RFC 793 / RFC 5961 的强制行为,目的是让对端立刻停止重传)。

之所以服务端发的这个 RST 能被网关正常转发、而前面的业务响应包被拦,原因有三:

  1. 状态防火墙普遍对 RST 特例放行——RST 是连接的"善后包",拦掉它反而会让半开会话堆积,比放行更危险;
  2. 前一步 reject 后该会话表项已被清理/标记终止,纯 RST 不再匹配"非法数据"规则;
  3. RST 无 payload、无 ACK 数据,天然不满足那些拦截规则的匹配条件。

这个细节本身就是"设备在做状态检测而非简单 ACL"的有力佐证:静态 ACL 是包级的,不会出现"同一五元组,有的拦有的放"。

5. 那个 ICMP Redirect 是什么信号?

抓包里网关在持续回 ICMP Type 5 / Code 1(Redirect for host)。路由器只在"主机把包发给我,但按我的路由表这个包应该从同一链路直接送达"时才发它。

所以它是症状物证,不是病因:它直接证明了"SRV-B 正在把本该二层直连的流量绕给网关",同时反证网关认为两台主机本就在同一网段(即规划就是 /23)。

有趣的是:Windows 默认忽略 ICMP Redirect(EnableICMPRedirect=0),所以 SRV-B 收到后照旧走网关,故障现象分毫未变。反过来说,如果 SRV-B 是 Linux(默认接受重定向),它很可能"听劝"改成直连发送,这个故障根本不会出现。这也解释了为什么这类配置错误在 Windows/Linux 混用环境里特别容易暴露。

六、解决方案

根治只有一条路:消除非对称路由。

  1. 统一两端子网掩码(首选)
    • 若两个网段本属同一广播域(本例即如此),把配置错误的一端改回正确掩码;
    • 若网络规划本就是两个独立子网,则必须保证双向流量都正常经网关三层转发。
  2. 网关侧整改(如果流量确实需要过网关)
    /ip firewall filter print stats     # 定位是哪些规则在拒绝(计数会持续增长)
    /ip firewall filter print detail    # 找 action=reject reject-with=tcp-reset 及状态相关规则
    /ip firewall connection print       # 观察会话是否长期停留在半开状态
    /ip settings set send-redirects=no  # 关闭 ICMP Redirect(安全基线要求)
    
    建议把对 invalid 状态流量的处理从 reject (tcp-reset) 改为 drop——伪造 RST 会同时干扰两端主机的连接状态,比静默丢弃更难排查。

改完掩码后验证(重新两端抓包):

  • 双向报文的以太网源/目的都是两端网卡,不再出现网关 MAC;
  • 不再出现 ICMP Redirect;
  • 无来源不明的 RST;
  • HTTP 一次成型(GET → 200 OK),RDP 协商一次成型。

七、经验沉淀(可以直接抄进排查手册)

诊断方法

  1. ping 通 ≠ 端口通,握手成功 ≠ 会话可用。 三者的判定依据完全不同:路由表 / 状态机 / 会话状态。
  2. 判断 RST 归属必须看以太网源 MAC,不能只看 IP。伪造 RST 的 IP 源地址是随便写的。
  3. 两端同时抓包是终结争议的唯一手段:单端只能看到现象,两端对比才能发现"发送方发了、接收方没收到"。
  4. 在抓包里永远打开 eth.src / ip.ttl / tcp.flags.str 这几列,它们比 IP 地址更能说明问题。
  5. ICMP Redirect 是路由配置错误的警报器:一旦出现,先怀疑掩码/路由配置。
  6. TTL 是最便宜的路径探测器:同网段直连不该出现 TTL 减 1。

常见命令

# Windows:判断 IP 是 DHCP 下发还是手工配置(PrefixOrigin 才是权威字段)
Get-NetIPAddress -AddressFamily IPv4 |
  Select-Object InterfaceAlias, IPAddress, PrefixLength, PrefixOrigin

route print                      # 看目标网段是 on-link 还是走网关
arp -a                           # 看 ARP/邻居表是否被网关代答
Get-ItemProperty HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters -Name EnableICMPRedirect
# Wireshark 常用过滤
tcp.flags.reset == 1                       # 所有 RST
tcp.port == 3389 && ip.addr == 10.10.102.200 # 锁定目标会话
icmp.type == 5                             # 只看重定向
tcp.analysis.retransmission                # 只看重传(判断是否"有去无回")

预防

  • 变更 IP 配置时必须同时核对掩码与网关,不要只核 IP 本身;
  • 批量配置优先走 DHCP,并核对作用域掩码(option 1)是否与规划一致;
  • 网络规划变更(比如把两个 /24 合并成 /23)后,务必巡检存量主机的掩码是否全部更新到位——这类历史遗留配置不一致往往潜伏很久,直到某个应用恰好在"发数据"时才暴露。

八、延伸思考:能不能靠"关掉状态检测"绕过这个问题?

不能。两个理由。

1. 现实中绝大多数路由器都做状态检测

决定因素不是"它叫不叫路由器",而是它上面配了什么:

设备/配置形态 是否有状态 非对称路由下的表现
纯三层转发(未配 ACL/NAT/防火墙) 无 正常转发,本故障不出现
无状态 ACL 无(包级匹配) 只有显式 deny + reset 才会拦
NAT / PAT 设备 必须做(否则回程无法反向转换) 会话不完整 → 丢包、连不上
状态防火墙(SPI / conntrack) 有 本案例:判 invalid → 丢弃或拒绝
uRPF / rp-filter 源地址校验 无状态,但有路径校验 静默丢包(比 RST 更隐蔽)
IPS / 应用识别 / 上网行为管理 有 + 深度检测 命中应用特征后重置

家用无线路由器、企业出口设备几乎都带 NAT 或防火墙,所以实际上几乎找不到"真的不做状态检测"的路由器。真正的纯转发只出现在核心交换机的三层接口、或未配置任何 ACL 的路由接口上。

2. 就算真的不检测,非对称路由照样坑你

只是症状换了一副面孔:

  • uRPF(严格反向路径校验)直接静默丢包——没有任何 RST,只有"有去无回";
  • NAT 只能转换一半——回程找不到映射表项,直接连不上;
  • 有状态集群(防火墙/负载均衡双机 HA)会话表同步不上;
  • 旁路审计、流量分析设备只能看到半条流——报表和日志失真(对安全审计要求高的场景,这本身就是问题)。

所以"关掉状态检测"不是解决方案,而是把干净利落的 RST 换成更隐蔽的丢包,让排查难度再上一个台阶。

3. 一个有意思的推论:同一种根因,四种完全不同的症状

这是因为设备对"异常状态流量"的处置动作是可配置的:

网关对 invalid 流量的处置 用户看到的现象 常见误诊方向
drop(静默丢弃) 无响应、无限重传 "服务卡死了""网络不通"
reject(ICMP) 报"目标主机不可达" "路由配错了"
reject(tcp-reset) 连接被 RST "服务端应用有问题"(本案例)
不检查状态 通常能正常访问 ——

同一个根因(非对称路由),四种截然不同的"病例"。 这也是这类问题特别容易被误诊、并反复在应用侧"排查"的原因。

临时验证技巧(RouterOS):可以对怀疑的流量下发 notrack 让它跳过连接跟踪:

/ip firewall raw add chain=prerouting src-address=<客户端> dst-address=<服务端> \
  protocol=tcp action=notrack

如果加上后立刻恢复正常,就坐实了"是 conntrack 状态判定导致的"。仅用于验证,不要长期保留(该流量会同时失去状态防护;且若涉及 NAT 会直接失效)。

最终结论:不要试图通过降低中间设备的检测强度来治病。唯一正确的做法始终是消除非对称路由,让双向流量路径对称——症状会随配置变化,根因不会。


posted on 2026-10-10 19:17  yangras  阅读(3)  评论(0)    收藏  举报

导航