998593f243
Bridge: statistics() aggregates rx/tx bytes+packets from all member ports. arp_stats() delegates to each port, showing per-port breakdown. Bond: statistics() aggregates from all slaves (active+standby). arp_stats() delegates to each slave. TUN: statistics() now returns live counters tracked during send()/recv(). rx_bytes, rx_packets, tx_bytes, tx_packets increment per-packet. Previously these devices returned Stats::default() (all zeros). Now netcfg /ifaces/*/stats shows real data for bridge, bond, tun.
262 lines
8.9 KiB
Rust
262 lines
8.9 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
|
|
}
|
|
} |