f06760d1ac
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
171 lines
4.4 KiB
Rust
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, .. }
|
|
));
|
|
}
|
|
}
|