feat(proto): add tunnel frame primitives

Build the shared protocol contract that the client, gateway, and relay will use
for Ethernet datagrams. The MVP needs these pieces agreed on before socket or
TAP work can be reasoned about safely.

This adds strict MAC parsing and client identity validation, Ethernet header
inspection, fixed overlay datagram encoding and decoding, and MTU helpers for
the no-fragmentation QUIC datagram design. The protocol crate stays
transport-agnostic so platform and network code can depend on it without
pulling in OS-specific behavior.

Remaining work is to put these primitives behind the control-plane handshake,
relay forwarding loop, Windows TAP client, and Linux AF_PACKET gateway.

Test Plan:
- cargo fmt --check
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings

Refs: PLAN.md Phase 1: prove the illusion
This commit is contained in:
2026-05-21 17:09:01 +02:00
parent b41f75fbc9
commit f06760d1ac
8 changed files with 756 additions and 12 deletions
Generated
+89
View File
@@ -25,7 +25,96 @@ version = "0.1.0"
[[package]] [[package]]
name = "lanparty-proto" name = "lanparty-proto"
version = "0.1.0" version = "0.1.0"
dependencies = [
"serde",
"thiserror",
]
[[package]] [[package]]
name = "lanparty-relay" name = "lanparty-relay"
version = "0.1.0" 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"
+9
View File
@@ -12,6 +12,15 @@ Monorepo for a Layer 2 over QUIC LAN party bridge.
- `lanparty-gateway`: Linux AF_PACKET gateway binary. - `lanparty-gateway`: Linux AF_PACKET gateway binary.
- `lanparty-relay`: public QUIC relay 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 ## Build
```bash ```bash
+2
View File
@@ -4,3 +4,5 @@ version.workspace = true
edition.workspace = true edition.workspace = true
[dependencies] [dependencies]
serde.workspace = true
thiserror.workspace = true
+108
View File
@@ -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<Self, ProtoError> {
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
}
));
}
}
+22 -12
View File
@@ -1,14 +1,24 @@
pub fn add(left: u64, right: u64) -> u64 { //! Shared protocol primitives for the LAN party L2 tunnel.
left + right //!
} //! 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 ethernet;
mod tests { mod mac;
use super::*; mod mtu;
mod overlay;
#[test] pub use ethernet::{
fn it_works() { ETHERNET_HEADER_LEN, EthernetFrame, MAX_STANDARD_ETHERNET_FRAME_LEN,
let result = add(2, 2); MAX_STANDARD_ETHERNET_PAYLOAD_LEN, MIN_ETHERNET_FRAME_LEN,
assert_eq!(result, 4); };
} 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,
};
+170
View File
@@ -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<MacAddr> 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<Self, Self::Err> {
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::<MacAddr>().unwrap_err(),
MacParseError::WrongOctetCount
);
assert!(matches!(
"02:00:00:00:00:xx".parse::<MacAddr>().unwrap_err(),
MacParseError::InvalidOctet { index: 5, .. }
));
assert!(matches!(
"02:00:00:00:00:0".parse::<MacAddr>().unwrap_err(),
MacParseError::InvalidOctet { index: 5, .. }
));
}
}
+72
View File
@@ -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<usize, MtuError> {
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<usize, MtuError> {
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,
..
}
));
}
}
+284
View File
@@ -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<Self, ProtoError> {
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<Self, ProtoError> {
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<Self, ProtoError> {
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<Self, ProtoError> {
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<Vec<u8>, 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<OverlayPacket<'_>, 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 { .. }
));
}
}