diff --git a/src/rust/lqos_config/src/etc/mod.rs b/src/rust/lqos_config/src/etc/mod.rs index 0c5e48e9..0cf29b26 100644 --- a/src/rust/lqos_config/src/etc/mod.rs +++ b/src/rust/lqos_config/src/etc/mod.rs @@ -50,6 +50,20 @@ pub fn load_config() -> Result<Config, LibreQoSConfigError> { Ok(lock.as_ref().unwrap().clone()) } +pub fn disable_xdp_bridge() -> Result<(), LibreQoSConfigError> { + let mut config = load_config()?; + let mut lock = CONFIG.lock().unwrap(); + + if let Some(bridge) = &mut config.bridge { + bridge.use_xdp_bridge = false; + } + + // Write the lock + *lock = Some(config); + + Ok(()) +} + /// Enables LTS reporting in the configuration file. pub fn enable_long_term_stats(license_key: String) -> Result<(), LibreQoSConfigError> { let mut config = load_config()?; diff --git a/src/rust/lqos_config/src/lib.rs b/src/rust/lqos_config/src/lib.rs index e7db6b9e..517c6cf3 100644 --- a/src/rust/lqos_config/src/lib.rs +++ b/src/rust/lqos_config/src/lib.rs @@ -13,7 +13,7 @@ mod program_control; mod shaped_devices; pub use authentication::{UserRole, WebUsers}; -pub use etc::{load_config, Config, enable_long_term_stats, Tunables, BridgeConfig, update_config}; +pub use etc::{load_config, Config, enable_long_term_stats, Tunables, BridgeConfig, update_config, disable_xdp_bridge}; pub use network_json::{NetworkJson, NetworkJsonNode, NetworkJsonTransport}; pub use program_control::load_libreqos; pub use shaped_devices::{ConfigShapedDevices, ShapedDevice}; diff --git a/src/rust/lqosd/src/preflight_checks.rs b/src/rust/lqosd/src/preflight_checks.rs index 453c2477..c59436cc 100644 --- a/src/rust/lqosd/src/preflight_checks.rs +++ b/src/rust/lqosd/src/preflight_checks.rs @@ -1,7 +1,8 @@ -use std::path::Path; +use std::{path::Path, process::Command}; use anyhow::Result; use log::{error, info}; +use lqos_config::Config; use lqos_sys::interface_name_to_index; fn check_queues(interface: &str) -> Result<()> { @@ -42,6 +43,106 @@ fn check_queues(interface: &str) -> Result<()> { Ok(()) } +#[derive(Debug)] +pub struct IpLinkInterface { + pub name: String, + pub index: u32, + pub operstate: String, + pub link_type: String, + pub master: Option<String>, +} + +pub fn get_interfaces_from_ip_link() -> Result<Vec<IpLinkInterface>> { + let output = Command::new("/sbin/ip") + .args(["-j", "link"]) + .output()?; + let output = String::from_utf8(output.stdout)?; + let output_json = serde_json::from_str::<serde_json::Value>(&output)?; + + let mut interfaces = Vec::new(); + for interface in output_json.as_array().unwrap() { + let name = interface["ifname"].as_str().unwrap().to_string(); + let index = interface["ifindex"].as_u64().unwrap() as u32; + let operstate = interface["operstate"].as_str().unwrap().to_string(); + let link_type = interface["link_type"].as_str().unwrap().to_string(); + let master = interface["master"].as_str().map(|s| s.to_string()); + + interfaces.push(IpLinkInterface { + name, + index, + operstate, + link_type, + master, + }); + } + + Ok(interfaces) +} + +fn check_interface_status(config: &Config, interfaces: &[IpLinkInterface]) -> Result<()> { + if let Some(stick) = &config.single_interface { + if let Some(iface) = interfaces.iter().find(|i| i.name == stick.interface) { + if iface.operstate != "UP" { + error!("Interface {} is not UP", stick.interface); + anyhow::bail!("Interface {} is not UP", stick.interface); + } + } + } + if let Some(bridge) = &config.bridge { + if let Some(iface) = interfaces.iter().find(|i| i.name == bridge.to_internet) { + if iface.operstate != "UP" { + error!("Interface {} is not UP", bridge.to_internet); + anyhow::bail!("Interface {} is not UP", bridge.to_internet); + } + } + if let Some(iface) = interfaces.iter().find(|i| i.name == bridge.to_network) { + if iface.operstate != "UP" { + error!("Interface {} is not UP", bridge.to_network); + anyhow::bail!("Interface {} is not UP", bridge.to_network); + } + } + } + + Ok(()) +} + +fn check_bridge_status(config: &Config, interfaces: &[IpLinkInterface]) -> Result<()> { + // On a stick mode is bridge-free + if config.on_a_stick_mode() { + return Ok(()); + } + + // Is the XDP bridge enabled? + if let Some(bridge) = &config.bridge { + if bridge.use_xdp_bridge { + for bridge_if in interfaces + .iter() + .filter(|bridge_if| bridge_if.link_type == "ether") + { + // We found a bridge. Check member interfaces to check that it does NOT include any XDP + // bridge members. + let in_bridge: Vec<&IpLinkInterface> = interfaces + .iter() + .filter(|member_if| { + if let Some(master) = &member_if.master { + master == &bridge_if.name + } else { + false + } + }) + .filter(|member_if| member_if.name == config.internet_interface() || member_if.name == config.isp_interface()) + .collect(); + + if in_bridge.len() == 2 { + error!("Bridge ({}) contains both the internet and ISP interfaces. This is not supported.", bridge_if.name); + anyhow::bail!("Bridge {} contains both the internet and ISP interfaces. This is not supported.", bridge_if.name); + } + } + } + } + Ok(()) +} + /// Runs a series of preflight checks to ensure that the configuration is sane pub fn preflight_checks() -> Result<()> { info!("Sanity checking configuration..."); @@ -78,6 +179,19 @@ pub fn preflight_checks() -> Result<()> { check_queues(&config.isp_interface())?; } + // Obtain the "IP link" output + let interfaces = get_interfaces_from_ip_link()?; + + // Are the interfaces up? + check_interface_status(&config, &interfaces)?; + + // Does the bridge system make sense? + if check_bridge_status(&config, &interfaces).is_err() { + log::warn!("Disabling XDP bridge"); + lqos_config::disable_xdp_bridge()?; + log::warn!("XDP bridge disabled in ACTIVE config. Please fix the configuration file."); + }; + info!("Sanity checks passed"); Ok(())