--- status: draft last_updated: 2026-09-20 (Phase 1 initial draft) --- # alksocks — BAST Document for the `alk/socks5` Wire Format This document is the **BAST (Binary Abstract Syntax Tree)** specification for the binary portions of the `alk/socks5` wire format — per the alk* family BAST convention (alktunnels AGENTS.md convention 12, carried from alktty; alktunnels `bast.md` is the precedent), the mandatory ASSOCIATE datagram framing ([ADR-003](decisions/003-udp-handle-swap.md)) is binary framing beyond pass-through, so the BAST document exists even though the format is small. It is a normative JSON document conforming to the BAST meta-schema at `https://alk.dev/bast/v1/schema` (JSON Schema Draft 2020-12); any Draft 2020-12 validator can check well-formedness. **alksocks does not depend on alktype.** The runtime codec is the vendored fast-socks5 address encoding + the length-framing codec (shaped on alktunnels' `frame_datagram`/`DatagramReader`, ADR-003); this BAST document is the human-readable contract describing what those round-trip. If runtime validation against the BAST becomes desirable later, alktype becomes an optional dep and this document feeds it. ## Scope The `alk/socks5` wire format is: a trivial JSON open op on the channels call plane (a UTF-8 JSON object — `{}`; specified in [wire.md](wire.md) §The Open Op and [ADR-001](decisions/001-alpn-and-open-op.md)), and a data plane inside the channel `BiStream` that is *mostly* RFC 1928 wire rules plus raw pass-through. Per the BAST format spec, a BAST document describes **binary data layouts** — not JSON payloads. The RFC 1928 conversation itself (negotiation, command, replies, the RFC UDP datagram header at front doors) is specified by RFC 1928 and implemented by the vendored fast-socks5 codecs ([vendor.md](vendor.md)) — it is not re-specified here. This document covers exactly the alksocks-specific binary framing: 1. **The ASSOCIATE datagram frame** (`[len: u16 BE][addr + data]`) — the one framed stage (ADR-003). CONNECT's pass-through and BIND's pass-through have no framing for BAST to describe. What this BAST deliberately does **not** cover: - **The RFC 1928 wire rules** — method negotiation, command/reply encoding, the `addr` sub-layout (ATYP + addr + port), the RFC UDP datagram header (RSV/FRAG/ATYP/addr/port, front doors only). The RFC is normative; the vendored codecs implement it. - **The open-op params JSON shape** (`{}`) — UTF-8 JSON on the call plane; ADR-001. - **The channels 8-byte chunk header** — the channels layer's own framing (alknet ADR-071/093); stripped transparently before the alksocks data plane sees bytes. Specified upstream, not here. - **The EOF sentinel** — the channels-level zero-length chunk (upstream); the alksocks codecs never emit it (the F-2 invariant). ## The BAST document ```json { "$schema": "https://alk.dev/bast/v1/schema", "$defs": { "AssociateFrame": { "kind": "struct", "endian": "big", "fields": [ { "name": "len", "kind": "uint16" }, { "name": "addr", "kind": "bytes", "encoding": "length-prefixed", "maxLength": 259, "description": "RFC 1928 address encoding: ATYP + addr + port (TargetAddr::to_be_bytes); v4 = 1 + 4 + 2 = 7, v6 = 1 + 16 + 2 = 19, domain = 1 + 1 + 255 + 2 = 259 (the length byte of the domain form counts)" }, { "name": "data", "kind": "bytes", "encoding": "length-prefixed", "maxLength": 65535, "description": "the datagram payload; len == 0 is a legal empty datagram, NOT EOF" } ] } } } ``` (Note: `addr + data` together are exactly `len` bytes — the two `encoding: length-prefixed` fields are the *decoder's* internal split of one frame's payload; on the wire `len` covers both, and `data`'s length is `len − len(addr)`, decodable because the ATYP determines the address length. The two-field split documents the decoder's contract, not extra wire bytes. **Joint constraint:** a frame is legal only when `len(addr) + len(data) ≤ 65535` — the per-field maxima are not independent (a 65507-byte datagram to a 255-char domain would need 65766 frame bytes and must be rejected `Oversize` at frame time; wire.md §The Data Plane carries the arithmetic).) ## Annotations ### `AssociateFrame` — binary, exact Maps 1:1 to the on-wire bytes: `len` as `uint16` big-endian (2 bytes), then exactly `len` bytes = `addr + data`. The `addr` sub-layout is the RFC 1928 address encoding as vendored (`TargetAddr::to_be_bytes()`: ATYP `0x01` IPv4 / `0x03` domain / `0x04` IPv6, then the address, then the u16 BE port). RSV/FRAG do not ride the tunnel wire (ADR-003); FRAG ≠ 0 is refused at front doors. One frame per datagram, in both directions of the channel `BiStream`, for the whole datagram stage (association lifetime = channel lifetime). Frames are self-delimiting on the byte stream; a decoder is an incremental state machine across chunk boundaries (boundary preservation is mandatory — an empty datagram is a frame with `len == 0`, never EOF; alktunnels F-2's invariant, met structurally). ### Layering note The frame codec sits in the wire layer, applied **exactly once**: `Egress::next_reply` returns *inner* bytes (`addr + data`); the producer's frame writer adds the single length prefix (ADR-003's framing-once invariant — the double-framing bug class POC #2 caught). The consumer side mirrors it: the session's send frames once, its reader state persists across receives. The BAST describes the wire inside the `BiStream` only; `pump_bidi` (ADR-050) is upstream's, not re-specified here. ## References - [ADR-003](decisions/003-udp-handle-swap.md) (the handle swap + the frame), [ADR-001](decisions/001-alpn-and-open-op.md) (the JSON open-op shape — outside BAST scope), [ADR-011](decisions/011-inline-vendor.md) (the vendored codecs) - [wire.md](wire.md) (the normative prose; §The Data Plane) - alktunnels `bast.md` (the BAST precedent — same meta-schema, same "binary framing only" scope rule; the length codec's original document) and alktunnels ADR-003 (the codec alksocks reuses) - POC #2 `poc-udp-associate-findings.md` (the frame's validation matrix: empty datagrams, splits/batches, garbage-ATYP refusal)