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:
+90
-14
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user