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.
This commit is contained in:
glm-5.3-flash committed 2026-09-25 05:55:27 +00:00
1 parent 431920ddef
commit a867f0cb81
6 files changed
+1433 -8

No files matched your search

+237
View File
@@ -0,0 +1,237 @@
---
id: implementation/local-providers
name: Local — the socket-backed seam providers (tcp_dial / udp_egress / bind_accept)
status: completed
depends_on: []
scope: moderate
risk: medium
impact: component
level: 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_split` halves are Send+Sync, verify
the boxing compiles). Dial failures map to `SocksDialError`
faithfully (`ConnectionRefused` for connect-refused,
`NetworkUnreachable`/`HostUnreachable` per `io::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
- [x] `cargo test` passes; `cargo test --all-features` passes
- [x] `cargo clippy --all-targets -- -D warnings` clean; `cargo fmt
--check` clean
- [x] Default wasm checks stay green (`cargo check`/`clippy --target
wasm32-unknown-unknown -- -D warnings`) — `local` code is
cfg-gated out of the default build
- [x] Default dependency graph has no `tokio/net`/`mio`/`socket2`
(`cargo tree` spot-check); `local = ["tokio/net"]` wired
- [x] `tcp_dial`/`udp_egress`/`bind_accept` land with the exact
producer.md §Wiring names and the landed seam types
- [x] The egress readiness contract honored (real async park; no hot
`Ok(None)`) — pinned by test
- [x] The accept future owns its listener end-to-end — pinned by a
drop-witness test (client-gone → listener closed)
- [x] 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"`.