Files
alkssh/tasks/implementation/w4-router-collision.md
T
glm-5.3-flash d300095687 feat(tunnel): ListenRouter token-collision pre-check — refuse, never replace (w4-router-collision)
Closes the wave-3 gate's booked finding item 4 (review-wave-3
§Booked findings): register on an already-held token now refuses
fail-closed instead of silently replacing the live route — a collided
mint (the ADR-008 discipline's upstream failure) can never steal a
live adapter's arrival routes.

- ListenRouter::register returns Result<(), String>; collision → Err,
  the held route untouched (src/tunnel_map.rs)
- Error surface decided + recorded: the wire-time collision is the
  request-time decline disposition (RequestDenied, reason
  token-collision), not a new AdapterError variant — a global-request
  hook's Err propagates out of russh 0.65's packet loop and kills the
  session, while a refused listen's vanilla contract is RequestDenied;
  AdapterError::ListenRegistration keeps its construction-time class
  (src/error.rs doc records the sibling)
- listen_request_dispatch: register moved before the bound-port
  out-param write; on refusal the listener cap releases, the uniform
  decline event emits, and the minted fabric listener tears down
  immediately (never a half state) (src/handler.rs)
- Wire row a_collided_mint_is_refused_and_never_steals_the_live_route
  pins the full shape through a mint-strategy construct on the fabric
  double (MintStrategy::WithCollisions — the test forces what the
  fabric's discipline prevents): second register refuses, first route
  still serves end to end, refused endpoint never serves, count
  released (tests/tunnel_mapping.rs)

Verification: cargo test (default + git/session/tunnel/sftp +
--all-features) · clippy ×2 -D warnings · fmt --check · cargo doc
--no-deps · taskgraph validate ✓ (35 tasks)
2026-10-29 07:56:24 +00:00

5.8 KiB
Raw Blame History

id, name, status, depends_on, scope, risk, impact, level
id name status depends_on scope risk impact level
w4-router-collision Wave 4 — ListenRouter token-collision pre-check (wave-3 gate booked item 4) completed
w3-tunnel-listen
single low isolated implementation

Description

Close the wave-3 gate's booked finding item 4 (review-wave-3 §Booked findings): a defensive ListenRouter token-collision pre-check. register silently replaces on token collision today; safety rests on the fabric's ADR-008 mint discipline (collision "can't happen" upstream). The gate's disposition: a fail-closed decline-the-listen-on-collision is a cheap wave-4 addition — the same posture the gate landed inline for duplicate tunnel/forwarded op registration (install_forwarded_surface now refuses with AdapterError::ListenRegistration instead of trusting the silent-replace).

  • The pre-check: a register on an already-held token refuses (the listen declines fail-closed) instead of silently replacing the held route — a collided mint can never steal a live adapter's arrival routes.
  • The error surface: reuse/extend the AdapterError::ListenRegistration class (decide + record — a distinct variant only if the gate's variant can't carry the distinction honestly).
  • Wire row(s): the collision constructible (two registers on one token — the test forces what the fabric's discipline prevents) → the second refuses, the first route still serves (the steal is impossible, row-pinned); the existing arrival/cancel/teardown rows stay green.

Acceptance Criteria

  • ListenRouter::register refuses on token collision (fail-closed, no silent replace); the error surface decided + recorded
  • Wire row(s): the second register refuses, the first route still serves; existing rows green
  • cargo test (+ per-feature + --all-features), clippy ×2 (-D warnings), fmt, cargo doc --no-deps pass

References

  • tasks/implementation/review-wave-3.md §Booked findings item 4 (and §Minor issues — the install_forwarded_surface pre-check this mirrors)
  • src/tunnel_map.rs (the ListenRouter), src/error.rs (the AdapterError class), docs/architecture/decisions/ (the fabric's ADR-008 mint discipline — the upstream guarantee the pre-check defends in depth against)

Notes

Implementation (2026-10-29)

  • ListenRouter::register (src/tunnel_map.rs) now returns Result<(), String>: a register on an already-held token returns Err and never replaces the held route — a collided mint can never steal a live adapter's arrival routes. The refusal detail carries the token + the steal posture.
  • Error surface (decided + recorded): the refusal surfaces as the request-time decline disposition (RequestDenied, reason token-collision), not a distinct AdapterError variant. The deciding fact: a global-request hook's Err is the taxonomy's only route into the request surface, and russh 0.65 propagates it with ? out of the packet loop — killing the whole session — while the vanilla contract for a refused listen is RequestDenied. AdapterError::ListenRegistration honestly carries the construction-time class it always did (the doc in src/error.rs records the wire-time sibling); the reuse is the shared fail-closed registration posture, no new variant needed.
  • listen_request_dispatch (src/handler.rs): the register moved before the &mut u32 out-param write (a refused listen never writes the bound port); on refusal the path releases the listener cap, emits decline_global_request(row, "token-collision") (the uniform vocabulary, closed — no dynamic fields), and tears the minted fabric listener down immediately in a spawned task (TunnelListener::close — the cancel path's mechanism; a refused registration never left a half state).
  • Wire row (tests/tunnel_mapping.rs a_collided_mint_is_refused_and_never_steals_the_live_route): the fabric double gained a mint-strategy construct (wire_fabric_with_listen_mint + MintStrategy::WithCollisions — the first N mints return a fixed token, then the ADR-008 counter continues; the test forces exactly what the fabric's discipline prevents). The row pins, against a 2-count listener ceiling (the ceiling that admits the collided mint past the cap — the cap row's shape would decline at the cap before the register — and makes the release observable): the first listen registers normally, the colliding mint's listen answers RequestDenied, its bound endpoint never serves again (teardown observable — the cancel row's bounded-retry shape), the listener count was released (a fresh port = 0 mint is admitted and registers — and its success reply proves the shape), and the first route still serves end to end (an arrival opens the echoed forwarded-tcpip and the bytes ride) — the steal is impossible, row-pinned.

Summary

Completed. The ListenRouter token-collision pre-check landed fail-closed (register refuses, never replaces) with the error-surface decision recorded: the wire-time collision is the decline disposition (token-collision / RequestDenied), not the taxonomy — an Err would kill the whole russh session — while AdapterError:: ListenRegistration keeps its construction-time class. The refusal path is the full fail-closed shape (cap release, uniform decline event, immediate fabric teardown — no half state). Row-pinned through a mint-strategy construct on the fabric double: second register refuses, first route still serves end to end, refused endpoint never serves, count released. Full verification set green: cargo test (default + git/session/tunnel/sftp + --all-features), clippy ×2 -D warnings, fmt, cargo doc --no-deps; existing arrival/cancel/ teardown rows unchanged and green.