diff --git a/docs/research/iroh-socks5-eval.md b/docs/research/iroh-socks5-eval.md index 420fa2b..f09f008 100644 --- a/docs/research/iroh-socks5-eval.md +++ b/docs/research/iroh-socks5-eval.md @@ -243,7 +243,11 @@ stand); **adopt iroh-socks5's UDP design as prior art for OQ-SK-03**: phase model — the RFC UDP header becomes a front-door codec concern, not a channels-wire concern. 2. Per-datagram addressing with no producer-side flow table suffices - (revisit alknet's flow-table posture in light of this). + for the *local egress* variant (revisit alknet's flow-table posture + in light of this). Caveat added 2026-09-13: under *composed* egress + (per-target alktunnels udp-tunnels, the OQ-SK-03 handle-swap + discussion) the flow table returns as a destination→tunnel-channel + table — a dial-callback policy, not a protocol-layer concern. 3. Control-stream EOF ends the association (`wait_until_closed` shape — on channels, the channel's EOF is the same signal; no extra control vocabulary needed). diff --git a/docs/research/phase-0.md b/docs/research/phase-0.md index 173cf20..50fbe80 100644 --- a/docs/research/phase-0.md +++ b/docs/research/phase-0.md @@ -406,8 +406,11 @@ the datagram stage must ride a channel. Sub-questions: `BiStream` carries length-prefixed datagrams (the alktunnels UDP codec shape, `[len: u16 BE]` per datagram; 65507 < 65535 so u16 suffices). The reply to the client rewrites `BND.ADDR`/`BND.PORT` - to a sentinel that means "same channel" — but vanilla SOCKS5 - clients will literally `sendto()` that address, so this shape only + — originally imagined as a sentinel address meaning "same channel," + but the handle swap below supersedes that: no virtualized address + is needed at all, because the in-band relay handle is the channel + itself, not an address. Vanilla SOCKS5 clients will literally + `sendto()` whatever address the reply carries, so this shape only works for *wrapper-aware* client halves (the crate's own consumer session, or an assembly layer bridging a real UDP socket). **iroh-socks5's refinement (2026-09-13 eval):** the in-band datagram @@ -427,10 +430,13 @@ the datagram stage must ride a channel. Sub-questions: channels-natively. Simplest; weakens the "ALPN as a service" story for UDP. - **What does `BND.ADDR`/`BND.PORT` reply contain on the channels - path?** RFC says the relay address; a channels path has none. - fast-socks5's `run_udp_proxy_custom` lets the wrapper supply the - reply and the relay half — the seam exists upstream; the semantics - are ours to define. + path?** Largely resolved (2026-09-13, see the handle-swap discussion + below): on the in-band path the relay handle *is the channel itself* + — no address needs to be invented, and the reply's `BND.ADDR`/ + `BND.PORT` are pure RFC-compatibility surface (zeros, or whatever + the front-door bridge reports). Only the vanilla-client front door + has a real socket address to report, and there it comes from the + real relay socket the assembly layer chose to bind. - **Per-datagram addressing** — the SOCKS5 UDP header carries the destination per datagram (ATYP + addr + port), so one association multiplexes many endpoints. alknet's model used a producer-side @@ -441,9 +447,63 @@ the datagram stage must ride a channel. Sub-questions: `{addr, data}` frame shape meets it structurally). - **Do vanilla (wrapper-unaware) clients need channels-native UDP at all?** If the only UDP ASSOCIATE consumers are wrapper-aware, the - sentinel-reply shape is fine and no virtualized relay address is + in-band shape needs no relay address at all (the handle is the + channel — see the handle swap below) and no virtualized address is ever invented. +**The handle swap (2026-09-13 discussion) — "tell the client about a +UDP port" → "tell the client about the UDP tunnel."** The design's +load-bearing reading of RFC 1928: the ASSOCIATE reply's `BND.ADDR`/ +`BND.PORT` is *just the handle for the relay*, and one relay serves the +whole association — the per-datagram destination is carried in the +datagram header (ATYP/addr/port), not in the handle. The relay port was +never "a port that goes to one place"; it is a multiplexing endpoint. +So swapping the handle preserves every RFC property: + +- one relay per association → **one channel per association** (the + CONNECT shape; no second channel, no correlation params); +- per-datagram destinations → `{addr, data}` frames in-band; +- lifetime tied to the control connection → channel EOF; +- "bind locally if you want" → the optional front door on either side + (a real UDP socket bridged to the channel, RFC codec at that + boundary only). + +Vanilla clients still literally `sendto()` the replied address, so the +in-band path is wrapper-aware-only by RFC necessity — the front-door +bridge is where a real socket address exists. + +**Producer egress — two variants (the "no ports" story has two +halves).** The handle swap settles the *consumer-facing* half. The +*producing-side* half is: after `{addr, data}` frames exit the channel, +what gets each datagram to `addr`? + +1. **Local egress** — the handler binds a real `UdpSocket` and + `send_to`s per datagram (iroh-socks5's `relay_udp_server` behind + this crate's `local` feature). No flow table, native-only. +2. **Composed egress (OQ-SK-01 Option A for the datagram stage)** — + the dial callback opens an alktunnels `udp`-substrate tunnel per + destination: a lazy destination→tunnel table (open on first + datagram, LRU-evict). The flow table is *reborn as channel + handles* — because alktunnels' udp substrate is **connected** + (`connect_udp(target)`: one tunnel, one fixed target) while + ASSOCIATE names a different destination per datagram. Cost: one + open per destination (bounded by the 256-channel cap; fine for + DNS + browsing; the per-destination latency is a POC #2 + measurement). Benefit: **per-target ACL for free** — the ACL + governing the udp-tunnel resource governs egress per destination + (dissolving most of OQ-SK-05 for the composed path), and hub + relaying is per-target terminate-and-re-produce. + +Both variants are dial-callback policy (OQ-SK-01's shape) — nothing in +the fork or the protocol layer sees the difference. **No upstream ask +exists here:** alktunnels' connected-udp substrate is correct as-is for +per-target composition; an "unconnected arbitrary-egress udp resource" +upstream would just be SOCKS5 again with different framing (circular). +Prior note retained: fast-socks5's `run_udp_proxy_custom` lets the +wrapper supply the reply and the relay half — the seam exists upstream; +the local-egress variant uses it or drives the typestate directly (the +unconditional-bind caveat above). + **Phase model vs alktty-style demux (2026-09-12 discussion).** An alternative shape surfaced: alktty's logical demux (input/output/error/ control sub-streams inside one channel, type byte per chunk, up to the @@ -475,12 +535,17 @@ datagrams. Hunch: phase model (simpler, RFC-shaped); the extensibility argument is the honest case for the demux if Phase 1 wants the vocabulary. Decide via ADR with the first consumer in sight. -Hunch (datagram stage, unchanged): wrapper-aware client halves ride the -same-channel length-prefixed-datagram shape; vanilla clients get UDP -ASSOCIATE only via the optional local backend. But this is the crate's -largest unknown — a POC candidate (OQ-SK-07 #2), decided by an ADR -before the first consumer (the datagram framing is wire-stable once -published). +Hunch (datagram stage, updated 2026-09-13 by the handle swap): +wrapper-aware client halves ride the same-channel `{addr, data}` frame +shape (one channel per association, per-datagram addressing in-band, +channel EOF = association lifetime); vanilla clients get UDP ASSOCIATE +only via the optional local front-door bridge. The remaining choice is +producer-egress policy (local socket vs per-target alktunnels +composition) — a dial-callback question owned by OQ-SK-01, with the +composed variant's ACL benefit making it the hunch for the base crate's +composed story. But this is still the crate's largest unknown — a POC +candidate (OQ-SK-07 #2), decided by an ADR before the first consumer +(the datagram framing is wire-stable once published). ### OQ-SK-04: fast-socks5 wasm posture (blocks the wasm-clean invariant) @@ -567,7 +632,11 @@ SOCKS5 is arbitrary-egress by nature — the open gate plus the target policy are the security boundary (AGENTS.md convention 12). alktunnels resolved that whatever ACL governs the socks5 resource governs everything reachable through it; OQ-SK-01's dial policy is the -mechanism. Residual questions for Phase 1: +mechanism. Residual questions for Phase 1 (note the 2026-09-13 +OQ-SK-03 handle-swap finding: for the *composed* UDP egress path, +per-target ACL comes free — each destination rides its own +alktunnels udp-tunnel channel governed by the tunnel resource's ACL, +dissolving most of this OQ for that path): - Does the base crate ship a per-target allowlist hook (producer-side policy injected at registration), or is target policy entirely the @@ -657,6 +726,13 @@ promising approaches"): flow table (per-datagram addressing in the frame); the sentinel question may dissolve for the in-band path (no `BND.ADDR` needed when the datagram stage needs no relay address). + **Baseline settled by the handle swap (2026-09-13, OQ-SK-03):** one + channel per association; `{addr, data}` frames in-band; channel EOF + ends the association; RFC codec only at front doors. The POC should + sketch **both egress variants** behind the dial callback — local + `UdpSocket` (no flow table) and composed per-target alktunnels + udp-tunnels (lazy destination→tunnel table, LRU) — and measure the + per-destination open cost of the composed variant empirically. 3. **noq `AsyncUdpSocket` impl POC** — the client-side story against noq 1.2: associate through a fast-socks5 server, wrap as `noq::AsyncUdpSocket`, complete a QUIC handshake. Derisks OQ-SK-06