diff --git a/docs/research/phase-0.md b/docs/research/phase-0.md index 6c52c6d..a2521a4 100644 --- a/docs/research/phase-0.md +++ b/docs/research/phase-0.md @@ -1,17 +1,17 @@ --- status: draft -last_updated: 2026-09-20 (fast-socks5 posture corrected + resolved: we -do NOT own it — inline vendor decided; the graduation landed upstream: -alkcall 0.8.0, alktunnels 0.2.0 superseding the BIND ask ledger's shape) +last_updated: 2026-09-20 (consolidation rewrite: current-state form, one +canonical statement per decision, reasoning trails trimmed to their +Phase 1 weight; no decisions changed) --- # alksocks — Phase 0 (Exploration) This document captures Phase 0 (Exploration) for the `alksocks` crate: vision, guiding principles, prior art, open questions (OQ-SK-01..NN), and -POC candidates. Phase 0's objective per `docs/sdd_process.md`: *capture -vision and guiding principles; research options; validate approaches; -converge on a recommended approach.* It is the input to Phase 1 +the converged recommendation. Phase 0's objective per `docs/sdd_process.md`: +*capture vision and guiding principles; research options; validate +approaches; converge on a recommended approach.* It is the input to Phase 1 (Architecture), where the Architect will produce `docs/architecture/` specs, ADRs, and the open-questions tracker. @@ -21,16 +21,23 @@ alkcall substrate; where alktunnels realizes the `-L`/`-R` forwarding flavors, alksocks realizes the `-D` flavor (dynamic / SOCKS5 proxy) — the one alktunnels explicitly deferred to composition. +Phase 0 converged 2026-09-14 and is POC-complete: every numbered POC on +the register either passed or was dropped with the scope decision +(§OQ-SK-07). The remaining open questions are Phase 1 ADR decisions, +each with its inputs collected (§What Phase 0 could NOT answer). + ## Vision and guiding principles **One sentence:** a SOCKS5 (RFC 1928) producer/consumer protocol crate on alkcall channels — an arbitrary-egress proxy service that never binds a -port unless explicitly configured to, wrapping `fast-socks5` so the RFC -conversation rides inside a channels data channel like any other produced -resource. +port unless explicitly configured to, carrying the RFC conversation inside +a channels data channel like any other produced resource. The RFC 1928 +implementation is the vendored fast-socks5 subset (the inline vendor, +§Layering) — the crate owns the protocol code in-tree. -**The three components (2026-09-12 clarification — they serve different -purposes and were at risk of being conflated):** +**Scope (resolved 2026-09-14, post-POC): socks + channels, nothing +else.** The original three-component split serves different purposes and +was at risk of being conflated; two of the three are this crate: 1. **Producer half** — a side exposes the SOCKS5 service as a produced, ACL-scoped resource (registers openable channels; the open handler @@ -41,34 +48,21 @@ purposes and were at risk of being conflated):** the service locally: bind a real `socks5://127.0.0.1:port` endpoint and relay each accepted connection into a channel (the ssh `-D` local-exposure shape, and the path tun2proxy/curl/browser point at). -3. **SOCKS client wrapper for QUIC runtimes** — `AsyncUdpSocket` - implementations (quinn and noq) so downstream clients (alknet) can - route QUIC through *any* third-party SOCKS5 server — not just one - produced by this crate. The motivating case is iroh's privacy - posture: iroh relays see the real IP (intended for relay-assisted - p2p), and a client that doesn't want to leak it needs a SOCKS hop - ahead of the QUIC dial. This component is a plain client library - (the alknet ADR-090 descendant); it neither produces nor consumes - channels. + +3. ~~**SOCKS client wrapper for QUIC runtimes** — `AsyncUdpSocket` + implementations (quinn and noq) so downstream clients can route QUIC + through any third-party SOCKS5 server (the iroh privacy posture: a + client that doesn't want to leak its real IP to relays needs a SOCKS + hop ahead of the QUIC dial).~~ **Moved to the alknet rewrite** — it is + alknet's own ancestor (ADR-090 + the quinn-proxy POC live there), it + neither produces nor consumes channels, and alknet is where downstream + clients will actually consume it. The transfer took OQ-SK-06 and POC + #3 with it. This crate's client-side story is the channels-native + consumer session (POC #1) plus the wrapper-aware associate session + (POC #2) and the optional front doors — both POC-validated. Components 1+2 form the producer/consumer protocol pair (channels in, RFC 1928 inside). Keep them structurally separate in the crate layout. -(Component 3's standalone-client posture was the reason it transferred -cleanly to alknet — see the scope resolution below.) - -**Scope resolution (2026-09-14, post-POC): component 3 moves to the -alknet rewrite.** The QUIC-runtime client wrapper's natural home is -alknet — it is alknet's own ancestor (ADR-090 + the quinn-proxy POC -live there), it neither produces nor consumes channels, and the -planned alknet rewrite is where downstream clients (iroh privacy -posture) will actually consume it. That pins alksocks' scope to the -producer/consumer pair only: **socks + channels.** Consequence: the -OQ-SK-06 noq research (and its would-be POC #3) transfers to alknet's -Phase 0; the quinn 0.11 variant is already proven there -(quinn-proxy POC). This crate's client-side story reduces to the -channels-native consumer session (`Socks5Stream::use_stream` over a -`BiStream` — POC #1) plus the wrapper-aware associate session (POC -#2) and the optional front doors. Guiding principles, inherited from the alk* family: @@ -87,27 +81,26 @@ Guiding principles, inherited from the alk* family: producing side, governed by the same op-level ACL as the resource. This crate is that conclusion's realization as a first-class protocol crate, not an assembly-layer afterthought. -3. **Wrap `fast-socks5`, preserve its genericity.** `fast-socks5`'s - explicit typestate server API (`Socks5ServerProtocol`) - is generic over `T: AsyncRead + AsyncWrite + Unpin`, and its - interception points (`run_tcp_proxy`, `run_udp_proxy_custom`, - `transfer`) accept any such `T`. The channels adapter feeds the state - machine a `BiStream`; a local backend feeds it a `TcpStream`. The - wrapper must not leak either substrate into the protocol layer - (alktty `TtyBackend` / alktunnels pump-halves inversion-point - precedent). +3. **The protocol layer is substrate-agnostic and generic over the + stream.** The vendored fast-socks5 typestate API + (`Socks5ServerProtocol`) is generic over + `T: AsyncRead + AsyncWrite + Unpin`, and its interception points + (`run_tcp_proxy`, `run_udp_proxy_custom`, `transfer`) accept any such + `T`. The channels adapter feeds the state machine a `BiStream`; a + local backend feeds it a `TcpStream`. The wrapper must not leak + either substrate into the protocol layer (alktty `TtyBackend` / + alktunnels pump-halves inversion-point precedent). 4. **Producer/consumer vocabulary.** Both sides of a channels connection can initiate; a producer registers openable SOCKS5 channels (`ChannelCore::register_openable`), a consumer opens them and speaks RFC 1928 inside. RFC 1928's own client/server roles keep their RFC names *inside* the protocol layer, but crate-level docs and API use producer/consumer. Avoid "SOCKS5 server" as a crate-level noun. -5. **Extract-and-improve from alknet.** alknet's SOCKS5 story is the - direct ancestor: the client side (`alknet-client/src/socks5.rs`, - ADR-090 — `Socks5UdpSocket` implements `quinn::AsyncUdpSocket` so - QUIC rides UDP ASSOCIATE, validated by the quinn-proxy POC) and the - server side (the `-D` capability alktunnels deferred). This crate - rehomes both halves behind one protocol crate and improves them. +5. **Extract-and-improve from alknet (producer half only).** The server + side is the `-D` capability alktunnels deferred; the client side + (alknet ADR-090's `Socks5UdpSocket`, the quinn-proxy POC) stays in + alknet per the scope resolution above. The crates share conclusions, + not code paths. 6. **The vpn-like endgame composition.** A SOCKS5 server + tun2proxy is the last step (ish) of "vpn-like without actually being a vpn": a user tunnels a produced SOCKS5 resource to a local port (component @@ -142,14 +135,12 @@ not start from zero. It inherits: handler_error / timeout`), never a phantom channel. The negotiate/ auth half of the RFC conversation still runs in-stream over the `BiStream` — establishment covers only the producer's accept policy, - not the RFC handshake. **Updated 2026-09-18 (alkcall 0.8.0, ADR-049 - amendment 3): the establisher can project reply fields** — - `Establishment::with_reply_field(s)` merged into the open reply, - consumable via `ChannelClient::open_channel_with_reply`. This is the - carrier alkcall review 008 built for alktunnels' bind-first `bound` - address; a SOCKS5 establisher that ever needs to return open-time - metadata has the same seam (additive; absent fields leave the reply - byte-identical). + not the RFC handshake. The establisher can project reply fields + (alkcall 0.8.0, `Establishment::with_reply_field(s)`, ADR-049 + amendment 3; consumable via `ChannelClient::open_channel_with_reply`) + — the carrier alktunnels' bind-first `bound` address rides; a SOCKS5 + establisher that ever needs to return open-time metadata has the same + seam (additive; absent fields leave the reply byte-identical). - **The producer/consumer model** — producer registers openable channels (authorization for free via `AccessControl`); consumer opens them via `ChannelClient` (alkcall ADR-037, ADR-043). Connection direction is @@ -158,28 +149,37 @@ not start from zero. It inherits: relays like any produced resource; the hub's terminate-and-re-produce proxy applies per hop, and a SOCKS5 resource is exactly the "further downstream" shape alktunnels described. No socks5-specific relay - work. **Updated 2026-09-18: the relay machinery is in-tree** — - alkcall 0.8.0 ships `ChannelRelay` + `HubLegTemplate` (ADR-051, the - review-008 bonus), so a hub leg is an install-template composition, - not assembly-layer hand-rolling; relayed replies carry extra fields - through per-hop-truthfully (the bind-first `bound` flows - end-to-end), and flavor-form ops (the direct op's - `channels/tunnel/direct` shape) re-produce as channels, not + work. The relay machinery is in-tree — alkcall 0.8.0 ships + `ChannelRelay` + `HubLegTemplate` (ADR-051), so a hub leg is an + install-template composition, not assembly-layer hand-rolling; + relayed replies carry extra fields through per-hop-truthfully (the + bind-first `bound` flows end-to-end), and flavor-form ops (the direct + op's `channels/tunnel/direct` shape) re-produce as channels, not forwarding stubs. - **The identity seam** — alkcall's CF-005/CF-006 (0.7.0): the per-call opener identity arrives on the open-op hooks. Auth mapping (identity → SOCKS5 credentials/ACL, if any) happens there, not via in-band SOCKS auth, unless RFC-facing username/password is needed for vanilla SOCKS5 - clients (OQ-SK-02). Still current at alkcall 0.8.0 — the 0.8.0 - additions (reply projection, relay) are additive and do not touch it. + clients (OQ-SK-02 — settled posture: split by the door). Still current + at alkcall 0.8.0 — the 0.8.0 additions (reply projection, relay) are + additive and do not touch it. - **The backend inversion point pattern** — substrate-specific types confined to feature-gated backend modules, injected at the assembly layer (alktty `TtyBackend` / alktunnels no-trait precedent). - **The wasm-clean default crate** — protocol-only code should compile - to `wasm32-unknown-unknown` (alktty/alktunnels precedent). Caveat - resolved: `fast-socks5` itself is not wasm-clean as published - (OQ-SK-04) — the inline vendor (resolved 2026-09-20) carries the - net-gate so the default-crate invariant holds. + to `wasm32-unknown-unknown` (alktty/alktunnels precedent). The + vendored fast-socks5 subset is wasm-clean: the inline vendor gates + kernel-net behind a default-on `net`-shaped feature (OQ-SK-04 + resolved; POC #4 verified the subset is functionally complete). +- **The upstream substrate this crate composes against** — alkcall + 0.8.0 (reply projection; `ChannelRelay`/`HubLegTemplate` per ADR-051; + flavor-form open-op ids in discovery per ADR-047 amendment 3) and + alktunnels 0.2.0 (the direct op `channels/tunnel/direct`, ADR-007; + the bind-first establisher + forwarded op `channels/tunnel/forwarded`, + ADR-008; `TunnelListener`). The graduation + (`alktunnels docs/research/tunnels-graduation.md`) took this crate's + asks as evidence and landed them in one version bump — see the + per-OQ notes (OQ-SK-01, OQ-SK-03, OQ-SK-08) for what changed in shape. ## Prior art @@ -214,14 +214,18 @@ load-bearing conclusions this crate inherits: is a byte stream end to end (RFC 1928 defines its own framing on the wire) — the channel payload is pure pass-through, 0 B overhead over the channels 8-byte header. UDP ASSOCIATE's datagram stage is the - exception (OQ-SK-03). + exception (OQ-SK-03, resolved shape below). -### fast-socks5 — the implementation to wrap (inline-vendor decision) +### fast-socks5 — the vendored implementation -`/workspace/fast-socks5` (v1.0.0, MIT — **third-party upstream, not -ours; posture corrected 2026-09-20, OQ-SK-04 resolved to the inline -vendor — see the OQ**). Read the source before designing against it. -Key surface points, verified: +`/workspace/fast-socks5` (v1.0.0, MIT — third-party upstream, not ours; +read-only reference checkout). **The posture is the inline vendor +(OQ-SK-04, resolved 2026-09-20): vendor the `T`-generic subset into this +crate behind a default-on `net`-shaped feature, carrying the wrapper's +needs in-tree.** `/workspace/fast-socks5` remains the differential-testing +reference for RFC edge cases (reply-code mapping, domain addressing, +fragmentation); note the vendor point (v1.0.0) in the vendored code. Key +surface points, verified: - **Explicit typestate server API** (`src/server.rs`): `Socks5ServerProtocol`, @@ -280,6 +284,15 @@ Key surface points, verified: proxy, refuse, or handle in-process. Structurally the shape of the channels open handler (inspect params → dial or refuse) and of the alknet ADR-090 client (hand the established stream to quinn). +- **What the vendor must add (the four carried needs):** the + kernel-net feature gate (POC #4: ~120 cfg lines, wasm-clean subset + functionally complete); **BIND** (absent upstream — fast-socks5 + parses `Socks5Command::TCPBind` but every handler refuses it, + `lib.rs:384-386` / `server.rs:813-816`; the wrapper drives the + typestate itself); the `run_udp_proxy_custom` bind seam (above); and + the publication of `new_reply` (the reply encoder is private; the + BIND reply#2 writer otherwise hand-rolls the 10-byte reply — + `poc-bind-findings.md`). ### iroh-socks5 — the structural sibling (evaluated, not the base) @@ -301,8 +314,8 @@ Full evaluation: `docs/research/iroh-socks5-eval.md`. Verified: on the tunnel wire; the RFC header codec applies only at the local front-door socket), needs no producer-side flow table (per-datagram addressing in the frame does the demux), ends the association on - control-stream EOF, and refuses FRAG ≠ 0. This is the phase-model - hunch's cleaner expression; adopt as the POC #2 design baseline. + control-stream EOF, and refuses FRAG ≠ 0. Adopted as the POC #2 + design baseline; POC #2 validated it. - **Why it is not the base:** iroh types are load-bearing in the data plane (`SendStream`/`RecvStream` in relay signatures; no generic `T` seam), the RFC surface is a subset (no-auth only, no auth-method @@ -324,20 +337,20 @@ there, likely inherited: ECN is lost through the SOCKS5 header; the proxy must support UDP ASSOCIATE; `may_fragment() == true` disables path MTU discovery. -Historical note: this section originally framed the client work as -"this crate's client half" — the 2026-09-14 scope decision (§Vision) -moved it to the alknet rewrite, where the same cleanup applies -(`ClientDialError` → thiserror types; the datagram codec sourced from -fast-socks5 rather than hand-rolled inline). The open question that -remained here was noq (OQ-SK-06, now also transferred). +**This work belongs to the alknet rewrite** (the 2026-09-14 scope +decision, §Vision): the client wrapper is alknet's ancestor-shaped +component — it neither produces nor consumes channels, and its open +research (the noq trait-shape split, OQ-SK-06, §OQ-SK-06) moves to +alknet's Phase 0. The alknet-side cleanup that applies there: the +hand-rolled codec in `socks5.rs` inlines the greeting/auth/CONNECT/ +ASSOCIATE byte logic twice — fast-socks5's client types and +`new_udp_header`/`parse_udp_request` supersede it. ### noq — the quinn fork the client story must also fit > **Transferred to the alknet rewrite (2026-09-14, the scope decision, -> §Vision).** The client wrapper is component 3 of the original -> three-component split; it moves to alknet wholesale, so this section -> is retained as background research for *alknet's* Phase 0, not as -> live scope here. See OQ-SK-06 for the transfer rationale. +> §Vision).** Retained as background research for *alknet's* Phase 0; +> see OQ-SK-06 for the transfer rationale. `/workspace/noq` (v1.2.0; iroh's fork of quinn — iroh depends on `noq = "1.2.0"`). Surface verified: @@ -360,9 +373,7 @@ remained here was noq (OQ-SK-06, now also transferred). `Arc`→`Box` change ripples into construction. Whether one crate can serve both quinn and noq behind feature flags (shared core, two thin trait-impl shells) or whether the shapes have diverged enough to - justify two impls is the research question (OQ-SK-06 — transferred - to the alknet rewrite; iroh's own adoption keeps noq support - practically important wherever the client wrapper lands). + justify two impls is the research question (OQ-SK-06, transferred). ### Anti-prior-art (what NOT to carry over) @@ -370,209 +381,142 @@ remained here was noq (OQ-SK-06, now also transferred). `socks5.rs` inlines the greeting/auth/CONNECT/ASSOCIATE byte logic twice (handshake for UDP, again for CONNECT). fast-socks5's client types and `new_udp_header`/`parse_udp_request` supersede it; the - wrapper should not vendor a second codec. + vendored crate provides the codecs — no second codec. - **`Socks5Server` (the legacy bind-based API)** — deprecated upstream; never the default path here. - **In-band SOCKS auth as the primary auth story** — alkcall's identity seam (CF-005/CF-006) authorizes at the open-op layer. In-band RFC username/password remains available only for vanilla-client - compatibility (OQ-SK-02). + compatibility at the front door (OQ-SK-02). ## Open Questions -These are the design questions Phase 0 must resolve (or explicitly -defer) before the architecture spec. Numbered OQ-SK-01.. so they can be -referenced, tracked, and promoted into `docs/architecture/ -open-questions.md` in Phase 1. Half-answers and hunches are marked as -such — the point of this document is to hold them without forcing -premature decisions. +These are the design questions Phase 0 resolved (or explicitly +deferred) before the architecture spec. Numbered OQ-SK-01.. so they can +be referenced, tracked, and promoted into `docs/architecture/ +open-questions.md` in Phase 1. Status is stated per question; what +remains open is named explicitly. The point of this document is to hold +the reasoning without forcing premature decisions. ### OQ-SK-01: Producer dial policy — what dials the target? -When the producer's SOCKS5 state machine reads a CONNECT command, the -target dial must happen *somewhere*. Options: +**Status: shape resolved empirically (POC #1, 2026-09-13); the A/B +assembly-layer choice is a Phase 1 policy decision.** The dial callback +exists and is called in-stream at the command-read interception point: +an injected `DialFn(target) -> boxed halves` closure (function-not-trait, +the alktunnels ADR-004 shape), returning `TargetHandle { read, write }` +(boxed halves, `+ Sync`, satisfies the `ChannelPlan` payload bound). +Per-target refusal is an RFC reply code, not an establishment failure; +session-level refusal stays channels-level (`channel:open_failed`). The +no-trait hunch is evidence-backed; `EgressFactory` (OQ-SK-03) and +`BindAcceptFactory` (OQ-SK-08) extend the same shape to the datagram and +accept planes. -- **Option A: dial via alktunnels** — the producer composes with a - local alktunnels consumer: the SOCKS5 handler opens an `alk/tunnel` - channel naming the target, and the two-pump data plane is - tunnel-channel ↔ SOCKS5-`BiStream`. Maximum composition ("further - downstream" made literal), but adds a runtime dependency and a hop. - **Updated 2026-09-16 (alktunnels 0.2.0, ADR-007): the dynamic-target - channel exists** — the direct op (`channels/tunnel/direct`, - `{substrate, target}` params, the ssh `direct-tcpip` shape, - `tunnel:direct` a separate ACL grant). Option A no longer needs a - pre-registered per-target resource: the SOCKS5 handler's dial - callback opens one direct-op channel per CONNECT target (per the - graduation research, this is exactly what the direct op was - graduated *for*). The udp substrate rides the same op — which also - simplifies OQ-SK-03's composed-egress vehicle below (one op, both - substrates). +What remains open is where the callback *comes from* (the dial policy): + +- **Option A: dial via alktunnels (composition)** — the SOCKS5 handler + opens an `alk/tunnel` channel naming the target, and the two-pump data + plane is tunnel-channel ↔ SOCKS5-`BiStream`. Since alktunnels 0.2.0 + (ADR-007) the dynamic-target channel exists — the direct op + (`channels/tunnel/direct`, `{substrate, target}` params, the ssh + `direct-tcpip` shape, `tunnel:direct` as a separate ACL grant) — so + Option A no longer needs a pre-registered per-target resource: one + direct-op channel per CONNECT target (what the direct op was + graduated *for*). The udp substrate rides the same op (one op, both + substrates — OQ-SK-03's composed egress uses it too). - **Option B: dial directly** — the producer dials the target itself (`TcpStream` behind the `local` feature, or an injected dial callback). No alktunnels dependency; the dial policy (allowlists, routing through another alksocks hop) is the caller's callback. -- **Option C: injected dialer trait** — a `Dialer`-shaped trait - (`async fn dial(target) -> impl AsyncRead + AsyncWrite`) with - alktunnels and local-TCP implementations behind features. Middle - ground; the trait is a backend-inversion-point decision (compare - alktunnels OQ-TN-05's resolution: *no trait*, a function producing - boxed halves sufficed). +- **Option C: injected dialer trait** — a `Dialer`-shaped trait with + alktunnels and local-TCP implementations behind features. The POC + evidence stands against the trait (two real implementations exist as + *callbacks*, not trait impls; alktunnels OQ-TN-05's no-trait + resolution holds). Considerations: substrate-agnostic-by-construction (AGENTS.md convention 7) favors injection over hardwiring; the "hub terminates and re-produces" story means a hub-hop SOCKS5 resource's dialer is just "open a channel further downstream," which is Option A's shape — so -whatever is decided must not *prevent* A when composing. Hunch: a dial -callback/trait at the protocol layer, with alktunnels and local-TCP -providers feature-gated — but alktunnels' no-trait precedent warns -against a trait unless two real implementations converge; decide with -the Phase 1 spec. POC #1 resolved the shape empirically in this -crate's layer (dial callback at the command-read interception point, -boxed halves, no trait); the A/B choice above it is now an -assembly-layer policy question — POC #5's scope-gate finding +whatever is decided must not *prevent* A when composing. The A/B choice +is an assembly-layer policy question; POC #5's scope-gate finding (`socks5:bind` ≠ `socks5:open`) plus ADR-007's own rationale ("dial `postgres-primary`" vs "dial anything" are different grants) both point the same way: separation lives at the ACL, not in the -protocol. +protocol. The dialer-trait-vs-callback half of this OQ is closed; the +Phase 1 ADR picks the default provider set and the params shape for the +composed path. ### OQ-SK-02: Auth mapping — identity seam vs in-band SOCKS auth -The producer's open-op hooks receive the per-call opener identity -(alkcall CF-005/CF-006). The SOCKS5 RFC conversation also carries its -own optional username/password auth (RFC 1929). Questions: - -- Is the identity seam the *only* auth story (the open op is - authorized; the in-band handshake is skipped via - `skip_auth_this_is_not_rfc_compliant` or a no-op NoAuth), with - RFC-facing username/password available only for the optional local - backend (where vanilla SOCKS5 clients connect)? -- If both exist, how do they compose — does in-band auth *replace* the - channel identity for target-policy purposes, or is it a second gate? -- Does the wrapper need a fast-socks5 `AuthMethod` implementation that - consults the alkcall `AuthContext`/`Identity` (filed upstream if the - hook shape doesn't fit — AGENTS.md convention 17)? - -(Answered in direction by the 2026-09-13 posture statement below — the -three answers map to the three bullets above: yes, the identity seam is -the only auth story on the producer side; planes-don't-compare, so the -"replace or second-gate" sub-question dissolves; probably-not, no -`AuthMethod` impl needed — the door checks credentials itself. Phase 1 -formalizes.) - -**Posture — settled direction (2026-09-13 discussion): "both," split -by the door.** The planned end-to-end story is the vpn-like -composition: a consumer connects to an endpoint producing the SOCKS5 -resource, *optionally* binds it locally (component 2's front door — -ssh `-D`'s shape, with the defining difference that no port is bound by -default), and points tun2proxy (`--proxy socks5://user:pass@host:port` -natively, per §Vision principle 6) at the local port. That makes "both" -auth mechanisms real, each on its own side of the door: +**Status: settled (2026-09-13): "both," split by the door.** The +open-op hooks receive the per-call opener identity (alkcall CF-005/ +CF-006); the RFC conversation also carries its own optional +username/password auth (RFC 1929). The settled posture: both mechanisms +are real, each on its own side of the door — the consumer's optional +front door (component 2's ssh `-D` shape, where the vanilla world meets +the ACL world; the vpn-like endgame composition points tun2proxy's +`user:pass` at it). - **Producer side: identity only.** The channels-native open is authorized by the alkcall ACL/identity seam (CF-005/CF-006); a producer-side password is moot — the ACL system is strictly better than a shared secret, and the producer's in-band conversation runs - NoAuth/no-op. A password there would add nothing the identity seam - doesn't already do. + NoAuth/no-op (fast-socks5's `skip_auth_this_is_not_rfc_compliant`, + or a no-op NoAuth method). A password there would add nothing the + identity seam doesn't already do. - **Consumer side (the front door): RFC 1929 where vanilla clients - need it.** The door is where the vanilla world meets the ACL world, - so it owns credential presentation: a vanilla client carrying - `user:pass` (tun2proxy, curl) gets a real RFC 1929 conversation at - the door. The door then maps the accepted credential to *local* - configuration — which channel identity/endpoint to open the producer - channel with — and that identity reaches the producer via the - CF-005/CF-006 seam. The SOCKS password never crosses the channel as - a SOCKS password; the channel identity never reaches the vanilla - client. + need it.** The door owns credential presentation: a vanilla client + carrying `user:pass` (tun2proxy, curl) gets a real RFC 1929 + conversation at the door. The door then maps the accepted credential + to *local* configuration — which channel identity/endpoint to open + the producer channel with — and that identity reaches the producer + via the CF-005/CF-006 seam. The SOCKS password never crosses the + channel as a SOCKS password; the channel identity never reaches the + vanilla client. The "replace or second-gate" sub-question dissolves: the mechanisms occupy different planes (in-band creds are door-local; identity is -channel-level). The NoAuth door (POC #1's raw-pipe shape, zero -protocol translation) remains valid for loopback-only or wrapper-aware -use; the cred-bearing door is the same bridge plus the greeting half -(fast-socks5's server typestate for auth + `read_command`, then the -consumer session in-band) — an assembly-layer choice, not a protocol -one. +channel-level). No fast-socks5 `AuthMethod` impl is needed — the door +checks credentials itself before handing the stream to the typestate +(the door is this crate's code; the trait was designed for embedders +who don't own the accept loop). The NoAuth door (POC #1's raw-pipe +shape, zero protocol translation) remains valid for loopback-only or +wrapper-aware use; the cred-bearing door is the same bridge plus the +greeting half (fast-socks5's server typestate for auth + `read_command`, +then the consumer session in-band) — an assembly-layer choice. Remaining for Phase 1: the door's credential→identity mapping shape (static config vs pluggable lookup); whether the producer-side optional local backend offers the same door surface (same code shape — -door = vanilla↔ACL boundary — so presumably yes); and whether the door -needs a fast-socks5 `AuthMethod` impl or simply checks credentials -itself before handing the stream to the typestate (likely the latter — -the door is this crate's code, the trait was designed for embedders -who don't own the accept loop). +door = vanilla↔ACL boundary — so presumably yes). ### OQ-SK-03: UDP ASSOCIATE without binding — the hard case -RFC 1928 §7: the client sends UDP ASSOCIATE over the TCP control -connection; the server replies with a UDP relay address (`BND.ADDR`/ -`BND.PORT`); the client then sends UDP datagrams (SOCKS5 UDP header + -payload) *to that address*. On channels there is no UDP relay socket — -the datagram stage must ride a channel. Sub-questions: +**Status: baseline settled (2026-09-13, the handle swap) and +POC-validated end to end (POC #2, 2026-09-14 — +`poc-udp-associate-findings.md`).** The datagram-framing ADR (the +one-way door) is Phase 1's, with the first consumer in sight; the +framing has empirical ground under it. -- **Where does the datagram stage live?** Options: - - **Same channel, extended protocol** — after ASSOCIATE, the channel's - `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` - — 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 - carries `{addr, data}` (boundary-preserving, no RFC header on the - tunnel wire) and the RFC UDP-header codec lives only at the - vanilla-client front doors — cleaner than putting the length - framing *inside* the RFC header stream as first hunched; see - `docs/research/iroh-socks5-eval.md`. - - **Second channel for the datagram stage** — the producer - establishes the association on channel 1, then the client opens a - second channel that becomes the relay. Keeps CONNECT-shaped - `BiStream` semantics clean but adds an open-op round trip and - needs correlation (params carry the association id?). - - **Local-backend bridge only** — UDP ASSOCIATE is offered only - through the optional local backend (which binds a real UDP relay - socket as the assembly layer's explicit choice), never - channels-natively. Simplest; weakens the "ALPN as a service" - story for UDP. -- **What does `BND.ADDR`/`BND.PORT` reply contain on the channels - 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 - per-endpoint flow table; iroh-socks5 demonstrates the simpler cut — - per-datagram addressing *in the frame* does the demultiplexing, no - flow table. Boundary preservation is mandatory (empty - datagram ≠ EOF — alktunnels F-2's lesson transfers directly; the - `{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 - 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: +**The handle swap — "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; +- per-datagram destinations → `{addr, data}` frames in-band (the + iroh-socks5 cut: boundary-preserving, no RFC header on the tunnel + wire, no producer-side flow table; RFC codec at front doors only). + The length prefix is u16 BE — 65507 (the max UDP payload) < 65535, + so u16 suffices; boundary preservation is mandatory (empty datagram + ≠ EOF — alktunnels F-2's lesson transfers directly; the frame shape + meets it structurally); - 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 @@ -580,216 +524,128 @@ So swapping the handle preserves every RFC property: 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. +bridge is where a real socket address exists. The reply's `BND.ADDR`/ +`BND.PORT` on the in-band path are pure RFC-compatibility surface (the +POC used the sentinel `127.0.0.1:0`); only the front door reports a +real address. **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`? +what gets each datagram to `addr`? Both are dial-callback policy +(OQ-SK-01's shape); the protocol layer sees neither. 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 (the per-connection/per-identity channel - defaults are 256 — ADR-040/041 policy knobs, configurable without a - wire change; the `u32` wire space is ~4 billion — so fine for DNS + - browsing, and the per-destination open 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. - **Updated 2026-09-16 (alktunnels 0.2.0, ADR-007): the composed - form's vehicle is now the direct op** — `TunnelSession::open_direct` - with `Substrate::Udp` dials a real UDP target through the same - dynamic-target op (`channels/tunnel/direct`), pinned by alktunnels' - own `open_direct` + `Substrate::Udp` test (a real UDP echo through - the establisher path). The lazy - destination→tunnel table's shape is unchanged — each entry is a - direct-op channel instead of a named-resource channel; the + this crate's `local` feature). No flow table, native-only. POC #2 + implemented it (`LocalEgress`); domain-form destinations resolve at + the egress (producer-side DNS — the ssh `-D` semantic, + POC-resolved as "the egress owns resolution"). +2. **Composed egress** — the dial callback opens an alktunnels + udp-substrate channel per destination: a lazy destination→tunnel + table (open on first datagram, LRU-evict; POC #2 implemented it as + `ComposedEgress`). The flow table is *reborn as channel handles* — + because alktunnels' udp substrate is **connected** (one tunnel, one + fixed target) while ASSOCIATE names a different destination per + datagram. The composed form's vehicle since alktunnels 0.2.0 (ADR-007) + is the **direct op** — `open_direct` + `Substrate::Udp` dials a real + UDP target through the dynamic-target op (`channels/tunnel/direct`), + pinned by alktunnels' own test (a real UDP echo through the + establisher path); the table entries are direct-op channels, and the per-destination ACL question shifts to the `tunnel:direct` grant + - the dialer's target policy (the ACL governs the capability, the - dial callback governs per-destination checks). POC #2's ~0.8 ms - per-destination open-cost measurement carries over (same op - mechanics; the direct op adds no round trip). + the dialer's target policy (the ACL governs the capability, the dial + callback governs per-destination checks). Cost: one open per + destination, measured ~0.8 ms (POC #2, N=20 loopback) — fine for DNS + + browsing; the 256-channel default cap bounds fan-out (OQ-SK-09). + Benefit: **per-target ACL for free** — 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). +**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). -**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 -u8 = 255 stream-type limit) applied to the SOCKS5 channel — a `data` -stream and a `udp-relay` stream type would carry the two phases -structurally. Analysis against RFC 1928's actual structure: **RFC 1928 -is one command per connection, and control and data never interleave in -either direction.** CONNECT's control conversation ends at the reply, -after which the channel is a pure byte stream (alktunnels' 0 B -pass-through conclusion); ASSOCIATE's control stream goes silent after -the reply — fast-socks5's own `wait_on_tcp` (`src/server.rs:1195`) -treats any post-reply control byte as protocol garbage -(`UnexpectedUdpControlGarbage`). So the per-chunk type byte demuxes a -problem SOCKS5 doesn't have; a **phase model** (raw RFC conversation -until the reply, then a mode switch synchronized by the reply itself — -pass-through for CONNECT, `[len: u16 BE]` datagrams for ASSOCIATE) -subsumes it with no type byte and no sentinel address: wrapper-aware -clients know "post-reply = datagram mode," the same way they'd know -"relay stream = datagram mode" under the demux. What the demux genuinely -buys that the phase model does not: an **additive in-band vocabulary** — -new stream types extend the wire additively (no format break), whereas -adding a post-reply phase later is a wire break. alktunnels accepted the -opposite ("no in-band control path, ever — the escape hatch is a new -ALPN, a wire change is not"); the demux reintroduces that vocabulary at -2 B/chunk (type byte + length prefix) on every datagram. The one-way-door -cost is real either way: demux bakes the vocabulary in before it has a -user; phase model keeps 0 B overhead on CONNECT and 2 B on ASSOCIATE -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. +**Phase model vs alktty-style demux (the extensibility trade — Phase 1 +ADR input).** An alternative shape surfaced: alktty's logical demux +(input/output/error/control sub-streams inside one channel, type byte +per chunk) applied to the SOCKS5 channel. Analysis against RFC 1928's +actual structure: **RFC 1928 is one command per connection, and control +and data never interleave in either direction.** CONNECT's control +conversation ends at the reply, after which the channel is a pure byte +stream; ASSOCIATE's control stream goes silent after the reply — +fast-socks5's own `wait_on_tcp` (`src/server.rs:1195`) treats any +post-reply control byte as protocol garbage +(`UnexpectedUdpControlGarbage`); the phase model needs no mode byte +(the reply itself synchronizes the switch — POC #2 confirmed post-reply +garbage is refused structurally). So the per-chunk type byte demuxes a +problem SOCKS5 doesn't have. What the demux genuinely buys: an +**additive in-band vocabulary** — new stream types extend the wire +additively (no format break), whereas adding a post-reply phase later +is a wire break. alktunnels accepted the opposite ("no in-band control +path, ever — the escape hatch is a new ALPN, a wire change is not"); +the demux reintroduces that vocabulary at 2 B/chunk (type byte + length +prefix) on every datagram. The one-way-door cost is real either way: +demux bakes the vocabulary in before it has a user; phase model keeps +0 B overhead on CONNECT and 2 B on ASSOCIATE datagrams. Evidence after +both POCs favors the phase model — no in-band control vocabulary was +needed. Decide via ADR with the first consumer in sight. -Hunch (datagram stage, updated 2026-09-13 by the handle swap; -POC #2-validated 2026-09-14; composed-egress vehicle updated -2026-09-16 — see variant 2 above): -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 direct-op -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. The datagram-framing ADR (the one-way door) is Phase -1's, with the first consumer in sight. +### OQ-SK-04: fast-socks5 wasm posture — RESOLVED: inline vendor -### OQ-SK-04: fast-socks5 wasm posture (blocks the wasm-clean invariant) +**RESOLVED 2026-09-20: inline vendor (decided).** Two facts settled it: +(1) the ownership premise was wrong — we do *not* own fast-socks5 +(third-party upstream), so the "file asks / PR" legs of the old +decision tree lose their cheap path; and (2) the wrapper's needs go +beyond the wasm gate regardless — BIND is absent upstream (POC #5 +proved the wrapper must drive it itself), and the two seam fixes +(`run_udp_proxy_custom`'s unconditional bind; private `new_reply`) were +only "asks" under the ownership assumption. The change set (net-gate + +BIND + bind seam + `new_reply`) is exactly the shape POC #4 measured as +minimal (~120 cfg lines + additive additions in the `T`-generic +region), so the vendor pays for itself without leaning on the adapter +story. Mechanics in the Convergence §Layering bullet; the reasoning +trail below is retained for the record. -**RESOLVED 2026-09-20: inline vendor (Case 1, decided).** Two facts -settled it: (1) the ownership premise was wrong — we do *not* own -fast-socks5 (third-party upstream), so the "file asks / PR" legs of -the old decision tree lose their cheap path; and (2) the wrapper's -needs go beyond the wasm gate regardless — BIND is absent upstream -(POC #5 proved the wrapper must drive it itself), and the two seam -fixes (`run_udp_proxy_custom`'s unconditional bind; private -`new_reply`) were only "asks" under the ownership assumption. The -change set (net-gate + BIND + bind seam + `new_reply`) is exactly the -shape POC #4 measured as minimal (~120 cfg lines + additive additions -in the `T`-generic region), so the vendor pays for itself without -leaning on the adapter story. Mechanics in the Convergence §Layering -bullet; the decision record below is retained for the reasoning trail. - -`fast-socks5` uses `tokio::net` (`TcpListener`, `TcpStream`, -`UdpSocket`) and `socket2` unconditionally (`Cargo.toml`: tokio features -`io-util, net, time, macros`; deps `socket2 = "0.5.8"`); `wasm32- -unknown-unknown` has no `tokio::net`. The wrapper's own protocol layer -can stay wasm-clean (the typestate API is generic over `T`), but -depending on the crate at all may break `cargo check --target -wasm32-unknown-unknown` at the dependency-graph level. - -**The decision tree (2026-09-12 clarification — two main forks, and the -preference between them was conditional). Retained as the reasoning -trail; both legs were written under the wrong ownership premise ("we -own upstream") — the resolved posture is the header note above:** +The decision tree (2026-09-12, both legs written under the wrong +ownership premise "we own upstream"): - **Case 1: wasm matters for this crate → fork-first.** Make the minimal fork and use it — offer it upstream as a PR (merge if they want it; we carry the fork regardless; never wait on approval). - The gating change is genuinely small (feature-gate - `tokio::net`/`socket2` behind a default-on `net` feature, per the - alktty `local` pattern); the long-term cost of maintaining a - minimal fork of a well-written lib trends toward zero. Concretely - "fork" likely means **vendoring the relevant subset into this + Concretely "fork" means **vendoring the relevant subset into this crate** (the codecs, typestate machinery, client `use_stream` path — the parts that are already `T`-generic) and gating the native-net - pieces behind the crate's own `local` feature, rather than - publishing a divergent crate. fast-socks5 remains the reference - checkout for differential testing of RFC edge cases (reply-code - mapping, domain addressing, fragmentation) and the PR source; if - upstream accepts the PR, the fork shrinks to a plain dependency. - (The vendoring shape is what was adopted 2026-09-20 — the PR is - optional courtesy, and "shrinks to a plain dependency" is moot since - the vendor also carries BIND + the seam fixes upstream lacks.) -- **Case 2: wasm doesn't matter for this crate → plain dependency, no - fork.** Ride the `router.rs` native path exactly as-is (the - alkhttp precedent — a crate in this suite can ship native-only). - This is strictly better *if its premise holds*: no fork burden, no - vendoring, upstream stays upstream. The alkhttp contrast is - instructive — alkhttp's substance (axum/hyper/reqwest) is - socket-native, so nobody misses wasm there. (Superseded: Case 2's - premise — a wrap-only relationship — was falsified by the BIND + - seam needs; see the resolution note above.) + pieces behind the crate's own `local` feature. (The vendoring shape + is what was adopted — the PR is optional courtesy, and "shrinks to a + plain dependency" is moot since the vendor also carries BIND + the + seam fixes upstream lacks.) +- **Case 2: wasm doesn't matter → plain dependency, no fork.** Ride the + `router.rs` native path exactly as-is (the alkhttp precedent — a + crate in this suite can ship native-only). Strictly better *if its + premise holds* — a wrap-only relationship with no carried changes. + (Superseded: the premise was falsified by the BIND + seam needs.) -**The root question was therefore: does wasm make sense for alksocks?** -(Retrospective note, 2026-09-20: the analysis below was written when -that was open; the ownership correction resolved the OQ ahead of the -adapter story — the vendor is justified by BIND + the seam fixes -alone. The adapter-story input below is retained as the -still-relevant product question for how much wasm surface to -exercise, not as a gate.) Analysis that was on file for the decision: - -- **What wasm would serve:** the protocol layer (components 1+2 of the - three-component split, §Vision) in a sandboxed adapter — the - alktty/alktunnels posture that a wasm-compiled protocol crate is the - protocol layer for downstream TS/Python adapters. The natural wasm - shape is a **consumer**: speak RFC 1928 client-side over a channels - `BiStream` (pure byte framing; the producer dials targets, so no - local sockets are needed on the client side). A wasm **producer** is - odd standalone (no kernel egress) but composes — its dialer can open - alktunnels channels further downstream, which is the hub story. -- **What wasm would not serve:** component 3 (the quinn/noq client - wrapper — since transferred to the alknet rewrite) is native-only in - practice — it wraps kernel UDP sockets for UDP ASSOCIATE; no wasm - story is expected there regardless. (The transfer does not change - this OQ's inputs: components 1+2 were always the wasm-relevant - surface.) -- **The alkhttp contrast, honestly weighed:** for alkhttp, - "an HTTP client accessed over a channel" was judged weird, and the - crate ships native-only. alksocks differs structurally: its protocol - substance *is* byte-framing over a generic `T` — the part that costs - nothing to keep wasm-clean. But "costs nothing under the fork" is - only an argument once Case 1 is chosen; it is not itself the use - case. The use case is a planned sandboxed SOCKS consumer. - -**Decision inputs (what was set to settle Case 1 vs Case 2):** - -1. **Is there a planned sandboxed (wasm) consumer of the SOCKS5 - service?** If the downstream TS/Python adapter story includes a - channels-connected sandbox that wants proxied egress, wasm matters - → Case 1. If not → Case 2. -2. **Is the fork genuinely minimal?** POC #4 (OQ-SK-07) verifies - empirically how invasive the gating is. If the gating turns out - structural (the `T`-generic surface can't be separated from - `tokio::net` without a rewrite), the fork cost rises and Case 2 - gains weight — reimplementing the protocol to serve a wasm story - nobody has yet defeats the purpose. - -Both inputs pointed the same way when aligned: fork-first in Case 1 is -the preferred branch *because* wasm is wanted there — not as a general -preference for forks over upstream asks. If the adapter story never -materializes, Case 2 (plain dep, router path) was framed as the -obvious choice — and no one should carry a fork for an invariant's -sake. (The ownership correction below is what changed the balance.) - -**The resolution (2026-09-20), restated:** the ownership correction -made "wait on upstream" legs of both cases unavailable, which pushes -every fast-socks5 change (wasm gate, BIND, bind seam, `new_reply`) -into code we carry — and once that set is carried, a plain-dep -Case 2 would mean wrapping a crate we can't change against needs it -doesn't serve (no BIND). The vendor is the only posture that fits all -four needs; POC #4 established the carry is small. Case 2's fallback -framing (alkhttp precedent) remains the record of what *would* have -been chosen had the crate been native-only with no BIND need. +The root question was whether wasm makes sense for alksocks; the +analysis on file: what wasm would serve is the protocol layer +(components 1+2) in a sandboxed adapter — the alktty/alktunnels posture +that a wasm-compiled protocol crate is the protocol layer for +downstream TS/Python adapters; the natural wasm shape is a **consumer** +(speak RFC 1928 client-side over a channels `BiStream` — pure byte +framing; no local sockets needed client-side). A wasm **producer** is +odd standalone (no kernel egress) but composes — its dialer can open +alktunnels channels further downstream (the hub story). What wasm would +not serve: the quinn/noq client wrapper (since transferred to alknet) — +native-only in practice. The alkhttp contrast, honestly weighed: +alkhttp's substance is socket-native so nobody misses wasm there; +alksocks differs structurally — its protocol substance *is* byte-framing +over a generic `T` — but "costs nothing under the fork" is only an +argument once Case 1 is chosen; the use case is a planned sandboxed +SOCKS consumer. (Retrospective, 2026-09-20: the ownership correction +resolved the OQ ahead of the adapter story — the vendor is justified by +BIND + the seam fixes alone. The sandboxed-wasm-consumer adapter story +remains a product input for how much wasm surface to exercise, not a +gate.) ### OQ-SK-05: Target policy and egress scoping @@ -797,25 +653,29 @@ 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 (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): +mechanism. Residual questions for Phase 1 (note: for the *composed* UDP +egress path, per-target ACL comes free — each destination rides its own +direct-op channel governed by the tunnel grant's ACL, OQ-SK-03 variant +2): - Does the base crate ship a per-target allowlist hook (producer-side policy injected at registration), or is target policy entirely the dialer's concern (the dial callback refuses)? The dialer-refuses - shape avoids a second policy layer (alktunnels' resolution pattern); - the allowlist-hook shape makes a common policy declarative. + shape avoids a second policy layer (alktunnels' resolution pattern, + evidence-backed); the allowlist-hook shape makes a common policy + declarative. - Is there a `SOCKS5_OPEN_SCOPE` (scope-gate on the open op, alktty `TTY_OPEN_SCOPE` shape) — presumably yes, but the exact scope-string - convention should follow alkcall's registry conventions. + convention should follow alkcall's registry conventions. POC-validated + in shape (`socks5:open`, `socks5:bind`); the convention ADR is + Phase 1's. - Domain-form targets (RFC 1928 ATYP 0x03): resolve producer-side (fast-socks5's `dns_resolve` config), refuse, or pass through to the - dialer unresolved? (DNS-on-the-producer is the SSH `-D` semantic; - wasm producers may not have a resolver at all — an interaction with - the vendored crate's wasm posture, OQ-SK-04 resolved 2026-09-20.) + dialer unresolved? POC #2 resolved the local-egress half ("the egress + owns resolution"); the general dialer-pass-through posture is the + remaining Phase 1 choice (wasm producers may not have a resolver at + all — an interaction with the vendored crate's wasm posture, + OQ-SK-04 resolved 2026-09-20). **Virtual address space / identity-scoped subnets (added 2026-09-14, post-POC #2).** A proposal surfaced during the findings review: give @@ -829,80 +689,40 @@ CONNECT/BIND requests — the translation layer is the dial callback 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 +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 variant 2, the +bind-first `bound` reply field). 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) - -> **Resolved by transfer (2026-09-14, the scope decision, §Vision).** -> Everything below is retained as the question record for *alknet's* -> Phase 0; it is no longer this crate's scope. The transfer rationale -> is at the end of this section. - -**Scope clarification (2026-09-12):** the SOCKS client wrapper is -component 3 of the three-component split (§Vision) — a standalone -client library for downstream users (alknet) that must work with *any* -RFC 1928 server, not just one this crate produces. The motivating case -is iroh's privacy posture: iroh relays (and the peer) see the client's -real IP — intended for relay-assisted p2p, but a client that doesn't -want to leak its IP needs a SOCKS hop ahead of the QUIC dial. This is -the alknet ADR-090 use case generalized; it neither produces nor -consumes channels. - -The client wrapper's UDP story is "implement the QUIC runtime's -abstract socket trait over a SOCKS5 UDP association." quinn 0.11 is -proven (quinn-proxy POC, ADR-090). noq (iroh's fork, v1.2.0) has the -same extension point (`AsyncUdpSocket`, -`Endpoint::new_with_abstract_socket`) but the trait shape changed: -`create_sender() -> Pin>` replaces -`create_io_poller` + `try_send`, and the constructor takes -`Box` instead of `Arc`. - -Questions: - -- Does this crate ship `Socks5UdpSocket` against noq (behind a feature - like `noq`), quinn (behind `quinn`), or both? Both implies a shared - substrate-free core (associate handshake, datagram codec, flow table) - with two thin trait shells — feasible only if the shared core is - genuinely trait-agnostic. iroh's adoption makes noq the - practically-important target; alknet's precedent is quinn. -- noq is not on crates.io at this version (iroh consumes it from the - n0 workspace/git) — how does this crate depend on it (git dep? wait - for publication? feature-gate so it is optional)? This may block - `cargo publish --dry-run` for the client features; a publish-lean - default (quinn optional, noq git-optional) may be needed. -- Do the ECN/MTU limitations carry over (they should — the SOCKS5 UDP - header has no ECN field), and does noq's `may_fragment` default - (`true`) behave the same as quinn's? - -This is a research question first (read noq's endpoint/driver loops; -write the trait-shape comparison), then possibly a POC (OQ-SK-07 #3). +### OQ-SK-06: noq client support — RESOLVED by transfer **Resolved by the scope decision (2026-09-14, §Vision): transferred to the alknet rewrite.** The client wrapper is alknet's ancestor-shaped component (ADR-090 + quinn-proxy POC already live there), consumes no channels, and has no consumers planned inside this crate — so its -research questions (noq trait-shape comparison, dependency posture, +research questions (the noq trait-shape comparison: `create_sender() -> +Pin>` replacing `create_io_poller` + `try_send`, the +`Box` constructor ownership change; the dependency +posture — noq is consumed from the n0 workspace/git, not crates.io; the ECN/MTU carry-over) move to alknet's Phase 0 wholesale. This crate's client-side story is complete without it: the channels-native consumer -session is POC-validated (POC #1's `use_stream` over a `BiStream`, -POC #2's associate session). The noq POC (OQ-SK-07 #3) is dropped from -this crate's POC list accordingly. +session is POC-validated (POC #1's `use_stream` over a `BiStream`, POC +#2's associate session). The noq POC (POC #3) is dropped from this +crate's register accordingly. The noq surface notes are retained under +§Prior art as alknet-facing background. -### OQ-SK-07: POC scope for what remains unvalidated +### OQ-SK-07: POC register (closed) -**POC register (2026-09-15 consolidation).** The list below grew -iteratively (a POC was added, dropped, and folded), so here is the -one-place accounting of all numbered POCs: +**All POCs resolved; the register is closed for this crate's Phase 0.** +Numbering is historical (do not renumber — findings files and commit +messages reference these numbers): | # | What | Status | Where | |---|------|--------|-------| @@ -912,100 +732,33 @@ one-place accounting of all numbered POCs: | 4 | fast-socks5 wasm fork-minimality | **Passed 2026-09-13** — folded into #1's worktree, shares #1's findings file | `poc-connect-wasm-findings.md` §POC #4 | | 5 | BIND (accept-side command) | **Passed 2026-09-15** | `poc-bind-findings.md` | -Numbering is historical (do not renumber — findings files and commit -messages reference these numbers). Detailed descriptions follow. +Highlights (details in the findings files): -Candidates, originally in rough priority order (the order is historical -— #3 was dropped and #5 appended after the fact): - -1. **Channels-native SOCKS5 CONNECT POC** — the producer half over a - real alkcall channels connection: consumer opens a SOCKS5 channel, - speaks RFC 1928 CONNECT (via fast-socks5's client types), producer's - open handler runs the typestate machine over the `BiStream`, dials a - local echo target, two-pump proxy loop completes. Validates: the - `register_openable_with_establisher` fit, `pump_bidi` as the data - plane, params shape (trivial — probably no params), and the - `BiStream`-as-`T` genericity claim. This is the crate's core value - proposition and the cheapest to validate (alktunnels' forward POC is - the template). **Run 2026-09-13 — passed** (`alksocks-connect-poc`, - 8 tests + a curl front-door example; findings in - `poc-connect-wasm-findings.md`: dial-at-command-read layering - resolved, identity seam works, refusal paths typed). -2. **UDP ASSOCIATE POC** — the OQ-SK-03 chosen shape, end to end: a - wrapper-aware client associates, sends length-prefixed datagrams - down the channel, producer relays to a real UDP endpoint. Validates: - the sentinel-reply semantics, the datagram codec (fast-socks5's - `new_udp_header`/`parse_udp_request` over the length framing), the - flow table, and empty-datagram handling (alktunnels F-2 transfers). - (Reuses POC #1's producer skeleton; note `run_udp_proxy_custom` - cannot be used as-is — see the corrected §Prior art caveat.) - **Design baseline updated 2026-09-13:** the iroh-socks5 eval - (`docs/research/iroh-socks5-eval.md`) suggests the in-band datagram - carries `{addr, data}` directly (no RFC UDP header on the tunnel - wire; RFC codec at front doors only) and drops the producer-side - 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. - **Run 2026-09-14 — passed** (`alksocks-udp-poc`, 11 tests; findings - in `poc-udp-associate-findings.md`: the handle-swap data plane - validated end to end, both egress variants implemented + composed - per-destination open cost measured at ~0.8 ms, front-door RFC-codec - composition confirmed, select!-driven stage with the egress - readiness contract as the load-bearing API finding). -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 - (the `create_sender`/`Box` shape changes) before the client spec is - written. (The quinn variant is already proven by the quinn-proxy - 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. (Before the decision the list was - #1–#4; after dropping #3 it 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. **Run 2026-09-15 — - passed** (`alksocks-bind-poc`, 9 tests; findings in - `poc-bind-findings.md`: the phase model extends end to end, both - accept variants implemented, the scope gate works, the - address-first-vs-accept-first deadlock is resolved in shape - (reply#1 first, the tunnel open rides the accept future), and the - alktunnels ask ledger is pinned at two items. **Ledger statuses - updated 2026-09-20** — see OQ-SK-08's landed note below: both - items superseded in shape by the bind-first/forwarded graduation.) +- **POC #1** validated the `BiStream`-as-`T` genericity claim, + `register_openable_with_establisher` fit, `pump_bidi` as the data + plane, the dial-at-command-read layering, the identity seam end to + end, typed refusal paths (session-level at the registry ACL; + target-level as RFC reply codes), and the curl front door. +- **POC #4** measured the fast-socks5 wasm gating at ~120 cfg lines + across 5 files; the wasm-clean subset is the *functional* protocol + subset (a full RFC 1928 CONNECT conversation passes + `--no-default-features`). +- **POC #2** validated the handle-swap baseline end to end (both + egress variants; composed per-destination open cost ~0.8 ms; + front-door RFC-codec composition) and produced the load-bearing API + findings: the datagram stage is `select!`-driven with the egress + readiness contract (`next_reply` must be `Pending` when no reply is + ready — an instantly-ready `Ok(None)` starves a biased select into + livelock), framing applied exactly once by the wire layer (inner + bytes at the egress seam), and the egress/session ownership must ride + the table entry (teardown-at-birth). +- **POC #5** validated the BIND phase model end to end (both accept + variants; the `socks5:bind` scope gate; failure mapping RFC-faithful; + the vanilla front door for BIND), pinned the composed-variant + sequencing against alktunnels 0.1.0 (superseded in shape by the + bind-first graduation — see OQ-SK-08), and surfaced the + ownership-rides-the-handle lesson as the third witness of the + teardown-at-birth pattern. POC placement conventions (inherited from alktunnels): a POC that needs code from this repo runs in a worktree/branch (`.worktrees/research/ @@ -1014,141 +767,112 @@ 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) +### OQ-SK-08: BIND — the accept-side command -**Status: POC-validated (2026-09-15, POC #5 — -`poc-bind-findings.md`); the ask ledger landed, superseded in shape -(2026-09-20, alkcall 0.8.0 + alktunnels 0.2.0 — see the landed note -below).** The shape space below is written as it stood pre-POC; the -POC resolved the design question (variants 1 + 2 implemented; the -sequencing pinned) and pinned the ask ledger. The remaining decision -is Phase 1's ADR (ship in base crate? default accept variant?). +**Status: use case found, POC-validated (POC #5, 2026-09-15); the +composed path composes against alktunnels 0.2.0 (the graduation +superseded the ask ledger's shape); the ship-in-base-crate and default +accept variant are Phase 1 ADRs.** -**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). **POC-confirmed:** `reply_success`'s -`sock_addr` parameter carries reply#1's `BND.ADDR` verbatim; the only -courtesy ask is publishing `new_reply` (the reply encoder is private). +**The gap (fast-socks5):** `Socks5Command::TCPBind` is parsed +(`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, carried in the vendor (no upstream ask). POC-confirmed: +`reply_success`'s `sock_addr` parameter carries reply#1's `BND.ADDR` +verbatim; the courtesy item is publishing `new_reply` (rides the +vendor — see §Prior art's four carried needs). -**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 +**The shape (convergent precedent: iroh-socks5's channels-native BIND — +server relays raw bytes post-reply for CONNECT/BIND, `docs/research/ +iroh-socks5-eval.md`):** 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 0.1.0's `producer.rs:207,392`; line numbers -shifted in 0.2.0 with the bind-first addition) proves accept-side +shape).** The consumer's `-R` path in alktunnels 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. +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: +**Accept variants (mirror of OQ-SK-03's egress variants):** 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 +2. **Composed (alktunnels listen resource)** — 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). + terminate-and-re-produce story). **Superseded in shape (2026-09-20): + the ask ledger landed via the bind-first/forwarded graduation** — + see below. 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. +**POC #5's resolution (2026-09-15 — `poc-bind-findings.md`):** variants +1 and 2 both implemented and passing. The hunch (variant 1 as the +faithful + zero-ask default) was right. Against alktunnels 0.1.0 the +composed variant needed a pinned workaround — **reply#1 first, the +tunnel open rides the accept future** (the accept-first establisher +could not open before reply#1 without deadlocking into the +establishment timeout) — plus a two-item ask ledger (learn the bound +address without consuming an accept; peer metadata on accepted +handles). -**POC #5's resolution (2026-09-15 — `poc-bind-findings.md`):** both -variants implemented and passing. The hunch was right, and variant 2's -sequencing is now pinned: **reply#1 first, the tunnel open rides the -accept future** (the first cut opened the listen-tunnel before -reply#1 and deadlocked into the establishment timeout — the app -server cannot dial until reply#1 tells it the address; the quiet wait -is exactly where the accept-pop block belongs). The ask ledger grew -to two items (address inspection; peer metadata on accepted handles — -reply#2's fidelity half), both POC-evidenced, both additive. The -scope gate (`socks5:bind`, checked at command dispatch from the -per-call opener identity) distinguishes ingress from egress and is -POC-validated. - -**The asks landed — superseded in shape (2026-09-20, alkcall 0.8.0 + -alktunnels 0.2.0).** The graduation (alktunnels -`docs/research/tunnels-graduation.md` → ADRs 007/008) took the -ledger's *two asks* as evidence and adopted the ssh -`forwarded-tcpip`-shaped alternative over the per-ask carriers: +**The graduation landed — superseded in shape (2026-09-20, alkcall +0.8.0 + alktunnels 0.2.0).** The graduation (alktunnels +`docs/research/tunnels-graduation.md` → ADRs 007/008) took the ledger's +*two asks* as evidence and adopted the ssh `forwarded-tcpip`-shaped +alternative over the per-ask carriers: - **Ask #1 (learn the bound address without consuming an accept) is dissolved, not implemented.** The bind-first establisher - (`bindfirst_establisher`, ADR-008 §1) binds during the - establishment phase — fast, no app-server dependency — so the - open-op reply natively carries the real OS-chosen bound address - (the `bound` reply field, projected via alkcall 0.8.0's - `Establishment::with_reply_field`, ADR-049 amendment 3; consumed - via `open_channel_with_reply` / `TunnelListener::bound()`). There - is no accept to avoid consuming: establishment = bind. This - *replaces* POC #5's pinned workaround ("reply#1 first, the tunnel - open rides the accept future") — the accept-first sequencing was a - workaround for the accept-first establisher, and the bind-first - shape removes the workaround's reason to exist. It also dissolves - POC #5's establishment-bound finding: the accept wait now lives in - the teardown-governed pump phase, so FTP-length BIND waits stop - resolving as spurious 10s-establishment refusals (ADR-008's stated - second win). The old accept-first surface (`listen_establisher` + - `AcceptQueue`) stays for the accept-as-open shape; both live in - alktunnels 0.2.0. + (`bindfirst_establisher`, ADR-008 §1) binds during the establishment + phase — fast, no app-server dependency — so the open-op reply + natively carries the real OS-chosen bound address (the `bound` reply + field, projected via alkcall 0.8.0's + `Establishment::with_reply_field`, ADR-049 amendment 3; consumed via + `open_channel_with_reply` / `TunnelListener::bound()`). There is no + accept to avoid consuming: establishment = bind. This *replaces* the + pinned "reply#1 first, the tunnel open rides the accept future" + workaround (it existed for the accept-first establisher) and + dissolves POC #5's establishment-bound finding: the accept wait now + lives in the teardown-governed pump phase, so FTP-length BIND waits + stop resolving as spurious 10s-establishment refusals (ADR-008's + stated second win). The old accept-first surface + (`listen_establisher` + `AcceptQueue`) stays in 0.2.0 for the + accept-as-open shape. - **Ask #2 (peer metadata on accepted handles) landed at the wire's native moment.** The forwarded op (`channels/tunnel/forwarded`, ADR-008 §3) carries `{listen, substrate, peer}` params per accept — the peer address rides the open-op params (deny-unknown, - schema-gated) rather than a `TargetHandle` field; `ForwardedOpener` - + `make_forwarded_op_handler` are the serving side. `TargetHandle` - itself gained no peer field — the graduation chose the - forwarded-op shape over the §3 option-i carrier, superseding the - ledger's original framing. -- **Correlation key refinement (ADR-008 Amendment 1, 2026-09-18):** - the key binding forwarded opens to listeners is a producer-minted - u32 token (`listen` reply field + forwarded param), not the - originally-planned channel-ID key (unimplementable against the - landed handler surface). + schema-gated) rather than a `TargetHandle` field; `ForwardedOpener` + + `make_forwarded_op_handler` are the serving side. Correlation is a + producer-minted u32 token (ADR-008 Amendment 1, 2026-09-18: the + `listen` reply field + forwarded param), not the originally-planned + channel-ID key. -Consequence for this crate's variant 2: the composed BIND path is -now *fully vanilla-fidelity* with no interim stand-ins — the factory -no longer carries an expected `bind_addr`, reply#1 advertises the -real `bound`, reply#2 carries the real peer, and no establishment -bound threatens long BIND waits. POC #5's findings remain accurate as -the record of what was validated against alktunnels 0.1.0; the -Phase 1 shape composes against 0.2.0 (`register_tunnel_bindfirst_ -openable` + `BindFirstInjections`, or `TunnelListener` on the -opener side). +Consequence for variant 2: the composed BIND path is now *fully +vanilla-fidelity* with no interim stand-ins — the factory no longer +carries an expected `bind_addr`, reply#1 advertises the real `bound`, +reply#2 carries the real peer, and no establishment bound threatens +long BIND waits. POC #5's findings remain accurate as the record of +what was validated against alktunnels 0.1.0; the Phase 1 shape composes +against 0.2.0 (`register_tunnel_bindfirst_openable` + +`BindFirstInjections`, or `TunnelListener` on the opener side). **Security boundary note:** BIND is an *inbound* listener — the accept-side twin of arbitrary egress. Whatever ACL governs the socks5 @@ -1157,25 +881,26 @@ 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. +listeners. POC-validated (`socks5:bind` checked at command dispatch +from the per-call opener identity; `ConnectionNotAllowed` (0x02) +refusal while CONNECT still serves the same identity). ### 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: +**Status: analysis complete (2026-09-14); the documented proxy-workload +recommendation is Phase 1's.** 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 spends +a downstream channel per destination; BIND spends an accept resource +per listener). -- **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. +- **No wire change** — rejected permanently: the caps are policy knobs + (configurable without a wire change; the `u32` channel-ID wire space + is ~4 billion). The resolution is config, not wire. +- **Assembly-layer config** is the mechanism (ADR-040/041): 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 @@ -1183,13 +908,13 @@ space is ~4 billion — so the resolution is config, not wire: 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. + 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 @@ -1199,18 +924,17 @@ 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. +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. +identity-scoped subnets** — is recorded under OQ-SK-05: BND.ADDR/ +DST.ADDR translation via the dial callback, no wire change. ## Survey / prior-art list @@ -1236,13 +960,15 @@ Candidate reading for the research specialist (to be expanded): one-stream-per-session shape. - alktunnels — `docs/research/phase-0-findings.md` (the `-D` composition conclusion, hub-owns-the-connection, the UDP codec - decision + F-2), `docs/architecture/` (params/ALPN/ACL ADR template), - POC summaries (POC placement and findings conventions). -- alkcall — `docs/architecture/decisions/` ADR-037/039/049/050 (channel - ops, params-is-ALPN-specific, establishment, pump_bidi), ledger - CF-005/CF-006 (identity seam), `src/channels/operations.rs` - (`ChannelCore`, `OpenHandler`, `Establishment`, `ChannelPlan`), - `src/channels/pump.rs`. + decision + F-2), `docs/research/tunnels-graduation.md` (the direct + op + bind-first/forwarded graduation — the substrate this crate + composes against), `docs/architecture/` (params/ALPN/ACL ADR + template), POC summaries (POC placement and findings conventions). +- alkcall — `docs/architecture/decisions/` ADR-037/039/047/049/050/051 + (channel ops, params-is-ALPN-specific, establishment, pump_bidi, + reply projection, hub relay), ledger CF-005/CF-006 (identity seam), + `src/channels/operations.rs` (`ChannelCore`, `OpenHandler`, + `Establishment`, `ChannelPlan`), `src/channels/pump.rs`. - alknet — `crates/alknet-client/src/socks5.rs` (ADR-090, the client ancestor), `docs/research/quinn-quic-proxy/findings.md` (the POC findings: trait + constructor surface, ECN/MTU limitations, @@ -1274,75 +1000,53 @@ Candidate reading for the research specialist (to be expanded): ## Convergence checklist (what Phase 0 must produce) -- [ ] Vision + guiding principles captured (this doc, §Vision — - including the three-component split) +- [x] Vision + guiding principles captured (this doc, §Vision — + including the component split and the scope resolution) - [x] Prior-art pass complete: fast-socks5 surface verified (§Prior art), alktunnels/alknet/noq lineage mapped, anti-prior-art list written; iroh-socks5 evaluated (structural sibling, not the base; OQ-SK-03 prior art — `docs/research/iroh-socks5-eval.md`); - **ownership premise corrected 2026-09-20** (fast-socks5 is + ownership premise corrected 2026-09-20 (fast-socks5 is third-party upstream; the posture is the inline vendor, OQ-SK-04 resolved) -- [x] OQ-SK-01 (dial policy) — researched, half-answered (injected - dialer hunch); **POC #1 resolved the shape empirically** (dial - callback at the command-read interception point, boxed halves, no - trait); final decision in Phase 1 against the spec -- [x] OQ-SK-02 (auth mapping) — posture drafted (identity seam - primary, in-band auth for the local backend); **POC #1 validated - the seam end to end** (per-call opener identity at the - establisher; channel-0 identity propagation is a producer - harness wiring requirement); **direction settled 2026-09-13** - ("both," split by the door: producer-side identity only — a - password there is moot against the ACL; consumer-side front door - owns RFC 1929 for vanilla clients and maps credentials to the - channel identity); Phase 1 formalizes the door's - credential→identity mapping -- [ ] OQ-SK-03 (UDP ASSOCIATE) — the shape space written (including - the phase-model-vs-alktty-demux analysis); resolve via research - + POC #2, ADR before the first consumer - **(POC #2 run 2026-09-14 — passed; the handle-swap baseline is - POC-validated end to end, findings in - `poc-udp-associate-findings.md`; the datagram framing is - wire-stable once published — the Phase 1 ADR decision remains)** -- [x] OQ-SK-04 (fast-socks5 wasm) — the decision tree was written - (Case 1: wasm wanted → fork-first; Case 2: wasm unwanted → - plain dep, router path); **POC #4 verified the fork is genuinely - minimal** (~120 lines of cfg gating; wasm-clean subset is - functionally complete); **resolved 2026-09-20: inline vendor** - (the ownership premise was corrected — we don't own fast-socks5; - BIND + the seam fixes must be carried regardless, so the vendor - pays for itself independent of the adapter story — see the OQ's - resolution note) -- [ ] OQ-SK-05 (target policy) — folded into the OQ-SK-01 decision; - scope-gate convention pinned in Phase 1; the 2026-09-14 - virtual-subnet proposal (dial-callback translation, no wire - change) recorded for Phase 1 -- [x] OQ-SK-08 (BIND) — new (2026-09-14): the gap + use case + - accept-side mechanism written; **POC #5 run 2026-09-15 — - passed** (the phase model extends end to end; both accept - variants implemented; the scope gate works; the ask ledger is - pinned); **the ask ledger landed 2026-09-18/20 — superseded in - shape by the bind-first/forwarded graduation (see OQ-SK-08's - landed note)**; the remaining Phase 1 ADR (ship in base crate? - default accept variant?) has empirical ground under it -- [ ] 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, - consumes no channels, has no consumers planned here); its - research questions move to alknet's Phase 0; this crate's +- [x] OQ-SK-01 (dial policy) — shape resolved empirically (POC #1: + dial callback at the command-read interception point, boxed + halves, no trait); the A/B assembly-layer choice is a Phase 1 + policy decision +- [x] OQ-SK-02 (auth mapping) — settled 2026-09-13 ("both," split by + the door; POC #1 validated the seam end to end); Phase 1 + formalizes the door's credential→identity mapping +- [x] OQ-SK-03 (UDP ASSOCIATE) — the handle-swap baseline settled + 2026-09-13 and POC-validated end to end (POC #2, 2026-09-14); + the datagram framing is wire-stable once published — the Phase 1 + ADR (framing + demux-vs-phase) remains, with empirical ground +- [x] OQ-SK-04 (fast-socks5 wasm) — resolved 2026-09-20: the inline + vendor (the ownership premise was corrected — we don't own + fast-socks5; BIND + the seam fixes must be carried regardless, + so the vendor pays for itself independent of the adapter story; + POC #4 de-risked the mechanism) +- [x] OQ-SK-05 (target policy) — dialer-refuses shape evidence-backed; + per-target-ACL-for-free validated for the composed egress path + (POC #2); scope-gate convention (`socks5:open`/`socks5:bind`) + POC-validated in shape; the allowlist variant, exact scope + convention, and the 2026-09-14 virtual-subnet proposal are + Phase 1 decisions +- [x] OQ-SK-06 (noq client) — resolved by the scope decision + (2026-09-14): transferred to the alknet rewrite; this crate's channels-native client story is POC-complete without it -- [ ] Targeted POC(s) run + summaries in `docs/research/` - (the convergence-blocking POCs are complete — see the OQ-SK-07 - register: #1 + #4 in `poc-connect-wasm-findings.md`, #2 in - `poc-udp-associate-findings.md`, #5 (BIND) in - `poc-bind-findings.md`; #3 dropped with the scope decision. - The register is closed — no pending POCs remain for this - crate's Phase 0.) +- [x] OQ-SK-07 (POC register) — closed: #1, #2, #4, #5 passed; #3 + dropped with the scope decision; findings in `docs/research/` +- [x] OQ-SK-08 (BIND) — use case + mechanism found; POC #5 validated + the shape end to end; the ask ledger landed 2026-09-18/20 + (superseded in shape by the bind-first/forwarded graduation); + the remaining Phase 1 ADR (ship in base crate? default accept + variant?) has empirical ground under it +- [x] OQ-SK-09 (channel capacity) — 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] Targeted POCs run + summaries in `docs/research/` — complete; + no pending POCs remain for this crate's Phase 0 - [x] Converge: recommended approach written up, ready to hand to the Architect for Phase 1 (§Convergence, 2026-09-14) @@ -1354,37 +1058,37 @@ ready for the Architect. ### The crate, scoped **alksocks = the SOCKS5 producer/consumer protocol pair on alkcall -channels (socks + channels, nothing else).** The three-component split +channels (socks + channels, nothing else).** The component split resolved: components 1+2 are this crate; component 3 (the quinn/noq -`AsyncUdpSocket` client wrapper) moves to the alknet rewrite, where -its ancestor work (ADR-090, quinn-proxy POC) already lives. No local -binds except the two explicit, optional, feature-gated assembly shapes +`AsyncUdpSocket` client wrapper) moves to the alknet rewrite, where its +ancestor work (ADR-090, quinn-proxy POC) already lives. No local binds +except the two explicit, optional, feature-gated assembly shapes (producer's local backend; consumer's front door). ### The recommended approach (validated pieces → crate shape) -- **Protocol base: fast-socks5's explicit typestate API** — the - `T: AsyncRead + AsyncWrite + Unpin` genericity carries both +- **Protocol base: the vendored fast-socks5 explicit typestate API** — + the `T: AsyncRead + AsyncWrite + Unpin` genericity carries both substrates (POC #1: a channels `BiStream` drops in directly; the - local backend would feed a `TcpStream`). Drive the typestate - directly for UDP ASSOCIATE (`run_udp_proxy_custom` is unusable — - unconditional kernel bind; file the upstream ask). + local backend feeds a `TcpStream`). Drive the typestate directly for + UDP ASSOCIATE (`run_udp_proxy_custom` is unusable on channels — + unconditional kernel bind; the vendor fixes the seam in-tree). - **Wire shape: the handle swap** (POC #2-validated) — one ALPN - (`alk/socks5`), one open op (`channels/socks5/sub`), trivial - params; the RFC conversation is the data plane; CONNECT = - `pump_bidi` pass-through post-reply (POC #1); ASSOCIATE = the - `select!`-driven datagram stage over `[len: u16 BE][addr + data]` - frames (alktunnels' length codec; RSV/FRAG only at front doors; - channel EOF ends the association; per-datagram addressing, no flow - table). -- **Egress seam: the dial-callback shape, extended** — `DialFn` for - CONNECT (POC #1's command-read interception point) + - `EgressFactory` for ASSOCIATE (POC #2), both function-not-trait, - both returning boxed halves. Local-socket and composed providers - are assembly-layer policies; the composed variant's per-destination - open cost is measured (~0.8 ms) and its per-target-ACL-for-free - benefit is validated. **Updated 2026-09-20: the composed providers - compose against alktunnels 0.2.0** — the direct op + (`alk/socks5`), one open op (`channels/socks5/sub`), trivial params; + the RFC conversation is the data plane; CONNECT = `pump_bidi` + pass-through post-reply (POC #1); ASSOCIATE = the `select!`-driven + datagram stage over `[len: u16 BE][addr + data]` frames (alktunnels' + length codec; RSV/FRAG only at front doors; channel EOF ends the + association; per-datagram addressing, no flow table); BIND = the + phase model extended (POC #5). +- **Egress/accept seams: the dial-callback shape, extended** — `DialFn` + for CONNECT (POC #1's command-read interception point), + `EgressFactory` for ASSOCIATE (POC #2), and `BindAcceptFactory` for + BIND (POC #5): all function-not-trait, all returning boxed halves. + Local-socket and composed providers are assembly-layer policies; the + composed variant's per-destination open cost is measured (~0.8 ms) + and its per-target-ACL-for-free benefit is validated. The composed + providers compose against alktunnels 0.2.0 — the direct op (`channels/tunnel/direct`, ADR-007) is the dynamic-target channel for CONNECT, and `open_direct` + `Substrate::Udp` is the composed ASSOCIATE vehicle (both substrates, one op, one grant: @@ -1398,88 +1102,48 @@ binds except the two explicit, optional, feature-gated assembly shapes protocol modules at the crate core; feature-gated backend modules (local binds) never imported by the protocol layer; producer + consumer + params modules mirroring POC #1/#2's structure. **The - fast-socks5 posture is the inline vendor (resolved 2026-09-20, - OQ-SK-04)**: vendor the `T`-generic subset into this crate (the - codecs, typestate machinery, client `use_stream` path), gated - behind a default-on `net`-shaped feature (POC #4: ~120 cfg lines, - wasm-clean subset functionally complete) — and carry the four - wrapper needs in the same vendor: BIND (absent upstream; POC #5's - typestate-driven shape), the `run_udp_proxy_custom` bind seam, the - `new_reply` publication, and the wasm gate. `/workspace/fast-socks5` - stays the differential-testing reference; note the vendor point - (v1.0.0) in the vendored code. -- **Open for Phase 1 ADRs** (inputs collected, decisions pending): - 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 - (must distinguish BIND from CONNECT/ASSOCIATE — OQ-SK-08's security - note); the door's credential→identity mapping shape; - 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). - (The wasm decision itself is settled — OQ-SK-04 resolved to the - inline vendor 2026-09-20; only the wasm-consumer adapter story - remains a product input, and it no longer gates the vendor.) -- **Upstream asks — landed (the first-real-consumer sweep paid off).** - The graduation (`alktunnels docs/research/tunnels-graduation.md`, - alkcall review 008) took every ask at once, in one-version bumps: - - **alkcall 0.8.0**: the establisher reply projection - (`Establishment::with_reply_field(s)` + `open_channel_with_reply`, - ADR-049 amendment 3 — the `bound` carrier) and flavor-form - open-op ids in discovery (ADR-047 amendment 3 — - `channels/tunnel/direct`-shaped ops re-produce through hub relay - as channels); plus the bonus in-tree `ChannelRelay` + - `HubLegTemplate` (ADR-051) — the hub re-produce machinery is - in-tree, not assembly-layer. - - **alktunnels 0.2.0 (ADRs 007/008)**: the direct op (dynamic-target - egress — reshapes OQ-SK-01 Option A and OQ-SK-03's composed - egress), the bind-first establisher + forwarded op (dissolving - both BIND asks in shape — see OQ-SK-08's landed note), - `TunnelListener`, and the `local` bind-first substrate half. - - **fast-socks5 — posture changed (2026-09-20): the items are no - longer asks.** The ownership premise was wrong (third-party - upstream, not ours), and the wrapper's needs exceed the wasm gate - (BIND absent upstream; the bind seam; private `new_reply`). All - four changes move into the inline vendor (OQ-SK-04 resolved — - see the Layering bullet); `/workspace/fast-socks5` remains the - reference checkout, and offering the net-gate upstream as a - courtesy PR stays optional. + fast-socks5 posture is the inline vendor (OQ-SK-04, resolved + 2026-09-20)**: vendor the `T`-generic subset into this crate (the + codecs, typestate machinery, client `use_stream` path), gated behind + a default-on `net`-shaped feature (POC #4: ~120 cfg lines, wasm-clean + subset functionally complete) — and carry the four wrapper needs in + the same vendor: BIND (absent upstream; POC #5's typestate-driven + shape), the `run_udp_proxy_custom` bind seam, the `new_reply` + publication, and the wasm gate. `/workspace/fast-socks5` stays the + differential-testing reference; note the vendor point (v1.0.0) in + the vendored code. +- **Open for Phase 1 ADRs** (inputs collected, decisions pending): the + demux-vs-phase extensibility trade (evidence now favors the phase + model — no in-band control vocabulary was needed across both POCs); + the datagram-framing ADR (the one-way door — decide with the first + consumer in sight); the params-format/ALPN naming ADRs (wire-stable + once 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 + (OQ-SK-02); the A/B egress provider default (OQ-SK-01); BIND's + base-crate inclusion + default accept variant (OQ-SK-08); the + proxy-workload channel-cap recommendation (OQ-SK-09); target-policy + shape + the virtual-subnet translation-layer shape (OQ-SK-05). The + wasm decision itself is settled; only the sandboxed-wasm-consumer + adapter story remains a product input, and nothing waits on it. ### What Phase 0 could NOT answer (handed to Phase 1) -- The datagram-framing ADR (the one-way door) — now has empirical - ground: framing validated, LRU/dedup semantics pinned, open cost +- The datagram-framing ADR (the one-way door) — empirical ground in + hand: framing validated, LRU/dedup semantics pinned, open cost measured. Decide with the first consumer in sight. -- ~~The wasm root question (OQ-SK-04)~~ **resolved 2026-09-20** (the - inline vendor) — the mechanism is de-risked (POC #4) and the - posture no longer leans on the adapter story (the vendor pays for - itself via BIND + the seam fixes); the sandboxed-wasm-consumer - adapter story remains a product input for later, but nothing waits - on it. - Target-policy shape (OQ-SK-05) — the dialer-refuses shape is evidence-backed; the declarative-allowlist variant stays a Phase 1 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, and POC #5 (2026-09-15) - validated the shape end to end** — OQ-SK-08 records the gap, the - accept-side-resource shape (mirroring alktunnels' `-R` far-side - listener), the address-first-vs-accept-first resolution (reply#1 - first, the tunnel open rides the accept future), the two-item ask - ledger, and the scope gate. 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. **Updated 2026-09-20:** the - upstream substrate for the composed variant landed — alktunnels - 0.2.0's bind-first establisher + forwarded op (ADR-008) *dissolves* - both ask-ledger items in shape (the real `bound` rides the open - reply natively; the peer rides the forwarded params; the accept - wait is pump-phase, so no establishment bound). The pinned - "open rides the accept future" sequencing is retired as the - accept-first workaround it was; the Phase 1 ADR composes variant 2 - against 0.2.0 with full vanilla fidelity. \ No newline at end of file + recommendation stay Phase 1 decisions. +- BIND — validated end to end (POC #5); the graduation supplied the + composed variant's full-fidelity substrate (alktunnels 0.2.0's + bind-first establisher + forwarded op). 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. +- The door's credential→identity mapping shape (OQ-SK-02) and the + scope-string convention — Phase 1 ADRs with POC ground under them. \ No newline at end of file