- BindSession::bind (open → no-auth → BIND → reply#1, capturing BND.ADDR) + reply1() (the BindReply1 handle) + await_inbound (the POC #5 read_reply2 — hand-read with the forked read_address codec, no second codec, ADR-011) returning BindDataPlane (into_inner / get_socket_mut + AsyncRead/AsyncWrite forwards) - Teardown ownership (the CONNECT session's posture): Drop sync-reap + structural EOF, close() graceful, teardown-at-birth on in-stream refusals, no Clone (compile_fail-asserted) - Typed error surface: open failures ride SocksSessionError::Open (ADR-049 reasons); the command gate's 0x02 and factory-failure reply#1 codes surface from bind as Rfc; accept-stage reply#2 codes surface at await_inbound - tests/consumer-bind.rs: 9 e2e tests over the harness (round-trip, data-plane accessors, gate refusal, both failure mappings, quiet-wait garbage, drop teardown + health, the CONNECT+BIND regression guard, the Send probe) Verification: cargo test (259 incl. the doctest), clippy -D warnings (native + wasm), fmt, both wasm checks, cargo doc clean.
7.0 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | ||
|---|---|---|---|---|---|---|---|---|---|
| implementation/core-consumer-bind | Core — consumer BIND: BindSession (reply#2 reader) + e2e tests | completed |
|
narrow | medium | component | implementation |
Description
Implement the BIND consumer session in src/consumer.rs per
consumer.md §The BIND client (POC #5's read_reply2 sketch): the
wrapper-aware client that opens the channel, sends BIND, reads
reply#1, awaits the inbound accept, and reads reply#2 before riding
the pass-through data plane.
Deliverables (port from /workspace/alksocks-bind-poc/src/consumer.rs,
adapted to the crate's error surface):
BindSession::bind(client, target): opens the channel, negotiates, sends the BIND command — the forked clientrequest()reads reply#1 (carrying BND.ADDR).- The reply#2 reader (
await_inbound): reply#2 is hand-read with the forked publicread_addresscodec (reply#1's codec applied again — no second codec, ADR-011's rationale). The session then rides the pass-through data plane (BindDataPlane-shaped:into_inner()/get_socket_mut()). - Error surface: a refused BIND (the command gate's 0x02, or a
factory failure's reply#1 code) surfaces as a typed session
error carrying the RFC reply code; accept-stage failures surface
at
await_inboundas reply#2's mapped code. - Teardown ownership: the session owns the channel; on Drop it
closes/joins. No
Clone(compile-asserted). - Tests (end-to-end through the harness): bind → reply#1
(BND.ADDR as the harness accept advertises) → reply#2 (the peer
address) → pass-through echo both directions; the command-gate
refusal surfaced as a typed error (a
socks5:open-only identity cannot bind); factory-failure and accept-stage-failure mappings surfaced; quiet-wait garbage from the client side ends the session; the three-command regression guard from the consumer side.
Acceptance Criteria
BindSessionwithbind/await_inbound/data-plane accessors per the POC shape- Reply#2 read via the forked
read_addresscodec (no second codec) - Typed error surface (reply-code-carrying session errors)
- Teardown ownership on Drop; no
Clone(compile-asserted) - E2E tests through the harness (round-trip, gate refusal, failure mappings, regression guard)
cargo testgreen; clippy/fmt clean; both wasm checks clean
References
- docs/architecture/consumer.md §The BIND client
- docs/architecture/decisions/007-bind-in-base.md (the client-side phase reading)
- POC source:
/workspace/alksocks-bind-poc/src/consumer.rs(BindReply1/BindDataPlane/read_reply2)
Notes
Filled by the implementation agent (2026-09-24):
- The POC's
BindReply1handle is absorbed into the teardown-owning session (the alktunnels ADR-005 posture the CONNECT session pinned):BindSessioncaptures reply#1's BND.ADDR atbind(the forked clientrequest()returns it once) and exposes the phase through borrows —reply1()yields theBindReply1handle (bind_addr+raw_mut),await_inbound(&mut self)reads reply#2 and returns the borrowedBindDataPlane(Ok(None)on a spent session). The session stays the single teardown owner for every shape; the POC's(session, channel_id)tuple and theBindDataPlaneownership split collapse into one owner. read_reply2is the POC's port: 4-byte head → version pin →ReplyError::from_u8→ the forked publicread_addresscodec (no second codec, ADR-011). Errors rideSocksSessionError(theSocksErrorconversions routed through the forked vocabulary, per the crate's error-surface convention —AddrError/ReplyErrorare wrapped intoSocksErrorfirst because the session error type exposesFrom<SocksError>, not the RFC inner enums directly).BindDataPlane<'s>borrows the session's stream (the CONNECT session'sSplitPipeshape, generalized):into_inner()yields the&mut Socks5Stream<BiStream>(the teardown owner rides the session),get_socket_mut()is the forked client's socket accessor (the POC shape), andAsyncRead/AsyncWriteare forwarded so the plane is a plain pipe (pump_bidi-shaped consumers work without the accessors).BiStreamis notSync, so the plane pinsSend(runtime probe) — same shape the forked client type has.- Teardown: Drop = sync reap + structural EOF (the CONNECT posture);
close()= half-close (the EOF sentinel reaches the producer's pump) then reap. Teardown-at-birth on the in-stream refusal path (command-gate 0x02, factory-failure reply#1): the adopted channel is reaped before the error returns. Post-reply#1 failures (accept-stage, quiet-wait garbage) keep the channel — reply#1 succeeded, the listener existed;close/Dropreaps. - The quiet-wait garbage path surfaces at
await_inboundas anIo-shaped error (the producer ends the session with no reply#2; the consumer's reply#2 read EOFs) — the client-side mirror of the producer'sUnexpectedUdpControlGarbageexit, pinned by test. - Harness reuse: no harness changes — the consumer-side topology's
wire_consumer_side_with+ the producer-bind task'secho_accept/refusing_accept*closures cover every test; the parked-accept factory for the quiet-wait garbage test is test-local (deterministic garbage-vs-accept branch).
Summary
Implemented the BIND consumer session in src/consumer.rs (POC #5's
shape, adapted to the crate's teardown-owning session posture):
BindSession::bind (open → no-auth → BIND → reply#1, capturing
BND.ADDR), reply1() (the BindReply1 handle — the app server's
out-of-band dial address + raw_mut), await_inbound (the POC's
read_reply2 — hand-read with the forked read_address codec, no
second codec) returning the BindDataPlane (into_inner/
get_socket_mut + AsyncRead/AsyncWrite forwards), and the
CONNECT session's teardown ownership (Drop sync-reap + structural
EOF, close() graceful, teardown-at-birth on in-stream refusals, no
Clone — compile_fail-asserted). Typed error surface throughout:
open failures are SocksSessionError::Open (ADR-049 reasons); the
command gate's 0x02 and factory-failure reply#1 codes surface from
bind as Rfc; accept-stage reply#2 codes surface at
await_inbound. E2E suite (tests/consumer-bind.rs, 9 tests over
the harness): the round-trip (reply#1 BND.ADDR → reply#2 peer →
pass-through echo both directions), the data-plane accessors
(into_inner/get_socket_mut/raw_mut on one pipe), the
command-gate refusal (typed 0x02, no phantom channel), the
factory-failure (reply#1 code) and accept-stage (reply#2 code)
mappings, quiet-wait garbage ending the session, drop teardown +
connection health, the CONNECT+BIND regression guard on one
registration, and the Send compile-shape probe. Verification:
cargo test (259 incl. the doctest), clippy -D warnings (all
targets), fmt, and both wasm checks clean.