use std::collections::HashSet;
use ipnetwork::Ipv4Network;
use super::netlink::{LinkConfig, LinkType, NetlinkHandle};
use crate::error::{NetError, Result};
pub const DEFAULT_BRIDGE_NAME: &str = "arcbox0";
pub const DEFAULT_MTU: u16 = 1500;
#[derive(Debug, Clone)]
pub struct BridgeConfig {
pub name: String,
pub ip_addr: Option<Ipv4Network>,
pub mtu: u16,
}
impl Default for BridgeConfig {
fn default() -> Self {
Self {
name: DEFAULT_BRIDGE_NAME.to_string(),
ip_addr: None,
mtu: DEFAULT_MTU,
}
}
}
impl BridgeConfig {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
..Default::default()
}
}
#[must_use]
pub fn with_ip(mut self, ip: Ipv4Network) -> Self {
self.ip_addr = Some(ip);
self
}
#[must_use]
pub fn with_mtu(mut self, mtu: u16) -> Self {
self.mtu = mtu;
self
}
}
pub struct LinuxBridge {
config: BridgeConfig,
netlink: NetlinkHandle,
ifindex: Option<u32>,
attached_interfaces: HashSet<String>,
}
impl LinuxBridge {
pub fn new(config: BridgeConfig) -> Result<Self> {
let netlink = NetlinkHandle::new()?;
Ok(Self {
config,
netlink,
ifindex: None,
attached_interfaces: HashSet::new(),
})
}
pub fn create(&mut self) -> Result<()> {
if self.ifindex.is_some() {
return Err(NetError::Bridge("bridge already created".to_string()));
}
let link_config = LinkConfig {
name: self.config.name.clone(),
link_type: LinkType::Bridge,
mtu: Some(self.config.mtu),
mac: None,
};
let ifindex = self.netlink.create_link(&link_config)?;
self.ifindex = Some(ifindex);
tracing::info!(
"Created bridge {} with ifindex {}",
self.config.name,
ifindex
);
Ok(())
}
pub fn delete(&mut self) -> Result<()> {
let ifindex = self
.ifindex
.ok_or_else(|| NetError::Bridge("bridge not created".to_string()))?;
self.netlink.delete_link(ifindex)?;
self.ifindex = None;
self.attached_interfaces.clear();
tracing::info!("Deleted bridge {}", self.config.name);
Ok(())
}
pub fn bring_up(&mut self) -> Result<()> {
let ifindex = self
.ifindex
.ok_or_else(|| NetError::Bridge("bridge not created".to_string()))?;
self.netlink.set_link_state(ifindex, true)?;
tracing::debug!("Brought up bridge {}", self.config.name);
Ok(())
}
pub fn bring_down(&mut self) -> Result<()> {
let ifindex = self
.ifindex
.ok_or_else(|| NetError::Bridge("bridge not created".to_string()))?;
self.netlink.set_link_state(ifindex, false)?;
tracing::debug!("Brought down bridge {}", self.config.name);
Ok(())
}
pub fn set_ip(&mut self, addr: Ipv4Network) -> Result<()> {
let ifindex = self
.ifindex
.ok_or_else(|| NetError::Bridge("bridge not created".to_string()))?;
self.netlink
.add_address(ifindex, ipnetwork::IpNetwork::V4(addr))?;
self.config.ip_addr = Some(addr);
tracing::debug!("Set IP {} on bridge {}", addr, self.config.name);
Ok(())
}
pub fn add_interface(&mut self, ifname: &str) -> Result<()> {
let bridge_ifindex = self
.ifindex
.ok_or_else(|| NetError::Bridge("bridge not created".to_string()))?;
let if_index = self.netlink.get_ifindex(ifname)?;
self.netlink.set_link_master(if_index, bridge_ifindex)?;
self.attached_interfaces.insert(ifname.to_string());
tracing::debug!("Added {} to bridge {}", ifname, self.config.name);
Ok(())
}
pub fn remove_interface(&mut self, ifname: &str) -> Result<()> {
if !self.attached_interfaces.contains(ifname) {
return Err(NetError::Bridge(format!(
"interface {} not attached to bridge",
ifname
)));
}
let if_index = self.netlink.get_ifindex(ifname)?;
self.netlink.set_link_master(if_index, 0)?;
self.attached_interfaces.remove(ifname);
tracing::debug!("Removed {} from bridge {}", ifname, self.config.name);
Ok(())
}
#[must_use]
pub fn name(&self) -> &str {
&self.config.name
}
#[must_use]
pub fn ifindex(&self) -> Option<u32> {
self.ifindex
}
#[must_use]
pub fn ip_addr(&self) -> Option<Ipv4Network> {
self.config.ip_addr
}
#[must_use]
pub fn mtu(&self) -> u16 {
self.config.mtu
}
#[must_use]
pub fn attached_interfaces(&self) -> Vec<&str> {
self.attached_interfaces
.iter()
.map(|s| s.as_str())
.collect()
}
#[must_use]
pub fn exists(&self) -> bool {
self.ifindex.is_some()
}
pub fn attach_existing(&mut self) -> Result<()> {
let ifindex = self.netlink.get_ifindex(&self.config.name)?;
self.ifindex = Some(ifindex);
tracing::info!(
"Attached to existing bridge {} with ifindex {}",
self.config.name,
ifindex
);
Ok(())
}
pub fn create_or_attach(&mut self) -> Result<()> {
match self.attach_existing() {
Ok(()) => Ok(()),
Err(_) => self.create(),
}
}
}
impl Drop for LinuxBridge {
fn drop(&mut self) {
if self.ifindex.is_some() {
tracing::debug!(
"LinuxBridge dropped, bridge {} still exists",
self.config.name
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bridge_config_default() {
let config = BridgeConfig::default();
assert_eq!(config.name, DEFAULT_BRIDGE_NAME);
assert_eq!(config.mtu, DEFAULT_MTU);
assert!(config.ip_addr.is_none());
}
#[test]
fn test_bridge_config_builder() {
let ip: Ipv4Network = "192.168.64.1/24".parse().unwrap();
let config = BridgeConfig::new("br-test").with_ip(ip).with_mtu(9000);
assert_eq!(config.name, "br-test");
assert_eq!(config.mtu, 9000);
assert_eq!(config.ip_addr, Some(ip));
}
#[test]
fn test_bridge_creation_requires_root() {
if unsafe { libc::geteuid() } != 0 {
eprintln!("Skipping test: requires root privileges");
return;
}
let config = BridgeConfig::new("arcbox-test");
let mut bridge = LinuxBridge::new(config).unwrap();
assert!(bridge.create().is_ok());
assert!(bridge.exists());
assert!(bridge.ifindex().is_some());
assert!(bridge.delete().is_ok());
assert!(!bridge.exists());
}
}