Skip to content

Multipath: complete server-side NAT rebinding migration - #524

Open
aswanthk777 wants to merge 4 commits into
Tencent:developfrom
aswanthk777:bonding/nat-rebind
Open

Multipath: complete server-side NAT rebinding migration#524
aswanthk777 wants to merge 4 commits into
Tencent:developfrom
aswanthk777:bonding/nat-rebind

Conversation

@aswanthk777

Copy link
Copy Markdown

Stacked on #523 (its commit is included as the base of this branch; the test below deadlocks without it).

get_or_create_path already recognizes a NAT rebinding (a known CID arriving from a new 4-tuple), creates a new path sharing the packet number space, and queues a PATH_CHALLENGE. But the migration never completes, in two ways:

  1. The new path is created with dcid_seq=None and nothing ever assigns one, and select_send_path skips dcid-less paths for probing — so the PATH_CHALLENGE is never actually sent. The path never validates, never becomes active, and the server keeps striping user data to the stale (dead) address until the connection idle-times-out.
  2. Nothing retires the superseded path. Each rebinding permanently leaks one path slot and one DCID; insert_path only evicts unused() paths (inactive AND dcid-less), which a formerly-active path never becomes. After max_paths rebindings, path creation fails outright.

This series completes the migration (see individual commit messages):

  • Replenish source CIDs when the peer retires one (RFC 9000 §5.1.1) — without this, chained rebindings drain the CID pool.
  • Complete server-side NAT rebinding migration — assign an unused DCID at path creation so the challenge can be sent; track the superseded path in Path::migrated_from and retire it on validation (deactivate, drop its 4-tuple mapping, free its DCID via RETIRE_CONNECTION_ID); re-bind the packet SCID to the freshest path so chained rebindings never dereference an evicted path id. Adds a conn_multipath_nat_rebind test driving two successive rebindings through an emulated NAT.
  • Make abandon_path() actually release the pathis_abandon was a write-only flag; the API was a silent no-op. Reuses the same retire machinery, and refuses to abandon the last active path.

Verified live on a multipath IP tunnel (server on a VPS, client behind an emulated rebinding NAT): the server migrated within seconds of each rebinding with zero packet loss across a continuous ping stream, where previously all tunnel traffic was stranded until an application-level liveness redial. Full suite: 559/0.

🤖 Generated with Claude Code

aswanthk added 4 commits July 21, 2026 20:04
The server-side anti-amplification helpers (inc/dec/cmp_anti_ampl_limit)
all stop applying the limit once verified_peer_address is set: the limit
value is frozen from that point on. need_send_validation_frames and
need_expand_padding_frames, however, kept consulting the frozen value.

For a path opened by a small packet — exactly what a NAT rebinding looks
like to a server — the sequence deadlocks:

1. The first packet on the new 4-tuple is small (e.g. a bare PING), so
   the path starts with anti_ampl_limit of only a few hundred bytes.
2. The server sends an unpadded PATH_CHALLENGE (padding is refused
   because anti_ampl_limit <= max_datagram_size), and the peer answers.
3. on_path_resp_received sets verified_peer_address, which freezes
   anti_ampl_limit; the path is promoted to ValidatingMTU and probes
   again, needing a challenge padded to MIN_CLIENT_INITIAL_LEN.
4. need_expand_padding_frames still compares the frozen limit against
   max_datagram_size and refuses the padding — forever. The path never
   reaches Validated, never becomes active, and the server keeps
   sending user data to the stale address.

Fix: skip the anti-amplification checks in both helpers once the peer
address is verified, matching the semantics of the other three helpers.
Address-validated paths are exempt from the 3x limit per RFC 9000
Section 8; the MTU-validating padded challenge is then allowed out and
validation completes.

Found while adding server-side NAT rebind handling; covered by the
conn_multipath_nat_rebind test added in a follow-up commit (the test
deadlocks at ValidatingMTU without this fix).
RFC 9000 Section 5.1.1: "An endpoint SHOULD supply a new connection ID
when the peer retires a connection ID."

try_schedule_control_frames issues active_conn_id_limit - 1 source CIDs
once, at handshake completion, and nothing ever tops the pool back up:
every RETIRE_CONNECTION_ID permanently shrinks it. Any workload that
retires CIDs over a connection lifetime — most notably a server
retiring the superseded path CID after each NAT rebinding — eventually
drains the pool, after which new paths cannot be created at all.

Emit ScidToAdvertise(1) after processing a retirement so the endpoint
generates a replacement, keeping the advertised pool at the size the
peer asked for.
get_or_create_path already recognizes a NAT rebinding (a known CID
arriving from a new 4-tuple), creates a new path sharing the packet
number space, and queues a PATH_CHALLENGE. But the migration never
completes, in two ways:

1. The new path is created with dcid_seq=None and nothing ever assigns
   one, and select_send_path skips dcid-less paths for probing — so the
   PATH_CHALLENGE is never actually sent. The path never validates,
   never becomes active, and the server keeps striping user data to
   the stale (dead) address until the connection idle-times-out.

2. Nothing retires the superseded path. Each rebinding permanently
   leaks one path slot and one DCID; insert_path only evicts unused()
   paths (inactive AND dcid-less), which a formerly-active path never
   becomes. After max_paths rebindings, path creation fails outright.

Complete the migration:

- Assign an unused DCID to the server-created path at creation time,
  so its PATH_CHALLENGE can actually be sent.
- Track the superseded path in Path::migrated_from. When the rebind
  path completes validation (PATH_RESPONSE handler), deactivate the
  old path, drop its 4-tuple mapping (PathMap::retire_path) so a NAT
  flapping back can re-create it, and free its DCID via
  RETIRE_CONNECTION_ID — which, with the previous commit, also makes
  the peer issue a replacement, so chained rebindings never drain the
  CID pool.
- Re-bind the packet SCID to the freshest path on every rebinding, so
  a later rebinding of the same CID never dereferences an evicted
  path id (and tolerate an already-evicted binding by treating the
  packet as opening a new path).

The new conn_multipath_nat_rebind test drives two successive rebinds
through an emulated NAT and asserts the server converges on the new
tuple each time, retires the stale one, keeps exactly one active path,
and still delivers stream data end-to-end. Note the test only passes
together with the earlier anti-amplification fix: rebind paths open
with a small packet, and without that fix validation deadlocks in
ValidatingMTU.

Verified live on a multipath IP tunnel (server on a VPS, client behind
an emulated rebinding NAT): the server migrated within seconds of each
rebinding with zero packet loss across a continuous 5/s ping stream,
where previously all tunnel traffic was stranded until a 15s
application-level liveness redial.
abandon_path() only set path.is_abandon = true — a flag that nothing
reads anywhere in the codebase. The path stayed active, the schedulers
kept sending on it, its DCID stayed bound, and its slot could never be
evicted: for the caller the API was a silent no-op.

Make it effective with the machinery the NAT rebinding migration
introduced: deactivate the path and drop its 4-tuple mapping
(PathMap::retire_path), and free its DCID via RETIRE_CONNECTION_ID so
the peer issues a replacement. Also refuse to abandon the last active
path — a connection with no active path cannot send at all.

is_abandon is kept for a future PATH_ABANDON frame implementation
(the frame codec exists but the frame is currently neither sent nor
acted upon when received).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant