Lands the composed accept (ADR-007 variant 2 / ADR-008): the BIND accept whose listener lives on an alktunnels 0.2.0 bind-first listen resource, at the graduation's vanilla fidelity. - `composed::bindfirst_accept` — the BindAcceptFactory: opens the listen channel (reply#1 advertises the open-op reply's real `bound`, the 0.8.0 reply projection — no expected-addr stand-in), inserts the minted `listen` token into the correlation registry, and returns the accept future parked on the token's oneshot. - `composed::register_forwarded_serving` — the forwarded-op serving half on the SOCKS5 producer's connection: the shared handler correlates by token and owns the middle pump (either-EOF completion; a three-party per-direction shutdown cascade would deadlock). - The Sync-bound wrinkle: `SyncHalves`/`OwnedWrite` (mutex Sync-proof, no unsafe) over the forwarded halves; the owner (`ForwardedOwner`, the TunnelListener) rides inside the write payload — dropping the handle drops the session, halves, and listener together (teardown structural, compiler-enforced). - `tests/composed-accept.rs`: the full story (real bound, real peer, raw pass-through round trip), client-gone teardown drop-witness, distinct tokens for concurrent BINDs, per-accept isolation of a FORBIDDEN forwarded open, late-forwarded correlation miss. - The producer-side adopted forwarded entry rides the per-adopter contract (transport-EOF reap), as upstream's bind-first suites concede; the no-leak probe reflects that shape. Verification: cargo test (default + --all-features), wasm check/clippy (hard gate), clippy --all-targets --all-features -D warnings, fmt --check — all clean.
10 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| implementation/composed-accept | Composed — the bind-first accept (the listener on a listen resource) | completed |
|
moderate | medium | component | 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:
src/composed/additions (featurecomposed) — the composed accept, named per producer.md §Wiring:bindfirst_accept(...)→BindAcceptFactory: the POC #5composed_bind_acceptshape updated to the graduation — the POC's pinned workaround (reply#1 first, the tunnel open rides the accept future; the factory carries an expectedbind_addr) is retired (ADR-007 §Consequences). The landed shape: the factory opens the bind-first listen channel (TunnelListener::open— the reply projection surfacesbound), reply#1 advertises the realbound, and the accept future correlates the forwarded sessions (thelistentoken → the adopted forwarded sessions the assembly correlates; the forwarded op'speerparam 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
BindAcceptwhose accept future waits for the ONE forwarded session matching the listener's token. The correlation registry is assembly-side bookkeeping (the alktunnelsTunnelListenerdocs: "forwarded sessions arrive as ordinary adopted sessions on the manager; the listener does not own the receipt queue"). The provider owns: theregister_tunnel_forwarded_openableserving 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<dyn AsyncRead + Send + Sync + Unpin>(BindTargetHandle's payloads); alktunnels'Taken HalvesyieldsSend + Unpinonly. 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
peerparam 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.
- 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 withcomposed::bindfirst_accept(...), a consumer BIND session: reply#1 carries the realbound, 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 testpasses;cargo test --all-featurespassescargo check --features composed --target wasm32-unknown- unknownclean (hard gate)cargo clippy --all-targets --all-features -- -D warningsclean;cargo fmt --checkclean- The graduation's shape lands (reply#1 = the real
bound; reply#2 = the forwarded op's realpeer) — 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<tokio::sync::Mutex<HashMap<u32, oneshot::Sender<ForwardedSession>>>>) keyed on thelistentoken. The factory inserts the token → oneshot right afterTunnelListener::open; the provider's shared forwarded handler (make_forwarded_handler, registered viaregister_forwarded_servingon the SOCKS5 producer's connection) removes the entry on the arriving forwarded open and deliversForwardedSession { 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 inlinepump_bidiawait inregister_openable's wrapper then reaps the entry). - The Sync-bound wrinkle: alktunnels' halves are
Send + Unpinonly; the landedBindTargetHandlepayloads carry+ Sync. Two adapters pin the no-unsafe Sync proof (Mutex<T: Send>isSync; contention never occurs — one pump task at a time):SyncHalves(read half) andOwnedWrite(write half + the owner inside). - The owner-wrapper rides inside the payloads
(
ForwardedOwner): the write payload owns theTunnelListener— 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 capturedlistener.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
BiStreamto a local duplex pair whose far end travels into the accept future's payloads. The bridge completes on EITHER direction's EOF (apump_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_forwardedcaller 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.rsno_stray_channelsdocuments the listen channel, and the forwarded entries are adopted sessions owned by their adopter).assert_only_channelstherefore 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.