- 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
253 lines
14 KiB
Markdown
253 lines
14 KiB
Markdown
---
|
|
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](wire.md), [producer.md](producer.md),
|
|
[consumer.md](consumer.md), [rfc1928.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](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 forked RFC 1928 core
|
|
([rfc1928.md](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](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 through 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 forked RFC 1928 core 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 forked 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.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/](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 (extracted + forked, `src/rfc1928/`); 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 | `local` + `composed`; no `net` feature (the RFC 1928 core is wasm-clean by construction; ADR-011 §Amendment); 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 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](decisions/013-targeted-fork.md) | The targeted fork | the extracted region is owned; `src/rfc1928/`; fork point `5b18d4d`; hand-port replaces re-vendor |
|
|
| [012](decisions/012-target-policy.md) | Target policy | dialer-refuses; domain targets pass through unresolved; virtual subnets = assembly policy |
|
|
| [014](decisions/014-idle-timeout-posture.md) | 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](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`) |