--- status: draft last_updated: 2026-09-20 (Phase 1 initial draft — the OQ-SK ledger promoted into ADRs 001-012 and open-questions.md) --- # alksocks — Overview 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**, carrying the RFC conversation inside a channels data channel like any other produced resource. 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. This document covers the crate's purpose, the resource model in brief, dependencies, the ALPN, feature gates, and the module map; component details are in the sibling documents ([wire.md](wire.md), [producer.md](producer.md), [consumer.md](consumer.md), [vendor.md](vendor.md)). ## What `alksocks` registers the `alk/socks5` ALPN's open op on alkcall channels and provides the typed consumer sessions plus the optional front doors: - **The producer half** ([producer.md](producer.md)): the `channels/socks5/sub` open op (registered via `ChannelCore::register_openable_with_establisher`, alkcall ADR-047/049). The establisher is a *session gate* (validates the opener identity; the SOCKS5 target is unknown at open time); the open handler runs the RFC 1928 state machine in-stream over the channel's `BiStream` — the vendored fast-socks5 typestate ([vendor.md](vendor.md)) drives negotiation, command read, and replies; the dial/egress/accept seams (ADR-004) supply the I/O planes; `alkcall::channels::pump_bidi` (ADR-050) is the data plane for CONNECT and BIND's post-reply#2 stage. - **The consumer half** ([consumer.md](consumer.md)): a typed channels-native session that opens `channels/socks5/sub` and speaks RFC 1928 inside them (`Socks5Stream::use_stream`-shaped; the associate session and the BIND reply#2 reader). *Wrapper-aware* means a client that knows the in-band conventions (the sentinel BND.ADDR, in-band frames) rather than a vanilla RFC 1928 client. Optionally it exposes the service locally: the **front door** — a real `socks5://127.0.0.1:port` endpoint relaying each accepted connection into a channel (the ssh `-D` local-exposure shape; the path curl/browser/tun2proxy point at). - **The optional producer local backend**: the producer may serve from a real listener (feature-gated; the same door shape in the other direction — vanilla clients → a locally-served SOCKS5 resource). The defining posture (the crate's reason to exist as a separate crate): **the protocol never binds**. Kernel binds exist only as the two explicit, optional, feature-gated assembly shapes above; the binding decision belongs to the caller. ## Why - The `-D` conclusion, realized as a first-class protocol crate: alktunnels' Phase 0 settled that `-D` composes at the assembly layer ("tunnel a socks5 connection"); this crate makes the SOCKS5 *service* itself a produced, ACL-scoped resource, so both channels-native consumers and vanilla SOCKS5 clients get the same service in-band or through the front door. - The vpn-like endgame composition: user tunnels a produced SOCKS5 resource to a local port (the front door), points tun2proxy (`--proxy socks5://user:pass@host:port` natively) at it, and the whole host's traffic rides the alk* egress. This crate is the SOCKS5 leg; alktunnels carries the tunnel; tun2proxy closes the loop. - The substrate-agnostic protocol layer: the vendored typestate is generic over `T: AsyncRead + AsyncWrite + Unpin` — a channels `BiStream` drops in directly (POC #1), a `TcpStream` drops in at the local backends. Neither substrate leaks into the protocol layer (the alktty `TtyBackend` / alktunnels pump-halves inversion-point precedent). ## The Resource Model in Brief A SOCKS5 channel's life (full detail in [wire.md](wire.md) and [producer.md](producer.md)): 1. **Open (the establishment phase).** The consumer opens `channels/socks5/sub` with trivial params (ADR-001). The establisher is the session gate — it checks/records the opener identity and does session-level policy; it never sees a target. Failure is a typed `channel:open_failed` (alkcall ADR-049); no phantom channel. 2. **RFC conversation (the control plane, in-stream).** The RFC 1928 handshake (method negotiation → command → reply) runs in-stream over the `BiStream`, driven by the vendored typestate. Command dispatch: CONNECT/ASSOCIATE require `socks5:open`; BIND requires `socks5:bind` (the command gate, ADR-006). 3. **Data plane.** CONNECT: the dial seam (ADR-004) produces boxed halves; `pump_bidi` two-pumps channel ↔ target — raw pass-through, 0 B overhead. ASSOCIATE: the reply's BND.ADDR is a pure RFC handle (the sentinel in-band); the datagram stage rides `[len: u16 BE][addr + data]` frames (ADR-003, the handle swap) to the egress seam. BIND: reply#1 → quiet wait → exactly one accept (the accept seam) → reply#2 → pass-through (ADR-007). 4. **Teardown.** Per-command: CONNECT/BIND end on channel EOF (the channels sentinel); ASSOCIATE's association lifetime = channel lifetime; BIND's quiet wait is `select!`-single-shot with the client-gone teardown riding the accept future's drop. Ownership rides the handle structurally (ADR-004). Producer and consumer are roles, not sides: both ends of a channels connection can be both simultaneously (alkcall ADR-022/ADR-037). RFC 1928's own client/server roles keep their RFC names *inside* the protocol layer; crate-level vocabulary is producer/consumer. ## Dependencies ``` alksocks (default — wasm-clean) ├── alkcall 0.8.0 (core types: Connection, ProtocolHandler, BiStream, │ AuthContext, Identity, AccessControl, OwnershipProvider; │ channels: ChannelCore/ChannelClient, │ register_openable_with_establisher, pump_bidi, │ Establishment/ChannelPlan + reply projection, │ ServingConfig identity seam — CF-005/006) ├── tokio (wasm-clean subset: rt, sync, io-util, macros, time) ├── serde/serde_json (params schema), thiserror (error surface), tracing └── (no backend deps — sockets live in feature-gated modules) alksocks (net feature, default-on) — non-wasm └── adds: tokio/net, socket2, tokio-stream (the vendored subset's kernel-net gate; ADR-009) alksocks (local feature) — non-wasm by design └── adds: the first-party local backends + front doors (implies net) alksocks (composed feature) └── adds: alktunnels 0.2.0 (the direct op egress; the bind-first listen accept; ADR-008's composed providers) — its default crate is wasm-clean; the composed providers use only the ungated channels surface (verify with the wasm check) ``` Same posture as alktty/alktunnels: the default crate is protocol-only and wasm-clean; the vendored fast-socks5 subset rides the `net` gate (ADR-011); substrate I/O is feature-gated (ADR-009). `fast-socks5` is NOT a Cargo dependency — it is vendored in-tree (ADR-011). ## Feature Gates | Feature | Contents | Wasm | |---------|----------|------| | *(default)* | the vendored `T`-generic subset (codecs, typestate), wire codec, open-op spec, the three seams' traits/types, producer + consumer session types — protocol only | yes | | `net` *(default-on)* | the vendored subset's kernel-net pieces (`tokio/net`, `socket2`): the local-socket default handlers, DNS resolve helper, `Socks5Datagram` kernel socket path | no (wasm builds use `default-features = false`) | | `local` | the crate's first-party local backends: TCP dial, UDP egress socket, BIND `TcpListener` accept, the front doors (consumer + producer local shapes) — implies `net` | no (build error on wasm, the alktty posture) | | `composed` | the composed providers over alktunnels 0.2.0: direct-op egress (lazy destination→tunnel table), bind-first listen accept — adds the alktunnels dep | yes (alktunnels' default crate is wasm-clean; the composed providers use only its ungated channels surface — verify with the wasm check at implementation) | ## Module Map ``` src/ lib.rs — re-exports; crate docs vendor/ — the vendored fast-socks5 T-generic subset (ADR-011; vendor point v1.0.0 noted in-tree): codecs, typestate, client use_stream path, carried changes (BIND reply writer, new_reply publication, the udp-bind seam) params.rs — the trivial params schema + SOCKS5_OPEN_SCOPE/ SOCKS5_BIND_SCOPE constants (ADR-001/006) wire.rs — the ASSOCIATE datagram frame codec, re-implemented in first-party code shaped on alktunnels' length codec (ADR-003; framing is wire-layer, applied exactly once) producer.rs — the open op spec (scope-gated), the session-gate establisher, the command-dispatch pump handler (typestate driving + the three seams), teardown consumer.rs — the channels-native session types (connect/associate/ bind reply#2 reader), teardown ownership door.rs — the front doors (feature `local`): the raw-pipe bridge + the RFC 1929 cred-bearing door + the credential→identity mapping (ADR-005); the UDP front door's RFC codec boundary (ADR-003) egress.rs — the egress/accept seam types (DialFn/EgressFactory/ BindAcceptFactory contracts — ADR-004) local/ — feature `local`: the socket-backed provider constructors (dial/egress/accept) composed/ — feature `composed`: the alktunnels-composed providers (direct-op egress, bind-first accept) error.rs — SocksError + the typed open-error surface (alkcall ADR-049 §4) ``` Public API surface is `lib.rs` re-exports (AGENTS.md convention 15). Backend modules are feature-gated and never imported from the protocol/adapter/client modules. ## Design Decisions All design decisions are documented as ADRs in [decisions/](decisions/). | ADR | Decision | Summary | |-----|----------|---------| | [001](decisions/001-alpn-and-open-op.md) | ALPN + open op + trivial params | `alk/socks5`, `channels/socks5/sub`, `{}` — the RFC conversation is the data plane | | [002](decisions/002-protocol-base.md) | Protocol base | fast-socks5's explicit typestate API (vendored); faithful reply-code mapping | | [003](decisions/003-udp-handle-swap.md) | The handle swap | one channel per association; `{addr, data}` frames in-band; RFC codec at front doors only | | [004](decisions/004-dial-callback-shape.md) | The dial-callback shape | function-not-trait at three seams (dial/egress/accept); ownership rides the handle | | [005](decisions/005-auth-split-by-door.md) | Auth: "both," split by the door | producer = identity seam; front door = RFC 1929 + credential→identity mapping (static config) | | [006](decisions/006-access-control.md) | Access-control posture | `socks5:open` gate + `socks5:bind` command gate; target policy = the dialer | | [007](decisions/007-bind-in-base.md) | BIND in the base crate | phase model extended (reply#1 → quiet wait → accept → reply#2 → pass-through); additive | | [008](decisions/008-provider-defaults.md) | Provider defaults | local-socket + composed (direct op) providers, both feature-gated; no implicit dialer | | [009](decisions/009-feature-gates.md) | Feature gates | `net` (default-on, the vendor's gate), `local`, `composed`; default crate wasm-clean | | [010](decisions/010-channel-capacity.md) | Channel capacity | no wire change; assembly-layer config + the documented proxy-workload recommendation | | [011](decisions/011-inline-vendor.md) | The inline vendor | the `T`-generic subset vendored in-tree; the four carried changes; v1.0.0 noted | | [012](decisions/012-target-policy.md) | Target policy | dialer-refuses; domain targets pass through unresolved; virtual subnets = assembly policy | ## Open Questions Open questions are tracked in [open-questions.md](open-questions.md). Key questions affecting this document: - **OQ-SK2-06**: sandboxed-wasm-consumer adapter story (deferred(scope) — a product input, nothing waits on it) - **OQ-SK2-08**: recommended proxy-workload cap figure (open — a docs tuning note; ADR-010 pins the posture) ## References - Phase 0: `docs/research/phase-0.md` (the OQ ledger OQ-SK-01..09 + the convergence), POC findings (`poc-connect-wasm-findings.md`, `poc-udp-associate-findings.md`, `poc-bind-findings.md`), `iroh-socks5-eval.md` - Upstream ADRs: alkcall ADR-037 (channel ops), ADR-039 (params is ALPN-specific), ADR-047 (openable ALPNs are operations), ADR-049 (establishment + reply projection), ADR-050 (`pump_bidi`), ADR-040/ 041 (channel caps), ADR-051 (hub relay), ledger CF-005/CF-006 (identity seam); alktunnels ADR-003 (length codec), ADR-007/008 (direct op; bind-first + forwarded), ADR-006 (ACL posture) - Siblings: alktty (`/workspace/@alkdev/alktty`), alktunnels (`/workspace/@alkdev/alktunnels`); reference checkout `/workspace/fast-socks5` (v1.0.0, MIT, third-party) - RFC 1928, RFC 1929; tun2proxy (`/workspace/tun2proxy`)