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
|
(`rt`, `sync`, `io-util`, `macros`, `time`) — **do NOT use `features
|
||||||
= ["full"]`** — and keep socket/platform I/O feature-gated. Note:
|
= ["full"]`** — and keep socket/platform I/O feature-gated. Note:
|
||||||
`fast-socks5` itself uses `tokio::net` and `socket2` unconditionally;
|
`fast-socks5` itself uses `tokio::net` and `socket2` unconditionally;
|
||||||
whether it compiles wasm-clean, needs a fork, or the wrapper
|
the resolved posture (OQ-SK-04) is fork-first: vendor the needed
|
||||||
reimplements the SOCKS5 state machine over `tokio::io` generics is an
|
subset and feature-gate native-net pieces (the alkhttp crate shows a
|
||||||
open question (see `docs/research/phase-0.md` OQ-SK-04) — do not
|
native-only crate is possible, but wasm-clean remains the preferred
|
||||||
assume either way until it is resolved.
|
posture here). Run the wasm check whenever a non-backend module
|
||||||
|
changes.
|
||||||
|
|
||||||
5. **Wire format is stable** — the SOCKS5 protocol itself is RFC 1928
|
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
|
(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
|
The SOCKS5 server must run without binding any port: the RFC 1928
|
||||||
conversation is carried inside a channels data channel, and target
|
conversation is carried inside a channels data channel, and target
|
||||||
dials happen on the producing side (or hop further through
|
dials happen on the producing side (or hop further through
|
||||||
alktunnels/alksocks). A local bind (a genuine `socks5://host:port`
|
alktunnels/alksocks). Local binds exist only as explicit, optional
|
||||||
endpoint) is an optional assembly-layer/feature-gated capability.
|
assembly-layer shapes on either side: the producer may serve from a
|
||||||
UDP ASSOCIATE is the hard case: RFC 1928 replies with a UDP relay
|
real listener (a feature-gated local backend), and the consumer may
|
||||||
address and the client sends datagrams to it, which seems to demand a
|
expose the resource on a local port (the ssh `-D` front door that
|
||||||
bind — how the relay address is virtualized (or the associate shape
|
vanilla clients — curl, browsers, tun2proxy — point at). The
|
||||||
adapted) is a Phase 0 question (OQ-SK-03). Binding decisions belong
|
protocol itself never binds. UDP ASSOCIATE is the hard case: RFC
|
||||||
to the caller (assembly layer), never the protocol crate.
|
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
|
10. **Backpressure and limits are inherited, not redefined** — bounded
|
||||||
per-channel buffers, the 256-channel per-connection cap, monotonic
|
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
|
One ALPN per protocol. The ALPN string is wire-stable once
|
||||||
published — decide via ADR before the first consumer.
|
published — decide via ADR before the first consumer.
|
||||||
|
|
||||||
17. **Upstream is ours — make asks early.** We own `fast-socks5`
|
17. **Upstream is ours — fork-first, PR as courtesy.** We own
|
||||||
(`/workspace/fast-socks5`, v1.0.0, MIT) and alkcall. If the wrapper
|
`fast-socks5` (`/workspace/fast-socks5`, v1.0.0, MIT) and alkcall.
|
||||||
needs a change upstream (a typestate hook, a generic split, a new
|
For changes this crate needs upstream, the preferred posture is:
|
||||||
event), file it and land it there rather than working around it
|
make the change as a minimal fork and use it — offer it upstream
|
||||||
locally — the alktunnels precedent (the E-01/E-02 sweep, filed from
|
as a PR and merge upstream if they want it, but never wait on
|
||||||
its Phase 0 and landed in alkcall 0.5.0 within a day). Keep
|
approval (see `docs/research/phase-0.md` OQ-SK-04's resolved
|
||||||
`/workspace/fast-socks5` as the reference checkout; if upstream
|
posture). For alkcall (the actively co-developed substrate) the
|
||||||
diverges from what we publish, note the fork point in the research
|
alk* precedent still applies: file asks early and land them there
|
||||||
docs.
|
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
|
## 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
|
conversation rides inside a channels data channel like any other produced
|
||||||
resource.
|
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:
|
Guiding principles, inherited from the alk* family:
|
||||||
|
|
||||||
1. **"ALPN as a service," not "a server."** The SOCKS5 service is a
|
1. **"ALPN as a service," not "a server."** The SOCKS5 service is a
|
||||||
produced, ACL-scoped resource on a channels connection — the same
|
produced, ACL-scoped resource on a channels connection — the same
|
||||||
shape as an alktty terminal or an alktunnels TCP tunnel. A genuine
|
shape as an alktty terminal or an alktunnels TCP tunnel. Kernel
|
||||||
`socks5://host:port` kernel-socket listener is one optional
|
binds exist only as explicit, optional assembly-layer shapes on
|
||||||
assembly-layer shape (a feature-gated local backend), never the
|
*either side* — the producer may serve from a real listener (a
|
||||||
protocol's home. This inverts the usual SOCKS5 deployment posture and
|
feature-gated local backend, component 1's optional shape), and the
|
||||||
is the crate's defining requirement.
|
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
|
2. **The `-D` conclusion, realized.** alktunnels' Phase 0 settled that
|
||||||
`-D` composes at the assembly layer: "`-D` is just tunnel a socks5
|
`-D` composes at the assembly layer: "`-D` is just tunnel a socks5
|
||||||
connection"; target selection lives in the SOCKS5 protocol at the
|
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
|
is generic over `T: AsyncRead + AsyncWrite + Unpin`, and its
|
||||||
interception points (`run_tcp_proxy`, `run_udp_proxy_custom`,
|
interception points (`run_tcp_proxy`, `run_udp_proxy_custom`,
|
||||||
`transfer`) accept any such `T`. The channels adapter feeds the state
|
`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
|
wrapper must not leak either substrate into the protocol layer
|
||||||
(alktty `TtyBackend` / alktunnels pump-halves inversion-point
|
(alktty `TtyBackend` / alktunnels pump-halves inversion-point
|
||||||
precedent).
|
precedent).
|
||||||
@@ -63,6 +91,16 @@ Guiding principles, inherited from the alk* family:
|
|||||||
QUIC rides UDP ASSOCIATE, validated by the quinn-proxy POC) and the
|
QUIC rides UDP ASSOCIATE, validated by the quinn-proxy POC) and the
|
||||||
server side (the `-D` capability alktunnels deferred). This crate
|
server side (the `-D` capability alktunnels deferred). This crate
|
||||||
rehomes both halves behind one protocol crate and improves them.
|
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
|
## 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
|
command in AGENTS.md (the expected-failure note is already written
|
||||||
there).
|
there).
|
||||||
|
|
||||||
**Fork vs reimplement vs wrap — collapses onto this OQ (2026-09-12
|
**Fork vs reimplement vs wrap — resolved as fork-first (2026-09-12
|
||||||
discussion).** The wrap surface was re-checked and **no structural fork
|
discussion, supersedes the earlier upstream-ask-first weighting).** The
|
||||||
is needed** for the channels path: CONNECT rides the interception points
|
wrap surface was re-checked and **no structural fork is needed** for the
|
||||||
as-is — `read_command` → own dial → `reply_success` → `transfer` (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
|
`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`
|
both sides of `transfer`. UDP is ours either way: `run_udp_proxy_custom`
|
||||||
takes a custom `transfer` closure for the relay half, `reply_success`
|
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
|
(`udp_bind_random_port`, socket2-based, called inside the default
|
||||||
handler before `run_udp_proxy_custom` even runs) — is in 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.
|
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
|
**Decision: fork-first, "fork" = inline in this crate (vendor-and-vet).**
|
||||||
upstream `net` feature-gate ask) if the gating lands cleanly;
|
Do the feature gating as a minimal, minimal-diff fork and use it —
|
||||||
reimplement if the gating turns out structural.** The reimplementation
|
offering it upstream as a PR (merge if they want it; we carry the fork
|
||||||
is genuinely small — RFC 1928 over `tokio::io` generics is the
|
regardless). Rationale: depending on someone else's approval cadence is
|
||||||
alktunnels-codec scale of code — and fast-socks5 would remain the
|
the worst part of the upstream-ask path; the long-term cost of
|
||||||
differential-test oracle for RFC edge cases (reply-code mapping,
|
maintaining a minimal fork of a well-written lib trends toward zero
|
||||||
domain addressing, fragmentation) either way. A full fork (divergent
|
(modern tooling/AI assistance makes small-diff rebase-and-review cheap);
|
||||||
copy we publish) is the least attractive branch: it duplicates the
|
and the changes needed here are genuinely small (feature-gate
|
||||||
maintenance without buying anything the two options above don't.
|
`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
|
### 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)
|
### 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
|
**Scope clarification (2026-09-12):** the SOCKS client wrapper is
|
||||||
socket trait over a SOCKS5 UDP association." quinn 0.11 is proven
|
component 3 of the three-component split (§Vision) — a standalone
|
||||||
(quinn-proxy POC, ADR-090). noq (iroh's fork, v1.2.0) has the same
|
client library for downstream users (alknet) that must work with *any*
|
||||||
extension point (`AsyncUdpSocket`, `Endpoint::new_with_abstract_socket`)
|
RFC 1928 server, not just one this crate produces. The motivating case
|
||||||
but the trait shape changed: `create_sender() -> Pin<Box<dyn UdpSender>>`
|
is iroh's privacy posture: iroh relays (and the peer) see the client's
|
||||||
replaces `create_io_poller` + `try_send`, and the constructor takes
|
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>`.
|
`Box<dyn AsyncUdpSocket>` instead of `Arc<dyn AsyncUdpSocket>`.
|
||||||
|
|
||||||
Questions:
|
Questions:
|
||||||
@@ -577,14 +644,22 @@ Candidate reading for the research specialist (to be expanded):
|
|||||||
- tun2proxy — `/workspace/tun2proxy` `src/udpgw.rs` and its SOCKS5
|
- tun2proxy — `/workspace/tun2proxy` `src/udpgw.rs` and its SOCKS5
|
||||||
files (`socks.rs`, `proxy_handler.rs`): UDP-over-stream framing and
|
files (`socks.rs`, `proxy_handler.rs`): UDP-over-stream framing and
|
||||||
flow-table prior art (analyzed in alktunnels phase-0-findings; the
|
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
|
- iroh — `/workspace/iroh` `iroh/Cargo.toml` (the noq dependency
|
||||||
posture: `noq = "1.2.0"` from the n0 workspace) — context for
|
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)
|
## 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
|
- [ ] Prior-art pass complete: fast-socks5 surface verified (§Prior
|
||||||
art), alktunnels/alknet/noq lineage mapped, anti-prior-art list
|
art), alktunnels/alknet/noq lineage mapped, anti-prior-art list
|
||||||
written
|
written
|
||||||
@@ -596,9 +671,9 @@ Candidate reading for the research specialist (to be expanded):
|
|||||||
the phase-model-vs-alktty-demux analysis); resolve via research
|
the phase-model-vs-alktty-demux analysis); resolve via research
|
||||||
+ POC #2, ADR before the first consumer
|
+ POC #2, ADR before the first consumer
|
||||||
- [ ] OQ-SK-04 (fast-socks5 wasm) — verify empirically (POC #4);
|
- [ ] OQ-SK-04 (fast-socks5 wasm) — verify empirically (POC #4);
|
||||||
the fork-vs-reimplement-vs-wrap decision collapses onto this
|
resolved posture: fork-first (vendor-and-vet, PR as courtesy) —
|
||||||
OQ; likely an upstream feature-gating ask; file early per
|
carry the minimal fork regardless of upstream's decision;
|
||||||
AGENTS.md convention 17
|
wasm-drop (alkhttp precedent) is the escape hatch, not the plan
|
||||||
- [ ] OQ-SK-05 (target policy) — folded into the OQ-SK-01 decision;
|
- [ ] OQ-SK-05 (target policy) — folded into the OQ-SK-01 decision;
|
||||||
scope-gate convention pinned in Phase 1
|
scope-gate convention pinned in Phase 1
|
||||||
- [ ] OQ-SK-06 (noq client) — research pass (trait-shape comparison,
|
- [ ] OQ-SK-06 (noq client) — research pass (trait-shape comparison,
|
||||||
|
|||||||
Reference in New Issue
Block a user