Files
alksocks/tasks/implementation/local-providers.md
T
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

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, though 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; per 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, so 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, made 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 → the 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, so 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, and 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), meaning 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, because 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 and 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, with ADR-012 in play), 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, porting 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, since 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, so 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, and 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; these are 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".