Files
alksocks/docs/research/phase-0.md
T
glm-5.3-flash 4c641ea9c0 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)
2026-09-12 09:19:54 +00:00

30 KiB

status, last_updated
status last_updated
draft 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_authread_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 supportnew_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 sideSocks5Stream<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 surfaceReplyError (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 UdpSenders 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 ArcBox 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 codecsocks5.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-socks5src/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