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
+29 -20
View File
@@ -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
+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,