- serve_udp_associate: sentinel reply (127.0.0.1:0 — the handle-swap surface) then the datagram stage; missing egress refuses RFC-faithfully (CommandNotSupported, ADR-008); egress-factory failure maps to reply#1's code - run_datagram_stage: select! over channel reads ∥ egress replies (the concurrent full-duplex shape), typed StageExit diagnostics (EOF = the clean exit; undecodable frames fail loud; the egress readiness contract tolerated as spurious wakeups) - wire: frame_inner — the reply-path half of framing-once (identical wire to frame(), pinned); egress inner bytes never re-prefixed - tests: the readiness-contract test (instant-Ok(None) egress does not starve the stage), empty-payload datagram legal (never EOF), undecodable-frame loud fail, EOF teardown + fresh associate, the egress drop witness (stage owns it), missing-egress refusal + CONNECT guard — in-process (duplex) and over channels (harness); the three-command regression guard drives all commands serving Verified: cargo test 174/174; clippy --all-targets -D warnings; fmt --check; wasm check + clippy (wasm32-unknown-unknown); doc.
632 lines
25 KiB
Rust
632 lines
25 KiB
Rust
//! The ASSOCIATE datagram frame codec (ADR-003): the crate's one
|
|
//! framed stage — `[len: u16 BE][addr + data]` per datagram, both
|
|
//! directions of the channel `BiStream`. CONNECT/BIND are raw
|
|
//! pass-through (no framing). Normative WHAT: `wire.md` §The Data
|
|
//! Plane; the binary contract: `bast.md` (`AssociateFrame`). The
|
|
//! length codec is first-party code shaped on alktunnels'
|
|
//! `frame_datagram`/`DatagramReader` (same F-2 behavior; no
|
|
//! dependency on it).
|
|
//!
|
|
//! The inner bytes are the RFC 1928 address encoding — exactly
|
|
//! `TargetAddr::to_be_bytes()` (ATYP + addr + port; the RFC UDP
|
|
//! header minus RSV/FRAG, which ride only vanilla front doors,
|
|
//! ADR-003) — followed by the datagram payload.
|
|
//!
|
|
//! **Framing is applied exactly once, by this layer.** The egress
|
|
//! seam speaks *inner* bytes (`addr + data`, no length prefix —
|
|
//! `Egress::next_reply` returns inner bytes); the producer's frame
|
|
//! writer adds the single length prefix, and the consumer's send path
|
|
//! frames via [`frame`] exactly once. The double-framing bug class
|
|
//! POC #2 caught (an egress framing its replies, the producer
|
|
//! prefixing again — the client parsed the length field as ATYP
|
|
//! `0x00`) is an API-design invariant: the wire layer owns the length
|
|
//! prefix; nothing else adds one.
|
|
//!
|
|
//! **The joint bound (the load-bearing constraint).** The u16 length
|
|
//! field covers `addr + data`, not the payload alone: a frame is
|
|
//! rejected ([`WireError::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 65507 + 259 = 65766 frame bytes. Max
|
|
//! payload per address form: 65276 (domain, 255 chars), 65516 (IPv6),
|
|
//! 65528 (IPv4). [`addr_encoding_len`] exposes the per-address
|
|
//! encoding size so callers can compute the joint bound; no unchecked
|
|
//! u16 arithmetic is used on the payload bound.
|
|
//!
|
|
//! **The F-2 invariant, at payload level.** A zero-length payload is
|
|
//! a legal empty datagram — a real datagram, never an EOF signal:
|
|
//! [`frame`] encodes it and [`DatagramReader`] decodes it as
|
|
//! `(addr, empty)`. (`len == 0` — zero inner bytes — is unreachable
|
|
//! from a compliant peer, since `addr` is always ≥ 7 bytes; a wire
|
|
//! `len` of 0 is protocol garbage and surfaces as
|
|
//! [`WireError::EmptyFrame`].)
|
|
//!
|
|
//! **EOF layering.** EOF is the channels-level sentinel on the
|
|
//! `BiStream`, outside this codec: the codec never emits zero-length
|
|
//! reads, so the sentinel and the datagram stream never collide (an
|
|
//! empty datagram and a zero-byte read are different shapes — the
|
|
//! reason boundary-preserving framing is mandatory for the datagram
|
|
//! stage at all). A stream end mid-frame is the session's fail-loud
|
|
//! truncated-datagram condition ([`DatagramReader::is_mid_frame`] is
|
|
//! the teardown diagnostic); the codec itself only buffers — it never
|
|
//! errors on truncation and never yields a partial frame.
|
|
|
|
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
|
use thiserror::Error;
|
|
|
|
use crate::rfc1928::consts;
|
|
use crate::rfc1928::util::target_addr::{AddrError, TargetAddr};
|
|
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
|
|
|
|
/// The u16 length-field bound on a frame's inner bytes (`addr + data`):
|
|
/// frames whose address encoding plus payload exceed this are rejected
|
|
/// at frame time (ADR-003 — never a wire overflow). The joint bound,
|
|
/// not a payload bound: see the module docs.
|
|
pub const MAX_INNER_LEN: usize = u16::MAX as usize;
|
|
|
|
const FRAME_LEN_FIELD: usize = 2;
|
|
|
|
/// Codec errors. Two families:
|
|
///
|
|
/// - **Frame time** ([`frame`]): [`WireError::Oversize`] (the joint
|
|
/// bound — `len(addr) + len(payload) > 65535` would wrap the u16
|
|
/// length field) and [`WireError::Address`] (the address encoding
|
|
/// refused — e.g. a domain longer than 255 bytes).
|
|
/// - **Decode time** ([`DatagramReader::feed`], per completed frame):
|
|
/// [`WireError::EmptyFrame`], [`WireError::BadAtyp`],
|
|
/// [`WireError::ShortAddress`], [`WireError::DomainNotUtf8`] — all
|
|
/// protocol-garbage shapes a compliant peer never produces. A bad
|
|
/// ATYP is its own variant so the producer stage can fail the
|
|
/// association loud (`udp_undecodable_frame_ends_association`,
|
|
/// POC #2) instead of panicking or guessing.
|
|
///
|
|
/// The reassembly itself is infallible: with the oversize bound
|
|
/// enforced at frame time, a declared length can never exceed what
|
|
/// the frame carries, so the incremental decode only buffers (bounded
|
|
/// by `u16::MAX`) and completes.
|
|
#[derive(Debug, Error)]
|
|
pub enum WireError {
|
|
/// A frame's inner bytes (`addr + data`) exceed
|
|
/// [`MAX_INNER_LEN`] — the u16 length field would wrap. Rejected
|
|
/// at frame time; carries `(total, max)`.
|
|
#[error("datagram frame too large for u16 length prefix: {0} bytes of addr + data (max {1})")]
|
|
Oversize(usize, usize),
|
|
|
|
/// The address could not be encoded (`TargetAddr::to_be_bytes`
|
|
/// refused — e.g. `AddrError::DomainLenTooLong`).
|
|
#[error("address encode: {0}")]
|
|
Address(#[from] AddrError),
|
|
|
|
/// A completed frame declared `len == 0` — zero inner bytes, no
|
|
/// address at all. Unreachable from a compliant peer (`addr` is
|
|
/// always ≥ 7 bytes); protocol garbage.
|
|
#[error("empty frame: no address bytes (len == 0 is unreachable from a compliant peer)")]
|
|
EmptyFrame,
|
|
|
|
/// The frame's first byte is not a known ATYP (`0x01`/`0x03`/
|
|
/// `0x04`) — post-reply control bytes parse as exactly this (the
|
|
/// frame's first byte is ATYP). The loud-fail shape:
|
|
/// `udp_undecodable_frame_ends_association`, POC #2.
|
|
#[error("unknown ATYP {0:#x}: protocol garbage")]
|
|
BadAtyp(u8),
|
|
|
|
/// A frame declared fewer bytes than its ATYP's address form
|
|
/// occupies (e.g. ATYP `0x04` with `len < 19`).
|
|
#[error("frame too short for its address form")]
|
|
ShortAddress,
|
|
|
|
/// The domain form's bytes are not valid UTF-8 (the forked
|
|
/// address codec validates UTF-8; mirrored here).
|
|
#[error("domain form is not valid UTF-8")]
|
|
DomainNotUtf8(#[source] std::str::Utf8Error),
|
|
}
|
|
|
|
/// The RFC 1928 address encoding's length for `addr` (ATYP + addr +
|
|
/// port — exactly `TargetAddr::to_be_bytes()`'s size): 7 bytes for
|
|
/// IPv4, 19 for IPv6, `4 + domain.len()` for domain form (259 max).
|
|
/// Callers compute the joint bound with it: a payload is framable for
|
|
/// `addr` iff `addr_encoding_len(addr) + payload.len() <= MAX_INNER_LEN`.
|
|
pub fn addr_encoding_len(addr: &TargetAddr) -> usize {
|
|
match addr {
|
|
TargetAddr::Ip(SocketAddr::V4(_)) => 1 + 4 + 2,
|
|
TargetAddr::Ip(SocketAddr::V6(_)) => 1 + 16 + 2,
|
|
TargetAddr::Domain(domain, _) => 1 + 1 + domain.len() + 2,
|
|
}
|
|
}
|
|
|
|
/// Frame one datagram: `[len: u16 BE][addr + data]` where `addr +
|
|
/// data` is `TargetAddr::to_be_bytes()` (ATYP + addr + port) followed
|
|
/// by `payload`. One frame per datagram; the single length prefix on
|
|
/// this wire (framing-once — see the module docs). A zero-length
|
|
/// payload is a legal empty datagram (the F-2 invariant, at payload
|
|
/// level). Rejects [`WireError::Oversize`] at frame time when
|
|
/// `len(addr) + len(payload) > 65535` — the joint bound; the payload
|
|
/// bound alone (65507) does not fit u16 unconditionally.
|
|
pub fn frame(addr: &TargetAddr, payload: &[u8]) -> Result<Bytes, WireError> {
|
|
let addr_bytes = addr.to_be_bytes()?;
|
|
let inner = addr_bytes
|
|
.len()
|
|
.checked_add(payload.len())
|
|
.unwrap_or(MAX_INNER_LEN + 1);
|
|
if inner > MAX_INNER_LEN {
|
|
return Err(WireError::Oversize(inner, MAX_INNER_LEN));
|
|
}
|
|
let mut buf = BytesMut::with_capacity(FRAME_LEN_FIELD + inner);
|
|
buf.put_u16(inner as u16);
|
|
buf.put_slice(&addr_bytes);
|
|
buf.put_slice(payload);
|
|
Ok(buf.freeze())
|
|
}
|
|
|
|
/// Frame pre-encoded inner bytes (`addr + data` — exactly what
|
|
/// [`crate::egress::Egress::next_reply`] returns) as one
|
|
/// `[len: u16 BE][inner]` frame. The reply-direction half of the
|
|
/// framing-once invariant: the egress speaks inner bytes (no length
|
|
/// prefix), and this adds the single prefix at the wire layer. Rejects
|
|
/// [`WireError::Oversize`] when the inner bytes exceed
|
|
/// [`MAX_INNER_LEN`] (the u16 length field would wrap).
|
|
pub fn frame_inner(inner: &[u8]) -> Result<Bytes, WireError> {
|
|
if inner.len() > MAX_INNER_LEN {
|
|
return Err(WireError::Oversize(inner.len(), MAX_INNER_LEN));
|
|
}
|
|
let mut buf = BytesMut::with_capacity(FRAME_LEN_FIELD + inner.len());
|
|
buf.put_u16(inner.len() as u16);
|
|
buf.put_slice(inner);
|
|
Ok(buf.freeze())
|
|
}
|
|
|
|
/// Incremental datagram decoder over the framed stage's read
|
|
/// direction. Feed it whatever `read()` yields; it reassembles
|
|
/// `[len: u16 BE][addr + data]` frames across chunk boundaries — a
|
|
/// frame may be split across chunks, several frames may share one
|
|
/// chunk, and partial headers wait for more bytes. Each completed
|
|
/// frame is decoded into `(TargetAddr, Bytes)` — the addr sub-layout
|
|
/// (ATYP determines the address length) is split off, the remainder
|
|
/// is the datagram payload.
|
|
///
|
|
/// Reassembly is infallible (see [`WireError`] docs — the oversize
|
|
/// bound is enforced at frame time); the only error per frame is the
|
|
/// addr decode (the garbage shapes). The reader keeps going after a
|
|
/// decode error (the frame's bytes are consumed either way); the
|
|
/// caller fails loud on the first one.
|
|
#[derive(Debug)]
|
|
pub struct DatagramReader {
|
|
buf: BytesMut,
|
|
current: Option<(usize, BytesMut)>,
|
|
}
|
|
|
|
impl Default for DatagramReader {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl DatagramReader {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
buf: BytesMut::new(),
|
|
current: None,
|
|
}
|
|
}
|
|
|
|
/// Feed raw bytes from the read half. Returns every frame that
|
|
/// completed with this feed, in order — zero or more per chunk.
|
|
/// Buffers partial frames across chunk boundaries (bounded by the
|
|
/// u16 declared length). Infallible reassembly; per-frame addr
|
|
/// decode errors are the [`WireError`] decode variants.
|
|
pub fn feed(&mut self, chunk: &[u8]) -> Vec<Result<(TargetAddr, Bytes), WireError>> {
|
|
self.buf.extend_from_slice(chunk);
|
|
let mut out = Vec::new();
|
|
loop {
|
|
if self.current.is_none() {
|
|
if self.buf.len() < FRAME_LEN_FIELD {
|
|
break;
|
|
}
|
|
let declared = u16::from_be_bytes([self.buf[0], self.buf[1]]) as usize;
|
|
self.buf.advance(FRAME_LEN_FIELD);
|
|
self.current = Some((declared, BytesMut::new()));
|
|
}
|
|
let Some((declared, acc)) = self.current.as_mut() else {
|
|
break;
|
|
};
|
|
if *declared == 0 {
|
|
self.current = None;
|
|
out.push(Err(WireError::EmptyFrame));
|
|
continue;
|
|
}
|
|
let take = (*declared - acc.len()).min(self.buf.len());
|
|
if take > 0 {
|
|
acc.extend_from_slice(&self.buf[..take]);
|
|
self.buf.advance(take);
|
|
}
|
|
if acc.len() == *declared {
|
|
let inner = std::mem::take(acc).freeze();
|
|
self.current = None;
|
|
out.push(decode_inner(&inner));
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// `true` while a partial frame is in flight (the teardown
|
|
/// diagnostic — a clean peer never ends the stream mid-frame; a
|
|
/// stream end while this is set is the session's fail-loud
|
|
/// truncated-datagram condition, never a silent partial
|
|
/// datagram).
|
|
pub fn is_mid_frame(&self) -> bool {
|
|
self.current.is_some()
|
|
}
|
|
}
|
|
|
|
/// Decode one frame's inner bytes (`addr + data`, the length prefix
|
|
/// already consumed) into `(addr, payload)`. The first byte is ATYP;
|
|
/// the address encoding is parsed by hand over the borrowed slice —
|
|
/// the same sub-layout the forked `read_address` accepts in-stream,
|
|
/// mirrored synchronously (the stage's decode must not be async over
|
|
/// a borrowed frame). RSV/FRAG cannot appear (ADR-003: they do not
|
|
/// ride the tunnel wire).
|
|
fn decode_inner(inner: &[u8]) -> Result<(TargetAddr, Bytes), WireError> {
|
|
if inner.is_empty() {
|
|
return Err(WireError::EmptyFrame);
|
|
}
|
|
let atyp = inner[0];
|
|
match atyp {
|
|
consts::SOCKS5_ADDR_TYPE_IPV4 => {
|
|
if inner.len() < 1 + 4 + 2 {
|
|
return Err(WireError::ShortAddress);
|
|
}
|
|
let ip = Ipv4Addr::new(inner[1], inner[2], inner[3], inner[4]);
|
|
let port = u16::from_be_bytes([inner[5], inner[6]]);
|
|
Ok((
|
|
TargetAddr::Ip(SocketAddr::from((ip, port))),
|
|
Bytes::copy_from_slice(&inner[7..]),
|
|
))
|
|
}
|
|
consts::SOCKS5_ADDR_TYPE_IPV6 => {
|
|
if inner.len() < 1 + 16 + 2 {
|
|
return Err(WireError::ShortAddress);
|
|
}
|
|
let mut octets = [0u8; 16];
|
|
octets.copy_from_slice(&inner[1..17]);
|
|
let ip = Ipv6Addr::from(octets);
|
|
let port = u16::from_be_bytes([inner[17], inner[18]]);
|
|
Ok((
|
|
TargetAddr::Ip(SocketAddr::from((ip, port))),
|
|
Bytes::copy_from_slice(&inner[19..]),
|
|
))
|
|
}
|
|
consts::SOCKS5_ADDR_TYPE_DOMAIN_NAME => {
|
|
if inner.len() < 2 {
|
|
return Err(WireError::ShortAddress);
|
|
}
|
|
let dlen = inner[1] as usize;
|
|
if inner.len() < 2 + dlen + 2 {
|
|
return Err(WireError::ShortAddress);
|
|
}
|
|
let domain =
|
|
std::str::from_utf8(&inner[2..2 + dlen]).map_err(WireError::DomainNotUtf8)?;
|
|
let port_at = 2 + dlen;
|
|
let port = u16::from_be_bytes([inner[port_at], inner[port_at + 1]]);
|
|
Ok((
|
|
TargetAddr::Domain(domain.to_owned(), port),
|
|
Bytes::copy_from_slice(&inner[port_at + 2..]),
|
|
))
|
|
}
|
|
other => Err(WireError::BadAtyp(other)),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::rfc1928::util::target_addr::AddrError as AddrErr;
|
|
use std::net::{SocketAddrV4, SocketAddrV6};
|
|
|
|
fn v4(port: u16) -> TargetAddr {
|
|
TargetAddr::Ip(SocketAddr::V4(SocketAddrV4::new(
|
|
Ipv4Addr::new(192, 168, 0, 1),
|
|
port,
|
|
)))
|
|
}
|
|
|
|
fn v6(port: u16) -> TargetAddr {
|
|
TargetAddr::Ip(SocketAddr::V6(SocketAddrV6::new(
|
|
Ipv6Addr::LOCALHOST,
|
|
port,
|
|
0,
|
|
0,
|
|
)))
|
|
}
|
|
|
|
fn domain(name: &str, port: u16) -> TargetAddr {
|
|
TargetAddr::Domain(name.to_owned(), port)
|
|
}
|
|
|
|
fn feed_one(
|
|
reader: &mut DatagramReader,
|
|
chunk: &[u8],
|
|
) -> Option<Result<(TargetAddr, Bytes), WireError>> {
|
|
reader.feed(chunk).into_iter().next()
|
|
}
|
|
|
|
#[test]
|
|
fn addr_encoding_len_matches_the_rfc_forms() {
|
|
assert_eq!(addr_encoding_len(&v4(80)), 7);
|
|
assert_eq!(addr_encoding_len(&v6(443)), 19);
|
|
assert_eq!(addr_encoding_len(&domain("example.com", 53)), 15);
|
|
assert_eq!(addr_encoding_len(&domain(&"a".repeat(255), 53)), 259);
|
|
}
|
|
|
|
#[test]
|
|
fn frame_layout_is_len_then_addr_then_payload() {
|
|
let addr = v4(80);
|
|
let framed = frame(&addr, b"hi").unwrap();
|
|
let addr_bytes = addr.to_be_bytes().unwrap();
|
|
let inner = (addr_bytes.len() + 2) as u16;
|
|
assert_eq!(&framed[..2], &inner.to_be_bytes());
|
|
assert_eq!(&framed[2..2 + addr_bytes.len()], &addr_bytes[..]);
|
|
assert_eq!(&framed[2 + addr_bytes.len()..], b"hi");
|
|
assert_eq!(framed.len(), 2 + inner as usize);
|
|
}
|
|
|
|
#[test]
|
|
fn round_trips_all_address_forms() {
|
|
for addr in [v4(80), v6(443), domain("example.com", 53)] {
|
|
let framed = frame(&addr, b"hello").unwrap();
|
|
let mut r = DatagramReader::new();
|
|
let dgs = r.feed(&framed);
|
|
assert_eq!(dgs.len(), 1);
|
|
let (src, payload) = dgs.into_iter().next().unwrap().unwrap();
|
|
assert_eq!(src, addr);
|
|
assert_eq!(&payload[..], b"hello");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn empty_payload_is_a_legal_datagram_not_eof() {
|
|
for addr in [v4(53), v6(53), domain("example.com", 53)] {
|
|
let framed = frame(&addr, b"").unwrap();
|
|
assert_eq!(framed.len(), 2 + addr_encoding_len(&addr));
|
|
let mut r = DatagramReader::new();
|
|
let dgs = r.feed(&framed);
|
|
assert_eq!(dgs.len(), 1, "an empty datagram is a real datagram");
|
|
let (src, payload) = dgs.into_iter().next().unwrap().unwrap();
|
|
assert_eq!(src, addr);
|
|
assert!(payload.is_empty());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn frame_split_across_awkward_chunks_reassembles() {
|
|
let framed = frame(&v4(80), b"0123456789").unwrap();
|
|
let mut r = DatagramReader::new();
|
|
let mut got = None;
|
|
for piece in framed.chunks(7) {
|
|
for res in r.feed(piece) {
|
|
got = Some(res);
|
|
}
|
|
}
|
|
let (src, payload) = got
|
|
.expect("datagram reassembled across 7-byte chunks")
|
|
.unwrap();
|
|
assert_eq!(src, v4(80));
|
|
assert_eq!(&payload[..], b"0123456789");
|
|
}
|
|
|
|
#[test]
|
|
fn byte_by_byte_feeding_reassembles_batched_frames_in_order() {
|
|
let mut buf = BytesMut::new();
|
|
buf.extend_from_slice(&frame(&v4(1), b"abc").unwrap());
|
|
buf.extend_from_slice(&frame(&domain("example.com", 2), b"de").unwrap());
|
|
buf.extend_from_slice(&frame(&v6(3), b"").unwrap());
|
|
let wire = buf.freeze();
|
|
let mut r = DatagramReader::new();
|
|
let mut got = Vec::new();
|
|
for piece in wire.chunks(1) {
|
|
got.extend(r.feed(piece));
|
|
}
|
|
assert_eq!(got.len(), 3);
|
|
let (src1, p1) = got.remove(0).unwrap();
|
|
assert_eq!(src1, v4(1));
|
|
assert_eq!(&p1[..], b"abc");
|
|
let (src2, p2) = got.remove(0).unwrap();
|
|
assert_eq!(src2, domain("example.com", 2));
|
|
assert_eq!(&p2[..], b"de");
|
|
let (src3, p3) = got.remove(0).unwrap();
|
|
assert_eq!(src3, v6(3));
|
|
assert!(p3.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn two_frames_batched_in_one_chunk() {
|
|
let mut buf = BytesMut::new();
|
|
buf.extend_from_slice(&frame(&v4(1), b"abc").unwrap());
|
|
buf.extend_from_slice(&frame(&v6(2), b"de").unwrap());
|
|
let mut r = DatagramReader::new();
|
|
let mut dgs = r.feed(&buf.freeze());
|
|
assert_eq!(dgs.len(), 2);
|
|
let (src1, p1) = dgs.remove(0).unwrap();
|
|
assert_eq!(src1, v4(1));
|
|
assert_eq!(&p1[..], b"abc");
|
|
let (src2, p2) = dgs.remove(0).unwrap();
|
|
assert_eq!(src2, v6(2));
|
|
assert_eq!(&p2[..], b"de");
|
|
}
|
|
|
|
#[test]
|
|
fn partial_header_at_chunk_boundary_waits() {
|
|
let framed = frame(&v4(80), b"xyz").unwrap();
|
|
let mut r = DatagramReader::new();
|
|
assert!(feed_one(&mut r, &framed[..1]).is_none());
|
|
assert!(
|
|
!r.is_mid_frame(),
|
|
"a partial header is not a frame in flight"
|
|
);
|
|
let pair = feed_one(&mut r, &framed[1..]).expect("frame after header completes");
|
|
let (src, payload) = pair.unwrap();
|
|
assert_eq!(src, v4(80));
|
|
assert_eq!(&payload[..], b"xyz");
|
|
}
|
|
|
|
#[test]
|
|
fn mid_frame_state_is_observable() {
|
|
let framed = frame(&v4(80), b"0123").unwrap();
|
|
let mut r = DatagramReader::new();
|
|
r.feed(&framed[..3]);
|
|
assert!(r.is_mid_frame());
|
|
r.feed(&framed[3..]);
|
|
assert!(!r.is_mid_frame());
|
|
}
|
|
|
|
#[test]
|
|
fn truncated_stream_never_yields_partial_frame() {
|
|
let framed = frame(&v4(80), b"payload").unwrap();
|
|
let mut r = DatagramReader::new();
|
|
r.feed(&framed[..3]);
|
|
assert!(r.is_mid_frame());
|
|
assert!(feed_one(&mut r, &[]).is_none());
|
|
assert!(r.is_mid_frame());
|
|
assert_eq!(
|
|
r.feed(&framed[3..framed.len() - 1]).len(),
|
|
0,
|
|
"one byte short must stay buffered, not yield a partial frame"
|
|
);
|
|
assert!(r.is_mid_frame());
|
|
}
|
|
|
|
#[test]
|
|
fn joint_bound_rejects_65507_payload_to_max_domain() {
|
|
let addr = domain(&"a".repeat(255), 53);
|
|
let err = frame(&addr, &vec![0u8; 65507]).unwrap_err();
|
|
assert!(matches!(err, WireError::Oversize(65766, 65535)));
|
|
}
|
|
|
|
#[test]
|
|
fn joint_bound_accepts_65507_payload_to_ipv4() {
|
|
let addr = v4(53);
|
|
let framed = frame(&addr, &vec![0u8; 65507]).unwrap();
|
|
assert_eq!(framed.len(), 2 + 65514);
|
|
}
|
|
|
|
#[test]
|
|
fn joint_bound_exact_limit_is_legal_and_one_more_rejects() {
|
|
for (addr, max_payload) in [
|
|
(v4(80), 65528usize),
|
|
(v6(80), 65516usize),
|
|
(domain(&"a".repeat(255), 80), 65276usize),
|
|
] {
|
|
let framed = frame(&addr, &vec![0u8; max_payload]).unwrap();
|
|
assert_eq!(framed.len(), 2 + MAX_INNER_LEN);
|
|
let err = frame(&addr, &vec![0u8; max_payload + 1]).unwrap_err();
|
|
assert!(matches!(err, WireError::Oversize(65536, 65535)));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn frame_inner_frames_the_egress_inner_bytes_exactly_once() {
|
|
let inner = {
|
|
let mut v = v4(53).to_be_bytes().unwrap().to_vec();
|
|
v.extend_from_slice(b"answer");
|
|
v
|
|
};
|
|
let framed = frame_inner(&inner).unwrap();
|
|
assert_eq!(&framed[..2], &(inner.len() as u16).to_be_bytes());
|
|
assert_eq!(&framed[2..], &inner[..]);
|
|
// The read direction decodes it back: the same (addr, payload)
|
|
// pair the egress encoded (the round-trip shape).
|
|
let mut r = DatagramReader::new();
|
|
let dgs = r.feed(&framed);
|
|
assert_eq!(dgs.len(), 1);
|
|
let (src, payload) = dgs.into_iter().next().unwrap().unwrap();
|
|
assert_eq!(src, v4(53));
|
|
assert_eq!(&payload[..], b"answer");
|
|
}
|
|
|
|
#[test]
|
|
fn frame_inner_rejects_inner_bytes_over_the_u16_bound() {
|
|
let err = frame_inner(&vec![0u8; MAX_INNER_LEN + 1]).unwrap_err();
|
|
assert!(matches!(err, WireError::Oversize(65536, 65535)));
|
|
let framed = frame_inner(&vec![0u8; MAX_INNER_LEN]).unwrap();
|
|
assert_eq!(framed.len(), 2 + MAX_INNER_LEN);
|
|
}
|
|
|
|
#[test]
|
|
fn frame_and_frame_inner_produce_identical_wires() {
|
|
// The reply path (frame_inner over pre-encoded inner bytes)
|
|
// and the forward path (frame over addr + payload) agree
|
|
// byte-for-byte — one codec, two callers.
|
|
let addr = domain("example.com", 53);
|
|
let payload = b"payload";
|
|
let mut inner = addr.to_be_bytes().unwrap().to_vec();
|
|
inner.extend_from_slice(payload);
|
|
assert_eq!(frame_inner(&inner).unwrap(), frame(&addr, payload).unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn oversize_rejected_at_frame_time() {
|
|
let addr = v4(80);
|
|
let err = frame(&addr, &vec![0u8; MAX_INNER_LEN - 6]).unwrap_err();
|
|
assert!(matches!(err, WireError::Oversize(65536, 65535)));
|
|
}
|
|
|
|
#[test]
|
|
fn domain_too_long_is_an_address_error() {
|
|
let addr = domain(&"a".repeat(256), 80);
|
|
let err = frame(&addr, b"").unwrap_err();
|
|
match err {
|
|
WireError::Address(AddrErr::DomainLenTooLong(256)) => {}
|
|
other => panic!("expected Address(DomainLenTooLong(256)), got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn garbage_atyp_decode_is_typed_not_panic() {
|
|
let wire = [0x00, 0x03, 0x7f, 0x01, 0x02];
|
|
let mut r = DatagramReader::new();
|
|
let dgs = r.feed(&wire);
|
|
assert_eq!(dgs.len(), 1);
|
|
match dgs.into_iter().next().unwrap() {
|
|
Err(WireError::BadAtyp(0x7f)) => {}
|
|
other => panic!("expected BadAtyp(0x7f), got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn empty_wire_frame_is_a_decode_error() {
|
|
let wire = [0x00, 0x00];
|
|
let mut r = DatagramReader::new();
|
|
let dgs = r.feed(&wire);
|
|
assert_eq!(dgs.len(), 1);
|
|
match dgs.into_iter().next().unwrap() {
|
|
Err(WireError::EmptyFrame) => {}
|
|
other => panic!("expected EmptyFrame, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn short_address_frame_is_a_decode_error() {
|
|
let wire = [0x00, 0x08, 0x04, 1, 2, 3, 4, 5, 6, 7];
|
|
let mut r = DatagramReader::new();
|
|
let dgs = r.feed(&wire);
|
|
assert_eq!(dgs.len(), 1);
|
|
match dgs.into_iter().next().unwrap() {
|
|
Err(WireError::ShortAddress) => {}
|
|
other => panic!("expected ShortAddress, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn non_utf8_domain_is_a_decode_error() {
|
|
let wire = vec![0x00, 0x07, 0x03, 3, 0xff, 0xfe, 0xfd, 0, 80];
|
|
let mut r = DatagramReader::new();
|
|
let dgs = r.feed(&wire);
|
|
assert_eq!(dgs.len(), 1);
|
|
match dgs.into_iter().next().unwrap() {
|
|
Err(WireError::DomainNotUtf8(_)) => {}
|
|
other => panic!("expected DomainNotUtf8, got {other:?}"),
|
|
}
|
|
}
|
|
}
|