- 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
237 lines
13 KiB
Markdown
237 lines
13 KiB
Markdown
---
|
|
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, 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_split` halves are Send+Sync, so 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, 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
|
|
|
|
- [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`), meaning `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`, 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"`. |