diff --git a/docs/research/phase-0.md b/docs/research/phase-0.md index 64e31d2..b9f9687 100644 --- a/docs/research/phase-0.md +++ b/docs/research/phase-0.md @@ -1,6 +1,7 @@ --- status: draft -last_updated: 2026-09-14 +last_updated: 2026-09-14 (post-POC #2 review: OQ-SK-08 BIND, OQ-SK-09 +capacity, OQ-SK-05 virtual-subnet addendum, POC #5) --- # alksocks — Phase 0 (Exploration) @@ -710,6 +711,30 @@ dissolving most of this OQ for that path): wasm producers may not have a resolver at all — another OQ-SK-04 interaction.) +**Virtual address space / identity-scoped subnets (added 2026-09-14, +post-POC #2).** A proposal surfaced during the findings review: give +identities scoped virtual subnets (e.g. 10.x.0.0/16 slices) so proxied +resources become IP-addressable — "BND.ADDR ↔ channel id" translation, +a hub exposing proxied resources per identity under ACL. The goal is +adoptable; the mechanism needs **no wire change**: DST.ADDR and +BND.ADDR are arbitrary RFC fields, so virtual addresses ride ordinary +CONNECT/BIND requests — the translation layer is the dial callback +(OQ-SK-01's shape: `dial(target)` matches a virtual subnet against a +per-identity resource table), per-identity scoping is resources + ACL ++ target policy (this OQ), and hub exposure is the +terminate-and-re-produce story with no new wire vocabulary. BND.ADDR +stays per the handle swap (OQ-SK-03): in-band the handle is the +channel (compat surface only); a real network-B address exists +wherever a local backend binds — the listener's `local_addr` (OQ-SK-08 +variants 1–2 cover BIND's reply#1). The one place a virtual subnet +becomes *routable from vanilla apps* is the full-tunnel composition: +the host routes the subnet into the proxy (tun2proxy), so +`connect(10.x.y.z:p)` is an ordinary CONNECT the producer translates. +Worth checking tun2proxy's virtual-network/virtual-DNS support as +prior art before Phase 1 decides whether the crate ships a +translation-table helper or leaves it entirely to assembly-layer +policy. + ### OQ-SK-06: noq client support (and the quinn/noq shape split) **Scope clarification (2026-09-12):** the SOCKS client wrapper is @@ -816,14 +841,36 @@ promising approaches"): POC — re-validating it here is optional.) **Dropped from this crate (2026-09-14, the scope decision):** the client wrapper transfers to the alknet rewrite (§Vision) — OQ-SK-06 - and this POC go with it. This crate's POC list is complete - (#1, #2, #4 all passed). + and this POC go with it. (At the time of the decision the list was + #1, #2, #4 — all passed; #5 (BIND) was added later with OQ-SK-08.) 4. **fast-socks5 wasm check** — minimal crate, `cargo check --target wasm32-unknown-unknown`, confirm/inflect OQ-SK-04. Cheap; can fold into #1's worktree. **Run 2026-09-13 — fork-minimality verified** (~120 lines of `#[cfg]` insertions; wasm-clean `--no-default- features` build + functional RFC round-trip test pass; findings in `poc-connect-wasm-findings.md`). +5. **BIND POC** — the RFC 1928 BIND command channels-native + (OQ-SK-08): the producer drives the typestate directly + (`read_command()` → match `TCPBind` — fast-socks5 parses but + refuses the command, `lib.rs:384-386`/`server.rs:813-816`; the + wrapper owns the whole BIND shape, no upstream ask), the phase + model extends naturally (request → reply#1 with `BND.ADDR` → + quiet wait → one inbound accept → reply#2 with the peer address → + raw pass-through data plane, zero per-packet headers in-band; + iroh-socks5's channels-native BIND is the convergent precedent, + `docs/research/iroh-socks5-eval.md` §BIND). Sketch **both accept + variants** behind the accept callback (OQ-SK-08): a feature-gated + real `TcpListener` backend (faithful RFC — a real network-B + address in reply#1, vanilla front-door clients work, FTP active + mode as the canonical test) and the composed alktunnels + listen-tunnel variant (`register_tunnel_listen_openable` + + `listen_establisher` + `AcceptQueue` — per-listener ACL for free, + hub-relayable). Plus a wrapper-aware in-band test and a vanilla + front-door BIND test ("app server" dials the bound address after + reply#1). Validates: the accept-side data plane (the mirror of the + dial callback), reply#1/#2 sequencing on one channel, and the + alktunnels listen-addr ask's shape. Completes the RFC 1928 command + surface (all three commands) in the crate. POC placement conventions (inherited from alktunnels): a POC that needs code from this repo runs in a worktree/branch (`.worktrees/research/ @@ -832,6 +879,134 @@ standalone crate in the global workspace with findings written into `docs/research/` here. Findings always land in `docs/research/` regardless of where the code lives. +### OQ-SK-08: BIND — the accept-side command (use case found) + +**Status: new (2026-09-14, post-POC #2).** Both POCs refused BIND as +RFC-correct; the findings review surfaced a real use case and a +mechanism, so "out of scope until a use case exists" has been met. + +**The gap, precisely:** fast-socks5 parses `TCPBind` +(`Socks5Command::TCPBind`, `lib.rs:91`) but every handler refuses it +(`lib.rs:384-386`, `server.rs:813-816` — `CommandNotSupported`); no +bind machinery exists. The typestate flow (`read_command()` returning +`cmd` + `TargetAddr`) means the wrapper drives the command dispatch +itself — implementing BIND is wrapper-side code, **zero upstream +ask** (unlike UDP ASSOCIATE's `run_udp_proxy_custom` bind, which sits +inside an upstream handler). + +**The shape (convergent precedent: iroh-socks5's channels-native +BIND, `docs/research/iroh-socks5-eval.md` §BIND):** one channel per +BIND session, phase model — request → reply#1 carrying `BND.ADDR` → +quiet wait (any control byte after reply#1 is protocol garbage, same +as ASSOCIATE's post-reply stage) → exactly one inbound accept → +reply#2 carrying the peer address → raw pass-through data plane (zero +per-packet headers in-band, same as CONNECT; only the ASSOCIATE +datagram stage is framed). RFC-refusal of BIND remains valid for old +consumers; support is purely additive (same ALPN, same params, no +framing change — no one-way-door exposure). + +**The mechanism — accept-side resources (the `-R` far-side listener +shape).** The consumer's `-R` path in alktunnels +(`register_tunnel_listen_openable` + `listen_establisher` + +`AcceptQueue`, `alktunnels/src/producer.rs:207,392`) proves accept-side +resources exist without forced binds; SOCKS5 BIND is their mirror +image: the producer-side listener (network B — the app server must +reach it) is the accept resource, each popped accept two-pumps against +the SOCKS5 `BiStream`. The phase-0 hand-off note ("a producer-side +BIND wants the accept-side shape, not a dial") predicted exactly this; +the composition supplies the use case it lacked. + +**Accept variants (mirror of OQ-SK-03's egress variants) — the one +real design question is address-first vs accept-first:** RFC BIND is +address-first (bind → advertise in reply#1 → client tells the app +server → it connects → accept → reply#2), while the listen establisher +is accept-first (opening the channel pops an already-accepted +connection — you cannot open before reply#1 to learn the address). +Three resolutions: + +1. **Local backend binds for real** (feature-gated `local`) — the + faithful RFC shape, iroh-socks5-faithful. A real network-B address + goes in reply#1, vanilla front-door clients work; FTP active mode + is the canonical use and the reason vanilla client libraries speak + BIND at all. No upstream ask. +2. **Composed + small alktunnels ask** — the listener lives on a + listen resource (per-listener ACL for free, hub-relayable per the + terminate-and-re-produce story); needs "learn the bound address + without consuming an accept" — the plan carries `local_addr` + + an inspect-only open, or a `listen/addr` query op. Small ask; we + own alktunnels (the same finding-first pattern that landed the + E-01/E-02 sweep in alkcall 0.5.0). +3. **Wrapper-aware only** — zeros in reply#1, reply#2 in-band; + simplest, but vanilla clients lose BIND and the use case mostly + evaporates. Retained for completeness; weak hunch against. + +Variant 1 is the POC #5 hunch (faithful + zero ask), with variant 2 +sketched behind the same accept callback — the composed variant's +value (hub story, per-listener ACL) is the reason to measure it, and +its blocker is the address-inspection ask. + +**Security boundary note:** BIND is an *inbound* listener — the +accept-side twin of arbitrary egress. Whatever ACL governs the socks5 +resource now also governs who can mint listeners on the producer's +network; per-listener policy (address/port ranges allowed to bind) is +the accept-callback's target-policy twin (OQ-SK-05's mirror). The +open-op gate (`SOCKS5_OPEN_SCOPE`) must distinguish CONNECT/ASSOCIATE +from BIND, or any identity with proxy egress can also open ingress +listeners. + +### OQ-SK-09: Channel capacity for proxy workloads (the 256 default) + +**Status: new (2026-09-14, post-POC #2).** The 256-channel default +cap (per-connection ADR-040; per-identity ADR-041) is tight for a +proxy workload: every proxied flow *is* a channel (a browser or +tun2proxy fan-out opens one CONNECT channel per flow; composed UDP +egress (OQ-SK-03) spends a downstream channel per destination; BIND +(OQ-SK-08) will spend an accept resource per listener). The caps are +policy knobs — configurable without a wire change, the `u32` wire +space is ~4 billion — so the resolution is config, not wire: + +- **No wire change.** Rejected permanently as unnecessary: the u32 + channel-ID space is not the constraint; the caps are policy. +- **Assembly-layer config** is the mechanism (ADR-040/041 are + explicitly "policy knobs, configurable without a wire change"). + A proxy-shaped deployment raises the per-connection/per-identity + caps; the default stays conservative for everyone else. +- **Documented proxy-workload default** for Phase 1: the crate docs + should recommend a raised cap (and its memory arithmetic: a session + is bounded + predictable — CONNECT/ASSOCIATE are bounded-buffer + pumps, BIND adds one listener + one accept) so deployments don't + discover the cap in production. +- **Defense-in-depth posture:** the cap's rationales both still apply + under SOCKS5. Per-connection memory bound — arguably *weaker* here, + since the three command shapes are well-known and bounded + (a session's memory is predictable); per-identity DoS brake — + arguably *stronger* here, since each SOCKS5 session maps to real + producer-side resources (a dial, a downstream channel, an accept + listener) and the cap is the brake on one identity opening + thousands. Layered defense: ACL → per-connection cap → per-identity + cap → dial/accept policy; relaxing the caps leans on the rest. + +**Considered and rejected: in-channel port multiplexing.** A related +proposal (2026-09-14 review) would reframe the cap by multiplexing +"ports" inside one channel — a `[port: u16][len][payload]` per-chunk +prefix (channel-id-as-IP, port-as-port; modeled on alktty's wire). +Rejected on the established grounds: it re-opens the demux-vs-phase +trade OQ-SK-03 already resolved (phase model, no in-band control +vocabulary needed across both POCs), it breaks the 0-B pass-through +that lets fast-socks5's raw `transfer` drop in unchanged (POC #1's +validated fit), it imposes chunk boundaries on a stream RFC 1928 +treats as unstructured, and it violates the one-channel-per-SOCKS5- +session convention (AGENTS.md #10). Channel-ID economy is not a wire +problem the u32 space + policy knobs fail at. **Revisit trigger:** if +a deployment hits a fixed upstream channel budget that config cannot +raise, the additive escape is a *new multiplexed ALPN* — never a +change to this ALPN's framing. + +**The related idea that survives — virtual address space / +identity-scoped subnets** — is recorded under OQ-SK-05 (added +2026-09-14): BND.ADDR/DST.ADDR translation via the dial callback, +no wire change. + ## Survey / prior-art list Candidate reading for the research specialist (to be expanded): @@ -923,7 +1098,17 @@ Candidate reading for the research specialist (to be expanded): functionally complete); the root question ("does wasm make sense for alksocks?") now reduces to the adapter-story input - [ ] OQ-SK-05 (target policy) — folded into the OQ-SK-01 decision; - scope-gate convention pinned in Phase 1 + scope-gate convention pinned in Phase 1; the 2026-09-14 + virtual-subnet proposal (dial-callback translation, no wire + change) recorded for Phase 1 +- [ ] OQ-SK-08 (BIND) — new (2026-09-14): the gap + use case + + accept-side mechanism written; resolve via POC #5 + the Phase 1 + ADR (ship in base crate? default accept variant?) +- [ ] OQ-SK-09 (channel capacity) — new (2026-09-14): the proxy- + workload analysis written (wire change rejected; config is the + mechanism; in-channel port multiplexing considered and + rejected); Phase 1 pins the documented proxy-workload + recommendation - [x] OQ-SK-06 (noq client) — **resolved by the scope decision (2026-09-14, §Vision): transferred to the alknet rewrite** (the client wrapper is alknet's ancestor-shaped component, @@ -931,9 +1116,10 @@ Candidate reading for the research specialist (to be expanded): research questions move to alknet's Phase 0; this crate's channels-native client story is POC-complete without it - [x] Targeted POC(s) run + summaries in `docs/research/` - (all three complete: #1 + #4 in `poc-connect-wasm-findings.md`, + (three complete: #1 + #4 in `poc-connect-wasm-findings.md`, #2 in `poc-udp-associate-findings.md`; #3 dropped with the - scope decision) + scope decision; **#5 (BIND) added 2026-09-14 with OQ-SK-08 — + pending**) - [x] Converge: recommended approach written up, ready to hand to the Architect for Phase 1 (§Convergence, 2026-09-14) @@ -991,9 +1177,19 @@ binds except the two explicit, optional, feature-gated assembly shapes the demux-vs-phase extensibility trade (evidence now favors the phase model — no in-band control vocabulary was needed across both POCs); the params-format/ALPN naming ADRs (wire-stable once - published, no consumers exist yet); `SOCKS5_OPEN_SCOPE` convention; - the door's credential→identity mapping shape; the wasm decision - (Case 1 vs Case 2) once the adapter story is confirmed. + published, no consumers exist yet); `SOCKS5_OPEN_SCOPE` convention + (must distinguish BIND from CONNECT/ASSOCIATE — OQ-SK-08's security + note); the door's credential→identity mapping shape; the wasm + decision (Case 1 vs Case 2) once the adapter story is confirmed; + BIND's base-crate inclusion + default accept variant (OQ-SK-08); + the proxy-workload channel-cap recommendation (OQ-SK-09); the + virtual-subnet translation-layer shape (OQ-SK-05 addendum). +- **Upstream asks (we own both upstreams; file early, land there):** + fast-socks5 — the `net` feature gate (POC #4's ~120-line change) + and the `run_udp_proxy_custom` unconditional-bind seam. alktunnels — + listen-addr inspection without consuming an accept (OQ-SK-08 + variant 2's blocker). First-real-consumer asks, the alkcall + E-01/E-02 precedent. ### What Phase 0 could NOT answer (handed to Phase 1) @@ -1004,7 +1200,17 @@ binds except the two explicit, optional, feature-gated assembly shapes adapter-story input is a product decision, not a research one. - Target-policy shape (OQ-SK-05) — the dialer-refuses shape is evidence-backed; the declarative-allowlist variant stays a Phase 1 - choice. -- BIND — RFC-refused in both POCs, out of scope until a use case - exists (the no-bind principle makes it structurally odd here; a - producer-side BIND wants the accept-side shape, not a dial). \ No newline at end of file + choice. (The 2026-09-14 virtual-subnet proposal is recorded there — + dial-callback translation, no wire change.) +- Channel capacity for proxy workloads (OQ-SK-09) — wire change + rejected; assembly-layer config + a documented proxy-workload cap + recommendation stay Phase 1 decisions. In-channel port + multiplexing considered and rejected there. +- BIND — RFC-refused in both POCs; **a use case and a mechanism were + found in the 2026-09-14 findings review** — OQ-SK-08 records the + gap, the accept-side-resource shape (mirroring alktunnels' `-R` + far-side listener), the address-first-vs-accept-first design + question, and POC #5. Deciding whether BIND ships in the base + crate (and which accept variant is default) is a Phase 1 ADR — + support is additive (old consumers still get the RFC refusal), so + nothing here is a one-way door. \ No newline at end of file