Files
alksocks/docs/architecture/bast.md
T
glm-5.3-flash d530eae5c4 wire: the ASSOCIATE datagram frame codec — frame + DatagramReader (ADR-003)
- frame(addr, payload): [len: u16 BE][addr + data] over
  TargetAddr::to_be_bytes() + payload; the joint bound enforced at
  frame time (checked_add; 65507 + 255-char domain = 65766 > 65535 →
  Oversize) with addr_encoding_len exposed (7 v4 / 19 v6 / 259 max
  domain) and MAX_INNER_LEN pinned
- DatagramReader: incremental reassembly across chunk boundaries
  (split frames, batched frames, partial headers), infallible,
  returning per-frame Results; the addr sub-layout decoded by hand
  (ATYP-keyed, UTF-8-validated) with typed decode errors —
  BadAtyp / ShortAddress / DomainNotUtf8 / EmptyFrame — so the
  producer stage fails the association loud on protocol garbage
  (udp_undecodable_frame_ends_association, POC #2)
- module docs state the framing-once invariant (the egress speaks
  inner bytes; this layer owns the single length prefix — the
  double-framing bug class POC #2 caught), the F-2 invariant at
  payload level (a zero-length payload is a legal empty datagram,
  never EOF; wire len == 0 is unreachable garbage), and the EOF
  layering (the channels-level sentinel, outside the codec)
- doc-sync: wire.md + bast.md residue corrected — "len == 0 is a
  legal empty datagram" replaced with the payload-level F-2
  statement (len == 0 unreachable since addr >= 7 bytes)
- lib.rs re-exports frame, DatagramReader, addr_encoding_len,
  MAX_INNER_LEN, WireError (convention 15)

Verification: cargo test (58 lib + 37 differential), clippy
--all-targets -- -D warnings, fmt --check, doc --no-deps, and both
wasm checks (check + clippy on wasm32-unknown-unknown) — all clean.
2026-09-23 15:02:24 +00:00

6.4 KiB
Raw Permalink Blame History

status, last_updated
status last_updated
draft 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) 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 forked RFC 1928 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 §The Open Op and ADR-001), 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 forked RFC 1928 codecs (rfc1928.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 forked 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

{
  "$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; a zero-length payload is a legal empty datagram, NOT EOF (the F-2 invariant at payload level — len == 0, zero inner bytes, is unreachable from a compliant peer since addr is always >= 7 bytes)" }
      ]
    }
  }
}

(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 forked (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 legal zero-length payload, never EOF; alktunnels F-2's invariant, met structurally and stated at payload level: len == 0 — zero inner bytes — is unreachable from a compliant peer, since addr is always ≥ 7 bytes).

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 (the handle swap + the frame), ADR-001 (the JSON open-op shape — outside BAST scope), ADR-011 (the forked codecs)
  • 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)