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.
This commit is contained in:
glm-5.3-flash committed 2026-09-23 15:02:24 +00:00
1 parent ce8148d711
commit d530eae5c4
5 files changed
+659 -15

No files matched your search

+6 -4
View File
@@ -74,7 +74,7 @@ What this BAST deliberately does **not** cover:
{ "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" }
"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)" }
]
}
}
@@ -109,9 +109,11 @@ 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).
(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
+9 -4
View File
@@ -123,9 +123,12 @@ the same channel:
- **Frame:** `[len: u16 BE][addr + data]` where `addr + data` is the
RFC address encoding (`TargetAddr::to_be_bytes()` — ATYP + addr +
port; the RFC UDP header minus RSV/FRAG). One frame per datagram,
both directions. `len = 0` is a legal empty datagram (never EOF —
the F-2 invariant, shaped on alktunnels ADR-003's codec,
re-implemented in first-party `src/wire.rs`). **The length covers
both directions. **A zero-length payload is a legal empty datagram
(never EOF — the F-2 invariant, shaped on alktunnels ADR-003's
codec, re-implemented in first-party `src/wire.rs`); `len == 0` —
zero inner bytes — is unreachable from a compliant peer, since
`addr` is always ≥ 7 bytes, so the operative invariant lives at
payload level.** The length covers
`addr + data`, not data alone — the joint bound is the
constraint:** a frame is rejected (`Oversize`, at frame time)
whenever `len(addr) + len(data) > 65535`. The UDP payload bound
@@ -209,7 +212,9 @@ UDP ASSOCIATE (the datagram stage, one direction's view):
```
[len:u16 BE][ATYP + addr + port + data] [len:u16 BE][...] ...
len == 0: an empty datagram (legal payload, NOT EOF)
zero-length data: an empty datagram (legal payload, NOT EOF —
the F-2 invariant at payload level)
len == 0 (no addr bytes): unreachable from a compliant peer
EOF: the channels-level sentinel, outside the codec
```