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)
5.8 KiB
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 |
|
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
registeron 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::ListenRegistrationclass (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::registerrefuses 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-depspass
References
- tasks/implementation/review-wave-3.md §Booked findings item 4 (and
§Minor issues — the
install_forwarded_surfacepre-check this mirrors) - src/tunnel_map.rs (the
ListenRouter), src/error.rs (theAdapterErrorclass), 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 returnsResult<(), String>: a register on an already-held token returnsErrand 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, reasontoken-collision), not a distinctAdapterErrorvariant. The deciding fact: a global-request hook'sErris 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 isRequestDenied.AdapterError::ListenRegistrationhonestly 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 u32out-param write (a refused listen never writes the bound port); on refusal the path releases the listener cap, emitsdecline_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.rsa_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 answersRequestDenied, its bound endpoint never serves again (teardown observable — the cancel row's bounded-retry shape), the listener count was released (a freshport = 0mint is admitted and registers — and its success reply proves the shape), and the first route still serves end to end (an arrival opens the echoedforwarded-tcpipand 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.