docs: BIND use case found (OQ-SK-08, POC #5) + capacity OQ-SK-09

Post-POC #2 findings review: a real BIND use case and mechanism
surfaced, plus a channel-capacity analysis for proxy workloads.

- OQ-SK-08: fast-socks5 parses but refuses TCPBind — the wrapper owns
  the whole shape (zero upstream ask). Phase-model extension: reply#1
  (BND.ADDR) -> quiet wait -> one inbound accept -> reply#2 (peer) ->
  raw pass-through; iroh-socks5's channels-native BIND is convergent
  precedent. Accept variants mirror OQ-SK-03's egress variants:
  feature-gated local listener (faithful RFC) vs composed alktunnels
  listen-tunnel (needs a listen-addr-inspection ask) vs wrapper-aware
  only. Security note: BIND is inbound egress — SOCKS5_OPEN_SCOPE must
  distinguish it from CONNECT/ASSOCIATE.
- OQ-SK-09: 256-channel default vs proxy fan-out; wire change
  rejected, assembly-layer config + documented proxy-workload
  recommendation is the mechanism; in-channel port multiplexing
  ([port:u16] per chunk) considered and rejected (re-opens OQ-SK-03's
  demux-vs-phase trade, breaks 0-B pass-through, AGENTS.md #10).
- OQ-SK-05 addendum: virtual address space / identity-scoped subnets
  adoptable with no wire change — DST.ADDR/BND.ADDR translation via
  the dial callback; hub exposure via terminate-and-re-produce.
- POC #5 (BIND, both accept variants, vanilla front-door test) added
  to OQ-SK-07; checklist + convergence updated; upstream-asks list
  pinned (fast-socks5: net gate + UDP bind seam; alktunnels:
  listen-addr inspection).

Verification: docs-only change (phase-0.md).
This commit is contained in:
2026-09-15 03:28:15 +00:00
parent 6c71c0e5d2
commit 601afa3922
+219 -13
View File
@@ -1,6 +1,7 @@
--- ---
status: draft 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) # 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 wasm producers may not have a resolver at all — another OQ-SK-04
interaction.) 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 12 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) ### OQ-SK-06: noq client support (and the quinn/noq shape split)
**Scope clarification (2026-09-12):** the SOCKS client wrapper is **Scope clarification (2026-09-12):** the SOCKS client wrapper is
@@ -816,14 +841,36 @@ promising approaches"):
POC — re-validating it here is optional.) POC — re-validating it here is optional.)
**Dropped from this crate (2026-09-14, the scope decision):** the **Dropped from this crate (2026-09-14, the scope decision):** the
client wrapper transfers to the alknet rewrite (§Vision) — OQ-SK-06 client wrapper transfers to the alknet rewrite (§Vision) — OQ-SK-06
and this POC go with it. This crate's POC list is complete and this POC go with it. (At the time of the decision the list was
(#1, #2, #4 all passed). #1, #2, #4 all passed; #5 (BIND) was added later with OQ-SK-08.)
4. **fast-socks5 wasm check** — minimal crate, `cargo check --target 4. **fast-socks5 wasm check** — minimal crate, `cargo check --target
wasm32-unknown-unknown`, confirm/inflect OQ-SK-04. Cheap; can fold wasm32-unknown-unknown`, confirm/inflect OQ-SK-04. Cheap; can fold
into #1's worktree. **Run 2026-09-13 — fork-minimality verified** into #1's worktree. **Run 2026-09-13 — fork-minimality verified**
(~120 lines of `#[cfg]` insertions; wasm-clean `--no-default- (~120 lines of `#[cfg]` insertions; wasm-clean `--no-default-
features` build + functional RFC round-trip test pass; findings in features` build + functional RFC round-trip test pass; findings in
`poc-connect-wasm-findings.md`). `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 POC placement conventions (inherited from alktunnels): a POC that needs
code from this repo runs in a worktree/branch (`.worktrees/research/ 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/` `docs/research/` here. Findings always land in `docs/research/`
regardless of where the code lives. 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 ## Survey / prior-art list
Candidate reading for the research specialist (to be expanded): 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 functionally complete); the root question ("does wasm make
sense for alksocks?") now reduces to the adapter-story input sense for alksocks?") now reduces to the adapter-story input
- [ ] OQ-SK-05 (target policy) — folded into the OQ-SK-01 decision; - [ ] 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 - [x] OQ-SK-06 (noq client) — **resolved by the scope decision
(2026-09-14, §Vision): transferred to the alknet rewrite** (2026-09-14, §Vision): transferred to the alknet rewrite**
(the client wrapper is alknet's ancestor-shaped component, (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 research questions move to alknet's Phase 0; this crate's
channels-native client story is POC-complete without it channels-native client story is POC-complete without it
- [x] Targeted POC(s) run + summaries in `docs/research/` - [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 #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 - [x] Converge: recommended approach written up, ready to hand to the
Architect for Phase 1 (§Convergence, 2026-09-14) 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 the demux-vs-phase extensibility trade (evidence now favors the
phase model — no in-band control vocabulary was needed across both phase model — no in-band control vocabulary was needed across both
POCs); the params-format/ALPN naming ADRs (wire-stable once POCs); the params-format/ALPN naming ADRs (wire-stable once
published, no consumers exist yet); `SOCKS5_OPEN_SCOPE` convention; published, no consumers exist yet); `SOCKS5_OPEN_SCOPE` convention
the door's credential→identity mapping shape; the wasm decision (must distinguish BIND from CONNECT/ASSOCIATE — OQ-SK-08's security
(Case 1 vs Case 2) once the adapter story is confirmed. 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) ### 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. adapter-story input is a product decision, not a research one.
- Target-policy shape (OQ-SK-05) — the dialer-refuses shape is - Target-policy shape (OQ-SK-05) — the dialer-refuses shape is
evidence-backed; the declarative-allowlist variant stays a Phase 1 evidence-backed; the declarative-allowlist variant stays a Phase 1
choice. choice. (The 2026-09-14 virtual-subnet proposal is recorded there —
- BIND — RFC-refused in both POCs, out of scope until a use case dial-callback translation, no wire change.)
exists (the no-bind principle makes it structurally odd here; a - Channel capacity for proxy workloads (OQ-SK-09) — wire change
producer-side BIND wants the accept-side shape, not a dial). 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.