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:
1 parent
431920ddef
commit
a867f0cb81
6 files changed
+1433
-8
No files matched your search
@@ -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"`.
|
||||
Reference in new issue
Block a user