--- id: implementation/local-backend-producer name: Producer-side local backend (the producer's front door, vanilla clients → a locally-served SOCKS5 resource) status: completed depends_on: [implementation/local-providers] scope: narrow risk: low impact: component level: implementation --- ## Description The producer-side local backend (feature `local`): the same door shape in the other direction; the producer serves from a real listener, so vanilla clients reach a *locally-served* SOCKS5 resource (producer.md §Producer-Side Local Backend). This is the shape where the SOCKS5 service rides a real `TcpListener` instead of (or alongside) channels: each accepted TCP connection is handed to the landed substrate-free entry `Socks5Producer::serve_socks5_over` directly, so the RFC conversation runs on the accepted socket while the seams dial locally. Deliverables: 1. **`src/local/` additions** (feature `local`), the producer-side listener shape: - A `serve_local`-shaped constructor on the local module (exact name at implementation; producer.md names no symbol; record the choice in Notes): takes a `Socks5Producer` (the landed builder type), a bind address, and the door-policy knobs (the idle-first-byte timeout + max-concurrent-sessions cap, the same two knobs the consumer door carries, per ADR-014's accept-path defense; an accepted socket is not a channel). - The accept loop: bind the listener, per accepted connection spawn `producer.serve_socks5_over(stream)`; per-connection failures are logged + isolated (the loop continues). The listener's lifetime is the caller's (a `close`/Drop shape per the alktunnels `TunnelListener` teardown posture). - **This is a front door for the producer side**: the door surface (NoAuth raw pipe vs RFC 1929 cred-bearing, ADR-005) is the consumer door's code shape. For the producer-side backend the NoAuth shape is the primary one (the producer IS the RFC conversation here, since `serve_socks5_over` runs `accept_no_auth` in-stream); a cred-bearing variant would wrap `accept_password_auth` before `serve_socks5_over`. Land the NoAuth shape; the cred-bearing producer-side door is a documented follow-up unless a test drives it (record the decision in Notes). - The BIND command gate on this path is the builder's `bind_policy` (the landed `Socks5ProducerBuilder::bind_policy`, i.e. the substrate-free entry's gate); document that the per-caller gate (the pump handler's identity-derived one) does not apply here because this path has no open-op identity. 2. **Tests** (`tests/local-backend-producer.rs`): a vanilla RFC 1928 client against the locally-served producer: a CONNECT round-trip through `local::tcp_dial()` seams (the whole stack on real sockets); BIND through the local backend (the faithful-RFC variant: real reply#1/reply#2 addresses); the idle-first-byte timeout; the session cap; per-connection failure isolation; the `bind_policy(false)` gate refuses BIND RFC-faithfully (0x02). ## Acceptance Criteria - [ ] `cargo test` passes; `cargo test --all-features` passes - [ ] `cargo clippy --all-targets -- -D warnings` clean; `cargo fmt --check` clean - [ ] Default wasm checks stay green (backend code is `local`-gated) - [ ] The backend serves via `Socks5Producer::serve_socks5_over` (the landed substrate-free entry), with no new protocol-layer code - [ ] The accept path owns the idle defense + session cap (the two knobs), pinned by tests - [ ] Per-connection failure isolation, pinned by test - [ ] The `bind_policy` gate is honored on this path, pinned by test ## References - docs/architecture/producer.md §Producer-Side Local Backend, §Wiring - docs/architecture/decisions/005-auth-split-by-door.md (the door surface), 014-idle-timeout-posture.md, 006 (the bind gate) - Landed surfaces: `src/producer.rs` (`Socks5Producer`/ `Socks5ProducerBuilder::bind_policy`/`serve_socks5_over`), `src/local/` (the providers from local-providers) - POC reference: `/workspace/alksocks-connect-poc/src/producer.rs` (the `serve_socks5_over` shape the POC validated) ## Notes - **Symbol choice**: the constructor is `ProducerDoor` (+ `ProducerDoorBuilder`/ `ProducerDoorGuard`) in `src/local/producer_door.rs`, named on the consumer door's sibling name (`FrontDoor`/`UdpFrontDoor`), not `serve_local`: the surface is a builder + accept-loop object (`serve()` binds, `serve_on(listener)` is the test/assembly seam over an existing listener), matching the consumer door's shape rather than a free function. - **Door surface decision**: the NoAuth shape landed; the cred-bearing producer-side door is a documented follow-up (no test drives it). On this path the producer IS the RFC conversation (`serve_socks5_over` runs `accept_no_auth` in-stream); there is no door-side protocol half to re-originate; a cred-bearing variant would wrap `accept_password_auth` before handing the stream to `serve_socks5_over`. - **Guard shape** (the alktunnels `TunnelListener` teardown posture): `serve`/`serve_on` return a `ProducerDoorGuard`; `close()` ends the accept loop gracefully and awaits the loop task; a Drop aborts it best-effort (sync, never awaits). In-flight sessions are never force-closed by the guard, as they own their sockets (per-connection isolation cuts both ways). - **BIND gate documentation**: the per-caller gate (the pump handler's identity-derived one) does not apply here because this path has no open-op identity; the builder's `bind_policy` is the whole gate, pinned by the `bind_policy(false)` test (0x02 refusal, CONNECT still serves). - **No protocol-layer code**: the accept loop rides `Socks5Producer::serve_socks5_over` directly; the only additions are accept-loop assembly (`peek` idle gate, `Semaphore` session cap, `try_acquire_owned` door-side refusal) and the `watch`-signaled guard. ## Summary Landed the producer-side local backend (feature `local`): `ProducerDoor` (`src/local/producer_door.rs`, exported from the crate root): a real `TcpListener` front door handing every accepted connection to the landed `Socks5Producer::serve_socks5_over` (NoAuth in-stream RFC conversation; `local::tcp_dial()` / `udp_egress()` / `bind_accept()` dial locally). The accept path owns ADR-014's two knobs (idle-first-byte peek + session-cap semaphore, door-side refusals); per-connection failures are logged + isolated; the `ProducerDoorGuard` carries the listener's lifetime (graceful `close()` / best-effort abort Drop). Pinned by `tests/local-backend-producer.rs` (9 tests): vanilla CONNECT round-trip on real sockets, faithful-RFC BIND (real reply#1/reply#2 addresses), UDP ASSOCIATE through the real egress, domain-target resolution at the seam, the idle timeout, the session cap (refusal while live sessions serve), failure isolation, and the `bind_policy(false)` gate. Verified: `cargo test` + `--all-features` green, clippy clean (default/all/wasm targets), `cargo fmt --check` clean, wasm checks stay green.