diff --git a/Cargo.lock b/Cargo.lock index 8389b33..ded73c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,7 +25,96 @@ version = "0.1.0" [[package]] name = "lanparty-proto" version = "0.1.0" +dependencies = [ + "serde", + "thiserror", +] [[package]] name = "lanparty-relay" version = "0.1.0" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/README.md b/README.md index d2c2038..0219fc6 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,15 @@ Monorepo for a Layer 2 over QUIC LAN party bridge. - `lanparty-gateway`: Linux AF_PACKET gateway binary. - `lanparty-relay`: public QUIC relay binary. +### `lanparty-proto` + +Transport-agnostic tunnel contract shared by all binaries: + +- overlay datagram header encoding and decoding +- Ethernet frame header parsing +- MAC address parsing and identity validation +- QUIC datagram to TAP MTU budget helpers + ## Build ```bash diff --git a/crates/lanparty-proto/Cargo.toml b/crates/lanparty-proto/Cargo.toml index 2eda2ad..a6f92c5 100644 --- a/crates/lanparty-proto/Cargo.toml +++ b/crates/lanparty-proto/Cargo.toml @@ -4,3 +4,5 @@ version.workspace = true edition.workspace = true [dependencies] +serde.workspace = true +thiserror.workspace = true diff --git a/crates/lanparty-proto/src/ethernet.rs b/crates/lanparty-proto/src/ethernet.rs new file mode 100644 index 0000000..077c336 --- /dev/null +++ b/crates/lanparty-proto/src/ethernet.rs @@ -0,0 +1,108 @@ +use crate::{MacAddr, ProtoError}; + +pub const ETHERNET_HEADER_LEN: usize = 14; +pub const MIN_ETHERNET_FRAME_LEN: usize = ETHERNET_HEADER_LEN; +pub const MAX_STANDARD_ETHERNET_PAYLOAD_LEN: usize = 1500; +pub const MAX_STANDARD_ETHERNET_FRAME_LEN: usize = + ETHERNET_HEADER_LEN + MAX_STANDARD_ETHERNET_PAYLOAD_LEN; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EthernetFrame<'a> { + bytes: &'a [u8], +} + +impl<'a> EthernetFrame<'a> { + pub fn parse(bytes: &'a [u8]) -> Result { + if bytes.len() < MIN_ETHERNET_FRAME_LEN { + return Err(ProtoError::EthernetFrameTooShort { + actual: bytes.len(), + minimum: MIN_ETHERNET_FRAME_LEN, + }); + } + + Ok(Self { bytes }) + } + + #[must_use] + pub const fn bytes(self) -> &'a [u8] { + self.bytes + } + + #[must_use] + pub const fn len(self) -> usize { + self.bytes.len() + } + + #[must_use] + pub const fn is_empty(self) -> bool { + self.bytes.is_empty() + } + + #[must_use] + pub fn destination(self) -> MacAddr { + let bytes = self.bytes; + MacAddr::new([bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5]]) + } + + #[must_use] + pub fn source(self) -> MacAddr { + let bytes = self.bytes; + MacAddr::new([bytes[6], bytes[7], bytes[8], bytes[9], bytes[10], bytes[11]]) + } + + #[must_use] + pub fn ethertype_or_len(self) -> u16 { + u16::from_be_bytes([self.bytes[12], self.bytes[13]]) + } + + #[must_use] + pub fn is_broadcast(self) -> bool { + self.destination().is_broadcast() + } + + #[must_use] + pub fn is_multicast(self) -> bool { + self.destination().is_multicast() + } + + #[must_use] + pub const fn is_jumbo(self) -> bool { + self.bytes.len() > MAX_STANDARD_ETHERNET_FRAME_LEN + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_ethernet_header_fields() { + let bytes = [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x08, 0x06, 1, + 2, 3, + ]; + let frame = EthernetFrame::parse(&bytes).unwrap(); + + assert_eq!(frame.destination(), MacAddr::BROADCAST); + assert_eq!( + frame.source(), + MacAddr::new([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0xee]) + ); + assert_eq!(frame.ethertype_or_len(), 0x0806); + assert!(frame.is_broadcast()); + assert!(frame.is_multicast()); + } + + #[test] + fn rejects_runt_frames() { + let error = EthernetFrame::parse(&[0; 13]).unwrap_err(); + + assert!(matches!( + error, + ProtoError::EthernetFrameTooShort { + actual: 13, + minimum: MIN_ETHERNET_FRAME_LEN + } + )); + } +} diff --git a/crates/lanparty-proto/src/lib.rs b/crates/lanparty-proto/src/lib.rs index b93cf3f..7506c55 100644 --- a/crates/lanparty-proto/src/lib.rs +++ b/crates/lanparty-proto/src/lib.rs @@ -1,14 +1,24 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +//! Shared protocol primitives for the LAN party L2 tunnel. +//! +//! This crate intentionally contains no socket, TAP, QUIC, or OS-specific +//! behavior. It is the small contract that the Windows client, Linux gateway, +//! and relay must all agree on. -#[cfg(test)] -mod tests { - use super::*; +mod ethernet; +mod mac; +mod mtu; +mod overlay; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub use ethernet::{ + ETHERNET_HEADER_LEN, EthernetFrame, MAX_STANDARD_ETHERNET_FRAME_LEN, + MAX_STANDARD_ETHERNET_PAYLOAD_LEN, MIN_ETHERNET_FRAME_LEN, +}; +pub use mac::{MacAddr, MacParseError}; +pub use mtu::{ + DEFAULT_DATAGRAM_SAFETY_MARGIN, DEFAULT_TAP_MTU, MIN_USEFUL_TAP_MTU, MtuError, + max_tap_mtu_for_datagram, recommended_tap_mtu, +}; +pub use overlay::{ + FrameType, OVERLAY_HEADER_LEN, OVERLAY_MAGIC, OVERLAY_VERSION, OverlayHeader, OverlayPacket, + ProtoError, decode_datagram, encode_datagram, +}; diff --git a/crates/lanparty-proto/src/mac.rs b/crates/lanparty-proto/src/mac.rs new file mode 100644 index 0000000..31f6228 --- /dev/null +++ b/crates/lanparty-proto/src/mac.rs @@ -0,0 +1,170 @@ +use std::{fmt, str::FromStr}; + +use thiserror::Error; + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum MacParseError { + #[error("MAC address must contain exactly 6 colon-separated octets")] + WrongOctetCount, + #[error("invalid MAC octet {index}: {value:?}")] + InvalidOctet { index: usize, value: String }, +} + +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Deserialize, serde::Serialize, +)] +pub struct MacAddr([u8; 6]); + +impl MacAddr { + pub const BROADCAST: Self = Self([0xff; 6]); + pub const ZERO: Self = Self([0; 6]); + + #[must_use] + pub const fn new(bytes: [u8; 6]) -> Self { + Self(bytes) + } + + #[must_use] + pub const fn octets(self) -> [u8; 6] { + self.0 + } + + #[must_use] + pub const fn is_zero(self) -> bool { + let bytes = self.0; + bytes[0] == 0 + && bytes[1] == 0 + && bytes[2] == 0 + && bytes[3] == 0 + && bytes[4] == 0 + && bytes[5] == 0 + } + + #[must_use] + pub const fn is_broadcast(self) -> bool { + let bytes = self.0; + bytes[0] == 0xff + && bytes[1] == 0xff + && bytes[2] == 0xff + && bytes[3] == 0xff + && bytes[4] == 0xff + && bytes[5] == 0xff + } + + #[must_use] + pub const fn is_multicast(self) -> bool { + self.0[0] & 0x01 != 0 + } + + #[must_use] + pub const fn is_unicast(self) -> bool { + !self.is_multicast() + } + + #[must_use] + pub const fn is_locally_administered(self) -> bool { + self.0[0] & 0x02 != 0 + } + + #[must_use] + pub const fn is_valid_unicast(self) -> bool { + self.is_unicast() && !self.is_zero() && !self.is_broadcast() + } + + #[must_use] + pub const fn is_valid_client_identity(self) -> bool { + self.is_valid_unicast() && self.is_locally_administered() + } +} + +impl From<[u8; 6]> for MacAddr { + fn from(value: [u8; 6]) -> Self { + Self(value) + } +} + +impl From for [u8; 6] { + fn from(value: MacAddr) -> Self { + value.octets() + } +} + +impl fmt::Display for MacAddr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let [a, b, c, d, e, g] = self.0; + write!(f, "{a:02x}:{b:02x}:{c:02x}:{d:02x}:{e:02x}:{g:02x}") + } +} + +impl FromStr for MacAddr { + type Err = MacParseError; + + fn from_str(value: &str) -> Result { + let mut octets = [0; 6]; + let mut count = 0; + + for (index, octet) in value.split(':').enumerate() { + if index >= octets.len() { + return Err(MacParseError::WrongOctetCount); + } + + if octet.len() != 2 { + return Err(MacParseError::InvalidOctet { + index, + value: octet.to_owned(), + }); + } + + octets[index] = + u8::from_str_radix(octet, 16).map_err(|_| MacParseError::InvalidOctet { + index, + value: octet.to_owned(), + })?; + count += 1; + } + + if count != octets.len() { + return Err(MacParseError::WrongOctetCount); + } + + Ok(Self(octets)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_and_formats_mac_addresses() { + let mac: MacAddr = "02:0a:ff:10:20:30".parse().unwrap(); + + assert_eq!(mac.octets(), [0x02, 0x0a, 0xff, 0x10, 0x20, 0x30]); + assert_eq!(mac.to_string(), "02:0a:ff:10:20:30"); + } + + #[test] + fn classifies_client_identity_addresses() { + assert!(MacAddr::new([0x02, 1, 2, 3, 4, 5]).is_valid_client_identity()); + assert!(!MacAddr::new([0x00, 1, 2, 3, 4, 5]).is_valid_client_identity()); + assert!(!MacAddr::new([0x03, 1, 2, 3, 4, 5]).is_valid_client_identity()); + assert!(!MacAddr::ZERO.is_valid_client_identity()); + assert!(!MacAddr::BROADCAST.is_valid_client_identity()); + } + + #[test] + fn rejects_wrong_mac_shape() { + assert_eq!( + "02:00:00:00:00".parse::().unwrap_err(), + MacParseError::WrongOctetCount + ); + assert!(matches!( + "02:00:00:00:00:xx".parse::().unwrap_err(), + MacParseError::InvalidOctet { index: 5, .. } + )); + assert!(matches!( + "02:00:00:00:00:0".parse::().unwrap_err(), + MacParseError::InvalidOctet { index: 5, .. } + )); + } +} diff --git a/crates/lanparty-proto/src/mtu.rs b/crates/lanparty-proto/src/mtu.rs new file mode 100644 index 0000000..9b25050 --- /dev/null +++ b/crates/lanparty-proto/src/mtu.rs @@ -0,0 +1,72 @@ +use thiserror::Error; + +use crate::{ETHERNET_HEADER_LEN, OVERLAY_HEADER_LEN}; + +pub const DEFAULT_TAP_MTU: usize = 1200; +pub const MIN_USEFUL_TAP_MTU: usize = 576; +pub const DEFAULT_DATAGRAM_SAFETY_MARGIN: usize = 16; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] +pub enum MtuError { + #[error( + "QUIC datagram payload budget {quic_max_datagram_size} is too small; need at least {required_minimum}" + )] + DatagramTooSmall { + quic_max_datagram_size: usize, + required_minimum: usize, + }, +} + +pub fn max_tap_mtu_for_datagram( + quic_max_datagram_size: usize, + safety_margin: usize, +) -> Result { + let overhead = OVERLAY_HEADER_LEN + ETHERNET_HEADER_LEN + safety_margin; + let required_minimum = overhead + MIN_USEFUL_TAP_MTU; + + if quic_max_datagram_size < required_minimum { + return Err(MtuError::DatagramTooSmall { + quic_max_datagram_size, + required_minimum, + }); + } + + Ok(quic_max_datagram_size - overhead) +} + +pub fn recommended_tap_mtu(quic_max_datagram_size: usize) -> Result { + let max = max_tap_mtu_for_datagram(quic_max_datagram_size, DEFAULT_DATAGRAM_SAFETY_MARGIN)?; + + Ok(max.min(DEFAULT_TAP_MTU)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn recommends_default_when_budget_allows() { + assert_eq!(recommended_tap_mtu(1400).unwrap(), DEFAULT_TAP_MTU); + } + + #[test] + fn clamps_to_available_budget() { + assert_eq!( + recommended_tap_mtu(OVERLAY_HEADER_LEN + ETHERNET_HEADER_LEN + 16 + 900).unwrap(), + 900 + ); + } + + #[test] + fn rejects_too_small_datagram_budget() { + let error = recommended_tap_mtu(128).unwrap_err(); + + assert!(matches!( + error, + MtuError::DatagramTooSmall { + quic_max_datagram_size: 128, + .. + } + )); + } +} diff --git a/crates/lanparty-proto/src/overlay.rs b/crates/lanparty-proto/src/overlay.rs new file mode 100644 index 0000000..d1a6535 --- /dev/null +++ b/crates/lanparty-proto/src/overlay.rs @@ -0,0 +1,284 @@ +use thiserror::Error; + +pub const OVERLAY_MAGIC: u32 = 0x534c_414e; // "SLAN" +pub const OVERLAY_VERSION: u8 = 1; +pub const OVERLAY_HEADER_LEN: usize = 22; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[repr(u8)] +pub enum FrameType { + Ethernet = 1, + Control = 2, + Keepalive = 3, +} + +impl FrameType { + #[must_use] + pub const fn as_u8(self) -> u8 { + self as u8 + } + + pub const fn from_u8(value: u8) -> Result { + match value { + 1 => Ok(Self::Ethernet), + 2 => Ok(Self::Control), + 3 => Ok(Self::Keepalive), + other => Err(ProtoError::UnknownFrameType(other)), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +pub struct OverlayHeader { + frame_type: FrameType, + room_id: u64, + peer_id: u32, + flags: u16, + payload_len: u16, +} + +impl OverlayHeader { + pub fn new( + frame_type: FrameType, + room_id: u64, + peer_id: u32, + flags: u16, + payload_len: usize, + ) -> Result { + let payload_len = u16::try_from(payload_len).map_err(|_| ProtoError::PayloadTooLarge { + len: payload_len, + max: u16::MAX as usize, + })?; + + Ok(Self { + frame_type, + room_id, + peer_id, + flags, + payload_len, + }) + } + + #[must_use] + pub const fn frame_type(self) -> FrameType { + self.frame_type + } + + #[must_use] + pub const fn room_id(self) -> u64 { + self.room_id + } + + #[must_use] + pub const fn peer_id(self) -> u32 { + self.peer_id + } + + #[must_use] + pub const fn flags(self) -> u16 { + self.flags + } + + #[must_use] + pub const fn payload_len(self) -> u16 { + self.payload_len + } + + #[must_use] + pub fn encode(self) -> [u8; OVERLAY_HEADER_LEN] { + let mut bytes = [0; OVERLAY_HEADER_LEN]; + bytes[0..4].copy_from_slice(&OVERLAY_MAGIC.to_be_bytes()); + bytes[4] = OVERLAY_VERSION; + bytes[5] = self.frame_type.as_u8(); + bytes[6..14].copy_from_slice(&self.room_id.to_be_bytes()); + bytes[14..18].copy_from_slice(&self.peer_id.to_be_bytes()); + bytes[18..20].copy_from_slice(&self.flags.to_be_bytes()); + bytes[20..22].copy_from_slice(&self.payload_len.to_be_bytes()); + bytes + } + + pub fn decode(bytes: &[u8]) -> Result { + if bytes.len() < OVERLAY_HEADER_LEN { + return Err(ProtoError::DatagramTooShort { + actual: bytes.len(), + minimum: OVERLAY_HEADER_LEN, + }); + } + + let magic = u32::from_be_bytes(bytes[0..4].try_into().expect("header magic slice length")); + if magic != OVERLAY_MAGIC { + return Err(ProtoError::BadMagic { actual: magic }); + } + + let version = bytes[4]; + if version != OVERLAY_VERSION { + return Err(ProtoError::UnsupportedVersion { actual: version }); + } + + Ok(Self { + frame_type: FrameType::from_u8(bytes[5])?, + room_id: u64::from_be_bytes(bytes[6..14].try_into().expect("room id slice length")), + peer_id: u32::from_be_bytes(bytes[14..18].try_into().expect("peer id slice length")), + flags: u16::from_be_bytes(bytes[18..20].try_into().expect("flags slice length")), + payload_len: u16::from_be_bytes( + bytes[20..22].try_into().expect("payload len slice length"), + ), + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct OverlayPacket<'a> { + header: OverlayHeader, + payload: &'a [u8], +} + +impl<'a> OverlayPacket<'a> { + pub fn new(header: OverlayHeader, payload: &'a [u8]) -> Result { + let declared = usize::from(header.payload_len); + + if payload.len() != declared { + return Err(ProtoError::PayloadLengthMismatch { + declared, + actual: payload.len(), + }); + } + + Ok(Self { header, payload }) + } + + #[must_use] + pub const fn header(self) -> OverlayHeader { + self.header + } + + #[must_use] + pub const fn payload(self) -> &'a [u8] { + self.payload + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum ProtoError { + #[error("datagram is too short: got {actual} bytes, need at least {minimum}")] + DatagramTooShort { actual: usize, minimum: usize }, + #[error("bad overlay magic 0x{actual:08x}")] + BadMagic { actual: u32 }, + #[error("unsupported overlay version {actual}")] + UnsupportedVersion { actual: u8 }, + #[error("unknown overlay frame type {0}")] + UnknownFrameType(u8), + #[error("payload length {len} exceeds wire maximum {max}")] + PayloadTooLarge { len: usize, max: usize }, + #[error("declared payload length {declared} does not match actual length {actual}")] + PayloadLengthMismatch { declared: usize, actual: usize }, + #[error("Ethernet frame is too short: got {actual} bytes, need at least {minimum}")] + EthernetFrameTooShort { actual: usize, minimum: usize }, +} + +pub fn encode_datagram( + frame_type: FrameType, + room_id: u64, + peer_id: u32, + flags: u16, + payload: &[u8], +) -> Result, ProtoError> { + let header = OverlayHeader::new(frame_type, room_id, peer_id, flags, payload.len())?; + let mut datagram = Vec::with_capacity(OVERLAY_HEADER_LEN + payload.len()); + datagram.extend_from_slice(&header.encode()); + datagram.extend_from_slice(payload); + Ok(datagram) +} + +pub fn decode_datagram(bytes: &[u8]) -> Result, ProtoError> { + let header = OverlayHeader::decode(bytes)?; + let payload = &bytes[OVERLAY_HEADER_LEN..]; + + OverlayPacket::new(header, payload) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encodes_and_decodes_datagrams() { + let payload = [1, 2, 3, 4]; + let datagram = encode_datagram( + FrameType::Ethernet, + 0x0102_0304_0506_0708, + 0x0a0b_0c0d, + 0x1001, + &payload, + ) + .unwrap(); + + assert_eq!(datagram.len(), OVERLAY_HEADER_LEN + payload.len()); + assert_eq!(&datagram[0..4], &OVERLAY_MAGIC.to_be_bytes()); + assert_eq!(datagram[4], OVERLAY_VERSION); + assert_eq!(datagram[5], FrameType::Ethernet.as_u8()); + + let packet = decode_datagram(&datagram).unwrap(); + let header = packet.header(); + assert_eq!(header.frame_type(), FrameType::Ethernet); + assert_eq!(header.room_id(), 0x0102_0304_0506_0708); + assert_eq!(header.peer_id(), 0x0a0b_0c0d); + assert_eq!(header.flags(), 0x1001); + assert_eq!(header.payload_len(), 4); + assert_eq!(packet.payload(), payload); + } + + #[test] + fn rejects_payload_length_mismatch() { + let mut datagram = encode_datagram(FrameType::Keepalive, 1, 2, 0, &[1, 2, 3]).unwrap(); + datagram.pop(); + + let error = decode_datagram(&datagram).unwrap_err(); + + assert!(matches!( + error, + ProtoError::PayloadLengthMismatch { + declared: 3, + actual: 2 + } + )); + } + + #[test] + fn rejects_bad_magic_and_version() { + let mut datagram = encode_datagram(FrameType::Control, 1, 2, 0, &[]).unwrap(); + datagram[0] = 0; + assert!(matches!( + decode_datagram(&datagram).unwrap_err(), + ProtoError::BadMagic { .. } + )); + + let mut datagram = encode_datagram(FrameType::Control, 1, 2, 0, &[]).unwrap(); + datagram[4] = 99; + assert!(matches!( + decode_datagram(&datagram).unwrap_err(), + ProtoError::UnsupportedVersion { actual: 99 } + )); + } + + #[test] + fn rejects_unknown_frame_type() { + let mut datagram = encode_datagram(FrameType::Control, 1, 2, 0, &[]).unwrap(); + datagram[5] = 99; + + assert_eq!( + decode_datagram(&datagram).unwrap_err(), + ProtoError::UnknownFrameType(99) + ); + } + + #[test] + fn rejects_payloads_too_large_for_header() { + let payload = vec![0; usize::from(u16::MAX) + 1]; + + assert!(matches!( + encode_datagram(FrameType::Ethernet, 1, 2, 0, &payload).unwrap_err(), + ProtoError::PayloadTooLarge { .. } + )); + } +}