Files
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

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"`.