- Rewrote em dash prose across the whole tree outside docs/reviews/ (the historical reviews stay verbatim, matching the alktunnels and alktty precedent): src doc comments and inline comments (587), docs/architecture (319), docs/research + sdd_process (484), tests (361), tasks (899), fuzz (28), README/CHANGELOG/Cargo.toml (77), AGENTS.md (85), and the .opencode/agents prompts (109) - Fenced content follows the precedent: comments, ASCII-diagram label separators, and template placeholders rewritten; wire data, JSON/BAST documents, byte diagrams, and transcripts survive verbatim; assert, expect, panic, error-display, and format message literals rewritten as prose; no code, fixture, or test data change - src/rfc1928/LICENSE: the two dashes in alksocks' own provenance header rewritten; the MIT legal text untouched - AGENTS.md adds Project Convention 18: no em dashes in prose; varied restructure, no spaced-hyphen substitutes, fenced prose rule, en dash numeric ranges exempt, docs/reviews/ exempt from the greps - tasks/*.md frontmatter: the name values rewritten by the sweep are now quoted (a raw colon inside an unquoted YAML scalar parsed as null); taskgraph validate reports 37 tasks, no issues Verification: cargo test; cargo clippy --all-targets -D warnings; cargo fmt --check; cargo doc --no-deps; fuzz corpus replay (cargo test --manifest-path fuzz/shared/Cargo.toml); wasm check and clippy for wasm32-unknown-unknown with -D warnings; em dash grep over the repo returns matches only in docs/reviews/ and the bast.md BAST JSON data fence
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 |
|
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, the frame_datagram/DatagramReader pair, with the 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, with 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:
frame(addr: &TargetAddr, payload: &[u8]) -> Result<Bytes, WireError>, one frame per datagram:[len: u16 BE][addr + data]whereaddr + dataisTargetAddr::to_be_bytes()(ATYP + addr + port) followed by the payload.- The joint bound (the load-bearing constraint): reject
Oversizeat frame time wheneverlen(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). Exposeaddr_encoding_len(addr)(7 v4 / 19 v6 / 259 max domain) so callers can compute the joint bound. DatagramReader, the incremental decoder: feed it whateverread()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 splitsaddr + data. Infallible decode direction (the oversize bound is enforced at frame time);is_mid_frame()diagnostic for teardown.- The F-2 invariant, correctly stated: a zero-length payload
is a legal empty datagram, and never EOF. (
len == 0is unreachable on this wire sinceaddris always ≥ 7 bytes; the invariant that matters is payload-level. See the doc-sync deliverable.) - Doc-sync (wire.md + bast.md residue): both docs say "
len = 0is a legal empty datagram" while also pinning thatlencoversaddr + data(joint bound);len = 0is unreachable sinceaddr≥ 7 bytes. Correct both docs to state the operative invariant at payload level ("a zero-length payload is a legal empty datagram, never EOF, per the F-2 invariant") in the same change; do not silently diverge. - 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_lenimplemented 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 testgreen; clippy--all-targets -- -D warningsclean;cargo fmt --checkcleancargo check --target wasm32-unknown-unknown+cargo clippy --target wasm32-unknown-unknown -- -D warningsclean
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 fromlib.rsper 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-frameResults 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)(theudp_undecodable_frame_ends_associationshape, POC #2),ShortAddress,DomainNotUtf8,EmptyFrame(wirelen == 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:
framecomputeslen(addr) + len(payload)withchecked_add(saturating past the bound, with no unchecked u16 arithmetic) and rejectsOversize(total, max)when it exceedsMAX_INNER_LEN.addr_encoding_lenexposes 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, since 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 sinceaddr≥ 7 bytes, and the residue was 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 theResultper frame, which is 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.