From 766e489f994d4764f16badfd2ae0bc9a62d19288 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Sat, 12 Sep 2026 10:21:29 +0000 Subject: [PATCH] docs: three-component split; fork-first (vendor-and-vet) posture; consumer-side bind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - phase-0 vision: components split — (1) producer half (expose the SOCKS5 service over channels), (2) consumer half (consume it, optionally bind it locally — the ssh -D front door + tun2proxy path), (3) standalone SOCKS client wrapper for quinn/noq that must work with any RFC 1928 server (iroh relay/peer IP-leak motivation, alknet ADR-090 generalized); the earlier docs conflated the consumer-side bind with the producer-side local backend - principle 6: the vpn-like endgame — tunnel a produced SOCKS5 resource to a local port, point tun2proxy (--proxy socks5:// natively) at it; verified tun2proxy src/args.rs - OQ-SK-04 resolved as fork-first: 'fork' = vendor-and-vet inline in the crate (minimal diff, feature-gate native-net), PR upstream as courtesy, never wait on approval; wasm-drop (alkhttp precedent) is the escape hatch, not the plan - AGENTS.md conventions 4/9/17 aligned (fork-first, binds on either side, wasm posture) --- AGENTS.md | 49 ++++++++------ docs/research/phase-0.md | 139 ++++++++++++++++++++++++++++++--------- 2 files changed, 136 insertions(+), 52 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index eba6a18..859b93d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -90,10 +90,11 @@ implementation agents. (`rt`, `sync`, `io-util`, `macros`, `time`) — **do NOT use `features = ["full"]`** — and keep socket/platform I/O feature-gated. Note: `fast-socks5` itself uses `tokio::net` and `socket2` unconditionally; - whether it compiles wasm-clean, needs a fork, or the wrapper - reimplements the SOCKS5 state machine over `tokio::io` generics is an - open question (see `docs/research/phase-0.md` OQ-SK-04) — do not - assume either way until it is resolved. + the resolved posture (OQ-SK-04) is fork-first: vendor the needed + subset and feature-gate native-net pieces (the alkhttp crate shows a + native-only crate is possible, but wasm-clean remains the preferred + posture here). Run the wasm check whenever a non-backend module + changes. 5. **Wire format is stable** — the SOCKS5 protocol itself is RFC 1928 (fixed); this crate's wire surface is the ALPN + the channel open-op @@ -145,13 +146,17 @@ implementation agents. The SOCKS5 server must run without binding any port: the RFC 1928 conversation is carried inside a channels data channel, and target dials happen on the producing side (or hop further through - alktunnels/alksocks). A local bind (a genuine `socks5://host:port` - endpoint) is an optional assembly-layer/feature-gated capability. - UDP ASSOCIATE is the hard case: RFC 1928 replies with a UDP relay - address and the client sends datagrams to it, which seems to demand a - bind — how the relay address is virtualized (or the associate shape - adapted) is a Phase 0 question (OQ-SK-03). Binding decisions belong - to the caller (assembly layer), never the protocol crate. + alktunnels/alksocks). Local binds exist only as explicit, optional + assembly-layer shapes on either side: the producer may serve from a + real listener (a feature-gated local backend), and the consumer may + expose the resource on a local port (the ssh `-D` front door that + vanilla clients — curl, browsers, tun2proxy — point at). The + protocol itself never binds. UDP ASSOCIATE is the hard case: RFC + 1928 replies with a UDP relay address and the client sends + datagrams to it, which seems to demand a bind — how the relay + address is virtualized (or the associate shape adapted) is a Phase 0 + question (OQ-SK-03). Binding decisions belong to the caller + (assembly layer), never the protocol crate. 10. **Backpressure and limits are inherited, not redefined** — bounded per-channel buffers, the 256-channel per-connection cap, monotonic @@ -222,15 +227,19 @@ implementation agents. One ALPN per protocol. The ALPN string is wire-stable once published — decide via ADR before the first consumer. -17. **Upstream is ours — make asks early.** We own `fast-socks5` - (`/workspace/fast-socks5`, v1.0.0, MIT) and alkcall. If the wrapper - needs a change upstream (a typestate hook, a generic split, a new - event), file it and land it there rather than working around it - locally — the alktunnels precedent (the E-01/E-02 sweep, filed from - its Phase 0 and landed in alkcall 0.5.0 within a day). Keep - `/workspace/fast-socks5` as the reference checkout; if upstream - diverges from what we publish, note the fork point in the research - docs. +17. **Upstream is ours — fork-first, PR as courtesy.** We own + `fast-socks5` (`/workspace/fast-socks5`, v1.0.0, MIT) and alkcall. + For changes this crate needs upstream, the preferred posture is: + make the change as a minimal fork and use it — offer it upstream + as a PR and merge upstream if they want it, but never wait on + approval (see `docs/research/phase-0.md` OQ-SK-04's resolved + posture). For alkcall (the actively co-developed substrate) the + alk* precedent still applies: file asks early and land them there + rather than working around them locally — the alktunnels precedent + (the E-01/E-02 sweep, filed from its Phase 0 and landed in alkcall + 0.5.0 within a day). Keep `/workspace/fast-socks5` as the reference + checkout; if the vendored subset diverges from upstream, note the + fork point in the research docs. ## Verification Commands diff --git a/docs/research/phase-0.md b/docs/research/phase-0.md index b52fdf1..c08d3b3 100644 --- a/docs/research/phase-0.md +++ b/docs/research/phase-0.md @@ -27,15 +27,43 @@ port unless explicitly configured to, wrapping `fast-socks5` so the RFC conversation rides inside a channels data channel like any other produced resource. +**The three components (2026-09-12 clarification — they serve different +purposes and were at risk of being conflated):** + +1. **Producer half** — a side exposes the SOCKS5 service as a produced, + ACL-scoped resource (registers openable channels; the open handler + runs the RFC state machine; target dials happen there or further + downstream). This is the `-D` service over channels. +2. **Consumer half** — a side opens SOCKS5 channels against a produced + service and speaks RFC 1928 inside them. *Optionally* it may expose + 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. + +Components 1+2 form the producer/consumer protocol pair (channels in, +RFC 1928 inside); component 3 is standalone and composes with any +RFC 1928 server. Keep them structurally separate in the crate layout. + Guiding principles, inherited from the alk* family: 1. **"ALPN as a service," not "a server."** The SOCKS5 service is a produced, ACL-scoped resource on a channels connection — the same - shape as an alktty terminal or an alktunnels TCP tunnel. A genuine - `socks5://host:port` kernel-socket listener is one optional - assembly-layer shape (a feature-gated local backend), never the - protocol's home. This inverts the usual SOCKS5 deployment posture and - is the crate's defining requirement. + shape as an alktty terminal or an alktunnels TCP tunnel. Kernel + binds exist only as explicit, optional assembly-layer shapes on + *either side* — the producer may serve from a real listener (a + feature-gated local backend, component 1's optional shape), and the + consumer may expose the resource on a local port (component 2's + optional shape, the ssh `-D` front door). The protocol itself never + binds; the binding decision belongs to the caller. 2. **The `-D` conclusion, realized.** alktunnels' Phase 0 settled that `-D` composes at the assembly layer: "`-D` is just tunnel a socks5 connection"; target selection lives in the SOCKS5 protocol at the @@ -47,7 +75,7 @@ Guiding principles, inherited from the alk* family: 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`; the local backend feeds it a `TcpStream`. The + 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). @@ -63,6 +91,16 @@ Guiding principles, inherited from the alk* family: 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. +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 + 2's optional shape), then points tun2proxy (`/workspace/tun2proxy` + — it takes `--proxy socks5://user:pass@host:port` natively, + `src/args.rs`) at it, and the whole host's traffic rides the alk* + egress. This crate is the SOCKS5 leg of that composition; alktunnels + carries the tunnel; tun2proxy closes the loop. (The udpgw framing + alktunnels' UDP format is based on is the same prior art + OQ-SK-03's datagram codec draws on.) ## What is already settled @@ -428,10 +466,11 @@ Option A upstream if confirmed. This OQ gates the wasm verification command in AGENTS.md (the expected-failure note is already written there). -**Fork vs reimplement vs wrap — collapses onto this OQ (2026-09-12 -discussion).** The wrap surface was re-checked and **no structural fork -is needed** for the channels path: CONNECT rides the interception points -as-is — `read_command` → own dial → `reply_success` → `transfer` (the +**Fork vs reimplement vs wrap — resolved as fork-first (2026-09-12 +discussion, supersedes the earlier upstream-ask-first weighting).** The +wrap surface was re-checked and **no structural fork is needed** for the +channels path: CONNECT rides the interception points as-is — +`read_command` → own dial → `reply_success` → `transfer` (the `examples/router.rs` shape), with the producer's `BiStream` as `T` on both sides of `transfer`. UDP is ours either way: `run_udp_proxy_custom` takes a custom `transfer` closure for the relay half, `reply_success` @@ -441,16 +480,33 @@ The one hardwired piece — `run_udp_proxy`'s peer relay socket (`udp_bind_random_port`, socket2-based, called inside the default handler before `run_udp_proxy_custom` even runs) — is in the *default* handler, not the protocol: the wrapper's custom closure never needs it. -So the fork-vs-reimplement decision is not about the channels surface -at all; it is driven by the wasm question above: **wrap (with the -upstream `net` feature-gate ask) if the gating lands cleanly; -reimplement if the gating turns out structural.** The reimplementation -is genuinely small — RFC 1928 over `tokio::io` generics is the -alktunnels-codec scale of code — and fast-socks5 would remain the -differential-test oracle for RFC edge cases (reply-code mapping, -domain addressing, fragmentation) either way. A full fork (divergent -copy we publish) is the least attractive branch: it duplicates the -maintenance without buying anything the two options above don't. + +**Decision: fork-first, "fork" = inline in this crate (vendor-and-vet).** +Do the feature gating as a minimal, minimal-diff fork and use it — +offering it upstream as a PR (merge if they want it; we carry the fork +regardless). Rationale: depending on someone else's approval cadence is +the worst part of the upstream-ask path; the long-term cost of +maintaining a minimal fork of a well-written lib trends toward zero +(modern tooling/AI assistance makes small-diff rebase-and-review cheap); +and the changes needed here are genuinely small (feature-gate +`tokio::net`/`socket2` behind a default-on `net` feature, per the +alktty `local` pattern). Concretely this likely 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 feature-gating PR, the fork shrinks to +a plain dependency; if not, we carry it — either way this crate's +timeline is not hostage to approval. What is *not* off the table: +dropping wasm outright is still available (Option C) as the +alkhttp precedent shows a crate can ship native-only — but it is not +preferred; the fork makes the wasm-clean default achievable without +anyone's approval. (The user also flagged the alternative of simply +not worrying about wasm for this crate and riding the `router.rs` +native path; that remains viable if the fork's gating turns out more +invasive than expected — an escape hatch, not the plan.) ### OQ-SK-05: Target policy and egress scoping @@ -476,12 +532,23 @@ mechanism. Residual questions for Phase 1: ### OQ-SK-06: noq client support (and the quinn/noq shape split) -The client half'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 +**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: @@ -577,14 +644,22 @@ Candidate reading for the research specialist (to be expanded): - tun2proxy — `/workspace/tun2proxy` `src/udpgw.rs` and its SOCKS5 files (`socks.rs`, `proxy_handler.rs`): UDP-over-stream framing and flow-table prior art (analyzed in alktunnels phase-0-findings; the - SOCKS5-specific parts feed OQ-SK-03). + SOCKS5-specific parts feed OQ-SK-03). Also the endgame composition + partner: it takes `--proxy socks5://...` upstream natively + (`src/args.rs`) — the vpn-like endgame is tunnel + local bind + + tun2proxy (§Vision, principle 6). +- alkhttp — `/workspace/@alkdev/alkhttp`: the native-only precedent in + the suite (axum/reqwest/hyper, no wasm target) — cited in OQ-SK-04 + as the "wasm-clean is preferred, not mandatory" escape hatch. - iroh — `/workspace/iroh` `iroh/Cargo.toml` (the noq dependency posture: `noq = "1.2.0"` from the n0 workspace) — context for - OQ-SK-06's dependency question. + OQ-SK-06's dependency question; also the privacy motivation for the + client wrapper (relays/peers see the real IP). ## Convergence checklist (what Phase 0 must produce) -- [ ] Vision + guiding principles captured (this doc, §Vision) +- [ ] Vision + guiding principles captured (this doc, §Vision — + including the three-component split) - [ ] Prior-art pass complete: fast-socks5 surface verified (§Prior art), alktunnels/alknet/noq lineage mapped, anti-prior-art list written @@ -596,9 +671,9 @@ Candidate reading for the research specialist (to be expanded): the phase-model-vs-alktty-demux analysis); resolve via research + POC #2, ADR before the first consumer - [ ] OQ-SK-04 (fast-socks5 wasm) — verify empirically (POC #4); - the fork-vs-reimplement-vs-wrap decision collapses onto this - OQ; likely an upstream feature-gating ask; file early per - AGENTS.md convention 17 + resolved posture: fork-first (vendor-and-vet, PR as courtesy) — + carry the minimal fork regardless of upstream's decision; + wasm-drop (alkhttp precedent) is the escape hatch, not the plan - [ ] OQ-SK-05 (target policy) — folded into the OQ-SK-01 decision; scope-gate convention pinned in Phase 1 - [ ] OQ-SK-06 (noq client) — research pass (trait-shape comparison,