docs: three-component split; fork-first (vendor-and-vet) posture; consumer-side bind

- 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)
This commit is contained in:
2026-09-12 10:21:29 +00:00
parent 216a5e965e
commit 766e489f99
2 changed files with 136 additions and 52 deletions
+107 -32
View File
@@ -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<Box<dyn UdpSender>>`
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<Box<dyn UdpSender>>` replaces
`create_io_poller` + `try_send`, and the constructor takes
`Box<dyn AsyncUdpSocket>` instead of `Arc<dyn AsyncUdpSocket>`.
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,