producer: the BIND phase model + the socks5:bind command gate (ADR-006/ADR-007)
The TCP BIND dispatch arm replaces its refusal with the full phase model (POC #5-validated scope), plus the command gate at dispatch: - The command gate: TCP BIND requires socks5:bind from the per-call opener identity (CF-005/CF-006) — the pump handler resolves it from the open-op AuthContext; serve_socks5_over<T> takes the resolved can_bind boolean explicitly. A scope-less identity gets the RFC-faithful ConnectionNotAllowed (0x02) with no factory call (the gate precedes the seam) while CONNECT still serves it. - The phase model (wire.md §BIND): the BindAcceptFactory at the command-read interception point → reply#1 carrying BND.ADDR (the factory's bind_addr) → the quiet wait (single-shot select!, exactly two exits: the one accept, or the control stream's EOF/garbage/ error; any byte = protocol garbage → session ends, no data plane; the accept future's drop IS the client-gone teardown) → reply#2 carrying the peer address (via the fork's published new_reply) → pump_bidi inline (the CONNECT shape, R-02). - Two-stage RFC-faithful failure mapping: factory failure → reply#1's mapped code; accept-stage failure → reply#2's mapped code, no data plane. No phantom listeners, no phantom data planes. - The no-factory default is the wrapper-aware in-band variant (ADR-007 variant 3, documented): sentinel reply#1 + sentinel reply#2 (0.0.0.0:0), the full phase model preserved — the channel is the connection. - The accept wait lives in the teardown-governed pump phase, never under the establishment bound (ADR-007). Tests: 16 unit tests (raw RFC client over duplex) + 7 harness integration tests over channels — happy path, the gate (0x02 while CONNECT serves, both scopes), quiet-wait garbage, client-gone teardown (owner-flag witness), both failure stages over channels, no-factory variant 3, and the three-command regression guard (CONNECT serves, BIND serves under its gate, ASSOCIATE still refuses). Verification: cargo test green (96 lib + 26 harness + 37 differential); clippy -D warnings clean (all targets, native + wasm32-unknown-unknown); cargo fmt clean; wasm check clean. Task: tasks/implementation/core-producer-bind.md (status → completed)
This commit is contained in:
1 parent
cfacc8a968
commit
048493fefa
4 files changed
+1219
-91
No files matched your search
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: implementation/core-producer-bind
|
||||
name: "Core — producer BIND: the phase model + the socks5:bind command gate"
|
||||
status: pending
|
||||
status: completed
|
||||
depends_on: [implementation/core-producer-connect]
|
||||
scope: moderate
|
||||
risk: medium
|
||||
@@ -72,16 +72,16 @@ Deliverables (port from `/workspace/alksocks-bind-poc/src/producer.rs`
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] The command gate: `socks5:bind` required at BIND dispatch;
|
||||
- [x] The command gate: `socks5:bind` required at BIND dispatch;
|
||||
`ConnectionNotAllowed` refusal; CONNECT unaffected (tested)
|
||||
- [ ] The phase model: reply#1 → quiet wait (single-shot, two
|
||||
- [x] 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 →
|
||||
- [x] 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
|
||||
- [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
|
||||
|
||||
@@ -96,8 +96,78 @@ Deliverables (port from `/workspace/alksocks-bind-poc/src/producer.rs`
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
- **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 — 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` — 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 — the client-gone test's owner flag witnesses the future's
|
||||
drop).
|
||||
- **Harness additions**: `bind_scoped_identity()` (both scopes — 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
|
||||
|
||||
> To be filled on completion
|
||||
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.
|
||||
Reference in new issue
Block a user