- Rewrote em dash prose across the whole tree outside docs/reviews/ (the historical reviews stay verbatim, matching the alktunnels and alktty precedent): src doc comments and inline comments (587), docs/architecture (319), docs/research + sdd_process (484), tests (361), tasks (899), fuzz (28), README/CHANGELOG/Cargo.toml (77), AGENTS.md (85), and the .opencode/agents prompts (109) - Fenced content follows the precedent: comments, ASCII-diagram label separators, and template placeholders rewritten; wire data, JSON/BAST documents, byte diagrams, and transcripts survive verbatim; assert, expect, panic, error-display, and format message literals rewritten as prose; no code, fixture, or test data change - src/rfc1928/LICENSE: the two dashes in alksocks' own provenance header rewritten; the MIT legal text untouched - AGENTS.md adds Project Convention 18: no em dashes in prose; varied restructure, no spaced-hyphen substitutes, fenced prose rule, en dash numeric ranges exempt, docs/reviews/ exempt from the greps - tasks/*.md frontmatter: the name values rewritten by the sweep are now quoted (a raw colon inside an unquoted YAML scalar parsed as null); taskgraph validate reports 37 tasks, no issues Verification: cargo test; cargo clippy --all-targets -D warnings; cargo fmt --check; cargo doc --no-deps; fuzz corpus replay (cargo test --manifest-path fuzz/shared/Cargo.toml); wasm check and clippy for wasm32-unknown-unknown with -D warnings; em dash grep over the repo returns matches only in docs/reviews/ and the bast.md BAST JSON data fence
14 KiB
status: draft
last_updated: 2026-09-28 (OQ-SK2-09 indexed: the UDP door's peer-pin
posture; OQ-SK2-06's trigger ownership checked; ADR-014 indexed: the
idle-timeout posture; the targeted fork, ADR-013, the module is
src/rfc1928/; the vendor extraction amendment, no net feature,
2026-09-21; Phase 1 initial draft 2026-09-20; 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, producer.md,
consumer.md, rfc1928.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): the
channels/socks5/subopen op (registered viaChannelCore::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'sBiStream; the forked RFC 1928 core (rfc1928.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): a typed
channels-native session that opens
channels/socks5/suband 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 through the front door, a realsocks5://127.0.0.1:portendpoint relaying each accepted connection into a channel (the ssh-Dlocal-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
-Dconclusion, realized as a first-class protocol crate: alktunnels' Phase 0 settled that-Dcomposes 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:portnatively) 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 forked RFC 1928 core is
generic over
T: AsyncRead + AsyncWrite + Unpin; a channelsBiStreamdrops in directly (POC #1), aTcpStreamdrops in at the local backends. Neither substrate leaks into the protocol layer (the alkttyTtyBackend/ alktunnels pump-halves inversion-point precedent).
The Resource Model in Brief
A SOCKS5 channel's life (full detail in wire.md and producer.md):
- Open (the establishment phase). The consumer opens
channels/socks5/subwith 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 typedchannel:open_failed(alkcall ADR-049); no phantom channel. - RFC conversation (the control plane, in-stream). The RFC 1928
handshake (method negotiation → command → reply) runs in-stream
over the
BiStream, driven by the forked typestate. Command dispatch: CONNECT/ASSOCIATE requiresocks5:open; BIND requiressocks5:bind(the command gate, ADR-006). - Data plane. CONNECT: the dial seam (ADR-004) produces boxed
halves;
pump_biditwo-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). - 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.10.1 (core types: Connection, ProtocolHandler, BiStream,
│ AuthContext, Identity, AccessControl, OwnershipProvider;
│ channels: ChannelCore/ChannelClient,
│ register_openable_with_establisher, pump_bidi,
│ Establishment/ChannelPlan + reply projection,
│ the 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 (local feature): non-wasm by design
└── adds: tokio/net + socket2 (the feature's own socket providers) and
the first-party local backends + front doors
alksocks (composed feature)
└── adds: alktunnels 0.4.1 (the direct op egress; the bind-first
listen accept; ADR-008's composed providers), whose 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 RFC 1928 core is an extraction
of fast-socks5's T-generic subset, containing zero kernel-net code,
so there is no net feature at all (ADR-011 §Amendment);
substrate I/O rides local alone (ADR-009 §Amendment).
fast-socks5 is NOT a Cargo dependency: it was extracted in-tree
and forked (ADR-013); /workspace/fast-socks5 stays as the
differential-testing behavioral reference.
Feature Gates
| Feature | Contents | Wasm |
|---|---|---|
| (default) | the RFC 1928 core (src/rfc1928/, ADR-013: codecs, typestate), wire codec, open-op spec, the three seams' traits/types, producer + consumer session types; protocol only, zero kernel-net code, wasm-clean by construction |
yes |
local |
the crate's first-party local backends: TCP dial, UDP egress socket, BIND TcpListener accept, the front doors (consumer + producer local shapes); brings tokio/net + socket2 into the graph |
no (build error on wasm, the alktty posture) |
composed |
the composed providers over alktunnels 0.4.1: 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
rfc1928/ : the RFC 1928 core: the targeted fork of
fast-socks5's T-generic subset (ADR-013;
extraction per ADR-011; fork point noted
in-tree): consts, codecs, errors, typestate,
client use_stream path; the fork's two
carried changes (BIND reply surface,
new_reply publication); no kernel-net code
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/.
| ADR | Decision | Summary |
|---|---|---|
| 001 | ALPN + open op + trivial params | alk/socks5, channels/socks5/sub, {}: the RFC conversation is the data plane |
| 002 | Protocol base | fast-socks5's explicit typestate API (extracted + forked, src/rfc1928/); faithful reply-code mapping |
| 003 | The handle swap | one channel per association; {addr, data} frames in-band; RFC codec at front doors only |
| 004 | The dial-callback shape | function-not-trait at three seams (dial/egress/accept); ownership rides the handle |
| 005 | Auth: "both," split by the door | producer = identity seam; front door = RFC 1929 + credential→identity mapping (static config) |
| 006 | Access-control posture | socks5:open gate + socks5:bind command gate; target policy = the dialer |
| 007 | BIND in the base crate | phase model extended (reply#1 → quiet wait → accept → reply#2 → pass-through); additive |
| 008 | Provider defaults | local-socket + composed (direct op) providers, both feature-gated; no implicit dialer |
| 009 | Feature gates | local + composed; no net feature (the RFC 1928 core is wasm-clean by construction; ADR-011 §Amendment); default crate wasm-clean |
| 010 | Channel capacity | no wire change; assembly-layer config + the documented proxy-workload recommendation |
| 011 | The extraction | the T-generic subset copied in-tree; two carried changes (BIND, new_reply); v1.0.0 noted (superseded as a posture by ADR-013) |
| 013 | The targeted fork | the extracted region is owned; src/rfc1928/; fork point 5b18d4d; hand-port replaces re-vendor |
| 012 | Target policy | dialer-refuses; domain targets pass through unresolved; virtual subnets = assembly policy |
| 014 | Idle-timeout posture | every phase teardown-governed, no protocol-layer idle bound; doors own the accept-path defense; revisitable additively |
Open Questions
Open questions are tracked in 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; the trigger is external by verified default)
- OQ-SK2-08: recommended proxy-workload cap figure (open, a docs tuning note; ADR-010 pins the posture)
- OQ-SK2-09: the UDP door's peer-pin posture (option 2, the same-IP lock + reply-port refresh, landed in 0.1.0; option 3, the control-conn-derived expected peer, deferred on a concrete wildcard-bind deployment)
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)