mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
Rename the Ethernet per-interface config flag from discovery to listen, so the receive/transmit toggle pair reads as the symmetric announce (transmit) / listen (receive) neighbor-beacon vocabulary. The old discovery: key is still accepted via a serde alias, so deployed configs load unchanged; to_yaml re-emits it under the canonical listen: name. Marked deprecated for removal at the v2 cutover. Updates the config field + accessor, the transport listen_enabled local, the one struct-literal test consumer, the chaos sim config generator, packaged fips.yaml examples, and the classified operator-facing docs (ethernet neighbor-beacon subsystem prose; the generic Transport discovery capability prose is left unchanged). Adds a compat parse test asserting the legacy alias, the new key, and that deny_unknown_fields still rejects unknown keys. Behavior-neutral.
741 lines
25 KiB
Rust
741 lines
25 KiB
Rust
//! Ethernet Transport Implementation
|
|
//!
|
|
//! Provides raw Ethernet transport for FIPS peer communication. On Linux,
|
|
//! uses AF_PACKET/SOCK_DGRAM sockets; on macOS, uses BPF devices (`/dev/bpf*`).
|
|
//! Works on wired Ethernet and WiFi interfaces (kernel mac80211 abstracts
|
|
//! 802.11 transparently on Linux).
|
|
|
|
pub mod addr;
|
|
pub mod io;
|
|
pub mod neighbor;
|
|
pub mod stats;
|
|
|
|
pub use addr::parse_mac_string;
|
|
|
|
use super::{
|
|
DiscoveredPeer, PacketTx, ReceivedPacket, Transport, TransportAddr, TransportError,
|
|
TransportId, TransportState, TransportType,
|
|
};
|
|
use crate::config::EthernetConfig;
|
|
use io::{AsyncPacketSocket, ETHERNET_BROADCAST, PacketSocket};
|
|
use neighbor::{FRAME_TYPE_BEACON, FRAME_TYPE_DATA, NeighborBuffer, build_beacon, parse_beacon};
|
|
use stats::EthernetStats;
|
|
|
|
use secp256k1::XOnlyPublicKey;
|
|
use std::sync::Arc;
|
|
use tokio::task::JoinHandle;
|
|
use tracing::{debug, info, trace, warn};
|
|
|
|
/// Ethernet transport for FIPS.
|
|
///
|
|
/// Uses AF_PACKET with SOCK_DGRAM for raw Ethernet frame I/O. A single
|
|
/// socket per interface serves all peers; links are virtual tuples of
|
|
/// (transport_id, remote_mac).
|
|
pub struct EthernetTransport {
|
|
/// Unique transport identifier.
|
|
transport_id: TransportId,
|
|
/// Optional instance name (for named instances in config).
|
|
name: Option<String>,
|
|
/// Configuration.
|
|
config: EthernetConfig,
|
|
/// Current state.
|
|
state: TransportState,
|
|
/// Async socket (None until started).
|
|
socket: Option<Arc<AsyncPacketSocket>>,
|
|
/// Channel for delivering received packets to Node.
|
|
packet_tx: PacketTx,
|
|
/// Receive loop task handle.
|
|
recv_task: Option<JoinHandle<()>>,
|
|
/// Beacon sender task handle.
|
|
beacon_task: Option<JoinHandle<()>>,
|
|
/// Local MAC address (after start).
|
|
local_mac: Option<[u8; 6]>,
|
|
/// Interface name (from config).
|
|
interface: String,
|
|
/// Effective payload MTU: interface MTU minus 3 bytes of frame header
|
|
/// (`[type:1][length:2 LE][payload]`). The 2-byte length field is required
|
|
/// to trim NIC minimum-frame padding before AEAD verification.
|
|
effective_mtu: u16,
|
|
/// Neighbor buffer for discovered peers.
|
|
neighbor_buffer: Arc<NeighborBuffer>,
|
|
/// Transport-level statistics.
|
|
stats: Arc<EthernetStats>,
|
|
/// Node's public key for beacon construction.
|
|
local_pubkey: Option<XOnlyPublicKey>,
|
|
}
|
|
|
|
impl EthernetTransport {
|
|
/// Create a new Ethernet transport.
|
|
pub fn new(
|
|
transport_id: TransportId,
|
|
name: Option<String>,
|
|
config: EthernetConfig,
|
|
packet_tx: PacketTx,
|
|
) -> Self {
|
|
let interface = config.interface.clone();
|
|
let neighbor_buffer = Arc::new(NeighborBuffer::new(transport_id));
|
|
let stats = Arc::new(EthernetStats::new());
|
|
|
|
Self {
|
|
transport_id,
|
|
name,
|
|
config,
|
|
state: TransportState::Configured,
|
|
socket: None,
|
|
packet_tx,
|
|
recv_task: None,
|
|
beacon_task: None,
|
|
local_mac: None,
|
|
interface,
|
|
effective_mtu: 1499, // default, updated on start
|
|
neighbor_buffer,
|
|
stats,
|
|
local_pubkey: None,
|
|
}
|
|
}
|
|
|
|
/// Get the instance name (if configured as a named instance).
|
|
pub fn name(&self) -> Option<&str> {
|
|
self.name.as_deref()
|
|
}
|
|
|
|
/// Get the interface name.
|
|
pub fn interface_name(&self) -> &str {
|
|
&self.interface
|
|
}
|
|
|
|
/// Get the local MAC address (only valid after start).
|
|
pub fn local_mac(&self) -> Option<[u8; 6]> {
|
|
self.local_mac
|
|
}
|
|
|
|
/// Set the node's public key for beacon construction.
|
|
///
|
|
/// Must be called before start if announce is enabled.
|
|
pub fn set_local_pubkey(&mut self, pubkey: XOnlyPublicKey) {
|
|
self.local_pubkey = Some(pubkey);
|
|
}
|
|
|
|
/// Get a reference to the statistics.
|
|
pub fn stats(&self) -> &Arc<EthernetStats> {
|
|
&self.stats
|
|
}
|
|
|
|
/// Start the transport asynchronously.
|
|
///
|
|
/// Creates the AF_PACKET socket, spawns the receive loop, and
|
|
/// optionally spawns the beacon sender task.
|
|
pub async fn start_async(&mut self) -> Result<(), TransportError> {
|
|
if !self.state.can_start() {
|
|
return Err(TransportError::AlreadyStarted);
|
|
}
|
|
|
|
self.state = TransportState::Starting;
|
|
|
|
// Create and bind AF_PACKET socket
|
|
let raw_socket = PacketSocket::open(&self.config.interface, self.config.ethertype())?;
|
|
|
|
// Get local MAC and MTU
|
|
let local_mac = raw_socket.local_mac()?;
|
|
let if_mtu = raw_socket.interface_mtu()?;
|
|
|
|
// Effective MTU: interface MTU minus 3 bytes for frame header
|
|
// (1 byte frame type + 2 bytes LE payload length)
|
|
let effective_mtu = if let Some(configured_mtu) = self.config.mtu {
|
|
// Config MTU cannot exceed interface MTU - 3
|
|
configured_mtu.min(if_mtu.saturating_sub(3))
|
|
} else {
|
|
if_mtu.saturating_sub(3)
|
|
};
|
|
self.effective_mtu = effective_mtu;
|
|
self.local_mac = Some(local_mac);
|
|
|
|
// Set buffer sizes
|
|
raw_socket.set_recv_buffer_size(self.config.recv_buf_size())?;
|
|
raw_socket.set_send_buffer_size(self.config.send_buf_size())?;
|
|
|
|
// Wrap in async
|
|
let async_socket = raw_socket.into_async()?;
|
|
let socket = Arc::new(async_socket);
|
|
self.socket = Some(socket.clone());
|
|
|
|
// Spawn receive loop
|
|
let transport_id = self.transport_id;
|
|
let packet_tx = self.packet_tx.clone();
|
|
let mtu = self.effective_mtu;
|
|
let listen_enabled = self.config.listen();
|
|
let neighbor_buffer = self.neighbor_buffer.clone();
|
|
let stats = self.stats.clone();
|
|
let recv_socket = socket.clone();
|
|
|
|
let recv_task = tokio::spawn(async move {
|
|
ethernet_receive_loop(
|
|
recv_socket,
|
|
transport_id,
|
|
packet_tx,
|
|
mtu,
|
|
listen_enabled,
|
|
neighbor_buffer,
|
|
stats,
|
|
)
|
|
.await;
|
|
});
|
|
self.recv_task = Some(recv_task);
|
|
|
|
// Spawn beacon sender if announce is enabled
|
|
if self.config.announce() {
|
|
if let Some(pubkey) = self.local_pubkey {
|
|
let beacon_socket = socket.clone();
|
|
let interval_secs = self.config.beacon_interval_secs();
|
|
let beacon_stats = self.stats.clone();
|
|
let beacon_transport_id = self.transport_id;
|
|
|
|
let beacon_interface = self.config.interface.clone();
|
|
let beacon_ethertype = self.config.ethertype();
|
|
|
|
let beacon_task = tokio::spawn(async move {
|
|
beacon_sender_loop(
|
|
beacon_socket,
|
|
pubkey,
|
|
interval_secs,
|
|
beacon_stats,
|
|
beacon_transport_id,
|
|
beacon_interface,
|
|
beacon_ethertype,
|
|
)
|
|
.await;
|
|
});
|
|
self.beacon_task = Some(beacon_task);
|
|
} else {
|
|
warn!(
|
|
transport_id = %self.transport_id,
|
|
"Announce enabled but no local pubkey set; beacons disabled"
|
|
);
|
|
}
|
|
}
|
|
|
|
self.state = TransportState::Up;
|
|
|
|
if let Some(ref name) = self.name {
|
|
info!(
|
|
name = %name,
|
|
interface = %self.interface,
|
|
mac = %format_mac(&local_mac),
|
|
mtu = effective_mtu,
|
|
if_mtu = if_mtu,
|
|
"Ethernet transport started"
|
|
);
|
|
} else {
|
|
info!(
|
|
interface = %self.interface,
|
|
mac = %format_mac(&local_mac),
|
|
mtu = effective_mtu,
|
|
if_mtu = if_mtu,
|
|
"Ethernet transport started"
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Stop the transport asynchronously.
|
|
pub async fn stop_async(&mut self) -> Result<(), TransportError> {
|
|
if !self.state.is_operational() {
|
|
return Err(TransportError::NotStarted);
|
|
}
|
|
|
|
// Signal the socket to shut down. On macOS this writes to the
|
|
// shutdown pipe, waking the reader thread's select() immediately.
|
|
// On Linux this is a no-op (AsyncFd cancellation handles it).
|
|
if let Some(ref socket) = self.socket {
|
|
socket.shutdown();
|
|
}
|
|
|
|
// Abort tasks. On Linux, safe to await since all I/O is
|
|
// AsyncFd-based and cancellation-safe. On macOS, do NOT await —
|
|
// on a current_thread runtime the aborted task can't be polled
|
|
// while we're blocked on the JoinHandle, causing a deadlock.
|
|
if let Some(task) = self.beacon_task.take() {
|
|
task.abort();
|
|
#[cfg(not(target_os = "macos"))]
|
|
{
|
|
let _ = task.await;
|
|
}
|
|
}
|
|
if let Some(task) = self.recv_task.take() {
|
|
task.abort();
|
|
#[cfg(not(target_os = "macos"))]
|
|
{
|
|
let _ = task.await;
|
|
}
|
|
}
|
|
|
|
// Drop socket
|
|
self.socket.take();
|
|
self.local_mac = None;
|
|
|
|
self.state = TransportState::Down;
|
|
|
|
info!(
|
|
transport_id = %self.transport_id,
|
|
interface = %self.interface,
|
|
"Ethernet transport stopped"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Send a packet asynchronously.
|
|
///
|
|
/// The data is prepended with a FRAME_TYPE_DATA prefix byte before
|
|
/// transmission.
|
|
pub async fn send_async(
|
|
&self,
|
|
addr: &TransportAddr,
|
|
data: &[u8],
|
|
) -> Result<usize, TransportError> {
|
|
if !self.state.is_operational() {
|
|
return Err(TransportError::NotStarted);
|
|
}
|
|
|
|
if data.len() > self.effective_mtu as usize {
|
|
return Err(TransportError::MtuExceeded {
|
|
packet_size: data.len(),
|
|
mtu: self.effective_mtu,
|
|
});
|
|
}
|
|
|
|
let dest_mac = parse_mac_addr(addr)?;
|
|
let socket = self.socket.as_ref().ok_or(TransportError::NotStarted)?;
|
|
|
|
// Prepend frame type prefix and 2-byte LE payload length.
|
|
// The length field lets the receiver trim Ethernet minimum-frame padding
|
|
// (NICs pad frames shorter than 46 bytes payload to 46 bytes with zeros,
|
|
// which would otherwise corrupt AEAD ciphertext verification).
|
|
let mut frame = Vec::with_capacity(3 + data.len());
|
|
frame.push(FRAME_TYPE_DATA);
|
|
frame.extend_from_slice(&(data.len() as u16).to_le_bytes());
|
|
frame.extend_from_slice(data);
|
|
|
|
let bytes_sent = socket.send_to(&frame, &dest_mac).await?;
|
|
self.stats.record_send(bytes_sent);
|
|
|
|
trace!(
|
|
transport_id = %self.transport_id,
|
|
remote_mac = %format_mac(&dest_mac),
|
|
bytes = bytes_sent,
|
|
"Ethernet frame sent"
|
|
);
|
|
|
|
// Return the data bytes sent (excluding frame type prefix and length field)
|
|
Ok(bytes_sent.saturating_sub(3))
|
|
}
|
|
}
|
|
|
|
impl Transport for EthernetTransport {
|
|
fn transport_id(&self) -> TransportId {
|
|
self.transport_id
|
|
}
|
|
|
|
fn transport_type(&self) -> &TransportType {
|
|
&TransportType::ETHERNET
|
|
}
|
|
|
|
fn state(&self) -> TransportState {
|
|
self.state
|
|
}
|
|
|
|
fn mtu(&self) -> u16 {
|
|
self.effective_mtu
|
|
}
|
|
|
|
fn start(&mut self) -> Result<(), TransportError> {
|
|
Err(TransportError::NotSupported(
|
|
"use start_async() for Ethernet transport".into(),
|
|
))
|
|
}
|
|
|
|
fn stop(&mut self) -> Result<(), TransportError> {
|
|
Err(TransportError::NotSupported(
|
|
"use stop_async() for Ethernet transport".into(),
|
|
))
|
|
}
|
|
|
|
fn send(&self, _addr: &TransportAddr, _data: &[u8]) -> Result<(), TransportError> {
|
|
Err(TransportError::NotSupported(
|
|
"use send_async() for Ethernet transport".into(),
|
|
))
|
|
}
|
|
|
|
fn discover(&self) -> Result<Vec<DiscoveredPeer>, TransportError> {
|
|
Ok(self.neighbor_buffer.take())
|
|
}
|
|
|
|
fn auto_connect(&self) -> bool {
|
|
self.config.auto_connect()
|
|
}
|
|
|
|
fn accept_connections(&self) -> bool {
|
|
self.config.accept_connections()
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Receive Loop
|
|
// ============================================================================
|
|
|
|
/// Ethernet receive loop — runs as a spawned task.
|
|
async fn ethernet_receive_loop(
|
|
socket: Arc<AsyncPacketSocket>,
|
|
transport_id: TransportId,
|
|
packet_tx: PacketTx,
|
|
mtu: u16,
|
|
listen_enabled: bool,
|
|
neighbor_buffer: Arc<NeighborBuffer>,
|
|
stats: Arc<EthernetStats>,
|
|
) {
|
|
// Buffer with headroom: frame type prefix + MTU + some extra
|
|
let mut buf = vec![0u8; mtu as usize + 100];
|
|
|
|
debug!(transport_id = %transport_id, "Ethernet receive loop starting");
|
|
|
|
loop {
|
|
match socket.recv_from(&mut buf).await {
|
|
Ok((len, src_mac)) => {
|
|
if len == 0 {
|
|
continue;
|
|
}
|
|
|
|
stats.record_recv(len);
|
|
|
|
let frame_type = buf[0];
|
|
match frame_type {
|
|
FRAME_TYPE_DATA => {
|
|
// Data frame: [type:1][length:2 LE][payload:N]
|
|
// Use the length field to trim Ethernet minimum-frame padding.
|
|
if len < 3 {
|
|
trace!("Data frame too short ({len} bytes), ignoring");
|
|
continue;
|
|
}
|
|
let payload_len = u16::from_le_bytes([buf[1], buf[2]]) as usize;
|
|
if payload_len > len - 3 {
|
|
trace!(
|
|
"Data frame length field ({payload_len}) exceeds \
|
|
available bytes ({}), ignoring",
|
|
len - 3
|
|
);
|
|
continue;
|
|
}
|
|
let data = buf[3..3 + payload_len].to_vec();
|
|
let addr = TransportAddr::from_bytes(&src_mac);
|
|
let packet = ReceivedPacket::new(transport_id, addr, data);
|
|
|
|
trace!(
|
|
transport_id = %transport_id,
|
|
remote_mac = %format_mac(&src_mac),
|
|
bytes = payload_len,
|
|
"Ethernet data frame received"
|
|
);
|
|
|
|
if packet_tx.send(packet).await.is_err() {
|
|
debug!(
|
|
transport_id = %transport_id,
|
|
"Packet channel closed, stopping receive loop"
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
FRAME_TYPE_BEACON => {
|
|
stats.record_beacon_recv();
|
|
|
|
if listen_enabled && let Some(pubkey) = parse_beacon(&buf[..len]) {
|
|
neighbor_buffer.add_peer(src_mac, pubkey);
|
|
trace!(
|
|
transport_id = %transport_id,
|
|
remote_mac = %format_mac(&src_mac),
|
|
"Neighbor beacon received"
|
|
);
|
|
}
|
|
}
|
|
_ => {
|
|
// Unknown frame type, ignore
|
|
trace!(
|
|
transport_id = %transport_id,
|
|
frame_type = frame_type,
|
|
"Unknown frame type, dropping"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
stats.record_recv_error();
|
|
warn!(
|
|
transport_id = %transport_id,
|
|
error = %e,
|
|
"Ethernet receive error"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
debug!(transport_id = %transport_id, "Ethernet receive loop stopped");
|
|
}
|
|
|
|
// ============================================================================
|
|
// Beacon Sender
|
|
// ============================================================================
|
|
|
|
/// Periodic beacon sender loop.
|
|
///
|
|
/// Detects stale AF_PACKET sockets (ENXIO / os error 6) that occur when
|
|
/// the underlying veth interface is destroyed and recreated (e.g., during
|
|
/// node churn in chaos tests). After `REOPEN_THRESHOLD` consecutive send
|
|
/// failures, attempts to open a fresh socket on the same interface.
|
|
async fn beacon_sender_loop(
|
|
mut socket: Arc<AsyncPacketSocket>,
|
|
pubkey: XOnlyPublicKey,
|
|
interval_secs: u64,
|
|
stats: Arc<EthernetStats>,
|
|
transport_id: TransportId,
|
|
interface: String,
|
|
ethertype: u16,
|
|
) {
|
|
/// Number of consecutive ENXIO errors before attempting socket reopen.
|
|
const REOPEN_THRESHOLD: u32 = 3;
|
|
|
|
let beacon = build_beacon(&pubkey);
|
|
let interval = tokio::time::Duration::from_secs(interval_secs);
|
|
|
|
debug!(
|
|
transport_id = %transport_id,
|
|
interval_secs,
|
|
"Beacon sender starting"
|
|
);
|
|
|
|
// Send an initial beacon immediately at startup
|
|
if let Err(e) = socket.send_to(&beacon, ÐERNET_BROADCAST).await {
|
|
warn!(
|
|
transport_id = %transport_id,
|
|
error = %e,
|
|
"Failed to send initial beacon"
|
|
);
|
|
} else {
|
|
stats.record_beacon_sent();
|
|
}
|
|
|
|
let mut interval_timer = tokio::time::interval(interval);
|
|
interval_timer.tick().await; // consume the immediate first tick
|
|
let mut consecutive_errors: u32 = 0;
|
|
|
|
loop {
|
|
interval_timer.tick().await;
|
|
|
|
match socket.send_to(&beacon, ÐERNET_BROADCAST).await {
|
|
Ok(_) => {
|
|
if consecutive_errors > 0 {
|
|
debug!(
|
|
transport_id = %transport_id,
|
|
"Beacon send recovered after {} errors", consecutive_errors,
|
|
);
|
|
}
|
|
consecutive_errors = 0;
|
|
stats.record_beacon_sent();
|
|
trace!(
|
|
transport_id = %transport_id,
|
|
"Beacon sent"
|
|
);
|
|
}
|
|
Err(e) => {
|
|
consecutive_errors += 1;
|
|
stats.record_send_error();
|
|
|
|
let is_enxio = format!("{e}").contains("os error 6");
|
|
|
|
// Log only the first error in a streak to avoid log spam
|
|
if consecutive_errors == 1 {
|
|
warn!(
|
|
transport_id = %transport_id,
|
|
error = %e,
|
|
"Failed to send beacon"
|
|
);
|
|
}
|
|
|
|
if is_enxio && consecutive_errors >= REOPEN_THRESHOLD {
|
|
info!(
|
|
transport_id = %transport_id,
|
|
consecutive_errors,
|
|
interface = %interface,
|
|
"Stale veth detected (ENXIO), attempting socket reopen"
|
|
);
|
|
match reopen_beacon_socket(&interface, ethertype) {
|
|
Ok(new_socket) => {
|
|
socket = Arc::new(new_socket);
|
|
consecutive_errors = 0;
|
|
info!(
|
|
transport_id = %transport_id,
|
|
interface = %interface,
|
|
"Beacon socket reopened successfully"
|
|
);
|
|
}
|
|
Err(e) => {
|
|
warn!(
|
|
transport_id = %transport_id,
|
|
error = %e,
|
|
interface = %interface,
|
|
"Failed to reopen beacon socket, will retry"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Attempt to open a fresh AF_PACKET socket for beacon sending.
|
|
///
|
|
/// This is called when the beacon sender detects that the underlying veth
|
|
/// has been recreated and the old socket FD is stale (ENXIO).
|
|
fn reopen_beacon_socket(
|
|
interface: &str,
|
|
ethertype: u16,
|
|
) -> Result<AsyncPacketSocket, TransportError> {
|
|
let raw_socket = PacketSocket::open(interface, ethertype)?;
|
|
raw_socket.into_async()
|
|
}
|
|
|
|
// ============================================================================
|
|
// MAC Address Helpers
|
|
// ============================================================================
|
|
|
|
/// Parse a TransportAddr as a 6-byte MAC address.
|
|
fn parse_mac_addr(addr: &TransportAddr) -> Result<[u8; 6], TransportError> {
|
|
let bytes = addr.as_bytes();
|
|
if bytes.len() != 6 {
|
|
return Err(TransportError::InvalidAddress(format!(
|
|
"expected 6-byte MAC, got {} bytes",
|
|
bytes.len()
|
|
)));
|
|
}
|
|
if bytes == [0, 0, 0, 0, 0, 0] {
|
|
return Err(TransportError::InvalidAddress(
|
|
"destination MAC is all zeros".into(),
|
|
));
|
|
}
|
|
let mut mac = [0u8; 6];
|
|
mac.copy_from_slice(bytes);
|
|
Ok(mac)
|
|
}
|
|
|
|
/// Format a MAC address as colon-separated hex for display.
|
|
pub fn format_mac(mac: &[u8; 6]) -> String {
|
|
format!(
|
|
"{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
|
|
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
|
|
)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tests
|
|
// ============================================================================
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_parse_mac_addr_valid() {
|
|
let addr = TransportAddr::from_bytes(&[0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
|
|
let mac = parse_mac_addr(&addr).unwrap();
|
|
assert_eq!(mac, [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_mac_addr_wrong_length() {
|
|
let addr = TransportAddr::from_bytes(&[0xaa, 0xbb, 0xcc]);
|
|
assert!(parse_mac_addr(&addr).is_err());
|
|
|
|
let addr = TransportAddr::from_string("192.168.1.1:2121");
|
|
assert!(parse_mac_addr(&addr).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_mac_addr_all_zeros() {
|
|
let addr = TransportAddr::from_bytes(&[0, 0, 0, 0, 0, 0]);
|
|
assert!(parse_mac_addr(&addr).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_mac() {
|
|
let mac = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff];
|
|
assert_eq!(format_mac(&mac), "aa:bb:cc:dd:ee:ff");
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_mac_leading_zeros() {
|
|
let mac = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06];
|
|
assert_eq!(format_mac(&mac), "01:02:03:04:05:06");
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_mac_string_valid() {
|
|
let mac = parse_mac_string("aa:bb:cc:dd:ee:ff").unwrap();
|
|
assert_eq!(mac, [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_mac_string_uppercase() {
|
|
let mac = parse_mac_string("AA:BB:CC:DD:EE:FF").unwrap();
|
|
assert_eq!(mac, [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_mac_string_invalid() {
|
|
assert!(parse_mac_string("aa:bb:cc").is_err());
|
|
assert!(parse_mac_string("not:a:mac:at:all:x").is_err());
|
|
assert!(parse_mac_string("").is_err());
|
|
assert!(parse_mac_string("aa-bb-cc-dd-ee-ff").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_frame_type_data_prefix() {
|
|
// Verify data frames have type prefix + 2-byte LE length + payload
|
|
let data = vec![1, 2, 3, 4];
|
|
let mut frame = Vec::with_capacity(3 + data.len());
|
|
frame.push(FRAME_TYPE_DATA);
|
|
frame.extend_from_slice(&(data.len() as u16).to_le_bytes());
|
|
frame.extend_from_slice(&data);
|
|
|
|
assert_eq!(frame[0], 0x00); // frame type
|
|
assert_eq!(u16::from_le_bytes([frame[1], frame[2]]), 4); // length
|
|
assert_eq!(&frame[3..], &[1, 2, 3, 4]); // payload
|
|
}
|
|
|
|
#[test]
|
|
fn test_data_frame_padding_trimmed() {
|
|
// Simulate Ethernet minimum-frame padding: a 4-byte payload produces
|
|
// a 7-byte frame (type + len + payload), padded to 46 bytes by NIC.
|
|
let payload = vec![0xAA, 0xBB, 0xCC, 0xDD];
|
|
let payload_len = payload.len() as u16;
|
|
|
|
// Build frame as sender would
|
|
let mut frame = Vec::with_capacity(3 + payload.len());
|
|
frame.push(FRAME_TYPE_DATA);
|
|
frame.extend_from_slice(&payload_len.to_le_bytes());
|
|
frame.extend_from_slice(&payload);
|
|
|
|
// Simulate NIC padding to 46 bytes
|
|
frame.resize(46, 0x00);
|
|
|
|
// Receiver extracts using length field
|
|
let recv_len = u16::from_le_bytes([frame[1], frame[2]]) as usize;
|
|
let extracted = &frame[3..3 + recv_len];
|
|
assert_eq!(extracted, &[0xAA, 0xBB, 0xCC, 0xDD]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_beacon_size() {
|
|
assert_eq!(neighbor::BEACON_SIZE, 34);
|
|
}
|
|
}
|