use anyhow::{anyhow, bail, Result};
pub const STP_MULTICAST_MAC: [u8; 6] = [0x01, 0x80, 0xC2, 0x00, 0x00, 0x00];
pub const LLC_DSAP: u8 = 0x42;
pub const LLC_SSAP: u8 = 0x42;
pub const LLC_CONTROL: u8 = 0x03;
pub const LLC_HEADER_LEN: usize = 3;
pub const ETHERNET_HEADER_LEN: usize = 14;
pub const MIN_ETHERNET_FRAME_LEN: usize = 60;
pub const MAX_8023_LENGTH: usize = 1500;
pub const PROTOCOL_ID: u16 = 0x0000;
pub const VERSION_STP: u8 = 0;
pub const VERSION_RSTP: u8 = 2;
pub const BPDU_TYPE_CONFIG: u8 = 0x00;
pub const BPDU_TYPE_RST: u8 = 0x02;
pub const BPDU_TYPE_TCN: u8 = 0x80;
pub const CONFIG_BPDU_LEN: usize = 35;
pub const RST_BPDU_LEN: usize = 36;
pub const TCN_BPDU_LEN: usize = 4;
pub const TIMER_TICKS_PER_SECOND: f64 = 256.0;
pub const BRIDGE_PRIORITY_STEP: u16 = 4096;
pub const BRIDGE_PRIORITY_MAX: u16 = 61440;
pub const SYSTEM_ID_EXTENSION_MAX: u16 = 4095;
pub const PORT_PRIORITY_STEP: u8 = 16;
pub const PORT_PRIORITY_MAX: u8 = 240;
pub const PORT_NUMBER_MAX: u16 = 4095;
pub fn parse_mac(s: &str) -> Result<[u8; 6]> {
let parts: Vec<&str> = s.split([':', '-']).collect();
if parts.len() != 6 {
bail!("MAC address must have 6 octets separated by ':' or '-', got '{s}'");
}
let mut out = [0u8; 6];
for (i, part) in parts.iter().enumerate() {
out[i] = u8::from_str_radix(part, 16)
.map_err(|_| anyhow!("invalid hex octet '{part}' in MAC address '{s}'"))?;
}
Ok(out)
}
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]
)
}
pub fn seconds_to_ticks(seconds: f64) -> Result<u16> {
if !seconds.is_finite() || seconds < 0.0 {
bail!("timer value must be a finite, non-negative number of seconds, got {seconds}");
}
let ticks = (seconds * TIMER_TICKS_PER_SECOND).round();
if ticks > u16::MAX as f64 {
bail!(
"timer value {seconds}s does not fit the 16-bit 1/256s field (maximum {:.4}s)",
u16::MAX as f64 / TIMER_TICKS_PER_SECOND
);
}
Ok(ticks as u16)
}
pub fn ticks_to_seconds(ticks: u16) -> f64 {
ticks as f64 / TIMER_TICKS_PER_SECOND
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PortRole {
#[default]
Unknown,
Alternate,
Root,
Designated,
}
impl PortRole {
pub fn as_str(self) -> &'static str {
match self {
PortRole::Unknown => "unknown",
PortRole::Alternate => "alternate",
PortRole::Root => "root",
PortRole::Designated => "designated",
}
}
pub fn from_name(name: &str) -> Result<Self> {
match name.trim().to_ascii_lowercase().as_str() {
"unknown" => Ok(PortRole::Unknown),
"alternate" | "backup" => Ok(PortRole::Alternate),
"root" => Ok(PortRole::Root),
"designated" => Ok(PortRole::Designated),
other => bail!(
"unknown port_role '{other}' (expected unknown, alternate, backup, root or designated)"
),
}
}
fn to_bits(self) -> u8 {
match self {
PortRole::Unknown => 0b00,
PortRole::Alternate => 0b01,
PortRole::Root => 0b10,
PortRole::Designated => 0b11,
}
}
fn from_bits(bits: u8) -> Self {
match bits & 0b11 {
0b01 => PortRole::Alternate,
0b10 => PortRole::Root,
0b11 => PortRole::Designated,
_ => PortRole::Unknown,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct BpduFlags {
pub topology_change: bool,
pub proposal: bool,
pub port_role: PortRole,
pub learning: bool,
pub forwarding: bool,
pub agreement: bool,
pub topology_change_ack: bool,
}
impl BpduFlags {
pub fn to_byte(self) -> u8 {
let mut byte = 0u8;
if self.topology_change {
byte |= 0x01;
}
if self.proposal {
byte |= 0x02;
}
byte |= self.port_role.to_bits() << 2;
if self.learning {
byte |= 0x10;
}
if self.forwarding {
byte |= 0x20;
}
if self.agreement {
byte |= 0x40;
}
if self.topology_change_ack {
byte |= 0x80;
}
byte
}
pub fn from_byte(byte: u8) -> Self {
Self {
topology_change: byte & 0x01 != 0,
proposal: byte & 0x02 != 0,
port_role: PortRole::from_bits(byte >> 2),
learning: byte & 0x10 != 0,
forwarding: byte & 0x20 != 0,
agreement: byte & 0x40 != 0,
topology_change_ack: byte & 0x80 != 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BridgeId {
pub priority: u16,
pub system_id_extension: u16,
pub mac: [u8; 6],
}
impl BridgeId {
pub fn new(priority: u16, system_id_extension: u16, mac: [u8; 6]) -> Result<Self> {
if priority % BRIDGE_PRIORITY_STEP != 0 {
bail!(
"bridge priority must be a multiple of {} (0, 4096, 8192, … {}); got {}. The \
low 12 bits of the field are the system ID extension (VLAN), not priority.",
BRIDGE_PRIORITY_STEP,
BRIDGE_PRIORITY_MAX,
priority
);
}
if priority > BRIDGE_PRIORITY_MAX {
bail!(
"bridge priority must be at most {}, got {}",
BRIDGE_PRIORITY_MAX,
priority
);
}
if system_id_extension > SYSTEM_ID_EXTENSION_MAX {
bail!(
"system_id_extension must be at most {} (12 bits), got {}",
SYSTEM_ID_EXTENSION_MAX,
system_id_extension
);
}
Ok(Self {
priority,
system_id_extension,
mac,
})
}
pub fn encode(&self) -> [u8; 8] {
let packed = (self.priority & 0xF000) | (self.system_id_extension & 0x0FFF);
let mut out = [0u8; 8];
out[0..2].copy_from_slice(&packed.to_be_bytes());
out[2..8].copy_from_slice(&self.mac);
out
}
pub fn decode(bytes: &[u8]) -> Result<Self> {
if bytes.len() < 8 {
bail!("bridge identifier needs 8 octets, got {}", bytes.len());
}
let packed = u16::from_be_bytes([bytes[0], bytes[1]]);
let mut mac = [0u8; 6];
mac.copy_from_slice(&bytes[2..8]);
Ok(Self {
priority: packed & 0xF000,
system_id_extension: packed & 0x0FFF,
mac,
})
}
pub fn mac_string(&self) -> String {
format_mac(&self.mac)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PortId {
pub priority: u8,
pub number: u16,
}
impl PortId {
pub fn new(priority: u8, number: u16) -> Result<Self> {
if priority % PORT_PRIORITY_STEP != 0 {
bail!(
"port priority must be a multiple of {} (0, 16, 32, … {}); got {}. The low 12 \
bits of the field are the port number, not priority.",
PORT_PRIORITY_STEP,
PORT_PRIORITY_MAX,
priority
);
}
if number > PORT_NUMBER_MAX {
bail!(
"port number must be at most {} (12 bits), got {}",
PORT_NUMBER_MAX,
number
);
}
Ok(Self { priority, number })
}
pub fn encode(&self) -> [u8; 2] {
let packed = (((self.priority & 0xF0) as u16) << 8) | (self.number & 0x0FFF);
packed.to_be_bytes()
}
pub fn decode(bytes: &[u8]) -> Result<Self> {
if bytes.len() < 2 {
bail!("port identifier needs 2 octets, got {}", bytes.len());
}
let packed = u16::from_be_bytes([bytes[0], bytes[1]]);
Ok(Self {
priority: ((packed >> 8) as u8) & 0xF0,
number: packed & 0x0FFF,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ConfigBpdu {
pub version: u8,
pub bpdu_type: u8,
pub flags: BpduFlags,
pub root: BridgeId,
pub root_path_cost: u32,
pub bridge: BridgeId,
pub port: PortId,
pub message_age_seconds: f64,
pub max_age_seconds: f64,
pub hello_time_seconds: f64,
pub forward_delay_seconds: f64,
}
impl ConfigBpdu {
pub fn is_rstp(&self) -> bool {
self.bpdu_type == BPDU_TYPE_RST
}
pub fn encode(&self) -> Result<Vec<u8>> {
if self.bpdu_type != BPDU_TYPE_CONFIG && self.bpdu_type != BPDU_TYPE_RST {
bail!(
"BPDU type must be 0x00 (configuration) or 0x02 (RST) here; 0x{:02x} is not a \
configuration-shaped BPDU",
self.bpdu_type
);
}
let root = BridgeId::new(
self.root.priority,
self.root.system_id_extension,
self.root.mac,
)?;
let bridge = BridgeId::new(
self.bridge.priority,
self.bridge.system_id_extension,
self.bridge.mac,
)?;
let port = PortId::new(self.port.priority, self.port.number)?;
let mut out = Vec::with_capacity(RST_BPDU_LEN);
out.extend_from_slice(&PROTOCOL_ID.to_be_bytes());
out.push(self.version);
out.push(self.bpdu_type);
out.push(self.flags.to_byte());
out.extend_from_slice(&root.encode());
out.extend_from_slice(&self.root_path_cost.to_be_bytes());
out.extend_from_slice(&bridge.encode());
out.extend_from_slice(&port.encode());
out.extend_from_slice(&seconds_to_ticks(self.message_age_seconds)?.to_be_bytes());
out.extend_from_slice(&seconds_to_ticks(self.max_age_seconds)?.to_be_bytes());
out.extend_from_slice(&seconds_to_ticks(self.hello_time_seconds)?.to_be_bytes());
out.extend_from_slice(&seconds_to_ticks(self.forward_delay_seconds)?.to_be_bytes());
debug_assert_eq!(out.len(), CONFIG_BPDU_LEN);
if self.bpdu_type == BPDU_TYPE_RST {
out.push(0x00);
}
Ok(out)
}
pub fn decode(body: &[u8]) -> Result<Self> {
if body.len() < CONFIG_BPDU_LEN {
bail!(
"configuration BPDU needs at least {} octets, got {}",
CONFIG_BPDU_LEN,
body.len()
);
}
let protocol_id = u16::from_be_bytes([body[0], body[1]]);
if protocol_id != PROTOCOL_ID {
bail!("protocol identifier must be 0x0000, got 0x{protocol_id:04x}");
}
Ok(Self {
version: body[2],
bpdu_type: body[3],
flags: BpduFlags::from_byte(body[4]),
root: BridgeId::decode(&body[5..13])?,
root_path_cost: u32::from_be_bytes([body[13], body[14], body[15], body[16]]),
bridge: BridgeId::decode(&body[17..25])?,
port: PortId::decode(&body[25..27])?,
message_age_seconds: ticks_to_seconds(u16::from_be_bytes([body[27], body[28]])),
max_age_seconds: ticks_to_seconds(u16::from_be_bytes([body[29], body[30]])),
hello_time_seconds: ticks_to_seconds(u16::from_be_bytes([body[31], body[32]])),
forward_delay_seconds: ticks_to_seconds(u16::from_be_bytes([body[33], body[34]])),
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Bpdu {
Config(ConfigBpdu),
TopologyChangeNotification,
}
impl Bpdu {
pub fn decode(body: &[u8]) -> Result<Self> {
if body.len() < TCN_BPDU_LEN {
bail!(
"BPDU needs at least {} octets, got {}",
TCN_BPDU_LEN,
body.len()
);
}
let protocol_id = u16::from_be_bytes([body[0], body[1]]);
if protocol_id != PROTOCOL_ID {
bail!("protocol identifier must be 0x0000, got 0x{protocol_id:04x}");
}
match body[3] {
BPDU_TYPE_TCN => Ok(Bpdu::TopologyChangeNotification),
BPDU_TYPE_CONFIG | BPDU_TYPE_RST => Ok(Bpdu::Config(ConfigBpdu::decode(body)?)),
other => bail!(
"unknown BPDU type 0x{other:02x} (expected 0x00 configuration, 0x02 RST or \
0x80 topology change notification)"
),
}
}
pub fn encode(&self) -> Result<Vec<u8>> {
match self {
Bpdu::Config(config) => config.encode(),
Bpdu::TopologyChangeNotification => Ok(encode_tcn_bpdu()),
}
}
}
pub fn encode_tcn_bpdu() -> Vec<u8> {
vec![0x00, 0x00, VERSION_STP, BPDU_TYPE_TCN]
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Frame8023 {
pub destination: [u8; 6],
pub source: [u8; 6],
pub payload: Vec<u8>,
}
pub fn encode_frame(destination: [u8; 6], source: [u8; 6], bpdu: &[u8]) -> Result<Vec<u8>> {
let client_len = LLC_HEADER_LEN + bpdu.len();
if client_len > MAX_8023_LENGTH {
bail!(
"802.3 client data is {client_len} octets; the length field only carries a length \
up to {MAX_8023_LENGTH} (0x0600 and above are EtherTypes, so a longer frame is read \
as Ethernet II and the BPDU is never parsed)"
);
}
let mut frame =
Vec::with_capacity(MIN_ETHERNET_FRAME_LEN.max(ETHERNET_HEADER_LEN + client_len));
frame.extend_from_slice(&destination);
frame.extend_from_slice(&source);
frame.extend_from_slice(&(client_len as u16).to_be_bytes());
frame.push(LLC_DSAP);
frame.push(LLC_SSAP);
frame.push(LLC_CONTROL);
frame.extend_from_slice(bpdu);
while frame.len() < MIN_ETHERNET_FRAME_LEN {
frame.push(0x00);
}
Ok(frame)
}
pub fn decode_frame(frame: &[u8]) -> Result<Frame8023> {
if frame.len() < ETHERNET_HEADER_LEN + LLC_HEADER_LEN {
bail!(
"802.3 frame needs at least {} octets to carry an LLC header, got {}",
ETHERNET_HEADER_LEN + LLC_HEADER_LEN,
frame.len()
);
}
let mut destination = [0u8; 6];
destination.copy_from_slice(&frame[0..6]);
let mut source = [0u8; 6];
source.copy_from_slice(&frame[6..12]);
let declared_len = u16::from_be_bytes([frame[12], frame[13]]) as usize;
if declared_len > MAX_8023_LENGTH {
bail!(
"not an 802.3 length-encapsulated frame: field at offset 12 is 0x{declared_len:04x}, \
which is an EtherType, not a length. BPDUs are never carried in Ethernet II frames."
);
}
let (dsap, ssap, control) = (frame[14], frame[15], frame[16]);
if dsap != LLC_DSAP || ssap != LLC_SSAP || control != LLC_CONTROL {
bail!(
"not a BPDU: LLC header is {:02x}/{:02x}/{:02x}, expected {:02x}/{:02x}/{:02x}",
dsap,
ssap,
control,
LLC_DSAP,
LLC_SSAP,
LLC_CONTROL
);
}
let available = frame.len() - ETHERNET_HEADER_LEN - LLC_HEADER_LEN;
let bpdu_len = declared_len.saturating_sub(LLC_HEADER_LEN).min(available);
let start = ETHERNET_HEADER_LEN + LLC_HEADER_LEN;
Ok(Frame8023 {
destination,
source,
payload: frame[start..start + bpdu_len].to_vec(),
})
}