- alksocks-udp-poc (/workspace/alksocks-udp-poc/): 11 tests pass, clippy -D warnings + fmt clean; the handle-swap baseline validated end to end (one channel per association, [len][addr+data] frames in-band, RSV/FRAG only at front doors, channel EOF = association lifetime, per-datagram addressing without a flow table) - Both egress variants implemented behind the EgressFactory callback; composed per-destination alktunnels udp-tunnel open cost measured at ~0.8 ms mean (N=20, in-process loopback) — comfortable for DNS + browsing - Front-door RFC-codec composition confirmed (RFC UDP header codec at the vanilla boundary only); CONNECT regression-guarded on the same registration - Load-bearing API findings for Phase 1: the select!-driven stage's egress readiness contract (Pending-when-idle; readable() not writable(); closed queues end, never spin) and the length framing applied exactly once by the wire layer (egress returns inner bytes) Verification: cargo test (11 pass), cargo clippy --all-targets -- -D warnings, cargo fmt --check, cargo doc --no-deps (all clean in the POC crate); phase-0.md OQ-SK-03/07 statuses updated.
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 — 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 — 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, no flow table — 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 — 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.