Files
alksocks/tasks/implementation/core-consumer-bind.md
T
glm-5.3-flash e196ed43e3 docs: remove em dashes from repo prose and forbid them going forward
- Rewrote em dash prose across the whole tree outside docs/reviews/
  (the historical reviews stay verbatim, matching the alktunnels and
  alktty precedent): src doc comments and inline comments (587),
  docs/architecture (319), docs/research + sdd_process (484), tests
  (361), tasks (899), fuzz (28), README/CHANGELOG/Cargo.toml (77),
  AGENTS.md (85), and the .opencode/agents prompts (109)
- Fenced content follows the precedent: comments, ASCII-diagram label
  separators, and template placeholders rewritten; wire data, JSON/BAST
  documents, byte diagrams, and transcripts survive verbatim; assert,
  expect, panic, error-display, and format message literals rewritten
  as prose; no code, fixture, or test data change
- src/rfc1928/LICENSE: the two dashes in alksocks' own provenance
  header rewritten; the MIT legal text untouched
- AGENTS.md adds Project Convention 18: no em dashes in prose; varied
  restructure, no spaced-hyphen substitutes, fenced prose rule, en
  dash numeric ranges exempt, docs/reviews/ exempt from the greps
- tasks/*.md frontmatter: the name values rewritten by the sweep are
  now quoted (a raw colon inside an unquoted YAML scalar parsed as
  null); taskgraph validate reports 37 tasks, no issues

Verification: cargo test; cargo clippy --all-targets -D warnings;
cargo fmt --check; cargo doc --no-deps; fuzz corpus replay
(cargo test --manifest-path fuzz/shared/Cargo.toml); wasm check and
clippy for wasm32-unknown-unknown with -D warnings; em dash grep over
the repo returns matches only in docs/reviews/ and the bast.md BAST
JSON data fence
2026-11-03 19:31:53 +00:00

6.9 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
implementation/core-consumer-connect
implementation/core-producer-bind
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):

  1. BindSession::bind(client, target): opens the channel, negotiates, sends the BIND command: the forked client request() reads reply#1 (carrying BND.ADDR).
  2. The reply#2 reader (await_inbound): reply#2 is hand-read with the forked public read_address codec (reply#1's codec applied again with no second codec, ADR-011's rationale). The session then rides the pass-through data plane (BindDataPlane-shaped: into_inner()/get_socket_mut()).
  3. 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_inbound as reply#2's mapped code.
  4. Teardown ownership: the session owns the channel; on Drop it closes/joins. No Clone (compile-asserted).
  5. 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

  • BindSession with bind/await_inbound/data-plane accessors per the POC shape
  • Reply#2 read via the forked read_address codec (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 test green; 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 BindReply1 handle is absorbed into the teardown-owning session (the alktunnels ADR-005 posture the CONNECT session pinned): BindSession captures reply#1's BND.ADDR at bind (the forked client request() returns it once) and exposes the phase through borrows: reply1() yields the BindReply1 handle (bind_addr + raw_mut), await_inbound(&mut self) reads reply#2 and returns the borrowed BindDataPlane (Ok(None) on a spent session). The session stays the single teardown owner for every shape; the POC's (session, channel_id) tuple and the BindDataPlane ownership split collapse into one owner.
  • read_reply2 is the POC's port: 4-byte head → version pin → ReplyError::from_u8 → the forked public read_address codec (no second codec, ADR-011). Errors ride SocksSessionError (the SocksError conversions routed through the forked vocabulary, per the crate's error-surface convention (AddrError/ReplyError are wrapped into SocksError first because the session error type exposes From<SocksError>, not the RFC inner enums directly).
  • BindDataPlane<'s> borrows the session's stream (the CONNECT session's SplitPipe shape, 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), and AsyncRead/AsyncWrite are forwarded so the plane is a plain pipe (pump_bidi-shaped consumers work without the accessors). BiStream is not Sync, so the plane pins Send (runtime probe), the 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/Drop reaps.
  • The quiet-wait garbage path surfaces at await_inbound as an Io-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's UnexpectedUdpControlGarbage exit, pinned by test.
  • Harness reuse: no harness changes. The consumer-side topology's wire_consumer_side_with + the producer-bind task's echo_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, with 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, with compile_fail asserting it). 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.