DanielLavrushin:ClientHello 分段时 B4 可能漏计重传,导致停滞升级不触发

DanielLavrushin 说明,ClientHello 的域名位于第二个 TCP segment 时,内核重传的首段没有域名,B4 的 stall escalation 和 IP Block Detection resend check 因而仍只计一次,连接可能等待客户端超时;在其测试中,域名位于第二段的停滞升级为 0/3。建议按序列号识别已解析域名连接的首段重传,并覆盖两类触发器的网络命名空间测试。

作者原文

Problem

The stall escalation and the IP Block Detection resend check both count copies of the ClientHello, and only for a packet whose parsed ClientHello gives a name (handler.go#L853-L857).

When a ClientHello spans two segments with the name in segment 2, the first pass counts once. The kernel then resends only the first unacknowledged segment, which has no name, so host stays empty (handler.go#L645, #L663) and the copy is not counted. The count stays at 1, so neither check fires (#L859, #L912).

The connection waits until the client gives up. With IP Block Detection and Cache Blocked IPs on, the silence check takes over later and bans the address if it is silent for neutral names too.

Evidence

Measured 2026-11-05 on Redcom with a uTLS Chrome client, the same on v1.89.1 and on main (1d2271e9).

| Stall escalation | Name in segment 1 | Name in segment 2 only |
| --- | --- | --- |
| Server that takes the connection and drops its data, rounds escalated | 3/3 | 0/3 |
| Blocked sites, set without a strategy, hosts escalated | 7/7 | 0/3 |

  • All three segment-2 rounds ended with the client's own 8 s timeout, without a reset from the resend check, and the silence check then banned the address.
  • The 0/3 sites were on v1.89.1. On each build, every host with at least one attempt that had the name in segment 1 escalated.
  • A capture of a two-segment hello that got no answer shows the kernel repeating only the first segment, with the usual growing gaps.

Reach

  • Chrome's ClientHello is about 1.7 KB and the position of the name varies per connection. In three headless Chrome loads of a Cloudflare site, 1 of 12 flows had it in segment 2.
  • A later connection with the name in segment 1 still triggers the escalation. So the usual cost is one stuck connection, and a host stays on the failing set only while all its attempts fall this way.
  • #484's hold does not change this: it covers the first pass, not the resends.

Suggested direction

Count a resend of segment 1 for a connection whose ClientHello already has a name:

  • truncatedClientHello identifies the segment and tlsCache holds the host.
  • Match it by sequence number, so a later segment of a slow client is not counted.
  • Keep the rules from #483 (a stall escalation starts the count over) and #489 (a reused tuple forgets it).

Cover both triggers in a network-namespace test with a server that drops data after the handshake and a forced hello layout.

Found during the smoke test of main before 1.90.0.