--- 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): 1. **The channels-native session** — a typed client that opens `channels/socks5/sub` and drives the RFC conversation in-stream (wrapper-aware; the POC #1 `use_stream` / POC #2 associate session shapes). 2. **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_stream` path — generic over the backing stream (POC #1: `Socks5Stream` impls `AsyncRead + AsyncWrite` itself, so the session over a `BiStream` is 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.ADDR` and 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()` returns `Some((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` — 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:pass` gets 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 forked `AuthMethod` impl needed — the door owns the accept loop). The mapped identity rides the open-op call's `auth_token` payload (CF-005 precedence (1)) via `TokenClient::wrap` — the client's channel-0 `CallConnection` is taken once per door (the door-only contract: a cred-bearing door's client is a door client; other shapes — the UDP door's association, 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) ```rust 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, 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](decisions/001-alpn-and-open-op.md) | ALPN + op | the consumer opens the one op | | [003](decisions/003-udp-handle-swap.md) | The associate session | sentinel BND.ADDR; frames once; reader state persists | | [004](decisions/004-dial-callback-shape.md) | 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](decisions/005-auth-split-by-door.md) | Door auth | RFC 1929 at the door; static credential→identity mapping | | [007](decisions/007-bind-in-base.md) | BIND client | the reply#2 reader; vanilla clients via the door | | [011](decisions/011-inline-vendor.md) | The extraction | the client `use_stream` path in-tree | | [013](decisions/013-targeted-fork.md) | The targeted fork | the core is owned (`src/rfc1928/`) | ## Open Questions Open questions are tracked in [open-questions.md](open-questions.md). None currently blocking this document. ## References - POC #1 (`poc-connect-wasm-findings.md` — the raw-pipe door, the `use_stream` fit), 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](producer.md) (the other half), [wire.md](wire.md) (the normative wire)