diff --git a/docs/research/phase-0-findings.md b/docs/research/phase-0-findings.md index de93501..fe62596 100644 --- a/docs/research/phase-0-findings.md +++ b/docs/research/phase-0-findings.md @@ -19,6 +19,13 @@ channel structure, alktunnels generalizes the tunnel handler shape to arbitrary bidirectional tunnels in the `ssh -L` / `ssh -D` sense — TCP, UDP, unix sockets, and other stream or datagram substrates. +The 2026-09-05 first revision adds the tun2proxy UDP gateway prior art +(`OQ-TN-01`, `OQ-TN-02`, `OQ-TN-07`, `OQ-TN-10`) and the transport story +clarification (TCP vs QUIC at the channels layer, invisible to this crate — +§What is already settled). This prior art was discussed with the POC agent +around the alknet-channels POC but never documented; it is captured here so +it survives into the spec. + ## What is already settled The foundation is POC-validated and ADR-pinned; this crate is not starting @@ -50,6 +57,65 @@ from zero. It inherits: - **The relay story** — tunnels traverse alkcall hub relays transparently via byte-for-byte data-channel forwarding with ID rewrite (alkcall ADR-042). No tunnel-specific relay work. +- **The transport story** — the underlying channels transport is TCP or + QUIC (QUIC preferred) but that is the alkcall layer's concern, not + this crate's: the tunnel protocol sees a `BiStream` and is + transport-agnostic like the sibling crates. UDP tunneling (below) + rides the same chunk stream; the stream-vs-datagram question is + about what the tunnel protocol frames *inside* the channel, not + about the transport. + +## Prior art: tun2proxy UDP gateway (`OQ-TN-01`, `OQ-TN-02`, `OQ-TN-07`, `OQ-TN-10`) + +`/workspace/tun2proxy/src/udpgw.rs` implements a UDP gateway over a TCP +stream — structurally the same problem this crate faces for UDP tunnels +over channels data channels. Discussed with the alknet-channels POC +agent as the example of "UDP over a stream substrate," but never +documented. Key mechanics, all of which generalize: + +- **Per-datagram length framing over the stream** — the packet format is + `LEN(u16 BE) | FLAGS(u8) | CONN_ID(u16) | [SOCKS5 address] | DATA` + (`udpgw.rs:82-88`). Boundary preservation is re-added by the protocol, + not by the substrate: exactly the "length-prefix each datagram inside + the channel" half-answer in OQ-TN-02, proven in production. +- **SOCKS5 address format travels per data packet** — `ATYP` + (0x01 IPv4 / 0x03 domain / 0x04 IPv6) + variable address + port + (`udpgw.rs:68-76`). This is concrete prior art for OQ-TN-01's + addressing: a scheme-tagged addressing encoding with v4/v6/domain + coverage already standardized. Note the asymmetry with TCP tunnels: + for UDP, the remote endpoint is per-datagram, not per-channel. +- **One stream carries many UDP flows** — `CONN_ID(u16)` multiplexes + associations over a single gateway connection (`udpgw.rs:66`), + with `keepalive` (0x01) and `error` (0x20) flag packets as the only + non-data frame types (`udpgw.rs:21-26`). This is the "one channel = + one association, per-endpoint multiplexing inside" half-answer in + OQ-TN-02, with the refinement that the per-endpoint multiplexing key + (`CONN_ID`) is protocol-level, allocated by the client, u16. +- **Flow lifecycle is packet-level** — `udp_timeout` idle expiry, + `keepalive_time` heartbeats on idle connections + (`UDPGW_KEEPALIVE_TIME = 30s`, `udpgw.rs:16`), and an MTU cap + (`parse_udp_response` rejects `data.len() > udp_mtu`, + `udpgw.rs:527`). Also `UDPGW_MAX_CONNECTIONS = 5` pooled gateway + connections *above* the packet layer — a throughput choice, not a + protocol requirement; channels gives us N channels already. +- **Implications for alktunnels:** + - UDP boundary preservation over the chunk stream is validated + prior art, not speculation — raises confidence in the OQ-TN-02 + half-answer considerably. + - The frame-type set (DATA/KEEPALIVE/ERR) is a useful minimal + vocabulary — it maps onto OQ-TN-09's establishment/error frame + question (tun2proxy uses flag packets, alktty uses typed control + chunks; both are self-contained frames inside the data stream). + - Per-datagram addressing (SOCKS5-style) vs per-channel addressing + (fixed target at open) is a real fork for the params design: TCP + tunnels fix the target at open; UDP associations may either fix + one endpoint at open or carry per-datagram addresses like udpgw. + This interaction is unresolved and feeds OQ-TN-01 + OQ-TN-07. + - NAT/keepalive concerns partially disappear on channels: the + underlying transport (QUIC/TCP) handles connection keepalive, and + channel liveness is the channels layer's concern. The tunnel + protocol likely needs only flow-level idle expiry, not + transport-level keepalive packets — TBD in the spec. ## Open Questions @@ -87,7 +153,11 @@ Considerations: **Status:** open — research needed. Half-answer (hunch): a scheme-tagged JSON object rather than a URL-ish string, so params stay typed and -extensible; exact shape TBD. +extensible; exact shape TBD. Strengthened 2026-09-05: the tun2proxy UDP +gateway (§Prior art) validates SOCKS5 ATYP addressing (v4/v6/domain) as +in-band prior art, and surfaces a fork — for UDP, remote addressing is +per-datagram (SOCKS5-style) rather than fixed-at-open like TCP. The +params design must account for both modes. ### OQ-TN-02: Datagram substrates (UDP) — boundary preservation @@ -111,12 +181,16 @@ re-chunked arbitrarily)? "association" carries many remote endpoints — does one tunnel channel carry one endpoint or many, and how are per-endpoint replies routed? -**Status:** open — research needed (survey SSH/russh/SOCKS5/tun2proxy -approaches; likely a targeted POC if boundary preservation is chosen). -Half-answer (hunch): length-prefix each datagram inside the channel -(boundary-preserving), and one channel = one association with per-endpoint -multiplexing inside, mirroring SOCKS5 UDP — but this is exactly the kind -of guess that needs survey + POC before it becomes an ADR. +**Status:** open — survey mostly resolved by tun2proxy prior art +(§Prior art): per-datagram length framing over the stream is +production-proven (`LEN | FLAGS | CONN_ID | [addr] | DATA`), one +stream carries many UDP flows via a protocol-level `CONN_ID`, and +flow lifecycle (idle timeout + keepalive) is packet-level. Remaining: +whether alktunnels fixes the UDP endpoint at open (per-channel, TCP- +like) or carries per-datagram addresses (udpgw-like), and whether a +u16 conn-id vocabulary is right for channels (vs the channel ID +itself doing the demux and one channel per UDP flow). A targeted POC +(OQ-TN-10 #1) is likely still +EV for the chosen shape. ### OQ-TN-03: Direction semantics (`-L` / `-R` / dynamic) @@ -237,7 +311,13 @@ consumer — ALPN strings are wire-stable once published. substrate enum). **Status:** open — needs the OQ-TN-02 outcome first (if datagrams need -different framing, option B gets stronger). +different framing, option B gets stronger). Note from the tun2proxy +prior art (§Prior art): udpgw runs its packet framing over a plain TCP +stream — one framing covers both the stream and datagram cases there. +If alktunnels follows the same shape (datagram framing self-describing +inside the channel), option A (single `alk/tunnel` ALPN) stays viable +even with UDP support; option B remains cleaner if the datagram +channel needs structurally different framing from the first chunk on. ### OQ-TN-08: Access control and ownership scope @@ -292,7 +372,11 @@ Phase 0 may need (in rough priority order, per the SDD process's 1. **UDP tunnel POC** — boundary-preserving length framing over a channels channel, per-endpoint multiplexing inside one association, - backpressure behavior. Derisks OQ-TN-02 (and OQ-TN-07's option B). + backpressure behavior. Partially derisked by the tun2proxy prior + art (§Prior art) — the POC now mainly validates *channels-layer* + fit: chunk-size vs datagram-size interaction, MTU cap against the + channels bounded buffers, idle-expiry mapping, and the chosen + endpoint-addressing shape. Derisks OQ-TN-02 (and OQ-TN-07's option B). 2. **Reverse-flow POC** — `-R`-style: the accept side listens, the far side carries. Derisks OQ-TN-03's advertisement/lifecycle shape. 3. **Unix socket + stdio bridge POC** — cheap; validates "substrate @@ -315,8 +399,12 @@ Candidate reading for the research specialist (to be expanded): - SOCKS5 (RFC 1928): addressing (ATYP), UDP ASSOCIATE framing, per-endpoint multiplexing — the closest standardized "arbitrary tunnel + UDP" model. -- tun2proxy (`/workspace/tun2proxy`): UDP-over-TCP tunnel framing in - production; also handles DNS over tunnel. +- tun2proxy (`/workspace/tun2proxy`, `src/udpgw.rs`): UDP gateway over + TCP — per-datagram length framing, SOCKS5 per-datagram addressing, + CONN_ID flow multiplexing, keepalive/ERR flag packets, MTU cap, + idle expiry. Analyzed in §Prior art. Its `socks.rs` / + `proxy_handler.rs` are also relevant for the `-D` (dynamic/SOCKS) + composition question (OQ-TN-03). - quinn-proxy-poc (`/workspace/quinn-proxy-poc`) and iroh (`/workspace/iroh`): datagram-native transports; how they model per-endpoint flows. @@ -331,10 +419,15 @@ Candidate reading for the research specialist (to be expanded): ## Convergence checklist (what Phase 0 must produce) - [ ] Survey notes: SSH/SOCKS5/tun2proxy addressing + UDP framing - (OQ-TN-01, OQ-TN-02) -- [ ] Recommendation: addressing format sketch (OQ-TN-01) + (OQ-TN-01, OQ-TN-02) — tun2proxy UDP gateway done (§Prior art); + SSH/SOCKS5 addressing survey still open +- [ ] Recommendation: addressing format sketch (OQ-TN-01) — SOCKS5 + ATYP validated as in-band encoding prior art; per-channel vs + per-datagram fork unresolved - [ ] Recommendation: datagram strategy (OQ-TN-02) + ALPN strategy - dependent on it (OQ-TN-07) + dependent on it (OQ-TN-07) — framing mechanics de-risked by + tun2proxy prior art; endpoint-at-open vs per-datagram addressing + fork remains - [ ] Recommendation: direction model (-L/-R/-D) (OQ-TN-03) + API surface sketch satisfying no-forced-binding (OQ-TN-04) - [ ] Decision input: backend trait vs no-trait (OQ-TN-05)