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

8.9 KiB

id, name, status, depends_on, scope, risk, impact, level
id name status depends_on scope risk impact level
implementation/core-producer-bind Core: producer BIND, the phase model + the socks5:bind command gate completed
implementation/core-producer-connect
moderate medium component implementation

Description

Implement the TCP BIND producer path in src/producer.rs per wire.md §BIND and ADR-007: replace the dispatch arm's refusal with the BIND phase model (POC #5's validated scope), plus the socks5:bind command gate (ADR-006).

Deliverables (port from /workspace/alksocks-bind-poc/src/producer.rs

  • accept.rs, adapted to the crate's seam types):
  1. The command gate: at BIND dispatch, require socks5:bind from the per-call opener identity (the open-op AuthContext's identity of the open-op AuthContext, CF-005/CF-006). An identity without it gets the RFC-faithful ConnectionNotAllowed (0x02) refusal while CONNECT still serves the same identity (POC #5 test 4). The gate is the open-op identity seam consumed at command-dispatch time: no new ops, no params change.
  2. The BIND phase model:
    • reply#1 carries BND.ADDR: the BindAcceptFactory is called first; its BindAccept.bind_addr is reply#1's carrier (the local variant's real listener; the sentinel on the wrapper-aware in-band path when no factory is wired, per ADR-007 variant 3).
    • The quiet wait: select!-single-shot with exactly two exits, the one inbound accept (the BindAccept future) or the control stream's EOF/garbage/error (the client-gone teardown riding the accept future's drop). Not a loop (the POC's never_loop finding: exactly two exits). Any byte in the quiet wait is protocol garbage → the session ends with no data plane.
    • reply#2 carries the peer address (the accepted handle's peer; sentinel 0.0.0.0:0 wrapper-aware).
    • Phase 2 is raw pass-through (pump_bidi inline, the CONNECT shape) between the channel and the accepted halves.
  3. Failure mapping is RFC-faithful end to end: factory failure (pre-reply#1) → reply#1 with the mapped code; accept-stage failure (post-reply#1) → reply#2 with the mapped code, no data plane. No phantom listeners, no phantom data planes.
  4. 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 must not run under the establisher's 10s bound (FTP-length BIND waits stop resolving as spurious establishment refusals).
  5. Missing-accept refusal: a registration without a BindAcceptFactory serves the wrapper-aware in-band default (sentinel reply#1 + sentinel reply#2, ADR-007 variant 3) or refuses; the ADR names variant 3 as the in-band default when neither provider is wired; implement variant 3 as the no-factory default and document the choice.
  6. Tests (via the harness + raw RFC client): BIND → reply#1 → (the harness accept resolves) → reply#2 with the peer → pass-through echo both directions; the command gate (a socks5:open-only identity gets 0x02 on BIND while CONNECT still works, the POC's test 4); quiet-wait garbage byte ends the session with no data plane; client-gone teardown (EOF mid-wait drops the accept future, so a fresh dial to the advertised address fails); factory failure → reply#1 mapped code; accept-stage failure → reply#2 mapped code, no data plane; CONNECT + ASSOCIATE regression guard (one registration, all three commands).

Acceptance Criteria

  • The command gate: socks5:bind required at BIND dispatch; ConnectionNotAllowed refusal; CONNECT unaffected (tested)
  • The phase model: reply#1 → quiet wait (single-shot, two exits) → accept → reply#2 → pump_bidi pass-through
  • Two-stage failure mapping (factory → reply#1; accept-stage → reply#2), tested
  • The accept wait is NOT under the establishment bound
  • No-factory default = wrapper-aware variant 3 (documented)
  • Three-command regression guard passes
  • cargo test green; clippy/fmt clean; both wasm checks clean

References

  • docs/architecture/wire.md §BIND (the normative phases)
  • docs/architecture/decisions/007-bind-in-base.md (the phase model + the three variants)
  • docs/architecture/decisions/006-access-control.md (the command gate)
  • POC sources: /workspace/alksocks-bind-poc/src/producer.rs, /workspace/alksocks-bind-poc/src/accept.rs (the quiet-wait + failure-mapping origin)

Notes

  • The gate seam: serve_socks5_over<T> gained a can_bind: bool parameter: the pump handler resolves it from the open-op AuthContext's per-call opener identity (CF-005/CF-006: the socks5:bind scope, never implied by socks5:open); the substrate-free entry takes the resolved boolean explicitly, so the gate is caller-policy at the raw-entry shape and identity-resolved at the channels shape. A scope-less BIND gets ConnectionNotAllowed (0x02) with no factory call (the gate precedes the seam, so no phantom listener), while CONNECT still serves the same identity (tested unit + over-channels).
  • Variant 3 as the no-factory default (documented in serve_tcp_bind's docs): when no BindAcceptFactory is wired, the BIND arm synthesizes a BindAccept: the sentinel reply#1 (via sentinel_bind_peer_addr() = 0.0.0.0:0 riding the accept future's instant resolution, so the full phase model is preserved: reply#1 → quiet wait → reply#2 → pass-through) + the sentinel reply#2 peer. The channel is the connection (the handle-swap reading); the in-band halves are tokio::io::empty/sink, and the data plane exists and EOFs.
  • The quiet wait is a single-shot select! with exactly two exits (bind_quiet_wait_and_serve): the accept branch (its drop IS the client-gone teardown, verified by an owner-guard test) or the control stream's read (EOF = clean end, any byte = protocol garbage → UnexpectedUdpControlGarbage, no reply#2, no data plane). Not a loop (the POC's never_loop finding).
  • Reply#2 uses the fork's published new_reply (carried change #2's payoff: no local re-encode); the peer address carries as TargetAddr::Ip → the real socket addr, domain-form peer → v4 unspecified with the port.
  • Failure mapping: factory failure → reply#1's mapped code, then Bug("bind factory refused in-stream") (surfaced in-stream, mirroring CONNECT's dial shape); accept-stage failure → reply#2's mapped code, then Bug("bind accept refused in-stream"). No phantom listeners, no phantom data planes.
  • Establishment bound: structurally satisfied. The quiet wait runs inside the pump handler's spawned task, which the alkcall open wrapper never bounds (the establisher's 10s bound is pre-handler); the harness round-trip + the substrate-free "parked accept still answering after a sleep" test pin the shape.
  • Test determinism: the quiet-wait tests use a parked accept factory (std::future::pending) so the garbage/EOF branch wins deterministically (an instantly-resolving accept races the garbage branch, and the client-gone test's owner flag witnesses the future's drop).
  • Harness additions: bind_scoped_identity() (both scopes, since a session needs the open gate AND the command gate) + BIND integration tests: round-trip over channels, the gate over channels, both-scopes identity serves, quiet-wait garbage over channels (the client EOF + R-02 pump-completion shape), the two-stage failure mapping over channels, the no-factory variant 3 over channels, and the three-command regression guard. The stale "ASSOCIATE/BIND refuse 0x07" test became ASSOCIATE-only (BIND now serves under the gate).
  • dev-dependency: tracing-subscriber (dev-only) added for the over-channels teardown debugging; init_trace() helper is used in two tests (harmless observability, no behavior pin).

Summary

Implemented the TCP BIND producer path (src/producer.rs): the socks5:bind command gate (ADR-006, resolved from the open-op identity at the pump handler, passed as can_bind to the substrate-free entry), the phase model (ADR-007 / POC #5: accept factory → reply#1 carrying BND.ADDR → the single-shot quiet wait (two exits; any byte = garbage, EOF = client-gone teardown riding the accept future's drop) → reply#2 carrying the peer → pump_bidi inline), two-stage RFC-faithful failure mapping (factory → reply#1, accept-stage → reply#2, no phantom listeners/data planes), and the no-factory wrapper-aware variant 3 default (documented). 16 unit tests + 7 harness integration tests cover the happy path, the gate (0x02 while CONNECT serves), quiet-wait garbage, client-gone teardown (owner-flag witness), both failure stages, the establishment-bound independence, and the three-command regression guard. cargo test (96+26+37), clippy -D warnings (native + wasm), fmt, and both wasm checks clean.