- 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
173 lines
8.6 KiB
Markdown
173 lines
8.6 KiB
Markdown
---
|
|
id: implementation/composed-egress
|
|
name: "Composed: the direct-op egress (lazy destination→tunnel table)"
|
|
status: completed
|
|
depends_on: []
|
|
scope: moderate
|
|
risk: medium
|
|
impact: component
|
|
level: implementation
|
|
---
|
|
|
|
## Description
|
|
|
|
The `composed` feature's egress provider (ADR-008's composed
|
|
variants): the ASSOCIATE egress that dials each destination through
|
|
an alktunnels 0.2.0 direct-op channel (`TunnelSession::open_direct`,
|
|
`Substrate::Udp`): per-target ACL for free (each destination rides
|
|
its own `tunnel:direct`-governed channel), the lazy
|
|
destination→tunnel table LRU-evicted at a cap (POC #2's
|
|
`ComposedEgress` shape).
|
|
|
|
Deliverables:
|
|
|
|
1. **`Cargo.toml`**: `composed = ["dep:alktunnels"]` (the only
|
|
feature that adds the alktunnels dependency, ADR-009); alktunnels
|
|
pinned `"0.2.0"` (the sibling pin posture; convention 11's
|
|
deliberate-bump rule applies). Verify the composed graph stays
|
|
wasm-clean (alktunnels' default crate is wasm-clean; the composed
|
|
providers use only its ungated channels surface, and the wasm check
|
|
with `--features composed` is a hard gate).
|
|
2. **`src/composed/mod.rs`** (feature `composed`): the composed
|
|
providers, named per producer.md §Wiring:
|
|
- `direct_egress(...)` → `EgressFactory`: the POC #2
|
|
`ComposedEgress` shape ported to the landed seam types: a lazy
|
|
destination→tunnel table: each new destination opens one
|
|
alktunnels UDP-tunnel channel (`open_direct`, `Substrate::Udp`),
|
|
a relay task per tunnel drains inbound datagrams into a shared
|
|
queue, the write half stays in the table (LRU-capped; dropping
|
|
a write half EOFs the tunnel producer-side; the session's Drop
|
|
reaps the channel entry (alktunnels ADR-005).
|
|
- **The Sync-bound wrinkle (the batch-1 review's seam contract vs
|
|
alktunnels' surface)**: the landed `TargetHandle`/`BindTarget
|
|
Handle` halves are `Box<dyn AsyncRead + Send + Sync + Unpin>`,
|
|
but alktunnels' `TakenHalves` yields `Send + Unpin` only. The
|
|
composed providers need an **owner-wrapper inside the handle
|
|
payloads** (egress.rs's documented posture: "keep-alive state a
|
|
substrate needs rides inside the halves; store a provider-side owner
|
|
wrapper"). For the egress the wrapper is the table entry's
|
|
session (the egress owns it, not a handle payload); for the
|
|
composed *accept* (the sibling task) the wrapper is load-bearing,
|
|
so design it here and share it.
|
|
- **The readiness contract** (load-bearing): `next_reply` parks on
|
|
the shared inbound queue (`recv().await`, Pending until a
|
|
relay task delivers); a closed queue (all relays ended) returns
|
|
`Err`, never a hot `Ok(None)` (the POC's closed-queue arm; the
|
|
stage's zero-yield defense, review 002 R-1, is the backstop,
|
|
not the design). Replies carry *inner* bytes (`addr + data`;
|
|
the reply address is the entry's recorded destination;
|
|
framing-once).
|
|
- **Per-datagram `send` is awaited inline** (the stage
|
|
interleaves sends with reply drains), so a slow egress applies
|
|
backpressure to the association, not to other channels
|
|
(wire.md's note: "the batch-2 composed-egress author inherits
|
|
this bound knowingly"). The table's open cost rides the send
|
|
path (lazy open on first datagram; the POC's measured ~0.8 ms).
|
|
- The resource/target mapping: `open_direct` takes a
|
|
`SubstrateAddr`: the destination's `TargetAddr` renders to it
|
|
(IP forms direct; domain form passes through unresolved,
|
|
producer-side resolution, the ssh `-D` semantic; ADR-012).
|
|
Constructor config carries the LRU cap (the 256-channel default
|
|
cap bounds fan-out, ADR-010's posture; the cap is config, not
|
|
wire).
|
|
- Module doc: names the ADRs (008/009), the per-target-ACL
|
|
benefit, the readiness contract, the framing-once invariant,
|
|
the backpressure bound.
|
|
3. **Tests** (`tests/composed-egress.rs`): the POC #2 shape end to
|
|
end over channels: a tunnel-producing peer (the alktunnels
|
|
registry shape, `register_tunnel_direct_openable`), the composed
|
|
egress against it, datagram round-trips through the tunnel
|
|
(multiple destinations → multiple tunnels; the LRU eviction at
|
|
cap pinned by test); the readiness contract re-pinned (park-then-
|
|
datagram, no spin); the closed-queue → association-end shape;
|
|
framing-once (no double prefix, the POC #2 bug class).
|
|
|
|
## Acceptance Criteria
|
|
|
|
- [x] `cargo test` passes; `cargo test --all-features` passes
|
|
- [x] `cargo check --features composed --target wasm32-unknown-
|
|
unknown` clean; the composed graph is wasm-clean (hard gate)
|
|
- [x] `cargo clippy --all-targets --all-features -- -D warnings`
|
|
clean; `cargo fmt --check` clean
|
|
- [x] `alktunnels` pinned `"0.2.0"`; the dep rides `composed` alone
|
|
(default graph unchanged, per the `cargo tree` spot-check)
|
|
- [x] The lazy table + LRU eviction land per the POC shape (pinned
|
|
by test)
|
|
- [x] The readiness contract honored (real async park; closed queue
|
|
→ `Err`, never a hot `Ok(None)`), pinned by test
|
|
- [x] Framing-once (replies are inner bytes; no double prefix),
|
|
pinned by test
|
|
- [x] Per-datagram send backpressure documented on the constructor
|
|
|
|
## References
|
|
|
|
- docs/architecture/decisions/008-provider-defaults.md (the composed
|
|
variants), 009-feature-gates.md, 003 (the handle swap), 010 (the
|
|
cap), 012 (resolution)
|
|
- docs/architecture/producer.md §Wiring (`composed::direct_egress`),
|
|
§The Three Seams
|
|
- POC reference: `/workspace/alksocks-udp-poc/src/egress.rs`
|
|
(`ComposedEgress`: the table, the relay tasks, the closed-queue
|
|
posture)
|
|
- Landed surfaces: `src/egress.rs` (the seam contracts + the
|
|
owner-wrapper posture), alktunnels 0.2.0
|
|
(`TunnelSession::open_direct`, `TakenHalves`,
|
|
`register_tunnel_direct_openable`)
|
|
|
|
## Notes
|
|
|
|
> Landed as `src/composed/mod.rs` (feature `composed`) +
|
|
> `tests/composed-egress.rs`, with a `wire_tunnel_direct` topology
|
|
> added to `tests/harness.rs` (the consumer-side topology with
|
|
> alktunnels' direct op on the producer's serving registry; the
|
|
> composed egress's `ChannelClient` is the direct-op caller).
|
|
>
|
|
> Two deviations from the POC shape, both deliberate:
|
|
>
|
|
> - **The closed-queue condition is a live-relay count, not a queue
|
|
> close.** The egress holds its own factory `Sender`, so the shared
|
|
> `mpsc` queue can never fully disconnect: `recv()` would never
|
|
> return `None` and the POC's closed-queue arm was unreachable
|
|
> through this surface. Relays instead emit an `Ended` marker on
|
|
> their tunnel's EOF; `next_reply` counts them (`live_relays`) and
|
|
> returns the association-end `Err` when the count reaches zero and
|
|
> the queue is drained. The readiness contract is unchanged (the
|
|
> wait is a real async park; a drained-but-live relay set parks).
|
|
> - **The tunnel codec is alktunnels' `wire::DatagramReader`
|
|
> (`[len]` + payload), not this crate's RFC-framed one**; the POC
|
|
> fed the tunnel read half into the SOCKS wire reader (a bug class
|
|
> the round-trip test caught during bring-up); the composed egress
|
|
> frames the tunnel wire once with the tunnel codec and the RFC
|
|
> wire once downstream (framing-once holds at both boundaries).
|
|
>
|
|
> The Sync-bound wrinkle resolves as designed: the shared
|
|
> `SessionOwner` wrapper (half + session in one owner) is exported
|
|
> for the composed accept (the sibling task); the egress itself owns
|
|
> the session in the table entry (the POC's shape) and the relay task
|
|
> owns the bare read half.
|
|
>
|
|
> `cargo tree --edges normal` spot-check: `alktunnels` rides
|
|
> `composed` alone (absent from the default normal graph; the
|
|
> dev-dep copy is dev-only).
|
|
|
|
## Summary
|
|
|
|
> The composed egress landed: `composed = ["dep:alktunnels"]`
|
|
> (alktunnels pinned "0.2.0"; dep rides `composed` alone),
|
|
> `src/composed/mod.rs` (`direct_egress`, the `EgressFactory`), the
|
|
> lazy destination→tunnel table with LRU eviction at cap (`DEFAULT_
|
|
> MAX_TUNNELS` = 256), per-tunnel relay tasks draining into a shared
|
|
> inbound queue, `SessionOwner` (the owner wrapper shared with the
|
|
> composed accept), the readiness contract (real async park; ended
|
|
> relays → association-end `Err`, never a hot `Ok(None)`), framing-
|
|
> once, per-target ACL via the direct op, domain pass-through
|
|
> unresolved (ADR-012), and the backpressure bound documented on the
|
|
> constructor. `tests/composed-egress.rs` (6 tests over the
|
|
> `wire_tunnel_direct` harness topology): round-trips per
|
|
> destination → one tunnel each, table reuse, LRU eviction/re-dial,
|
|
> the readiness park, the closed-relay-set `Err`, framing-once
|
|
> through the wire layer, and the domain pass-through. Verification:
|
|
> `cargo test` (default) + `--all-features` pass; the wasm check with
|
|
> `--features composed` clean; clippy (`--all-targets --all-features
|
|
-- -D warnings`) clean; `cargo fmt --check` clean; `cargo doc
|
|
> --no-deps` clean. |