docs: repo init — gitignore, AGENTS.md, Phase 0 draft
- .gitignore (target/, node_modules/, .worktrees/, Cargo.lock) - AGENTS.md: project conventions adapted from alktunnels for the SOCKS5 wrapper crate (no-forced-binding posture, fast-socks5 genericity, producer/consumer vocabulary, upstream-asks rule, verification commands with the wasm caveat for OQ-SK-04) - docs/research/phase-0.md: Phase 0 draft — vision, prior art (fast-socks5 surface, alktunnels -D conclusion, alknet ADR-090 client, noq trait-shape divergence), open questions OQ-SK-01..07, POC candidates, convergence checklist - sdd_process.md: package name fixed (alkcall -> alksocks)
This commit is contained in:
@@ -0,0 +1,551 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-09-12
|
||||
---
|
||||
|
||||
# alksocks — Phase 0 (Exploration)
|
||||
|
||||
This document captures Phase 0 (Exploration) for the `alksocks` crate:
|
||||
vision, guiding principles, prior art, open questions (OQ-SK-01..NN), and
|
||||
POC candidates. Phase 0's objective per `docs/sdd_process.md`: *capture
|
||||
vision and guiding principles; research options; validate approaches;
|
||||
converge on a recommended approach.* It is the input to Phase 1
|
||||
(Architecture), where the Architect will produce `docs/architecture/`
|
||||
specs, ADRs, and the open-questions tracker.
|
||||
|
||||
Drafted 2026-09-12, emerging from the initial setup discussion. The crate
|
||||
is the sibling of alktty (`alk/tty`) and alktunnels (`alk/tunnel`) on the
|
||||
alkcall substrate; where alktunnels realizes the `-L`/`-R` forwarding
|
||||
flavors, alksocks realizes the `-D` flavor (dynamic / SOCKS5 proxy) —
|
||||
the one alktunnels explicitly deferred to composition.
|
||||
|
||||
## Vision and guiding principles
|
||||
|
||||
**One sentence:** a SOCKS5 (RFC 1928) producer/consumer protocol crate on
|
||||
alkcall channels — an arbitrary-egress proxy service that never binds a
|
||||
port unless explicitly configured to, wrapping `fast-socks5` so the RFC
|
||||
conversation rides inside a channels data channel like any other produced
|
||||
resource.
|
||||
|
||||
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.
|
||||
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
|
||||
producing side, governed by the same op-level ACL as the resource.
|
||||
This crate is that conclusion's realization as a first-class protocol
|
||||
crate, not an assembly-layer afterthought.
|
||||
3. **Wrap `fast-socks5`, preserve its genericity.** `fast-socks5`'s
|
||||
explicit typestate server API (`Socks5ServerProtocol<T, states::*>`)
|
||||
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
|
||||
wrapper must not leak either substrate into the protocol layer
|
||||
(alktty `TtyBackend` / alktunnels pump-halves inversion-point
|
||||
precedent).
|
||||
4. **Producer/consumer vocabulary.** Both sides of a channels connection
|
||||
can initiate; a producer registers openable SOCKS5 channels
|
||||
(`ChannelCore::register_openable`), a consumer opens them and speaks
|
||||
RFC 1928 inside. RFC 1928's own client/server roles keep their RFC
|
||||
names *inside* the protocol layer, but crate-level docs and API use
|
||||
producer/consumer. Avoid "SOCKS5 server" as a crate-level noun.
|
||||
5. **Extract-and-improve from alknet.** alknet's SOCKS5 story is the
|
||||
direct ancestor: the client side (`alknet-client/src/socks5.rs`,
|
||||
ADR-090 — `Socks5UdpSocket` implements `quinn::AsyncUdpSocket` so
|
||||
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.
|
||||
|
||||
## What is already settled
|
||||
|
||||
The foundation is POC-validated and ADR-pinned upstream; this crate does
|
||||
not start from zero. It inherits:
|
||||
|
||||
- **The channels data path** — demux→Connection→handler→mux, validated
|
||||
by the alknet-channels POC and production alktty/alktunnels. The SOCKS5
|
||||
payload is raw bytes inside a channels data channel's `BiStream`;
|
||||
channels strips its 8-byte header transparently (alknet ADR-093 /
|
||||
alkcall ADR-035). SOCKS5 CONNECT is one `BiStream` per session — the
|
||||
RFC conversation *is* the stream; no sub-demux, no framing, no flow
|
||||
key (one channel per SOCKS5 session).
|
||||
- **The two-pump data plane** — the CONNECT proxy loop is the canonical
|
||||
two-pump shape; `alkcall::channels::pump_bidi` (ADR-050) is pinned
|
||||
upstream and alktunnels-validated. Use it; do not hand-roll.
|
||||
- **The establishment story** — session opens use
|
||||
`register_openable_with_establisher` (alkcall ADR-049 + amendment 2):
|
||||
the establisher runs as an awaited, bounded establishment phase; a
|
||||
refused session is a typed `channel:open_failed` call error
|
||||
(`reason ∈ dial_failed / unknown_resource / resource_shortage /
|
||||
handler_error / timeout`), never a phantom channel. The negotiate/
|
||||
auth half of the RFC conversation still runs in-stream over the
|
||||
`BiStream` — establishment covers only the producer's accept policy,
|
||||
not the RFC handshake.
|
||||
- **The producer/consumer model** — producer registers openable channels
|
||||
(authorization for free via `AccessControl`); consumer opens them via
|
||||
`ChannelClient` (alkcall ADR-037, ADR-043). Connection direction is
|
||||
independent of service direction.
|
||||
- **The relay/hub story** — a SOCKS5 resource traverses alkcall hub
|
||||
relays like any produced resource; the hub's terminate-and-re-produce
|
||||
proxy (alktunnels' refinement of alkcall ADR-042) applies per hop, and
|
||||
a SOCKS5 resource is exactly the "further downstream" shape alktunnels
|
||||
described. No socks5-specific relay work.
|
||||
- **The identity seam** — alkcall 0.7.0's CF-005/CF-006: the per-call
|
||||
opener identity arrives on the open-op hooks. Auth mapping (identity →
|
||||
SOCKS5 credentials/ACL, if any) happens there, not via in-band SOCKS
|
||||
auth, unless RFC-facing username/password is needed for vanilla SOCKS5
|
||||
clients (OQ-SK-02).
|
||||
- **The backend inversion point pattern** — substrate-specific types
|
||||
confined to feature-gated backend modules, injected at the assembly
|
||||
layer (alktty `TtyBackend` / alktunnels no-trait precedent).
|
||||
- **The wasm-clean default crate** — protocol-only code should compile
|
||||
to `wasm32-unknown-unknown` (alktty/alktunnels precedent). Caveat:
|
||||
`fast-socks5` itself is not yet known to be wasm-clean (OQ-SK-04).
|
||||
|
||||
## Prior art
|
||||
|
||||
### alktunnels Phase 0 — the closest sibling
|
||||
|
||||
`/workspace/@alkdev/alktunnels/docs/research/phase-0-findings.md`. The
|
||||
load-bearing conclusions this crate inherits:
|
||||
|
||||
- **Hub-owns-the-connection model** — role follows the resource;
|
||||
whoever can reach the target is the producer, whoever wants the bytes
|
||||
is the consumer. The "exposed port" is a virtual, ACL-scoped resource,
|
||||
not a bind. For alksocks: the producer is the side that dials targets
|
||||
(the egress side); the consumer is the side that wants proxied
|
||||
egress. "A SOCKS5 service is the same shape [as a produced tunnel
|
||||
resource], further downstream" is alktunnels' own wording — this
|
||||
crate exists to make it literal.
|
||||
- **`-D` composes at the assembly layer** — the original half-answer,
|
||||
now with the mechanism named: a SOCKS5 server at some assembly layer
|
||||
is just a consumer opening channels with per-connection dynamic
|
||||
targets. The refinement this crate adds: the SOCKS5 *service* is
|
||||
itself a produced resource (producer half), so a vanilla SOCKS5 client
|
||||
(curl, a browser, ssh -D's own client half) can attach through the
|
||||
optional local backend, and channels-native consumers get the same
|
||||
service in-band. Both halves wrap the same protocol layer.
|
||||
- **Target policy = resource policy.** alktunnels OQ-TN-08 dissolved
|
||||
dynamic-target policy: whatever ACL governs the socks5 resource
|
||||
governs everything reachable through it, plus whatever policy the
|
||||
SOCKS5 implementation itself applies downstream. No target allowlists
|
||||
in the base crate unless Phase 1 wants them (OQ-SK-05).
|
||||
- **The codec conclusion does NOT transfer.** alktunnels needed
|
||||
length-framing only for datagram substrates; a SOCKS5 CONNECT session
|
||||
is a byte stream end to end (RFC 1928 defines its own framing on the
|
||||
wire) — the channel payload is pure pass-through, 0 B overhead over
|
||||
the channels 8-byte header. UDP ASSOCIATE's datagram stage is the
|
||||
exception (OQ-SK-03).
|
||||
|
||||
### fast-socks5 — the implementation to wrap
|
||||
|
||||
`/workspace/fast-socks5` (v1.0.0, MIT, we own upstream). Read the source
|
||||
before designing against it. Key surface points, verified:
|
||||
|
||||
- **Explicit typestate server API** (`src/server.rs`):
|
||||
`Socks5ServerProtocol<T, states::Opened|Authenticated|CommandRead>`,
|
||||
generic over `T: AsyncRead + AsyncWrite + Unpin`. Flow:
|
||||
`start(inner)` → `negotiate_auth(&methods)` →
|
||||
`finish_auth()` / `accept_no_auth` / `accept_password_auth` →
|
||||
`read_command()` → (`reply_success`, `reply_error`). The legacy
|
||||
`Socks5Server`/`Socks5Socket`/`Incoming` API (binds a `TcpListener`)
|
||||
is deprecated — the wrapper uses the explicit API only.
|
||||
- **Auth surface** — the `AuthMethod<T>` trait (metadata: `method_id`,
|
||||
`new`) + `AuthMethodSuccessState<T>` (carry the socket back out);
|
||||
`StandardAuthentication` enum for NoAuth + Password with static
|
||||
dispatch; custom methods implement the trait. Username/password check
|
||||
is a closure (`accept_password_auth`). The auth *decision* is where
|
||||
the alkcall identity seam plugs in (OQ-SK-02).
|
||||
- **Command handling is swappable** — the interception points:
|
||||
- `run_tcp_proxy(proto, addr, timeout, nodelay)` — dials, replies,
|
||||
then `transfer(inbound, outbound)` (a `copy_bidirectional` wrapper,
|
||||
itself generic). This is where a channels-native dial replaces the
|
||||
`tokio::net` dial: the wrapper's producer half intercepts here,
|
||||
dials via alktunnels/alkcall primitives (or its own dial policy),
|
||||
and returns the `T` back.
|
||||
- `run_udp_proxy_custom(proto, addr, peer_bind_ip, reply_ip, transfer)`
|
||||
— the customizable UDP ASSOCIATE handler: the wrapper supplies a
|
||||
custom `transfer` closure that owns the relay half (OQ-SK-03).
|
||||
- `transfer(inbound, outbound)` — plain two-pump copy; a channels
|
||||
`BiStream` is a legal `T` on either side.
|
||||
- **UDP support** — `new_udp_header(target)` / `parse_udp_request(buf)`
|
||||
are public and pure (no socket I/O) — the SOCKS5 UDP datagram header
|
||||
codec is reusable without the `Socket2`-based relay machinery. The
|
||||
default `run_udp_proxy` binds two kernel sockets (`Socket2`, random
|
||||
ports) — the *binds are in the default handler, not in the protocol*,
|
||||
which is exactly the seam the no-bind requirement needs.
|
||||
- **Client side** — `Socks5Stream<S>` (generic over the backing socket;
|
||||
`use_stream` upgrades any `AsyncRead + AsyncWrite + Unpin` already
|
||||
connected) and `Socks5Datagram<S>` (UDP associate; also accepts a
|
||||
caller-supplied socket via `use_socket`). `Socks5Stream` impls
|
||||
`AsyncRead + AsyncWrite` itself, so a consumer session over a
|
||||
channels `BiStream` is the intended use, not a hack. Note:
|
||||
`Socks5Stream::connect` convenience constructors dial
|
||||
`tokio::net::TcpStream` and are non-wasm; `use_stream` is the
|
||||
substrate-free path.
|
||||
- **Error surface** — `ReplyError` (the RFC reply codes, `as_u8`/
|
||||
`from_u8`) and `SocksError`; `SocksServerError` on the explicit API.
|
||||
Map these faithfully; never collapse a refusal into a generic error.
|
||||
- **`router.rs` example** — the "conditional interception" template: a
|
||||
server that inspects the command/target, then decides whether to
|
||||
proxy, refuse, or handle in-process. Structurally the shape of the
|
||||
channels open handler (inspect params → dial or refuse) and of the
|
||||
alknet ADR-090 client (hand the established stream to quinn).
|
||||
|
||||
### alknet's SOCKS5 client + the quinn-proxy POC — the client-side ancestor
|
||||
|
||||
`/workspace/@alkdev/alknet/crates/alknet-client/src/socks5.rs` (ADR-090)
|
||||
implements `Socks5UdpSocket: quinn::AsyncUdpSocket` — a SOCKS5 UDP
|
||||
ASSOCIATE tunnel wrapped as the socket QUIC polls. The quinn-proxy POC
|
||||
(`/workspace/@alkdev/alknet/docs/research/quinn-quic-proxy/findings.md`)
|
||||
validated the approach end-to-end (5/5 clean runs) against quinn 0.11:
|
||||
one public trait (`AsyncUdpSocket`), one public constructor
|
||||
(`Endpoint::new_with_abstract_socket`), no fork. Known limitations
|
||||
there, likely inherited: ECN is lost through the SOCKS5 header; the
|
||||
proxy must support UDP ASSOCIATE; `may_fragment() == true` disables
|
||||
path MTU discovery.
|
||||
|
||||
This crate's client half rehomes that work (cleaned up, `ClientDialError`
|
||||
→ thiserror types, and the datagram codec sourced from fast-socks5
|
||||
rather than hand-rolled inline). The open question is noq (OQ-SK-06).
|
||||
|
||||
### noq — the quinn fork the client story must also fit
|
||||
|
||||
`/workspace/noq` (v1.2.0; iroh's fork of quinn — iroh depends on
|
||||
`noq = "1.2.0"`). Surface verified:
|
||||
|
||||
- `noq::AsyncUdpSocket` exists (`noq/src/runtime/mod.rs:44`) but with a
|
||||
**different shape** than quinn 0.11: `create_io_poller` +
|
||||
`try_send` are replaced by `create_sender() -> Pin<Box<dyn UdpSender>>`
|
||||
with `poll_send(transmit, cx)` — a sender-object split (any number of
|
||||
`UdpSender`s per socket, each holding its own waker).
|
||||
- `RecvMeta` gained fields (`interface_index`, `timestamp`) but stays
|
||||
default-constructible; `Transmit` is unchanged in the fields the
|
||||
SOCKS5 wrapper touches (`destination`, `contents`, `ecn`, `src_ip`,
|
||||
`segment_size`).
|
||||
- `Endpoint::new_with_abstract_socket` exists (`noq/src/endpoint.rs:162`)
|
||||
with the same doc-comment intent, but takes
|
||||
`Box<dyn AsyncUdpSocket>` rather than `Arc<dyn AsyncUdpSocket>` — the
|
||||
poller-removal reshuffle also changed the ownership shape.
|
||||
- The alknet `Socks5UdpSocket` does **not** drop in unchanged: the impl
|
||||
must be rewritten against `create_sender`/`poll_send`, and the
|
||||
`Arc`→`Box` change ripples into construction. Whether one crate can
|
||||
serve both quinn and noq behind feature flags (shared core, two thin
|
||||
trait-impl shells) or whether the shapes have diverged enough to
|
||||
justify two impls is the research question (OQ-SK-06). iroh's own
|
||||
adoption makes noq support the practically-important target.
|
||||
|
||||
### Anti-prior-art (what NOT to carry over)
|
||||
|
||||
- **The alknet client's hand-rolled SOCKS5 codec** —
|
||||
`socks5.rs` inlines the greeting/auth/CONNECT/ASSOCIATE byte logic
|
||||
twice (handshake for UDP, again for CONNECT). fast-socks5's client
|
||||
types and `new_udp_header`/`parse_udp_request` supersede it; the
|
||||
wrapper should not vendor a second codec.
|
||||
- **`Socks5Server` (the legacy bind-based API)** — deprecated upstream;
|
||||
never the default path here.
|
||||
- **In-band SOCKS auth as the primary auth story** — alkcall's identity
|
||||
seam (CF-005/CF-006) authorizes at the open-op layer. In-band RFC
|
||||
username/password remains available only for vanilla-client
|
||||
compatibility (OQ-SK-02).
|
||||
|
||||
## Open Questions
|
||||
|
||||
These are the design questions Phase 0 must resolve (or explicitly
|
||||
defer) before the architecture spec. Numbered OQ-SK-01.. so they can be
|
||||
referenced, tracked, and promoted into `docs/architecture/
|
||||
open-questions.md` in Phase 1. Half-answers and hunches are marked as
|
||||
such — the point of this document is to hold them without forcing
|
||||
premature decisions.
|
||||
|
||||
### OQ-SK-01: Producer dial policy — what dials the target?
|
||||
|
||||
When the producer's SOCKS5 state machine reads a CONNECT command, the
|
||||
target dial must happen *somewhere*. Options:
|
||||
|
||||
- **Option A: dial via alktunnels** — the producer composes with a
|
||||
local alktunnels consumer: the SOCKS5 handler opens an `alk/tunnel`
|
||||
channel naming the target, and the two-pump data plane is
|
||||
tunnel-channel ↔ SOCKS5-`BiStream`. Maximum composition ("further
|
||||
downstream" made literal), but adds a runtime dependency and a hop.
|
||||
- **Option B: dial directly** — the producer dials the target itself
|
||||
(`TcpStream` behind the `local` feature, or an injected dial
|
||||
callback). No alktunnels dependency; the dial policy (allowlists,
|
||||
routing through another alksocks hop) is the caller's callback.
|
||||
- **Option C: injected dialer trait** — a `Dialer`-shaped trait
|
||||
(`async fn dial(target) -> impl AsyncRead + AsyncWrite`) with
|
||||
alktunnels and local-TCP implementations behind features. Middle
|
||||
ground; the trait is a backend-inversion-point decision (compare
|
||||
alktunnels OQ-TN-05's resolution: *no trait*, a function producing
|
||||
boxed halves sufficed).
|
||||
|
||||
Considerations: substrate-agnostic-by-construction (AGENTS.md
|
||||
convention 7) favors injection over hardwiring; the "hub terminates and
|
||||
re-produces" story means a hub-hop SOCKS5 resource's dialer is just
|
||||
"open a channel further downstream," which is Option A's shape — so
|
||||
whatever is decided must not *prevent* A when composing. Hunch: a dial
|
||||
callback/trait at the protocol layer, with alktunnels and local-TCP
|
||||
providers feature-gated — but alktunnels' no-trait precedent warns
|
||||
against a trait unless two real implementations converge; decide with
|
||||
the Phase 1 spec.
|
||||
|
||||
### OQ-SK-02: Auth mapping — identity seam vs in-band SOCKS auth
|
||||
|
||||
The producer's open-op hooks receive the per-call opener identity
|
||||
(alkcall CF-005/CF-006). The SOCKS5 RFC conversation also carries its
|
||||
own optional username/password auth (RFC 1929). Questions:
|
||||
|
||||
- Is the identity seam the *only* auth story (the open op is
|
||||
authorized; the in-band handshake is skipped via
|
||||
`skip_auth_this_is_not_rfc_compliant` or a no-op NoAuth), with
|
||||
RFC-facing username/password available only for the optional local
|
||||
backend (where vanilla SOCKS5 clients connect)?
|
||||
- If both exist, how do they compose — does in-band auth *replace* the
|
||||
channel identity for target-policy purposes, or is it a second gate?
|
||||
- Does the wrapper need a fast-socks5 `AuthMethod` implementation that
|
||||
consults the alkcall `AuthContext`/`Identity` (filed upstream if the
|
||||
hook shape doesn't fit — AGENTS.md convention 17)?
|
||||
|
||||
Hunch: identity-at-the-open-op is the primary gate for
|
||||
channels-native consumers; in-band username/password exists for the
|
||||
local-backend path only. Needs a decision (and possibly an upstream
|
||||
ask) in Phase 1.
|
||||
|
||||
### OQ-SK-03: UDP ASSOCIATE without binding — the hard case
|
||||
|
||||
RFC 1928 §7: the client sends UDP ASSOCIATE over the TCP control
|
||||
connection; the server replies with a UDP relay address (`BND.ADDR`/
|
||||
`BND.PORT`); the client then sends UDP datagrams (SOCKS5 UDP header +
|
||||
payload) *to that address*. On channels there is no UDP relay socket —
|
||||
the datagram stage must ride a channel. Sub-questions:
|
||||
|
||||
- **Where does the datagram stage live?** Options:
|
||||
- **Same channel, extended protocol** — after ASSOCIATE, the channel's
|
||||
`BiStream` carries length-prefixed datagrams (the alktunnels UDP
|
||||
codec shape, `[len: u16 BE]` per datagram; 65507 < 65535 so u16
|
||||
suffices). The reply to the client rewrites `BND.ADDR`/`BND.PORT`
|
||||
to a sentinel that means "same channel" — but vanilla SOCKS5
|
||||
clients will literally `sendto()` that address, so this shape only
|
||||
works for *wrapper-aware* client halves (the crate's own consumer
|
||||
session, or an assembly layer bridging a real UDP socket).
|
||||
- **Second channel for the datagram stage** — the producer
|
||||
establishes the association on channel 1, then the client opens a
|
||||
second channel that becomes the relay. Keeps CONNECT-shaped
|
||||
`BiStream` semantics clean but adds an open-op round trip and
|
||||
needs correlation (params carry the association id?).
|
||||
- **Local-backend bridge only** — UDP ASSOCIATE is offered only
|
||||
through the optional local backend (which binds a real UDP relay
|
||||
socket as the assembly layer's explicit choice), never
|
||||
channels-natively. Simplest; weakens the "ALPN as a service"
|
||||
story for UDP.
|
||||
- **What does `BND.ADDR`/`BND.PORT` reply contain on the channels
|
||||
path?** RFC says the relay address; a channels path has none.
|
||||
fast-socks5's `run_udp_proxy_custom` lets the wrapper supply the
|
||||
reply and the relay half — the seam exists upstream; the semantics
|
||||
are ours to define.
|
||||
- **Per-datagram addressing** — the SOCKS5 UDP header carries the
|
||||
destination per datagram (ATYP + addr + port), so one association
|
||||
multiplexes many endpoints: per-endpoint flow table producer-side
|
||||
(the alknet `Socks5UdpSocket` model, and the udpgw prior art
|
||||
alktunnels documented). Boundary preservation is mandatory (empty
|
||||
datagram ≠ EOF — alktunnels F-2's lesson transfers directly).
|
||||
- **Do vanilla (wrapper-unaware) clients need channels-native UDP at
|
||||
all?** If the only UDP ASSOCIATE consumers are wrapper-aware, the
|
||||
sentinel-reply shape is fine and no virtualized relay address is
|
||||
ever invented.
|
||||
|
||||
Hunch: wrapper-aware client halves ride the same-channel
|
||||
length-prefixed-datagram shape; vanilla clients get UDP ASSOCIATE only
|
||||
via the optional local backend. But this is the crate's largest
|
||||
unknown — a POC candidate (OQ-SK-07 #2), decided by an ADR before the
|
||||
first consumer (the datagram framing is wire-stable once published).
|
||||
|
||||
### OQ-SK-04: fast-socks5 wasm posture (blocks the wasm-clean invariant)
|
||||
|
||||
`fast-socks5` uses `tokio::net` (`TcpListener`, `TcpStream`,
|
||||
`UdpSocket`) and `socket2` unconditionally (`Cargo.toml`: tokio features
|
||||
`io-util, net, time, macros`; deps `socket2 = "0.5.8"`); `wasm32-
|
||||
unknown-unknown` has no `tokio::net`. The wrapper's own protocol layer
|
||||
can stay wasm-clean (the typestate API is generic over `T`), but
|
||||
depending on the crate at all may break `cargo check --target
|
||||
wasm32-unknown-unknown` at the dependency-graph level. Options:
|
||||
|
||||
- **Option A: upstream feature-gating ask** — add a feature to
|
||||
fast-socks5 that gates `tokio::net`/`socket2` behind a default-on
|
||||
`net` feature (like alktty's `local`), leaving the codecs, typestate
|
||||
machinery, and client `use_stream` path wasm-clean. We own upstream
|
||||
(AGENTS.md convention 17); the alk* precedent is to make asks early.
|
||||
- **Option B: protocol reimplementation** — the wrapper reimplements
|
||||
the SOCKS5 state machine over `tokio::io` generics (RFC 1928 is
|
||||
small); fast-socks5 remains the native-path implementation. Duplicates
|
||||
protocol logic; against the "wrap, don't fork" vision unless A fails.
|
||||
- **Option C: wasm drops off the invariant for this crate** — accept a
|
||||
native-only default. Against the alktty/alktunnels precedent;
|
||||
document as an ADR-worthy exception if forced.
|
||||
|
||||
Action: verify the breakage empirically (a minimal crate depending on
|
||||
fast-socks5, `cargo check --target wasm32-unknown-unknown`), then take
|
||||
Option A upstream if confirmed. This OQ gates the wasm verification
|
||||
command in AGENTS.md (the expected-failure note is already written
|
||||
there).
|
||||
|
||||
### OQ-SK-05: Target policy and egress scoping
|
||||
|
||||
SOCKS5 is arbitrary-egress by nature — the open gate plus the target
|
||||
policy are the security boundary (AGENTS.md convention 12). alktunnels
|
||||
resolved that whatever ACL governs the socks5 resource governs
|
||||
everything reachable through it; OQ-SK-01's dial policy is the
|
||||
mechanism. Residual questions for Phase 1:
|
||||
|
||||
- Does the base crate ship a per-target allowlist hook (producer-side
|
||||
policy injected at registration), or is target policy entirely the
|
||||
dialer's concern (the dial callback refuses)? The dialer-refuses
|
||||
shape avoids a second policy layer (alktunnels' resolution pattern);
|
||||
the allowlist-hook shape makes a common policy declarative.
|
||||
- Is there a `SOCKS5_OPEN_SCOPE` (scope-gate on the open op, alktty
|
||||
`TTY_OPEN_SCOPE` shape) — presumably yes, but the exact scope-string
|
||||
convention should follow alkcall's registry conventions.
|
||||
- Domain-form targets (RFC 1928 ATYP 0x03): resolve producer-side
|
||||
(fast-socks5's `dns_resolve` config), refuse, or pass through to the
|
||||
dialer unresolved? (DNS-on-the-producer is the SSH `-D` semantic;
|
||||
wasm producers may not have a resolver at all — another OQ-SK-04
|
||||
interaction.)
|
||||
|
||||
### 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
|
||||
`Box<dyn AsyncUdpSocket>` instead of `Arc<dyn AsyncUdpSocket>`.
|
||||
|
||||
Questions:
|
||||
|
||||
- Does this crate ship `Socks5UdpSocket` against noq (behind a feature
|
||||
like `noq`), quinn (behind `quinn`), or both? Both implies a shared
|
||||
substrate-free core (associate handshake, datagram codec, flow table)
|
||||
with two thin trait shells — feasible only if the shared core is
|
||||
genuinely trait-agnostic. iroh's adoption makes noq the
|
||||
practically-important target; alknet's precedent is quinn.
|
||||
- noq is not on crates.io at this version (iroh consumes it from the
|
||||
n0 workspace/git) — how does this crate depend on it (git dep? wait
|
||||
for publication? feature-gate so it is optional)? This may block
|
||||
`cargo publish --dry-run` for the client features; a publish-lean
|
||||
default (quinn optional, noq git-optional) may be needed.
|
||||
- Do the ECN/MTU limitations carry over (they should — the SOCKS5 UDP
|
||||
header has no ECN field), and does noq's `may_fragment` default
|
||||
(`true`) behave the same as quinn's?
|
||||
|
||||
This is a research question first (read noq's endpoint/driver loops;
|
||||
write the trait-shape comparison), then possibly a POC (OQ-SK-07 #3).
|
||||
|
||||
### OQ-SK-07: POC scope for what remains unvalidated
|
||||
|
||||
Candidates, in rough priority order (per the SDD process's "validate
|
||||
promising approaches"):
|
||||
|
||||
1. **Channels-native SOCKS5 CONNECT POC** — the producer half over a
|
||||
real alkcall channels connection: consumer opens a SOCKS5 channel,
|
||||
speaks RFC 1928 CONNECT (via fast-socks5's client types), producer's
|
||||
open handler runs the typestate machine over the `BiStream`, dials a
|
||||
local echo target, two-pump proxy loop completes. Validates: the
|
||||
`register_openable_with_establisher` fit, `pump_bidi` as the data
|
||||
plane, params shape (trivial — probably no params), and the
|
||||
`BiStream`-as-`T` genericity claim. This is the crate's core value
|
||||
proposition and the cheapest to validate (alktunnels' forward POC is
|
||||
the template).
|
||||
2. **UDP ASSOCIATE POC** — the OQ-SK-03 chosen shape, end to end: a
|
||||
wrapper-aware client associates, sends length-prefixed datagrams
|
||||
down the channel, producer relays to a real UDP endpoint. Validates:
|
||||
the sentinel-reply semantics, the datagram codec (fast-socks5's
|
||||
`new_udp_header`/`parse_udp_request` over the length framing), the
|
||||
flow table, and empty-datagram handling (alktunnels F-2 transfers).
|
||||
3. **noq `AsyncUdpSocket` impl POC** — the client-side story against
|
||||
noq 1.2: associate through a fast-socks5 server, wrap as
|
||||
`noq::AsyncUdpSocket`, complete a QUIC handshake. Derisks OQ-SK-06
|
||||
(the `create_sender`/`Box` shape changes) before the client spec is
|
||||
written. (The quinn variant is already proven by the quinn-proxy
|
||||
POC — re-validating it here is optional.)
|
||||
4. **fast-socks5 wasm check** — minimal crate, `cargo check --target
|
||||
wasm32-unknown-unknown`, confirm/inflect OQ-SK-04. Cheap; can fold
|
||||
into #1's worktree.
|
||||
|
||||
POC placement conventions (inherited from alktunnels): a POC that needs
|
||||
code from this repo runs in a worktree/branch (`.worktrees/research/
|
||||
<task-id>/` per the SDD process); a self-contained POC runs as a
|
||||
standalone crate in the global workspace with findings written into
|
||||
`docs/research/` here. Findings always land in `docs/research/`
|
||||
regardless of where the code lives.
|
||||
|
||||
## Survey / prior-art list
|
||||
|
||||
Candidate reading for the research specialist (to be expanded):
|
||||
|
||||
- RFC 1928 (SOCKS5) — the fixed protocol: method negotiation, CONNECT,
|
||||
BIND, UDP ASSOCIATE, reply codes. RFC 1929 (username/password).
|
||||
- fast-socks5 `/workspace/fast-socks5` — `src/server.rs` (typestate
|
||||
API, interception points, auth traits), `src/client.rs`
|
||||
(`Socks5Stream`/`Socks5Datagram`, `use_stream`), `src/lib.rs`
|
||||
(`new_udp_header`/`parse_udp_request`, `ReplyError`),
|
||||
`examples/router.rs` (conditional interception), `examples/
|
||||
custom_auth_server.rs`.
|
||||
- alktunnels — `docs/research/phase-0-findings.md` (the `-D`
|
||||
composition conclusion, hub-owns-the-connection, the UDP codec
|
||||
decision + F-2), `docs/architecture/` (params/ALPN/ACL ADR template),
|
||||
POC summaries (POC placement and findings conventions).
|
||||
- alkcall — `docs/architecture/decisions/` ADR-037/039/049/050 (channel
|
||||
ops, params-is-ALPN-specific, establishment, pump_bidi), ledger
|
||||
CF-005/CF-006 (identity seam), `src/channels/operations.rs`
|
||||
(`ChannelCore`, `OpenHandler`, `Establishment`, `ChannelPlan`),
|
||||
`src/channels/pump.rs`.
|
||||
- alknet — `crates/alknet-client/src/socks5.rs` (ADR-090, the client
|
||||
ancestor), `docs/research/quinn-quic-proxy/findings.md` (the POC
|
||||
findings: trait + constructor surface, ECN/MTU limitations,
|
||||
BotBrowser production precedent).
|
||||
- noq — `/workspace/noq`: `noq/src/runtime/mod.rs` (`AsyncUdpSocket`,
|
||||
`UdpSender`), `noq/src/endpoint.rs` (`new_with_abstract_socket`),
|
||||
`noq-udp/src/lib.rs` (`RecvMeta`, `Transmit`), `Cargo.toml` (workspace
|
||||
versioning / publication posture).
|
||||
- alktty — backend inversion point (`TtyBackend`), `TTY_OPEN_SCOPE`
|
||||
scope-gating shape, feature-gated `local` backend, wasm-clean
|
||||
default-crate verification commands.
|
||||
- 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).
|
||||
- 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.
|
||||
|
||||
## Convergence checklist (what Phase 0 must produce)
|
||||
|
||||
- [ ] Vision + guiding principles captured (this doc, §Vision)
|
||||
- [ ] Prior-art pass complete: fast-socks5 surface verified (§Prior
|
||||
art), alktunnels/alknet/noq lineage mapped, anti-prior-art list
|
||||
written
|
||||
- [ ] OQ-SK-01 (dial policy) — researched, half-answered (injected
|
||||
dialer hunch); decide in Phase 1 against the spec
|
||||
- [ ] OQ-SK-02 (auth mapping) — posture drafted (identity seam
|
||||
primary, in-band auth for the local backend); decide in Phase 1
|
||||
- [ ] OQ-SK-03 (UDP ASSOCIATE) — the shape space written; resolve via
|
||||
research + POC #2, ADR before the first consumer
|
||||
- [ ] OQ-SK-04 (fast-socks5 wasm) — verify empirically (POC #4);
|
||||
likely an upstream feature-gating ask; file early per AGENTS.md
|
||||
convention 17
|
||||
- [ ] 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,
|
||||
dependency posture) + POC #3 if the research is not decisive
|
||||
- [ ] Targeted POC(s) run + summaries in `docs/research/`
|
||||
(OQ-SK-07; #1 first — it validates the core value proposition)
|
||||
- [ ] Converge: recommended approach written up, ready to hand to the
|
||||
Architect for Phase 1
|
||||
Reference in New Issue
Block a user