--- id: implementation/composed-accept name: Composed — the bind-first accept (the listener on a listen resource) status: completed depends_on: [implementation/composed-egress] scope: moderate risk: medium impact: component level: implementation --- ## Description The `composed` feature's accept provider (ADR-008's composed variants; ADR-007 variant 2): the BIND accept whose listener lives on an alktunnels 0.2.0 bind-first listen resource (`register_tunnel_bindfirst_openable` + `BindFirstInjections`) — per-listener ACL for free, hub-relayable. **Vanilla-fidelity with no stand-ins since the graduation** (ADR-007): reply#1 advertises the real OS-chosen `bound` (the open-op reply's `bound` field, alkcall 0.8.0 reply projection); reply#2 carries the real peer (the forwarded op's `peer` param). Deliverables: 1. **`src/composed/` additions** (feature `composed`) — the composed accept, named per producer.md §Wiring: - `bindfirst_accept(...)` → `BindAcceptFactory`: the POC #5 `composed_bind_accept` shape **updated to the graduation** — the POC's pinned workaround (reply#1 first, the tunnel open rides the accept future; the factory carries an expected `bind_addr`) is **retired** (ADR-007 §Consequences). The landed shape: the factory opens the bind-first listen channel (`TunnelListener::open` — the reply projection surfaces `bound`), reply#1 advertises the real `bound`, and the accept future correlates the forwarded sessions (the `listen` token → the adopted forwarded sessions the assembly correlates; the forwarded op's `peer` param is reply#2's peer). - **The correlation shape is the design center** (the POC's upstream-ask ledger retired by the graduation): the factory returns `BindAccept` whose accept future waits for the ONE forwarded session matching the listener's token. The correlation registry is assembly-side bookkeeping (the alktunnels `TunnelListener` docs: "forwarded sessions arrive as ordinary adopted sessions on the manager; the listener does not own the receipt queue"). The provider owns: the `register_tunnel_forwarded_openable` serving side (the opener half — the SOCKS5 producer's connection must serve the forwarded op), the token→waiter map, and the per-forwarded pump. Design the wait shape against the landed alkcall 0.8.0 adopt surface (`manager.adopt_channel`) and record it in Notes. - **The Sync-bound wrinkle** (shared with composed-egress): the accepted halves must be `Box` (`BindTargetHandle`'s payloads); alktunnels' `Taken Halves` yields `Send + Unpin` only. The owner-wrapper rides **inside the handle payloads** (egress.rs's documented posture) — the wrapper owns the adopted channel session so dropping the handle drops everything (the teardown-at-birth discipline, compiler-enforced). This is the load-bearing wrapper: design it once (with composed-egress's needs), pin it with a test. - **Peer-address fidelity**: the forwarded op's `peer` param is reply#2's real peer (the graduation's win); the wrapper-aware sentinel (`0.0.0.0:0`) is the in-band default's shape, not this variant's. - **The establishment bound covers only the open-op round trip** (ADR-007): the accept wait lives in the teardown-governed pump phase — the quiet wait's `select!` drop of the accept future IS the client-gone teardown; the listener (inside the future) closes with it. The forwarded-op correlation must survive a client-gone drop without leaking the listener (the accept future owns it end-to-end — the seam contract). - Module doc: names the ADRs (007/008), the graduation's vanilla-fidelity win, the correlation shape, the owner-wrapper. 2. **Tests** (`tests/composed-accept.rs`): the full composed BIND story over channels — a bind-first tunnel producer (`register_tunnel_bindfirst_openable` + injections), the SOCKS5 producer with `composed::bindfirst_accept(...)`, a consumer BIND session: reply#1 carries the real `bound`, an app server dials the far listener, reply#2 carries the real peer, the pass-through data plane round-trips; the client-gone teardown releases the listener (drop-witness); two concurrent BIND sessions get distinct tokens; a forwarded-open failure is per-accept isolated. ## Acceptance Criteria - [ ] `cargo test` passes; `cargo test --all-features` passes - [ ] `cargo check --features composed --target wasm32-unknown- unknown` clean (hard gate) - [ ] `cargo clippy --all-targets --all-features -- -D warnings` clean; `cargo fmt --check` clean - [ ] The graduation's shape lands (reply#1 = the real `bound`; reply#2 = the forwarded op's real `peer`) — no expected-addr stand-in — pinned by test - [ ] The owner-wrapper rides inside the handle payloads (Sync halves; teardown structural) — pinned by test - [ ] The client-gone teardown releases the listener (the accept future owns it end-to-end) — pinned by drop-witness test - [ ] The forwarded-op serving side + correlation land; per-accept isolation — pinned by tests ## References - docs/architecture/decisions/007-bind-in-base.md (variant 2 + the graduation), 008-provider-defaults.md, 004 (the accept seam's contracts), 006 (the command gate) - docs/architecture/producer.md §Wiring (`composed::bindfirst_accept`), §The Three Seams - POC reference: `/workspace/alksocks-bind-poc/src/accept.rs` (`composed_bind_accept` — the superseded-in-shape workaround, the owner-wrapper's first witness) - Landed surfaces: `src/egress.rs` (`BindAccept`/`BindTargetHandle` — the contracts), alktunnels 0.2.0 (`register_tunnel_bindfirst_ openable`, `BindFirstInjections`, `TunnelListener`, `register_tunnel_forwarded_openable`, `ForwardedHandler`) ## Notes - **The correlation shape** (the design center): a `CorrelationRegistry` (`Arc>>>`) keyed on the `listen` token. The factory inserts the token → oneshot right after `TunnelListener::open`; the provider's shared forwarded handler (`make_forwarded_handler`, registered via `register_forwarded_serving` on the SOCKS5 producer's connection) removes the entry on the arriving forwarded open and delivers `ForwardedSession { far duplex halves, peer }`. A dropped receiver (client-gone) or a miss makes the handler drop the forwarded channel stream — teardown-at-birth, R-02 (the inline `pump_bidi` await in `register_openable`'s wrapper then reaps the entry). - **The Sync-bound wrinkle**: alktunnels' halves are `Send + Unpin` only; the landed `BindTargetHandle` payloads carry `+ Sync`. Two adapters pin the no-unsafe Sync proof (`Mutex` is `Sync`; contention never occurs — one pump task at a time): `SyncHalves` (read half) and `OwnedWrite` (write half + the owner inside). - **The owner-wrapper rides inside the payloads** (`ForwardedOwner`): the write payload owns the `TunnelListener` — dropping the handle drops the listener → the listen channel EOFs → the far teardown cascades (teardown structural, compiler-enforced). The edition-2021 disjoint-capture trap (an earlier draft captured `listener.channel_id`, dropping the listener at the factory's return — EOF at birth) is why the whole struct is rebound into the accept future. - **The three-party bridge pump**: the forwarded handler bridges the forwarded `BiStream` to a local duplex pair whose far end travels into the accept future's payloads. The bridge completes on EITHER direction's EOF (a `pump_bidi`-then-drop, not a per-direction shutdown cascade — a three-party half-close cycle would deadlock: each pump's completion-shutdown needs the next hop's EOF, and the chain closes on itself). Dropping everything EOFs the SOCKS5 producer-side pump, ends the per-accept pump, and tears the forwarded channel down. - **Test concession (producer-side adopted forwarded entry)**: the producer's `open_forwarded` caller adopts the opener-allocated forwarded channel id on the producer-side manager; that entry's reap rides the per-adopter contract + transport EOF, NOT the listener's teardown (alktunnels ADR-005; upstream's own bind-first suites make the same concession — `producer_bindfirst.rs` `no_stray_channels` documents the listen channel, and the forwarded entries are adopted sessions owned by their adopter). `assert_only_channels` therefore treats odd producer-side ids as the adopted-forwarded shape. - **Forwarded-open failure semantics**: a FORBIDDEN forwarded open is per-accept isolated AT THE TUNNEL LAYER (the producer's accept loop drops that connection and keeps serving; the alktunnels producer-suite pin). The SOCKS5 accept future stays parked — the SOCKS5 producer never learns of the tunnel-layer failure — so no reply#2 arrives; the quiet wait is unaffected. Pinned by test. ## Summary Landed `composed::bindfirst_accept` + `composed::register_forwarded_serving` (the composed accept, ADR-007 variant 2 / ADR-008) on the alktunnels 0.2.0 bind-first listen resource, at the graduation's vanilla fidelity: reply#1 advertises the open-op reply's real `bound` (the alkcall 0.8.0 reply projection; no expected-addr stand-in), reply#2 carries the forwarded op's real `peer` param. The correlation registry (token → oneshot) is assembly bookkeeping; the provider owns the forwarded-op serving half, the token→waiter map, and the per-forwarded middle pump (either-EOF completion shape). The Sync-bound wrinkle is met by `SyncHalves`/`OwnedWrite` (mutex Sync-proof, no unsafe) with the owner (`ForwardedOwner`, the `TunnelListener`) riding inside the write payload — dropping the handle drops everything (teardown structural, compiler-enforced). `tests/composed-accept.rs` pins: the full story (real `bound`, real `peer`, raw pass-through round trip), the client-gone teardown (drop-witness: the accept future owns the listener end-to-end; no leaked listen channel), two concurrent BIND sessions' distinct tokens, per-accept isolation of a FORBIDDEN forwarded open, and the late-forwarded correlation miss dropping the channel at birth. Verified: `cargo test`, `cargo test --all-features`, `cargo check/clippy --target wasm32-unknown-unknown` (hard gate), `cargo clippy --all-targets --all-features -- -D warnings`, `cargo fmt --check` — all clean.