- 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
173 lines
8.9 KiB
Markdown
173 lines
8.9 KiB
Markdown
---
|
|
id: implementation/core-producer-bind
|
|
name: "Core: producer BIND, the phase model + the socks5:bind command gate"
|
|
status: completed
|
|
depends_on: [implementation/core-producer-connect]
|
|
scope: moderate
|
|
risk: medium
|
|
impact: component
|
|
level: 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
|
|
|
|
- [x] The command gate: `socks5:bind` required at BIND dispatch;
|
|
`ConnectionNotAllowed` refusal; CONNECT unaffected (tested)
|
|
- [x] The phase model: reply#1 → quiet wait (single-shot, two
|
|
exits) → accept → reply#2 → `pump_bidi` pass-through
|
|
- [x] Two-stage failure mapping (factory → reply#1; accept-stage →
|
|
reply#2), tested
|
|
- [x] The accept wait is NOT under the establishment bound
|
|
- [x] No-factory default = wrapper-aware variant 3 (documented)
|
|
- [x] Three-command regression guard passes
|
|
- [x] `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. |