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,
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).
+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
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