Files
alksocks/docs/architecture/consumer.md
T
glm-5.3-flash 1addedfdd0 assembly: the cross-cutting integration suite + docs sync (batch 2 closes)
- tests/assembly-integration.rs (7 tests, local + composed): the full
  -D story (vanilla cred-bearing client -> TCP door + UDP door ->
  producer w/ local providers -> real echo targets; CONNECT + BIND +
  ASSOCIATE live, one-command-per-connection, teardown + fresh
  sessions), the composed story (direct_egress + bindfirst_accept +
  real TCP dial; all three commands; the lazy-table dial-count
  witness), mixed assembly (channels-native + door clients on one
  registration; composed seams beside native sessions), the assembly
  teardown matrix (UDP-door idle bound; channel reaped; fresh
  association after), the producer door driving all three commands,
  and the -D domain-target pin (resolution at the producer's seam,
  ADR-012).
- Fix the drift the suite surfaced: ComposedEgress::next_reply ended
  fresh (never-opened-table) associations at birth — the ended-relay
  Err now fires only once a tunnel existed and every relay ended; the
  fresh table parks (the readiness contract). Regression-pinned in
  tests/composed-egress.rs (fresh_association_parks_before_any_
  tunnel_is_opened).
- Harness: the composed-accept machinery single-sourced into
  tests/harness.rs (the composed suites' shapes + the five-gate
  composed identity); composed-accept.rs consumes the shared shapes.
- Docs sync: README Current State (the assembly layer landed; both
  suites named), lib.rs crate docs (the assembly-layer paragraph),
  consumer.md (the TokenClient door-only contract + the
  one-command-per-connection posture reconciled).
- Task assembly-integration-suite: completed; Notes + Summary filled
  (the local-on-wasm failure observed: mio's unsupported-wasm error).

Verification: cargo test (375), cargo test --all-features (615),
clippy --all-targets -D warnings (default + all-features), fmt --check,
doc --no-deps (0 warnings), both wasm checks (default + composed),
local-on-wasm fails as designed.
2026-09-25 23:03:13 +00:00

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):

  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)

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, 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 (the other half), wire.md (the normative wire)