//! 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 { 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 { 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> { 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> { 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:?}"), } } }