Files
alksocks/tasks/implementation/core-wire-codec.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

9.0 KiB

id, name, status, depends_on, scope, risk, impact, level
id name status depends_on scope risk impact level
implementation/core-wire-codec Core — the ASSOCIATE datagram frame codec (src/wire.rs) completed
implementation/core-crate-deps
narrow low component implementation

Description

Implement src/wire.rs per wire.md §The Data Plane and bast.md: the first-party [len: u16 BE][addr + data] frame codec for the UDP ASSOCIATE datagram stage (ADR-003), re-implemented in first-party code shaped on alktunnels' length codec (/workspace/@alkdev/ alktunnels/src/wire.rs — frame_datagram/DatagramReader — same F-2 behavior, but the payload here is the RFC 1928 address encoding plus data, not a bare payload).

The codec is the crate's one framed stage (CONNECT/BIND are raw pass-through — no framing). Framing is applied exactly once, by this layer: Egress::next_reply returns inner bytes (addr + data); the producer's frame writer adds the single length prefix (the double-framing bug class POC #2 caught is an API-design invariant this module's docs must state).

Deliverables:

  1. frame(addr: &TargetAddr, payload: &[u8]) -> Result<Bytes, WireError> — one frame per datagram: [len: u16 BE][addr + data] where addr + data is TargetAddr::to_be_bytes() (ATYP + addr + port) followed by the payload.
  2. The joint bound (the load-bearing constraint): reject Oversize at frame time whenever len(addr) + len(payload) > 65535. The UDP payload bound alone (65507) does NOT fit u16 unconditionally — a 65507-byte datagram to a 255-character domain is 65766 frame bytes. Max data: 65276 for domain-form destinations, 65516 for IPv6, 65528 for IPv4. No unchecked u16 arithmetic on the payload bound (wire.md's explicit requirement). Expose addr_encoding_len(addr) (7 v4 / 19 v6 / 259 max domain) so callers can compute the joint bound.
  3. DatagramReader — the incremental decoder: feed it whatever read() yields; it reassembles frames across chunk boundaries (split frames, batched frames, partial headers). Returns (TargetAddr, Bytes) per completed frame — it decodes the addr sub-layout (ATYP determines the address length) and splits addr + data. Infallible decode direction (the oversize bound is enforced at frame time); is_mid_frame() diagnostic for teardown.
  4. The F-2 invariant, correctly stated: a zero-length payload is a legal empty datagram — never EOF. (len == 0 is unreachable on this wire since addr is always ≥ 7 bytes; the invariant that matters is payload-level. See the doc-sync deliverable.)
  5. Doc-sync (wire.md + bast.md residue): both docs say "len = 0 is a legal empty datagram" while also pinning that len covers addr + data (joint bound) — len = 0 is unreachable since addr ≥ 7 bytes. Correct both docs to state the operative invariant at payload level ("a zero-length payload is a legal empty datagram, never EOF — the F-2 invariant") in the same change; do not silently diverge.
  6. EOF layering note in the module docs: EOF is the channels-level sentinel on the BiStream, outside this codec; the codec never emits zero-length reads, so the layers never collide (the alktunnels wire.rs module-doc posture).

Tests: round-trip (v4/v6/domain), empty payload legal + not EOF, split across awkward chunks, batched frames in one chunk, partial header waits, joint-bound rejection (the 65507 + 255-char-domain case), oversize at frame time, mid-frame state observable, garbage ATYP decode error (the udp_undecodable_frame_ends_association shape — the decode error type must distinguish a bad ATYP so the producer stage can fail the association loud).

Acceptance Criteria

  • frame/DatagramReader/addr_encoding_len implemented per the joint bound; no unchecked u16 arithmetic
  • Zero-length payload round-trips as a legal empty datagram
  • Split/batch/partial-header reassembly tests pass
  • Joint-bound rejection test (65507-byte payload + 255-char domain → Oversize) passes
  • Garbage-ATYP decode surfaces a typed error (not a panic)
  • wire.md + bast.md residue corrected (payload-level F-2 statement) in the same change
  • cargo test green; clippy --all-targets -- -D warnings clean; cargo fmt --check clean
  • cargo check --target wasm32-unknown-unknown + cargo clippy --target wasm32-unknown-unknown -- -D warnings clean

References

  • docs/architecture/wire.md §The Data Plane (the frame + the joint bound arithmetic)
  • docs/architecture/bast.md (the BAST contract for the frame)
  • docs/architecture/decisions/003-udp-handle-swap.md (framing-once, F-2)
  • Template: /workspace/@alkdev/alktunnels/src/wire.rs (the length codec + its test matrix — shape on it, do not depend on it)
  • POC sketch: /workspace/alksocks-udp-poc/src/datagram.rs (frame/inner/unframe/addr_encoding_len)

Notes

  • API shape (src/wire.rs, re-exported from lib.rs per convention 15): frame(addr, payload) -> Result<Bytes, WireError>, DatagramReader::feed(&[u8]) -> Vec<Result<(TargetAddr, Bytes), WireError>> + is_mid_frame(), addr_encoding_len(&TargetAddr) -> usize, MAX_INNER_LEN (65535). The reader returns per-frame Results rather than panicking or skipping: reassembly is infallible (oversize enforced at frame time bounds the buffer by the u16 declared length), so the only failure per completed frame is the addr decode, typed distinctly — BadAtyp(u8) (the udp_undecodable_frame_ends_association shape, POC #2), ShortAddress, DomainNotUtf8, EmptyFrame (wire len == 0 — unreachable from a compliant peer; protocol garbage). The reader consumes the frame's bytes either way and keeps going; the caller fails the association loud on the first error.
  • The joint bound: frame computes len(addr) + len(payload) with checked_add (saturating past the bound — no unchecked u16 arithmetic) and rejects Oversize(total, max) when it exceeds MAX_INNER_LEN. addr_encoding_len exposes 7 (v4) / 19 (v6) / 4 + domain.len() (domain, 259 max) so callers compute the joint bound themselves. Tests pin the load-bearing case (65507-byte payload + 255-char domain → 65766 > 65535 → Oversize), the per-form maxima at the exact limit (65276 domain / 65516 v6 / 65528 v4) and one-over rejection.
  • Decode direction: hand-parsed addr sub-layout over the borrowed frame (the sync mirror of the forked read_address — the stage's decode must not be async), UTF-8-validated like the fork's codec, payload = the remainder. TargetAddr::to_be_bytes()'s output is the encode direction verbatim.
  • Doc-sync: wire.md §UDP ASSOCIATE + the byte diagram and bast.md's BAST field description + the Annotations paragraph now state the F-2 invariant at payload level ("a zero-length payload is a legal empty datagram, never EOF"), with len == 0 (zero inner bytes) noted unreachable from a compliant peer since addr ≥ 7 bytes — the residue corrected in this same change.
  • A first cut returned bare (TargetAddr, Bytes) per frame; the garbage-ATYP deliverable (typed decode error, not a panic) forced the Result per frame — the shape the producer stage's loud-fail needs.

Summary

Implemented the ASSOCIATE datagram frame codec in src/wire.rs (ADR-003): frame builds [len: u16 BE][addr + data] over TargetAddr::to_be_bytes() + payload, enforcing the joint bound (len(addr) + len(payload) > 65535 → Oversize at frame time; checked_add, no unchecked u16 arithmetic) with addr_encoding_len exposed for callers; DatagramReader reassembles frames across chunk boundaries (split frames, batched frames, partial headers) and decodes each completed frame's addr sub-layout (ATYP-keyed, UTF-8-validated) returning (TargetAddr, Bytes), infallibly reassembling with typed per-frame decode errors (BadAtyp / ShortAddress / DomainNotUtf8 / EmptyFrame) so the producer stage fails loud on protocol garbage; the zero-length payload is a legal empty datagram (the F-2 invariant at payload level), EOF is documented as the channels-level sentinel outside the codec, and framing-once (the egress speaks inner bytes; this layer owns the single length prefix) is stated as the module's API-design invariant. wire.md and bast.md's len == 0 statements were corrected to the payload-level F-2 phrasing in the same change. 19 unit tests cover the matrix (round-trips v4/v6/domain, empty payload legal + not EOF, byte-by-byte split reassembly, batching, partial-header waits, joint-bound rejection incl. the 65507 + 255-char-domain case, exact-limit acceptance per address form, oversize at frame time, mid-frame observability, truncated-stream buffering, all four decode-error shapes); lib.rs re-exports frame, DatagramReader, addr_encoding_len, MAX_INNER_LEN, WireError. Verified: cargo test (58 lib + 37 differential), cargo clippy --all-targets -- -D warnings, cargo fmt --check, cargo doc --no-deps, cargo check --target wasm32-unknown-unknown, cargo clippy --target wasm32-unknown-unknown -- -D warnings — all clean.