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:
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user