Files
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

138 lines
6.9 KiB
Markdown

---
id: implementation/core-consumer-bind
name: "Core: consumer BIND, BindSession (reply#2 reader) + e2e tests"
status: completed
depends_on: [implementation/core-consumer-connect, implementation/core-producer-bind]
scope: narrow
risk: medium
impact: component
level: 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
- [x] `BindSession` with `bind`/`await_inbound`/data-plane accessors
per the POC shape
- [x] Reply#2 read via the forked `read_address` codec (no second
codec)
- [x] Typed error surface (reply-code-carrying session errors)
- [x] Teardown ownership on Drop; no `Clone` (compile-asserted)
- [x] E2E tests through the harness (round-trip, gate refusal,
failure mappings, regression guard)
- [x] `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.