docs: OQ-SK-03 handle swap — the relay handle is the channel itself

- the ASSOCIATE reply's BND.ADDR/BND.PORT is a relay handle, not a
  destination (per-datagram destinations ride the datagram header);
  swapping "tell the client about a UDP port" for "tell the client
  about the UDP tunnel" preserves every RFC property: one channel per
  association, {addr, data} frames in-band, channel EOF = association
  lifetime, optional front-door binds on either side
- sentinel-address hunch superseded: no virtualized relay address is
  ever needed on the in-band path; BND.ADDR there is pure
  RFC-compatibility surface
- producer egress has two variants, both dial-callback policy (OQ-SK-01
  shape): local UdpSocket (no flow table, native-only) or composed
  per-target alktunnels udp-tunnels (lazy destination->tunnel table;
  flow table reborn as channel handles; per-target ACL for free —
  dissolves most of OQ-SK-05 for the composed path)
- no upstream ask: alktunnels' connected-udp substrate is correct as-is
  for per-target composition; an unconnected-egress resource would be
  SOCKS5 again (circular)
- POC #2 baseline settled: sketch both egress variants behind the dial
  callback, measure per-destination open cost of the composed variant
This commit is contained in:
2026-09-13 16:01:32 +00:00
parent cf5432209a
commit c3ccba4d59
2 changed files with 95 additions and 15 deletions
+5 -1
View File
@@ -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, phase model — the RFC UDP header becomes a front-door codec concern,
not a channels-wire concern. not a channels-wire concern.
2. Per-datagram addressing with no producer-side flow table suffices 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 — 3. Control-stream EOF ends the association (`wait_until_closed` shape —
on channels, the channel's EOF is the same signal; no extra control on channels, the channel's EOF is the same signal; no extra control
vocabulary needed). vocabulary needed).
+90 -14
View File
@@ -406,8 +406,11 @@ the datagram stage must ride a channel. Sub-questions:
`BiStream` carries length-prefixed datagrams (the alktunnels UDP `BiStream` carries length-prefixed datagrams (the alktunnels UDP
codec shape, `[len: u16 BE]` per datagram; 65507 < 65535 so u16 codec shape, `[len: u16 BE]` per datagram; 65507 < 65535 so u16
suffices). The reply to the client rewrites `BND.ADDR`/`BND.PORT` suffices). The reply to the client rewrites `BND.ADDR`/`BND.PORT`
to a sentinel that means "same channel" — but vanilla SOCKS5 — originally imagined as a sentinel address meaning "same channel,"
clients will literally `sendto()` that address, so this shape only 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 works for *wrapper-aware* client halves (the crate's own consumer
session, or an assembly layer bridging a real UDP socket). session, or an assembly layer bridging a real UDP socket).
**iroh-socks5's refinement (2026-09-13 eval):** the in-band datagram **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" channels-natively. Simplest; weakens the "ALPN as a service"
story for UDP. story for UDP.
- **What does `BND.ADDR`/`BND.PORT` reply contain on the channels - **What does `BND.ADDR`/`BND.PORT` reply contain on the channels
path?** RFC says the relay address; a channels path has none. path?** Largely resolved (2026-09-13, see the handle-swap discussion
fast-socks5's `run_udp_proxy_custom` lets the wrapper supply the below): on the in-band path the relay handle *is the channel itself*
reply and the relay half — the seam exists upstream; the semantics — no address needs to be invented, and the reply's `BND.ADDR`/
are ours to define. `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 - **Per-datagram addressing** — the SOCKS5 UDP header carries the
destination per datagram (ATYP + addr + port), so one association destination per datagram (ATYP + addr + port), so one association
multiplexes many endpoints. alknet's model used a producer-side 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). `{addr, data}` frame shape meets it structurally).
- **Do vanilla (wrapper-unaware) clients need channels-native UDP at - **Do vanilla (wrapper-unaware) clients need channels-native UDP at
all?** If the only UDP ASSOCIATE consumers are wrapper-aware, the 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. 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 **Phase model vs alktty-style demux (2026-09-12 discussion).** An
alternative shape surfaced: alktty's logical demux (input/output/error/ alternative shape surfaced: alktty's logical demux (input/output/error/
control sub-streams inside one channel, type byte per chunk, up to the 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 argument is the honest case for the demux if Phase 1 wants the
vocabulary. Decide via ADR with the first consumer in sight. vocabulary. Decide via ADR with the first consumer in sight.
Hunch (datagram stage, unchanged): wrapper-aware client halves ride the Hunch (datagram stage, updated 2026-09-13 by the handle swap):
same-channel length-prefixed-datagram shape; vanilla clients get UDP wrapper-aware client halves ride the same-channel `{addr, data}` frame
ASSOCIATE only via the optional local backend. But this is the crate's shape (one channel per association, per-datagram addressing in-band,
largest unknown — a POC candidate (OQ-SK-07 #2), decided by an ADR channel EOF = association lifetime); vanilla clients get UDP ASSOCIATE
before the first consumer (the datagram framing is wire-stable once only via the optional local front-door bridge. The remaining choice is
published). 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) ### 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 policy are the security boundary (AGENTS.md convention 12). alktunnels
resolved that whatever ACL governs the socks5 resource governs resolved that whatever ACL governs the socks5 resource governs
everything reachable through it; OQ-SK-01's dial policy is the 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 - Does the base crate ship a per-target allowlist hook (producer-side
policy injected at registration), or is target policy entirely the 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 flow table (per-datagram addressing in the frame); the sentinel
question may dissolve for the in-band path (no `BND.ADDR` needed question may dissolve for the in-band path (no `BND.ADDR` needed
when the datagram stage needs no relay address). 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 3. **noq `AsyncUdpSocket` impl POC** — the client-side story against
noq 1.2: associate through a fast-socks5 server, wrap as noq 1.2: associate through a fast-socks5 server, wrap as
`noq::AsyncUdpSocket`, complete a QUIC handshake. Derisks OQ-SK-06 `noq::AsyncUdpSocket`, complete a QUIC handshake. Derisks OQ-SK-06