Files
softlan-vpn/crates/lanparty-proto/src/mac.rs
T
ddidderr f06760d1ac 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
2026-05-21 17:09:01 +02:00

171 lines
4.4 KiB
Rust

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, .. }
));
}
}