- 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
12 KiB
status, last_updated
| status | last_updated |
|---|---|
| complete | 2026-09-14 |
alksocks POC findings: channels-native UDP ASSOCIATE
POC #2 from phase-0 §OQ-SK-07, run 2026-09-14. Crate:
alksocks-udp-poc (standalone, /workspace/alksocks-udp-poc/), against
alkcall 0.7.1 (crates.io), alktunnels 0.1.0 (crates.io, local feature),
fast-socks5 1.0.0 (crates.io). 11 tests, all pass; cargo clippy --all-targets -- -D warnings and cargo fmt --check clean.
Validates OQ-SK-03's design baseline (the handle swap, settled
2026-09-13): one channel per association; [len: u16 BE][addr + data] frames in-band; channel EOF ends the association; the RFC UDP
header codec lives only at vanilla-client front doors. Both producer
egress variants are implemented behind the egress callback, and the
composed variant's per-destination open cost is measured.
What was validated
- The handle-swap data plane was confirmed end to end. The
ASSOCIATE conversation and the datagram stage ride the same
channel
BiStream. The producer drives the typestate directly (accept_no_auth → read_command → reply_success(sentinel));run_udp_proxy_customis not used (its unconditionaludp_bind_random_portmakes it unusable on channels, per the corrected §Prior art caveat; driving the typestate directly is the workaround the upstream ask would retire). - The in-band frame codec is confirmed. The wire is alktunnels'
boundary-preserving length codec (
frame_datagram/DatagramReader; empty payload legal, never EOF; frames split/batch across channel chunks transparently) over the RFC address encoding (TargetAddr::to_be_bytes(): ATYP + addr + port). RSV/FRAG do not ride the tunnel wire: fragmentation cannot even be expressed in-band; it is refused at front doors (FRAG ≠ 0 → refuse, the iroh-socks5 v1 posture). The RFC-header codecs (new_udp_header/parse_udp_request) are used only at the front-door boundary, exactly as the baseline specifies. - Per-datagram addressing without a flow table was confirmed. One relay
loop per association; each frame carries its destination; the
local-egress variant relays through one real
UdpSocketwith per-datagramsend_to/recv_from(iroh-socks5's cut). Five in-order datagrams round-trip over one association (udp_multiple_datagrams_one_association). - Association lifetime = channel lifetime, confirmed. The
consumer's
shutdown()half-closes the stream; the mux's implicit-EOF sentinel (REQ-CH-01) reaches the producer's stage read as EOF; the stage exits and the connection stays healthy (a fresh associate succeeds immediately after). No extra control vocabulary needed; the channels-level EOF is the association's end-of-life signal, as the iroh-socks5 eval predicted. - Control silence is structurally enforced, confirmed. The
frame's first byte is ATYP, so any post-reply control byte parses
as a garbage ATYP and fails the association loud
(
udp_undecodable_frame_ends_association, with the unassigned ATYP 0x7F). The phase model needs no explicit mode byte: the reply itself synchronizes the mode switch. - Both egress variants: implemented and passing.
- Local egress (
LocalEgress): one real UDP socket per association; per-datagram addressing; no flow table. Domain-form destinations resolve at the egress (producer-side DNS, the ssh-Dsemantic, POC-resolved as "the egress owns resolution"). - Composed egress (
ComposedEgress): a lazy destination→alktunnels-udp-tunnel table (each new destination opens onealk/tunnelchannel withsubstrate: udp), LRU-evicted at a cap; a relay task per tunnel drains replies into the association's shared inbound queue. Per-target ACL comes free (the tunnel resource's ACL governs each destination's egress). The full stack round-trips: SOCKS5 consumer → SOCKS5 producer → composed egress →alk/tunnelchannel → tunnel producer → local UDP echo (udp_composed_egress_round_trip).
- Local egress (
- The open-cost measurement (the empirical OQ-SK-03 input).
Per-destination tunnel open, N=20 fresh associations, in-process
loopback transport: mean ≈ 810–825 µs, min ≈ 765–800 µs, p95 ≤
~1.0 ms (three runs: 825/807/819 µs mean). Sub-millisecond;
comfortable for DNS + browsing. The 256-channel default cap
(ADR-040/041, configurable) bounds any single association's
destination fan-out; the wire space itself is
u32. The LRU cap + the dedup assertion (open_count == 1for repeated same-resource destinations) are pinned byudp_composed_egress_lazy_table_lru_and_open_cost. - The front door: RFC codec composition at the vanilla boundary,
confirmed. An in-process bridge exposes the association on a real
UDP socket; the door's pump decodes RFC-header'd datagrams
(
parse_udp_request), sends{addr, data}frames in-band, and returns replies RFC-header'd (new_udp_header). An RFC datagram (the shape curl/tun2proxy present) round-trips through the door (udp_front_door_vanilla_client_round_trip). The full raw-pipe + reply-rewrite control door is the crate's component-2 assembly shape (the CONNECT front door in POC #1 already validated that pipe shape); this test pins the UDP-specific claim: RFC codec at the door, frames in-band, zero payload translation. - CONNECT on the same registration, regression-guarded. One ALPN
(
alk/socks5), one op (channels/socks5/sub), trivial params; the CONNECT path is the POC #1 data plane unchanged (connect_still_served_on_the_same_registration). - The identity-seam wiring requirement re-confirmed for the
composed path: the tunnel producer's channel-0 accept hook must
set the opener identity (
set_identity), or scope-gated tunnel opens fail closed withFORBIDDEN "authentication required"; the same POC #1 finding, now witnessed from the consumer side of the composed stack.
Structural findings (the load-bearing discoveries)
The datagram stage is select!-driven, and the egress reply
future's readiness contract is load-bearing. The stage runs channel
reads and the egress's reply stream concurrently (tokio::select!).
Three readiness hazards were hit and fixed in this POC, all of them
generalizable API-design notes for the crate's Phase 1:
- The poll-then-block window loses replies. The first iteration interleaved "drain egress replies (non-blocking), then block on the channel read"; the echo's reply arriving during the channel block was never picked up. The fix is the concurrent select shape, which is also the honest full-duplex shape (an egress may produce unsolicited inbound datagrams at any time).
- An instantly-ready
Ok(None)starves the other branch withbiased. Anext_replythat returnsOk(None)immediately (drained) makes the biased select take it every iteration, a livelock that starves the mux/demux on a current-thread runtime. The egress contract is therefore:next_replymust bePendingwhen no reply is ready (Ok(None)is a spurious wakeup, not a hot loop). TheLocalEgressparks onreadable(); notewritable()is the wrong readiness for UDP (always ready, the send-readiness, spins forever);readable()is the datagram-arrival readiness. TheComposedEgressparks on the shared inbound queue'srecv(); a closed queue (all relay tasks gone) must end the association (an error), never spin. - The length framing is applied exactly once, by the wire layer.
Egress::next_replyreturns the inner bytes (addr + data); the producer'swrite_frameadds the single length prefix. (The first iteration framed in the egress and prefixed in the producer, double framing; the client parsed the length field as ATYP 0x00.)
Double framing is the one wire bug the POC caught, and it
localizes a Phase 1 API decision: the egress (dial-callback) layer
speaks inner bytes; the length codec is the wire layer's. The
consumer side mirrors it: UdpAssociation::send_datagram frames once,
its DatagramReader state (mid-frame position + batched-datagram
pending queue) persists across recv_datagram calls; a datagram
split across channel chunks survives a caller-driven receive loop
(the first cut dropped the reader state per call; the echo test's
2-byte-prefix chunks exposed it).
The per-association egress instance. EgressFactory is called
once per ASSOCIATE; the datagram stage owns the egress; stage exit
drops it (composed tunnels' write halves EOF their channels; the
adopted channel entries are reaped by the sessions' Drop, alktunnels
ADR-005). One structural bug here: the first cut bound the opened
session to a local let _session = ... binding, which dropped at the
end of the insert block, tearing the channel down at birth (the
relay task saw EOF instantly). The session must live inside the table
entry. The crate's Phase 1 equivalent: the egress owns its tunnels
end-to-end, and the compiler should enforce it (no _ bindings).
Wire/API notes for the wrapper
TargetAddr::to_be_bytes()is the in-band address encoding (ATYP + addr + port), the RFC UDP header minus RSV/FRAG.unframedecodes it by hand (synchronously over a borrowed slice);fast_socks5::util::target_addr::read_addressis the async in-stream equivalent.frame/inner/unframein the POC'sdatagram.rsare the Phase 1 codec's direct sketch.alktunnels::wire::frame_datagram/DatagramReaderare the length codec, reused unchanged, the shared framing story with alktunnels' udp substrate (per the user's codec decision: depend on alktunnels, do not hand-roll).- The ASSOCIATE reply's
BND.ADDRis the sentinel127.0.0.1:0on the channels path (the handle is the channel; the address is pure RFC-compatibility surface). The consumer session receives it (relay_addr) and never uses it for addressing. Only the front door has a real address to report. LocalEgress::next_replyframes replies with the sender's address (the RFC's per-datagram source semantics,recv_from'sfrom), not the original destination. For echo targets the two coincide; a real producer would attribute by egress socket (the composed variant's table entry carries the destination explicitly; its replies are attributed correctly by construction).- The composed egress's per-destination attribution: the table's
reply_addris recorded at tunnel-open time (the datagram's original destination); the tunnel IS the per-destination association, the eval's noted advantage of the composed shape. parse_udp_requestis async (though it completes immediately over a borrowed slice); a sync mirror (unframe) is used where the decode must not be async. Phase 1 may want an upstream ask: sync pure address-decode helpers (the codec is pure; only its signature is async).
OQ status after this POC
- OQ-SK-03 (UDP ASSOCIATE): the handle-swap baseline is
POC-validated end to end: one channel per association,
{addr, data}frames in-band (RSV/FRAG only at front doors), channel EOF = association lifetime, per-datagram addressing without a producer flow table. The datagram framing is wire-stable once published, and the Phase 1 ADR has empirical ground under it. The demux-vs-phase question (the one-way-door extensibility argument) stays an ADR decision for Phase 1 with the first consumer in sight, but the POC adds evidence for the phase model: no in-band control vocabulary was needed, and post-reply garbage is refused structurally. - OQ-SK-01 (dial policy): the egress seam is the dial-callback
shape extended to the datagram stage (
EgressFactory, theDialFnanalogue). Both egress variants behind the callback, the protocol layer sees neither; the no-trait, boxed-halves precedent now holds for the datagram plane too. - OQ-SK-05 (target policy): the composed egress's per-target-ACL- for-free claim is validated (each destination rides its own tunnel channel, governed by the tunnel resource's ACL). The egress-owns-DNS resolution posture is POC-resolved for the local variant.
- OQ-SK-07: #1, #2, #4 complete; #3 (noq
AsyncUdpSocket) remains.
POC placement
Standalone crate (global workspace, /workspace/alksocks-udp-poc/)
per the placement conventions; it composes three published crates
(alkcall 0.7.1, alktunnels 0.1.0, fast-socks5 1.0.0) and needs no code
from this repo. Findings land here; the crate is the artifact.