- 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
9.1 KiB
status: draft last_updated: 2026-09-25 (the cred-bearing door's call-connection contract reconciled: TokenClient's door-only posture, one command per accepted connection; the BIND spent posture reconciled: the take-guarded reply#2 reader, review 002 C-1/D-1; the landed session signatures reconciled 2026-09-24; Phase 1 initial draft 2026-09-20)
alksocks: The Consumer Half
The consumer opens SOCKS5 channels against a produced service and speaks RFC 1928 inside them. Two consumption shapes, structurally separate (the Phase 0 scope resolution):
- The channels-native session: a typed client that opens
channels/socks5/suband drives the RFC conversation in-stream (wrapper-aware; the POC #1use_stream/ POC #2 associate session shapes). - The front door (optional, feature
local): a real local endpoint (TCP for CONNECT/BIND; UDP for ASSOCIATE) that vanilla clients point at; the door bridges each accepted connection/socket onto a channel, with the RFC codec living only at that boundary.
Both wrap the same protocol layer; neither is required by the other.
The Channels-Native Sessions
Socks5ChannelSession (CONNECT)
Opens channels/socks5/sub via ChannelClient::open_channel
(alkcall ADR-037/043), then drives the client side of the RFC
conversation in-stream over the channel's BiStream:
- the forked client typestate's
use_streampath, generic over the backing stream (POC #1:Socks5StreamimplsAsyncRead + AsyncWriteitself, so the session over aBiStreamis the intended use, not a hack); - target construction via
ToTargetAddr(IP-string forms parse to IP targets; domain form passes through unresolved, ADR-012); - after the reply, the session is a raw byte pipe: the consumer reads/writes payload bytes directly (0 B framing, CONNECT).
The associate session
The UDP ASSOCIATE client (POC #2's shape):
- opens the channel, negotiates, sends ASSOCIATE, reads the reply;
receives the sentinel
BND.ADDRand never uses it for addressing (the handle is the channel; ADR-003); send_datagram(dst, bytes)frames once ([len: u16 BE][addr + data], the RFC 1928 address encoding) and writes the frame;recv_datagram()returnsSome((source_addr, bytes))per datagram; its incremental reader state (mid-frame position + batched-datagram pending queue) persists across calls (a datagram split across channel chunks survives a caller-driven receive loop; POC #2's finding);shutdown()half-closes the stream; the mux's implicit-EOF sentinel reaches the producer's stage as EOF and the association ends (association lifetime = channel lifetime);- teardown ownership: the session owns the channel; on Drop it closes/joins (the alktunnels ADR-005 posture: the consumer session type owns teardown), combined with ADR-004's ownership-rides-the-handle contract.
The BIND client
The BIND reply#2 reader (POC #5's read_reply2 sketch): the forked
client request() reads reply#1; reply#2 is hand-read with the
forked public read_address codec (reply#1's codec applied again;
no second codec). The session then rides the pass-through data
plane. The reply#2 phase is take-guarded: the one normative reader
is await_inbound, and the first read consumes the session's stream
(the returned plane owns it; the session stays the teardown owner),
so a second call returns the family's spent shape (Ok(None), the
same posture the sibling sessions pin) and cannot re-read the live
data plane; a failed read restores the inner (the producer ends the
session on every post-reply#1 failure exit, so a re-call EOFs;
never a live data-plane re-read either way). The
reply1() handle is a view (the advertised BND.ADDR + the raw
pre-reply#2 stream), not a reader. The BIND client is a
wrapper-aware-consumer convenience; vanilla clients reach BIND
through the front door instead.
Error surface
SocksServerError / SocksError (the forked surface) mapped
faithfully; channel-open failures are the typed
channel:open_failed reasons (alkcall ADR-049). A refused session
never yields a phantom channel; a refused target is an RFC reply
code surfaced through the session type.
The Front Doors (feature local, optional)
The consumer's optional local exposure, the ssh -D shape (POC #1:
curl end-to-end; POC #2 test 8: the UDP door; POC #5 test 8: the
BIND door):
- The CONNECT/BIND door: bind a real TCP listener; for each
accepted connection, pipe it raw onto a channel
BiStream(copy_bidirectional, zero protocol translation, POC #1's literal 0 B pass-through conclusion). The door's credential half (below) is the only RFC work it does. - The UDP door: bind a real UDP socket; decode RFC-header'd
datagrams (
parse_udp_request), send{addr, data}frames in-band, return replies RFC-header'd (new_udp_header): the RFC codec at the door boundary only (ADR-003). FRAG ≠ 0 → refuse (the v1 posture). - The BIND door's reply#2 fidelity: the local variant's real listener/peer addresses pass through the pipe unchanged (the conversation is the vanilla client's own); the door adds nothing.
Door variants and auth (ADR-005)
- The NoAuth door (POC #1's raw-pipe shape): zero protocol translation; for loopback-only or wrapper-aware use.
- The cred-bearing door (RFC 1929): a vanilla client carrying
user:passgets a real RFC 1929 conversation at the door; the door maps the accepted credential to local configuration (which channel identity/endpoint to open the producer channel with) via a static config table (ADR-005:user:pass →channel identity/endpoint selection). The SOCKS password never crosses the channel as a SOCKS password; the channel identity never reaches the vanilla client. The door checks credentials itself (no forkedAuthMethodimpl needed; the door owns the accept loop). The mapped identity rides the open-op call'sauth_tokenpayload (CF-005 precedence (1)) viaTokenClient::wrap; the client's channel-0CallConnectionis taken once per door (the door-only contract: a cred-bearing door's client is a door client; other shapes, such as the UDP door's association and channels-native sessions, ride their own connections to the same producer registration, one registration, many connections). The vanilla conversation is one command per accepted connection (the door's RFC half consumes the greeting + one command; the data plane rides to EOF).
Door variant selection (NoAuth vs cred-bearing; the UDP door's bind address) is assembly-layer configuration.
Wiring (Assembly Layer)
let session = Socks5ChannelSession::connect(
&channel_client, target).await?;
let (mut read, mut write) = session.split(); // post-reply payload pipe
let assoc = AssociateSession::associate(&channel_client).await?;
assoc.send_datagram(dst, &bytes).await?; // frames once, ADR-003
let (src, bytes) = assoc.recv_datagram().await?.expect("a datagram");
let door = FrontDoor::builder()
.credentials(cred_table) // RFC 1929 mapping, ADR-005
.bind("127.0.0.1:1080")
.build();
door.serve(channel_client).await?;
The landed session signatures: the constructors take &ChannelClient
(shared; many sessions ride one client, the concurrency posture);
send_datagram takes impl ToTargetAddr (the CONNECT session's
target-construction posture, ADR-012); recv_datagram() returns
Result<Option<(TargetAddr, Bytes)>, SocksSessionError>; None is
the channel EOF (the association's clean end), never an empty
datagram (F-2: an empty payload is a legal datagram).
Design Decisions
| ADR | Decision | Summary |
|---|---|---|
| 001 | ALPN + op | the consumer opens the one op |
| 003 | The associate session | sentinel BND.ADDR; frames once; reader state persists |
| 004 | Ownership rides the handle | handle payloads own channel sessions; the session-teardown posture is inherited from alktunnels ADR-005 (the consumer session type owns teardown) |
| 005 | Door auth | RFC 1929 at the door; static credential→identity mapping |
| 007 | BIND client | the reply#2 reader; vanilla clients via the door |
| 011 | The extraction | the client use_stream path in-tree |
| 013 | The targeted fork | the core is owned (src/rfc1928/) |
Open Questions
Open questions are tracked in open-questions.md. None currently blocking this document.
References
- POC #1 (
poc-connect-wasm-findings.md: the raw-pipe door, theuse_streamfit), POC #2 (poc-udp-associate-findings.md: the associate session, the reader-state finding, the UDP door), POC #5 (poc-bind-findings.md: the reply#2 reader, the BIND door) - Upstream: alkcall ADR-037/043 (channel open), alktunnels ADR-005 (consumer session owns teardown; the posture inherited), the CF-005/CF-006 seam
- producer.md (the other half), wire.md (the normative wire)