Files
RedBear-OS/netstack/src/link/bridge.rs
T
Red Bear OS 596e73a92e bridge: 5 unit tests for FDB learn/age/lookup
Bridge MAC learning tests:
- learn_stores_unicast_mapping: stored MAC resolves on correct port
- learn_ignores_multicast: broadcast/multicast never enters FDB
- learn_replaces_existing_entry_on_new_port: MAC moves update port
- age_entries_removes_expired_macs: 300s timeout respected
- lookup_returns_none_for_unknown_mac: unknown MAC returns None

Total tests across netstack: 20 (5 table + 4 conntrack + 6 nat + 5 bridge)
All passing.
2026-07-08 21:20:32 +03:00

321 lines
11 KiB
Rust

//! 802.1D MAC Learning Bridge — mirrors Linux 7.1's `net/bridge/`.
//!
//! Reference files:
//! - `net/bridge/br.c` — bridge core (`br_add_bridge`, `br_del_bridge`)
//! - `net/bridge/br_fdb.c` — forwarding database (MAC learning + aging)
//! - `net/bridge/br_forward.c` — frame forwarding logic
//! - `net/bridge/br_input.c` — ingress frame handling
//! - `net/bridge/br_device.c` — bridge as a `net_device`
//!
//! The bridge composes multiple link-layer devices and forwards Ethernet
//! frames between them based on a dynamically-learned MAC→port table.
//! Unknown destinations are flooded to all other ports, mirroring Linux's
//! `br_flood()` in `br_forward.c`.
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
use smoltcp::time::{Duration, Instant};
use smoltcp::wire::{
EthernetAddress, EthernetFrame, EthernetProtocol, EthernetRepr, IpAddress, IpCidr,
};
use super::stp::{self, BPDU_MAC, PortState, StpState};
use super::LinkDevice;
use super::Stats;
const MAC_AGE_TIMEOUT: Duration = Duration::from_secs(300);
struct MacEntry {
port: usize,
last_seen: Instant,
}
pub struct BridgeDevice {
name: Rc<str>,
ports: RefCell<Vec<Box<dyn LinkDevice>>>,
mac_table: RefCell<BTreeMap<EthernetAddress, MacEntry>>,
stp: RefCell<Option<StpState>>,
recv_buffer: Vec<u8>,
ip_address: Option<IpCidr>,
}
impl BridgeDevice {
pub fn new(name: &str) -> Self {
Self {
name: name.into(),
ports: RefCell::new(Vec::new()),
mac_table: RefCell::new(BTreeMap::new()),
stp: RefCell::new(None),
recv_buffer: Vec::with_capacity(1500),
ip_address: None,
}
}
pub fn add_port<T: LinkDevice + 'static>(&self, dev: T) {
let mac = dev.mac_address();
self.ports.borrow_mut().push(Box::new(dev));
if let Some(stp) = self.stp.borrow_mut().as_mut() {
stp.port_states.push(PortState::Forwarding);
}
let _ = mac;
}
/// Enable STP with the given bridge priority and MAC.
/// Must be called after all ports are added.
pub fn enable_stp(&self, priority: u16, bridge_mac: EthernetAddress) {
let port_count = self.ports.borrow().len();
*self.stp.borrow_mut() = Some(StpState::new(priority, bridge_mac, port_count));
}
fn learn(&self, mac: EthernetAddress, port: usize, now: Instant) {
if !mac.is_unicast() {
return;
}
self.mac_table.borrow_mut().insert(
mac,
MacEntry {
port,
last_seen: now,
},
);
}
fn age_entries(&self, now: Instant) {
self.mac_table
.borrow_mut()
.retain(|_, e| now < e.last_seen + MAC_AGE_TIMEOUT);
}
fn lookup(&self, mac: EthernetAddress) -> Option<usize> {
self.mac_table.borrow().get(&mac).map(|e| e.port)
}
fn flood(&self, packet: &[u8], now: Instant, except_port: Option<usize>) {
for (idx, port) in self.ports.borrow_mut().iter_mut().enumerate() {
if Some(idx) == except_port {
continue;
}
if self.stp.borrow().as_ref().is_some_and(|s| s.is_blocked(idx)) {
continue;
}
port.send(IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED), packet, now);
}
}
}
impl LinkDevice for BridgeDevice {
fn send(&mut self, _next_hop: IpAddress, packet: &[u8], now: Instant) {
if packet.len() < 14 {
return;
}
let frame = EthernetFrame::new_unchecked(packet);
let Ok(repr) = EthernetRepr::parse(&frame) else {
return;
};
let dst_mac = repr.dst_addr;
if repr.dst_addr.is_broadcast() || repr.dst_addr.is_multicast() {
self.flood(packet, now, None);
} else if let Some(port_idx) = self.lookup(dst_mac) {
if self.stp.borrow().as_ref().is_some_and(|s| s.is_blocked(port_idx)) {
return;
}
if let Some(port) = self.ports.borrow_mut().get_mut(port_idx) {
port.send(IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED), packet, now);
}
} else {
self.flood(packet, now, None);
}
}
fn recv(&mut self, now: Instant) -> Option<&[u8]> {
self.age_entries(now);
// STP hello timer — send periodic BPDUs if we're the root bridge
if self.stp.borrow_mut().as_mut().is_some_and(|s| s.send_hello(now)) {
let bpdu = {
let stp = self.stp.borrow();
stp.as_ref().map(|s| s.build_bpdu())
};
if let Some(bpdu) = bpdu {
for port in self.ports.borrow_mut().iter_mut() {
port.send(IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED), &bpdu, now);
}
}
}
let mut received: Option<(usize, Vec<u8>, EthernetAddress, EthernetProtocol)> = None;
{
let mut ports = self.ports.borrow_mut();
for (port_idx, port) in ports.iter_mut().enumerate() {
if let Some(buf) = port.recv(now) {
let frame = EthernetFrame::new_unchecked(buf);
let Ok(repr) = EthernetRepr::parse(&frame) else {
continue;
};
self.learn(repr.src_addr, port_idx, now);
received = Some((port_idx, buf.to_vec(), repr.dst_addr, repr.ethertype));
break;
}
}
}
if let Some((port_idx, packet, dst_mac, ethertype)) = received {
// BPDU: process via STP, don't forward
if dst_mac == BPDU_MAC {
let response = self.stp.borrow_mut().as_mut()
.and_then(|s| s.process_bpdu(port_idx, &packet, now));
if let Some(rsp) = response {
if let Some(port) = self.ports.borrow_mut().get_mut(port_idx) {
port.send(IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED), &rsp, now);
}
}
return None;
}
if ethertype == EthernetProtocol::Arp
|| ethertype == EthernetProtocol::Ipv4
|| ethertype == EthernetProtocol::Ipv6
{
if dst_mac.is_broadcast() || dst_mac.is_multicast() {
self.recv_buffer = packet.clone();
self.flood(&self.recv_buffer, now, Some(port_idx));
return Some(&self.recv_buffer);
} else if let Some(dst_port_idx) = self.lookup(dst_mac) {
if dst_port_idx != port_idx
&& !self.stp.borrow().as_ref().is_some_and(|s| s.is_blocked(dst_port_idx))
{
let mut ports = self.ports.borrow_mut();
if let Some(target) = ports.get_mut(dst_port_idx) {
target.send(IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED), &packet, now);
}
return None;
}
}
self.recv_buffer = packet;
return Some(&self.recv_buffer);
}
}
None
}
fn name(&self) -> &Rc<str> {
&self.name
}
fn can_recv(&self) -> bool {
self.ports.borrow().iter().any(|p| p.can_recv())
}
fn mac_address(&self) -> Option<EthernetAddress> {
None
}
fn set_mac_address(&mut self, _addr: EthernetAddress) {}
fn ip_address(&self) -> Option<IpCidr> {
self.ip_address
}
fn set_ip_address(&mut self, addr: IpCidr) {
self.ip_address = Some(addr);
}
fn arp_table(&self) -> String {
let mut out = String::from("Bridge FDB:\n");
for (mac, entry) in self.mac_table.borrow().iter() {
out.push_str(&format!(" {} port={} last_seen={}\n", mac, entry.port, entry.last_seen));
}
if self.mac_table.borrow().is_empty() {
out.push_str(" (empty)\n");
}
out
}
fn link_state(&self) -> &'static str {
if self.ports.borrow().is_empty() { "down" } else { "up" }
}
fn statistics(&self) -> Stats {
let mut total = Stats::default();
for port in self.ports.borrow().iter() {
let s = port.statistics();
total.rx_bytes += s.rx_bytes;
total.rx_packets += s.rx_packets;
total.tx_bytes += s.tx_bytes;
total.tx_packets += s.tx_packets;
}
total
}
fn arp_stats(&self) -> String {
let mut out = String::new();
for (i, port) in self.ports.borrow().iter().enumerate() {
let s = port.arp_stats();
if !s.is_empty() {
out.push_str(&format!("port{}: {}", i, s));
}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use smoltcp::wire::EthernetAddress;
fn mac(a: u8, b: u8, c: u8, d: u8, e: u8, f: u8) -> EthernetAddress {
EthernetAddress([a, b, c, d, e, f])
}
#[test]
fn learn_stores_unicast_mapping() {
let b = BridgeDevice::new("br0");
let m = mac(0x00, 0x11, 0x22, 0x33, 0x44, 0x55);
b.learn(m, 2, Instant::from_secs(0));
assert_eq!(b.lookup(m), Some(2),
"Learned MAC must be found on port 2");
}
#[test]
fn learn_ignores_multicast() {
let b = BridgeDevice::new("br0");
let multicast = EthernetAddress::BROADCAST;
b.learn(multicast, 1, Instant::from_secs(0));
assert_eq!(b.lookup(multicast), None,
"Multicast MACs must not be learned");
}
#[test]
fn learn_replaces_existing_entry_on_new_port() {
let b = BridgeDevice::new("br0");
let m = mac(0x00, 0x11, 0x22, 0x33, 0x44, 0x55);
b.learn(m, 1, Instant::from_secs(0));
assert_eq!(b.lookup(m), Some(1));
b.learn(m, 2, Instant::from_secs(10));
assert_eq!(b.lookup(m), Some(2),
"MAC move from port 1 to port 2 must update FDB");
}
#[test]
fn age_entries_removes_expired_macs() {
let b = BridgeDevice::new("br0");
let m1 = mac(0x00, 0x11, 0x22, 0x33, 0x44, 0x55);
let m2 = mac(0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE);
b.learn(m1, 1, Instant::from_secs(0));
b.learn(m2, 2, Instant::from_secs(MAC_AGE_TIMEOUT.secs() as i64)); // Age boundary
// Age just past the 300s timeout
b.age_entries(Instant::from_secs(MAC_AGE_TIMEOUT.secs() as i64 + 1));
assert_eq!(b.lookup(m1), None, "m1 (learned at 0) must be aged out");
assert_eq!(b.lookup(m2), Some(2),
"m2 (learned at 300s) must still be valid");
}
#[test]
fn lookup_returns_none_for_unknown_mac() {
let b = BridgeDevice::new("br0");
assert_eq!(b.lookup(mac(0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01)), None);
}
}