1use anyhow::{anyhow, bail, Result};
55
56pub const STP_MULTICAST_MAC: [u8; 6] = [0x01, 0x80, 0xC2, 0x00, 0x00, 0x00];
58
59pub const LLC_DSAP: u8 = 0x42;
61pub const LLC_SSAP: u8 = 0x42;
62pub const LLC_CONTROL: u8 = 0x03;
63pub const LLC_HEADER_LEN: usize = 3;
64
65pub const ETHERNET_HEADER_LEN: usize = 14;
67
68pub const MIN_ETHERNET_FRAME_LEN: usize = 60;
70
71pub const MAX_8023_LENGTH: usize = 1500;
77
78pub const PROTOCOL_ID: u16 = 0x0000;
79
80pub const VERSION_STP: u8 = 0;
81pub const VERSION_RSTP: u8 = 2;
82
83pub const BPDU_TYPE_CONFIG: u8 = 0x00;
84pub const BPDU_TYPE_RST: u8 = 0x02;
85pub const BPDU_TYPE_TCN: u8 = 0x80;
86
87pub const CONFIG_BPDU_LEN: usize = 35;
88pub const RST_BPDU_LEN: usize = 36;
89pub const TCN_BPDU_LEN: usize = 4;
90
91pub const TIMER_TICKS_PER_SECOND: f64 = 256.0;
93
94pub const BRIDGE_PRIORITY_STEP: u16 = 4096;
96pub const BRIDGE_PRIORITY_MAX: u16 = 61440;
98pub const SYSTEM_ID_EXTENSION_MAX: u16 = 4095;
100
101pub const PORT_PRIORITY_STEP: u8 = 16;
103pub const PORT_PRIORITY_MAX: u8 = 240;
105pub const PORT_NUMBER_MAX: u16 = 4095;
107
108pub fn parse_mac(s: &str) -> Result<[u8; 6]> {
114 let parts: Vec<&str> = s.split([':', '-']).collect();
115 if parts.len() != 6 {
116 bail!("MAC address must have 6 octets separated by ':' or '-', got '{s}'");
117 }
118 let mut out = [0u8; 6];
119 for (i, part) in parts.iter().enumerate() {
120 out[i] = u8::from_str_radix(part, 16)
121 .map_err(|_| anyhow!("invalid hex octet '{part}' in MAC address '{s}'"))?;
122 }
123 Ok(out)
124}
125
126pub fn format_mac(mac: &[u8; 6]) -> String {
128 format!(
129 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
130 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
131 )
132}
133
134pub fn seconds_to_ticks(seconds: f64) -> Result<u16> {
143 if !seconds.is_finite() || seconds < 0.0 {
144 bail!("timer value must be a finite, non-negative number of seconds, got {seconds}");
145 }
146 let ticks = (seconds * TIMER_TICKS_PER_SECOND).round();
147 if ticks > u16::MAX as f64 {
148 bail!(
149 "timer value {seconds}s does not fit the 16-bit 1/256s field (maximum {:.4}s)",
150 u16::MAX as f64 / TIMER_TICKS_PER_SECOND
151 );
152 }
153 Ok(ticks as u16)
154}
155
156pub fn ticks_to_seconds(ticks: u16) -> f64 {
158 ticks as f64 / TIMER_TICKS_PER_SECOND
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
167pub enum PortRole {
168 #[default]
170 Unknown,
171 Alternate,
173 Root,
175 Designated,
178}
179
180impl PortRole {
181 pub fn as_str(self) -> &'static str {
182 match self {
183 PortRole::Unknown => "unknown",
184 PortRole::Alternate => "alternate",
185 PortRole::Root => "root",
186 PortRole::Designated => "designated",
187 }
188 }
189
190 pub fn from_name(name: &str) -> Result<Self> {
193 match name.trim().to_ascii_lowercase().as_str() {
194 "unknown" => Ok(PortRole::Unknown),
195 "alternate" | "backup" => Ok(PortRole::Alternate),
196 "root" => Ok(PortRole::Root),
197 "designated" => Ok(PortRole::Designated),
198 other => bail!(
199 "unknown port_role '{other}' (expected unknown, alternate, backup, root or designated)"
200 ),
201 }
202 }
203
204 fn to_bits(self) -> u8 {
205 match self {
206 PortRole::Unknown => 0b00,
207 PortRole::Alternate => 0b01,
208 PortRole::Root => 0b10,
209 PortRole::Designated => 0b11,
210 }
211 }
212
213 fn from_bits(bits: u8) -> Self {
214 match bits & 0b11 {
215 0b01 => PortRole::Alternate,
216 0b10 => PortRole::Root,
217 0b11 => PortRole::Designated,
218 _ => PortRole::Unknown,
219 }
220 }
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
229pub struct BpduFlags {
230 pub topology_change: bool,
232 pub proposal: bool,
234 pub port_role: PortRole,
236 pub learning: bool,
238 pub forwarding: bool,
240 pub agreement: bool,
242 pub topology_change_ack: bool,
244}
245
246impl BpduFlags {
247 pub fn to_byte(self) -> u8 {
248 let mut byte = 0u8;
249 if self.topology_change {
250 byte |= 0x01;
251 }
252 if self.proposal {
253 byte |= 0x02;
254 }
255 byte |= self.port_role.to_bits() << 2;
256 if self.learning {
257 byte |= 0x10;
258 }
259 if self.forwarding {
260 byte |= 0x20;
261 }
262 if self.agreement {
263 byte |= 0x40;
264 }
265 if self.topology_change_ack {
266 byte |= 0x80;
267 }
268 byte
269 }
270
271 pub fn from_byte(byte: u8) -> Self {
272 Self {
273 topology_change: byte & 0x01 != 0,
274 proposal: byte & 0x02 != 0,
275 port_role: PortRole::from_bits(byte >> 2),
276 learning: byte & 0x10 != 0,
277 forwarding: byte & 0x20 != 0,
278 agreement: byte & 0x40 != 0,
279 topology_change_ack: byte & 0x80 != 0,
280 }
281 }
282}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
294pub struct BridgeId {
295 pub priority: u16,
297 pub system_id_extension: u16,
299 pub mac: [u8; 6],
300}
301
302impl BridgeId {
303 pub fn new(priority: u16, system_id_extension: u16, mac: [u8; 6]) -> Result<Self> {
306 if priority % BRIDGE_PRIORITY_STEP != 0 {
307 bail!(
308 "bridge priority must be a multiple of {} (0, 4096, 8192, … {}); got {}. The \
309 low 12 bits of the field are the system ID extension (VLAN), not priority.",
310 BRIDGE_PRIORITY_STEP,
311 BRIDGE_PRIORITY_MAX,
312 priority
313 );
314 }
315 if priority > BRIDGE_PRIORITY_MAX {
316 bail!(
317 "bridge priority must be at most {}, got {}",
318 BRIDGE_PRIORITY_MAX,
319 priority
320 );
321 }
322 if system_id_extension > SYSTEM_ID_EXTENSION_MAX {
323 bail!(
324 "system_id_extension must be at most {} (12 bits), got {}",
325 SYSTEM_ID_EXTENSION_MAX,
326 system_id_extension
327 );
328 }
329 Ok(Self {
330 priority,
331 system_id_extension,
332 mac,
333 })
334 }
335
336 pub fn encode(&self) -> [u8; 8] {
337 let packed = (self.priority & 0xF000) | (self.system_id_extension & 0x0FFF);
338 let mut out = [0u8; 8];
339 out[0..2].copy_from_slice(&packed.to_be_bytes());
340 out[2..8].copy_from_slice(&self.mac);
341 out
342 }
343
344 pub fn decode(bytes: &[u8]) -> Result<Self> {
347 if bytes.len() < 8 {
348 bail!("bridge identifier needs 8 octets, got {}", bytes.len());
349 }
350 let packed = u16::from_be_bytes([bytes[0], bytes[1]]);
351 let mut mac = [0u8; 6];
352 mac.copy_from_slice(&bytes[2..8]);
353 Ok(Self {
354 priority: packed & 0xF000,
355 system_id_extension: packed & 0x0FFF,
356 mac,
357 })
358 }
359
360 pub fn mac_string(&self) -> String {
361 format_mac(&self.mac)
362 }
363}
364
365#[derive(Debug, Clone, Copy, PartialEq, Eq)]
372pub struct PortId {
373 pub priority: u8,
375 pub number: u16,
377}
378
379impl PortId {
380 pub fn new(priority: u8, number: u16) -> Result<Self> {
381 if priority % PORT_PRIORITY_STEP != 0 {
382 bail!(
383 "port priority must be a multiple of {} (0, 16, 32, … {}); got {}. The low 12 \
384 bits of the field are the port number, not priority.",
385 PORT_PRIORITY_STEP,
386 PORT_PRIORITY_MAX,
387 priority
388 );
389 }
390 if number > PORT_NUMBER_MAX {
391 bail!(
392 "port number must be at most {} (12 bits), got {}",
393 PORT_NUMBER_MAX,
394 number
395 );
396 }
397 Ok(Self { priority, number })
398 }
399
400 pub fn encode(&self) -> [u8; 2] {
401 let packed = (((self.priority & 0xF0) as u16) << 8) | (self.number & 0x0FFF);
402 packed.to_be_bytes()
403 }
404
405 pub fn decode(bytes: &[u8]) -> Result<Self> {
406 if bytes.len() < 2 {
407 bail!("port identifier needs 2 octets, got {}", bytes.len());
408 }
409 let packed = u16::from_be_bytes([bytes[0], bytes[1]]);
410 Ok(Self {
411 priority: ((packed >> 8) as u8) & 0xF0,
412 number: packed & 0x0FFF,
413 })
414 }
415}
416
417#[derive(Debug, Clone, PartialEq)]
426pub struct ConfigBpdu {
427 pub version: u8,
430 pub bpdu_type: u8,
432 pub flags: BpduFlags,
433 pub root: BridgeId,
434 pub root_path_cost: u32,
435 pub bridge: BridgeId,
436 pub port: PortId,
437 pub message_age_seconds: f64,
438 pub max_age_seconds: f64,
439 pub hello_time_seconds: f64,
440 pub forward_delay_seconds: f64,
441}
442
443impl ConfigBpdu {
444 pub fn is_rstp(&self) -> bool {
446 self.bpdu_type == BPDU_TYPE_RST
447 }
448
449 pub fn encode(&self) -> Result<Vec<u8>> {
451 if self.bpdu_type != BPDU_TYPE_CONFIG && self.bpdu_type != BPDU_TYPE_RST {
452 bail!(
453 "BPDU type must be 0x00 (configuration) or 0x02 (RST) here; 0x{:02x} is not a \
454 configuration-shaped BPDU",
455 self.bpdu_type
456 );
457 }
458 let root = BridgeId::new(
461 self.root.priority,
462 self.root.system_id_extension,
463 self.root.mac,
464 )?;
465 let bridge = BridgeId::new(
466 self.bridge.priority,
467 self.bridge.system_id_extension,
468 self.bridge.mac,
469 )?;
470 let port = PortId::new(self.port.priority, self.port.number)?;
471
472 let mut out = Vec::with_capacity(RST_BPDU_LEN);
473 out.extend_from_slice(&PROTOCOL_ID.to_be_bytes());
474 out.push(self.version);
475 out.push(self.bpdu_type);
476 out.push(self.flags.to_byte());
477 out.extend_from_slice(&root.encode());
478 out.extend_from_slice(&self.root_path_cost.to_be_bytes());
479 out.extend_from_slice(&bridge.encode());
480 out.extend_from_slice(&port.encode());
481 out.extend_from_slice(&seconds_to_ticks(self.message_age_seconds)?.to_be_bytes());
482 out.extend_from_slice(&seconds_to_ticks(self.max_age_seconds)?.to_be_bytes());
483 out.extend_from_slice(&seconds_to_ticks(self.hello_time_seconds)?.to_be_bytes());
484 out.extend_from_slice(&seconds_to_ticks(self.forward_delay_seconds)?.to_be_bytes());
485 debug_assert_eq!(out.len(), CONFIG_BPDU_LEN);
486
487 if self.bpdu_type == BPDU_TYPE_RST {
488 out.push(0x00);
490 }
491 Ok(out)
492 }
493
494 pub fn decode(body: &[u8]) -> Result<Self> {
496 if body.len() < CONFIG_BPDU_LEN {
497 bail!(
498 "configuration BPDU needs at least {} octets, got {}",
499 CONFIG_BPDU_LEN,
500 body.len()
501 );
502 }
503 let protocol_id = u16::from_be_bytes([body[0], body[1]]);
504 if protocol_id != PROTOCOL_ID {
505 bail!("protocol identifier must be 0x0000, got 0x{protocol_id:04x}");
506 }
507 Ok(Self {
508 version: body[2],
509 bpdu_type: body[3],
510 flags: BpduFlags::from_byte(body[4]),
511 root: BridgeId::decode(&body[5..13])?,
512 root_path_cost: u32::from_be_bytes([body[13], body[14], body[15], body[16]]),
513 bridge: BridgeId::decode(&body[17..25])?,
514 port: PortId::decode(&body[25..27])?,
515 message_age_seconds: ticks_to_seconds(u16::from_be_bytes([body[27], body[28]])),
516 max_age_seconds: ticks_to_seconds(u16::from_be_bytes([body[29], body[30]])),
517 hello_time_seconds: ticks_to_seconds(u16::from_be_bytes([body[31], body[32]])),
518 forward_delay_seconds: ticks_to_seconds(u16::from_be_bytes([body[33], body[34]])),
519 })
520 }
521}
522
523#[derive(Debug, Clone, PartialEq)]
525pub enum Bpdu {
526 Config(ConfigBpdu),
528 TopologyChangeNotification,
531}
532
533impl Bpdu {
534 pub fn decode(body: &[u8]) -> Result<Self> {
535 if body.len() < TCN_BPDU_LEN {
536 bail!(
537 "BPDU needs at least {} octets, got {}",
538 TCN_BPDU_LEN,
539 body.len()
540 );
541 }
542 let protocol_id = u16::from_be_bytes([body[0], body[1]]);
543 if protocol_id != PROTOCOL_ID {
544 bail!("protocol identifier must be 0x0000, got 0x{protocol_id:04x}");
545 }
546 match body[3] {
547 BPDU_TYPE_TCN => Ok(Bpdu::TopologyChangeNotification),
548 BPDU_TYPE_CONFIG | BPDU_TYPE_RST => Ok(Bpdu::Config(ConfigBpdu::decode(body)?)),
549 other => bail!(
550 "unknown BPDU type 0x{other:02x} (expected 0x00 configuration, 0x02 RST or \
551 0x80 topology change notification)"
552 ),
553 }
554 }
555
556 pub fn encode(&self) -> Result<Vec<u8>> {
557 match self {
558 Bpdu::Config(config) => config.encode(),
559 Bpdu::TopologyChangeNotification => Ok(encode_tcn_bpdu()),
560 }
561 }
562}
563
564pub fn encode_tcn_bpdu() -> Vec<u8> {
566 vec![0x00, 0x00, VERSION_STP, BPDU_TYPE_TCN]
567}
568
569#[derive(Debug, Clone, PartialEq, Eq)]
575pub struct Frame8023 {
576 pub destination: [u8; 6],
577 pub source: [u8; 6],
578 pub payload: Vec<u8>,
581}
582
583pub fn encode_frame(destination: [u8; 6], source: [u8; 6], bpdu: &[u8]) -> Result<Vec<u8>> {
596 let client_len = LLC_HEADER_LEN + bpdu.len();
597 if client_len > MAX_8023_LENGTH {
598 bail!(
599 "802.3 client data is {client_len} octets; the length field only carries a length \
600 up to {MAX_8023_LENGTH} (0x0600 and above are EtherTypes, so a longer frame is read \
601 as Ethernet II and the BPDU is never parsed)"
602 );
603 }
604 let mut frame =
605 Vec::with_capacity(MIN_ETHERNET_FRAME_LEN.max(ETHERNET_HEADER_LEN + client_len));
606 frame.extend_from_slice(&destination);
607 frame.extend_from_slice(&source);
608 frame.extend_from_slice(&(client_len as u16).to_be_bytes());
609 frame.push(LLC_DSAP);
610 frame.push(LLC_SSAP);
611 frame.push(LLC_CONTROL);
612 frame.extend_from_slice(bpdu);
613 while frame.len() < MIN_ETHERNET_FRAME_LEN {
614 frame.push(0x00);
615 }
616 Ok(frame)
617}
618
619pub fn decode_frame(frame: &[u8]) -> Result<Frame8023> {
624 if frame.len() < ETHERNET_HEADER_LEN + LLC_HEADER_LEN {
625 bail!(
626 "802.3 frame needs at least {} octets to carry an LLC header, got {}",
627 ETHERNET_HEADER_LEN + LLC_HEADER_LEN,
628 frame.len()
629 );
630 }
631 let mut destination = [0u8; 6];
632 destination.copy_from_slice(&frame[0..6]);
633 let mut source = [0u8; 6];
634 source.copy_from_slice(&frame[6..12]);
635
636 let declared_len = u16::from_be_bytes([frame[12], frame[13]]) as usize;
637 if declared_len > MAX_8023_LENGTH {
638 bail!(
639 "not an 802.3 length-encapsulated frame: field at offset 12 is 0x{declared_len:04x}, \
640 which is an EtherType, not a length. BPDUs are never carried in Ethernet II frames."
641 );
642 }
643
644 let (dsap, ssap, control) = (frame[14], frame[15], frame[16]);
645 if dsap != LLC_DSAP || ssap != LLC_SSAP || control != LLC_CONTROL {
646 bail!(
647 "not a BPDU: LLC header is {:02x}/{:02x}/{:02x}, expected {:02x}/{:02x}/{:02x}",
648 dsap,
649 ssap,
650 control,
651 LLC_DSAP,
652 LLC_SSAP,
653 LLC_CONTROL
654 );
655 }
656
657 let available = frame.len() - ETHERNET_HEADER_LEN - LLC_HEADER_LEN;
660 let bpdu_len = declared_len.saturating_sub(LLC_HEADER_LEN).min(available);
661 let start = ETHERNET_HEADER_LEN + LLC_HEADER_LEN;
662 Ok(Frame8023 {
663 destination,
664 source,
665 payload: frame[start..start + bpdu_len].to_vec(),
666 })
667}