Files
alksocks/tasks/implementation/local-providers.md
T
glm-5.3-flash a867f0cb81 local: the socket-backed seam providers — tcp_dial / udp_egress / bind_accept
The `local` feature's first-party providers (ADR-008's local variants,
ADR-009's gate): the three seams' faithful-RFC assembly-layer
constructors over the landed seam types; the protocol layer never
sees them.

- Cargo.toml: `local = ["tokio/net"]` (no socket2 — the POCs ran
  tokio-only); the default graph stays net/mio/socket2-free
  (cargo tree spot-checked both ways)
- src/local/: `tcp_dial()` (real TcpStream per CONNECT, domain
  resolution at the provider — the ssh -D semantic; the resolved
  address set iterated, faithful SocksDialError mapping,
  NotAllowed never synthesized), `udp_egress()` (one real UdpSocket
  per association bound 127.0.0.1:0 — the explicit bind decision;
  the POC #2 LocalEgress readiness shape: park on readable(),
  try_recv_from, WouldBlock -> spurious Ok(None); inner bytes
  to_be_bytes() + data, framing-once; resolve at the egress),
  `bind_accept(bind_host)` (real TcpListener per BIND session,
  ADR-007 variant 1: requested port honored when non-zero, local_addr
  as reply#1's BND.ADDR, the accept future owning the listener
  end-to-end, peer byte-exact; two-stage failure mapping; the bind
  host is the caller's explicit choice)
- lib.rs: cfg-gated module + re-exports; crate doc updated
- tests: 12 unit (real-socket round-trips, the readiness contract's
  park-then-datagram, the drop-closes-the-listener witness, the
  refusal mappings) + 6 integration (tests/local-providers.rs,
  feature-gated: the three commands end to end through the producer's
  real dispatch path with real targets, connect-refused -> 0x05
  through the producer's reply, the readiness contract re-pinned
  through the datagram stage, client-gone -> the advertised port
  frees)
- task status -> completed (acceptance criteria checked; Notes +
  Summary filled)

Verification: cargo test (157 green) + --all-features green,
clippy -D warnings clean (default + all-features), fmt --check clean,
both wasm checks clean, the dependency-graph spot-checks green.
2026-09-25 05:55:27 +00:00

13 KiB

id, name, status, depends_on, scope, risk, impact, level
id name status depends_on scope risk impact level
implementation/local-providers Local — the socket-backed seam providers (tcp_dial / udp_egress / bind_accept) completed
moderate medium component implementation

Description

The first batch-2 task: the local feature's first-party socket-backed providers — the three seams' faithful-RFC constructors (ADR-008's local variants, ADR-009's feature gate). These are the assembly-layer convenience constructors over the landed seam types (src/egress.rs); the protocol layer never sees them.

Deliverables:

  1. Cargo.toml feature wiring — local = ["tokio/net"] (the feature's own socket providers; socket2 only if a provider actually needs it — the POCs ran tokio-only, so default to NOT adding it). tokio stays default-features = false in [dependencies]; the net feature rides local alone (ADR-009). Verify the default graph stays net-free (cargo tree spot-check) and the default wasm checks stay green.
  2. src/local/mod.rs (feature local) — the provider constructors, named per producer.md §Wiring:
    • tcp_dial() → DialFn: TcpStream::connect per CONNECT target; domain targets resolve here (the ssh -D semantic — ADR-012: resolution is the provider's job); the connected stream splits into the TargetHandle halves (`Box<dyn AsyncRead + Send + Sync
      • Unpin>—TcpStream::into_splithalves are Send+Sync, verify the boxing compiles). Dial failures map toSocksDialError faithfully (ConnectionRefusedfor connect-refused,NetworkUnreachable/HostUnreachableperio::ErrorKind, NotAllowed` never synthesized by this provider).
    • udp_egress() → EgressFactory: one real UdpSocket per association (bind 127.0.0.1:0 — the assembly-layer bind decision, explicit here), per-datagram send_to/recv_from, no flow table; domain destinations resolve at the egress (resolve_target-shaped — port the POC #2 LocalEgress helper). The readiness contract is load-bearing: next_reply parks on sock.readable().await then try_recv_from (WouldBlock → Ok(None) spurious-wakeup shape; a real async park — never a hot Ok(None)); replies carry inner bytes (TargetAddr::to_be_bytes() + data — the sender's address per the RFC's per-datagram source semantics; framing-once — the length prefix is the wire layer's). send takes payload only. Egress failures map to EgressError (bind failure → General; resolve failure → Unreachable).
    • bind_accept() → BindAcceptFactory: a real TcpListener per BIND session (the faithful RFC shape, ADR-007 variant 1): the requested target's port honored when non-zero (FTP active mode), port 0 → ephemeral; local_addr() is reply#1's bind_addr; the accept future owns the listener end-to-end (the quiet wait's drop IS the client-gone teardown — the listener lives inside the future, never in a separate binding); accept()'s peer is reply#2's BindTargetHandle.peer (byte-exact). Factory failures → BindAcceptFactoryError (bind refused/held port → Refused or Shortage per the io error; accept-stage failures → BindAcceptError). Address policy is the caller's: the constructor takes the bind host (loopback default is the POC's posture — a wildcard bind is the caller's explicit choice).
    • Module doc: names the ADRs (008/009), the never-binds posture (the providers ARE the explicit bind decisions), and the seam contracts each constructor must honor (readiness, ownership, accept-future-owns, framing-once).
  3. Tests (src/local/ unit + a tests/local-providers.rs integration file): real sockets (dev-deps already carry full tokio). Echo TCP server for tcp_dial round-trips; a real UDP echo for the egress (datagram round-trip through the producer stage — the POC #2 shape); a real BIND accept (dial the listener, reply#1/reply#2 carry the real addresses); the readiness contract re-pinned with the real socket egress (park-then-datagram, no spin); refusal mappings (connect-refused → 0x05 through the producer's reply).

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 (cargo check/clippy --target wasm32-unknown-unknown -- -D warnings) — local code is cfg-gated out of the default build
  • Default dependency graph has no tokio/net/mio/socket2 (cargo tree spot-check); local = ["tokio/net"] wired
  • tcp_dial/udp_egress/bind_accept land with the exact producer.md §Wiring names and the landed seam types
  • The egress readiness contract honored (real async park; no hot Ok(None)) — pinned by test
  • The accept future owns its listener end-to-end — pinned by a drop-witness test (client-gone → listener closed)
  • Refusal mappings table-tested against the landed error types' to_reply_error()

References

  • docs/architecture/decisions/008-provider-defaults.md (the local variants), 009-feature-gates.md (the feature layout), 004 (the seams), 007 (BIND variant 1), 012 (resolution at the egress)
  • docs/architecture/producer.md §Wiring, §The Three Seams
  • POC reference: /workspace/alksocks-udp-poc/src/egress.rs (LocalEgress — the readiness shape), /workspace/alksocks-bind- poc/src/accept.rs (local_bind_accept)
  • Landed seams: src/egress.rs (the contracts' normative doc comments)

Notes

Filled by the implementation agent (2026-09-25).

  • Cargo.toml: local = ["tokio/net"] (the feature's own socket providers; no socket2 — the POCs ran tokio-only and nothing here needs it). tokio stays default-features = false with the wasm-clean subset; the net feature rides local alone (ADR-009). cargo tree spot-checks: the default graph (-e normal,features --no-default-features) carries no tokio/net/mio/socket2 (inverting -i mio prints nothing); with --features local --target all the net feature appears rooted at alksocks feature "local", dragging mio + socket2 in through tokio (the dev-dep full also appears in dev builds — pre-existing, test-only, not the feature's doing).
  • src/local/mod.rs (feature local, one module file + a tests submodule per convention 15):
    • tcp_dial() → DialFn: TcpStream::connect per CONNECT target; domain targets resolve here (the ssh -D semantic — ADR-012), iterating the resolved address set (the -D connect-multi-address posture: first success wins, the last failure is the reported error); into_split halves boxed as Send + Sync + Unpin (compiles, pinned by the round-trip test). Refusal mapping (dial_error_of): ConnectionRefused → SocksDialError::Refused, NetworkUnreachable/TimedOut → NetworkUnreachable, HostUnreachable → HostUnreachable, resolution failure → HostUnreachable, everything else → General; NotAllowed never synthesized.
    • udp_egress() → EgressFactory: one real UdpSocket per association (bind 127.0.0.1:0 — the explicit assembly-layer bind decision, LocalEgress::bind); per-datagram send_to/recv_from, no flow table; domain destinations resolve at the egress (resolve_target — the POC #2 LocalEgress helper ported). The readiness contract is the load-bearing shape: next_reply parks on sock.readable().await (a real async park on receive readiness — writable() is always ready for UDP and would spin), then try_recv_from (WouldBlock → the spurious-wakeup Ok(None)); replies carry inner bytes (TargetAddr::to_be_bytes() + data — the sender's address per the RFC's per-datagram source semantics; framing-once); send takes payload only. Bind failure → General; resolve failure → Unreachable.
    • bind_accept(bind_host) → BindAcceptFactory: a real TcpListener per BIND session (ADR-007 variant 1); the requested port honored when non-zero, port 0 → ephemeral; local_addr() is reply#1's bind_addr; the accept future owns the listener end-to-end (the listener lives inside the future — the quiet wait's drop IS the client-gone teardown); accept()'s peer is reply#2's BindTargetHandle.peer (byte-exact). Factory failures → BindAcceptFactoryError (PermissionDenied/AddrInUse → Shortage, AddrNotAvailable → Refused, else General); accept-stage failures → BindAcceptError (General). Address policy is the caller's: the constructor takes the bind host (IpAddr-convertible; loopback is the POC's posture — the wildcard bind is the caller's explicit 0.0.0.0).
    • Module doc names ADR-008/009, the never-binds posture (the providers ARE the explicit bind decisions), and the four seam contracts (readiness, ownership-rides-the-handle, framing-once, accept-future-owns).
  • src/lib.rs: #[cfg(feature = "local")] pub mod local; + the tcp_dial/udp_egress/bind_accept/LocalEgress re-exports; the crate doc's "not yet present" paragraph updated (the composed providers remain pending).
  • Tests: 12 unit tests (src/local/tests.rs — the TCP echo round-trip incl. domain resolution, connect-refused/unresolvable mappings, the UDP echo round-trips (inner bytes + sender address, in-order, empty payload), the park-then-datagram readiness probe, the BIND accept (real reply#1/reply#2, requested-port honored, held-port refusal, the drop-closes-the-listener witness)) + 6 integration tests (tests/local-providers.rs, feature-gated with #![cfg(feature = "local")] — CONNECT through a real echo target, connect-refused → 0x05 through the producer's reply, ASSOCIATE through the producer's datagram stage with the real egress, the readiness contract re-pinned through the stage (park-then-datagram, no spin), BIND end-to-end with real addresses both directions, and the client-gone-mid-quiet-wait drop-witness (the listener's port frees)).
  • Deliberate deviations from the POC code: the dial iterates the full resolved address set (the POC resolved to the first address); TimedOut maps to NetworkUnreachable and PermissionDenied to General in the dial table (the POC had no io-error mapping at all); bind_accept takes the bind host as a parameter (the POC hardcoded 127.0.0.1) so the wildcard bind is the caller's spelled-out choice; the held-port factory refusal maps to Shortage/Refused per the io kind (the POC mapped everything to Shortage).

Summary

Filled on completion (2026-09-25).

Implemented the local feature's socket-backed seam providers (ADR-008's local variants, ADR-009's feature gate) — the three seams' faithful-RFC assembly-layer constructors over the landed seam types (src/egress.rs): tcp_dial() (real TcpStream per CONNECT, domain resolution at the provider, faithful SocksDialError mapping), udp_egress() (one real UdpSocket per association bound 127.0.0.1:0, per-datagram send/recv, the POC #2 LocalEgress readiness shape — park on readable(), try_recv_from, inner bytes to_be_bytes() + data, framing-once), and bind_accept(bind_host) (a real TcpListener per BIND session, requested port honored, the accept future owning the listener end-to-end, two-stage failure mapping). The protocol layer never sees them — feature-gated (local = ["tokio/net"]), the default graph stays net/mio/socket2-free (cargo tree spot-checked both ways), and the wasm checks stay green. 18 new tests (12 unit + 6 integration) pin: the real-socket round-trips through the producer's actual dispatch path (echo TCP, UDP echo through the framed datagram stage, BIND end-to-end with byte-exact real addresses), the readiness contract with the real socket (park-then-datagram, no spin), the accept-future-owns-its-listener contract (drop-witness: client-gone → the advertised port frees), and the refusal mappings table-tested against the landed error types' to_reply_error() (connect-refused → 0x05 through the producer's reply, held port → Shortage/Refused → the RFC catch-all, unresolvable domain → HostUnreachable/NetworkUnreachable).

Verification: cargo test (157 green across the 9 targets) + cargo test --all-features green, cargo clippy --all-targets -- -D warnings clean (default + --all-features), cargo fmt --check clean, both wasm checks (cargo check + cargo clippy -D warnings on wasm32-unknown-unknown) clean, cargo tree spot-checks: the default graph carries no tokio/net/mio/socket2 and --features local --target all roots the net feature at alksocks feature "local".