Feat: add XDP interface compatibility shim

This commit is contained in:
Herbert Wolverson committed 2026-08-10 17:02:09 -05:00
1 parent 482c856dd7
commit f7d53d8cc6
22 files changed
+1304 -58

No files matched your search

+3
View File
@@ -11987,6 +11987,7 @@ mod tests {
to_internet: "internet0".to_string(),
to_network: "isp0".to_string(),
mtu: None,
compatibility_shim: false,
}),
..Config::default()
});
@@ -12265,6 +12266,7 @@ mod tests {
to_internet: "lo".to_string(),
to_network: "lo".to_string(),
mtu: None,
compatibility_shim: false,
}),
..lqos_config::Config::default()
};
@@ -14120,6 +14122,7 @@ mod tests {
to_internet: "__bakery-missing-wan__".to_string(),
to_network: "__bakery-missing-lan__".to_string(),
mtu: None,
compatibility_shim: false,
}),
..Config::default()
};
@@ -195,6 +195,7 @@ fn migrate_bridge(
to_internet: python_config.interface_b.clone(),
to_network: python_config.interface_a.clone(),
mtu: None,
compatibility_shim: false,
});
}
Ok(())
@@ -20,6 +20,15 @@ pub struct BridgeConfig {
/// Optional MTU for LibreQoS-managed Linux bridge interfaces.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mtu: Option<u32>,
/// Route traffic through LibreQoS-managed veth devices when the selected
/// physical interfaces cannot host the XDP programs directly.
#[serde(default, skip_serializing_if = "bool_is_false")]
pub compatibility_shim: bool,
}
fn bool_is_false(value: &bool) -> bool {
!*value
}
impl Default for BridgeConfig {
@@ -29,10 +38,26 @@ impl Default for BridgeConfig {
to_internet: "eth0".to_string(),
to_network: "eth1".to_string(),
mtu: None,
compatibility_shim: false,
}
}
}
impl BridgeConfig {
/// Returns whether the veth interface compatibility shim is enabled.
pub fn compatibility_shim_enabled(&self) -> bool {
self.compatibility_shim
}
/// Checks invariants required by the veth interface compatibility shim.
pub fn validate_compatibility_shim(&self) -> Result<(), &'static str> {
if self.compatibility_shim_enabled() && !self.use_xdp_bridge {
return Err("bridge.compatibility_shim requires bridge.use_xdp_bridge = true");
}
Ok(())
}
}
/// Represents a single-interface bridge
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Allocative)]
pub struct SingleInterfaceConfig {
+43 -2
View File
@@ -301,6 +301,9 @@ impl Config {
}
if let Some(bridge) = &self.bridge {
validate_interface_mtu("bridge.mtu", bridge.mtu)?;
bridge
.validate_compatibility_shim()
.map_err(str::to_string)?;
}
if let Some(single_interface) = &self.single_interface {
validate_interface_mtu("single_interface.mtu", single_interface.mtu)?;
@@ -625,7 +628,11 @@ impl Config {
/// Calculate the unterface facing the Internet
pub fn internet_interface(&self) -> String {
if let Some(bridge) = &self.bridge {
bridge.to_internet.clone()
if bridge.compatibility_shim_enabled() {
crate::SHIM_INTERNET_LQOS.to_string()
} else {
bridge.to_internet.clone()
}
} else if let Some(single_interface) = &self.single_interface {
single_interface.interface.clone()
} else {
@@ -636,7 +643,11 @@ impl Config {
/// Calculate the interface facing the ISP
pub fn isp_interface(&self) -> String {
if let Some(bridge) = &self.bridge {
bridge.to_network.clone()
if bridge.compatibility_shim_enabled() {
crate::SHIM_NETWORK_LQOS.to_string()
} else {
bridge.to_network.clone()
}
} else if let Some(single_interface) = &self.single_interface {
single_interface.interface.clone()
} else {
@@ -705,6 +716,36 @@ mod test {
assert_eq!(config.version, "1.5");
}
#[test]
fn compatibility_shim_uses_managed_veth_interfaces() {
let raw = include_str!("example.toml").replace(
"use_xdp_bridge = true",
"use_xdp_bridge = true\ncompatibility_shim = true",
);
let config = Config::load_from_string(&raw).expect("compatibility shim should load");
assert_eq!(config.internet_interface(), crate::SHIM_INTERNET_LQOS);
assert_eq!(config.isp_interface(), crate::SHIM_NETWORK_LQOS);
assert!(
config
.bridge
.as_ref()
.is_some_and(BridgeConfig::compatibility_shim_enabled)
);
}
#[test]
fn compatibility_shim_requires_xdp_bridge() {
let raw = include_str!("example.toml").replace(
"use_xdp_bridge = true",
"use_xdp_bridge = false\ncompatibility_shim = true",
);
let error = Config::load_from_string(&raw)
.expect_err("compatibility shim without XDP bridge should fail");
assert!(error.contains("compatibility_shim requires bridge.use_xdp_bridge = true"));
}
#[test]
fn local_api_config_defaults_when_section_is_absent() {
let legacy = remove_sections(include_str!("example.toml"), &["local_api"]);
+18
View File
@@ -157,3 +157,21 @@ pub fn topology_import_has_shaped_devices(config: &Config) -> bool {
/// Used as a constant in determining buffer preallocation
pub const SUPPORTED_CUSTOMERS: usize = 100_000;
/// lqosd-facing veth on the Internet side of the interface compatibility shim.
pub const SHIM_INTERNET_LQOS: &str = "v_inet_lq";
/// Bridge-facing peer of [`SHIM_INTERNET_LQOS`].
pub const SHIM_INTERNET_PEER: &str = "v_inet_phy";
/// lqosd-facing veth on the subscriber-network side of the compatibility shim.
pub const SHIM_NETWORK_LQOS: &str = "v_isp_lq";
/// Bridge-facing peer of [`SHIM_NETWORK_LQOS`].
pub const SHIM_NETWORK_PEER: &str = "v_isp_phy";
/// Two-port Linux bridge joining the Internet interface to its veth peer.
pub const SHIM_INTERNET_BRIDGE: &str = "br_lq_inet";
/// Two-port Linux bridge joining the subscriber-network interface to its veth peer.
pub const SHIM_NETWORK_BRIDGE: &str = "br_lq_isp";
+130 -21
View File
@@ -58,6 +58,9 @@ pub struct InterfaceCandidate {
#[serde(default)]
pub details: Vec<String>,
pub bridge_eligible: bool,
/// Whether the interface can be used behind the veth compatibility shim.
#[serde(default)]
pub compatibility_shim_eligible: bool,
pub single_interface_eligible: bool,
#[serde(default)]
pub current_selection: bool,
@@ -496,27 +499,31 @@ fn collect_interface_restrictions(
}
}
fn is_plain_bond_interface(iface: &str) -> bool {
iface.starts_with("bond") && !iface.contains('.') && !iface.contains(':')
}
fn interface_name_role_reason(iface: &str) -> Option<&'static str> {
if iface == "lo" {
Some("Loopback interface.")
} else if iface.contains('.') || iface.contains(':') {
Some("VLAN or alias interface.")
} else if [
"br",
"bond",
"docker",
"veth",
"virbr",
"ifb",
"wg",
"tun",
"tap",
"tailscale",
"zt",
"vmnet",
]
.iter()
.any(|prefix| iface.starts_with(prefix))
} else if is_plain_bond_interface(iface)
|| [
"br",
"docker",
"veth",
"virbr",
"ifb",
"wg",
"tun",
"tap",
"tailscale",
"zt",
"vmnet",
]
.iter()
.any(|prefix| iface.starts_with(prefix))
{
Some("Virtual, bridge, or tunnel interface.")
} else {
@@ -537,26 +544,34 @@ fn build_interface_candidates(
.map(|iface| {
let mut details = Vec::new();
if let Some(reason) = interface_name_role_reason(iface) {
let role_reason = interface_name_role_reason(iface);
if let Some(reason) = role_reason {
details.push(reason.to_string());
}
if default_interface_name.is_some_and(|default_iface| default_iface == iface) {
let is_management_interface =
default_interface_name.is_some_and(|default_iface| default_iface == iface);
if is_management_interface {
details.push("Current management/default-route interface.".to_string());
}
if !queue_caps.get(iface).copied().unwrap_or(false) {
details.push("Does not appear to have multi-queue RX/TX support.".to_string());
}
if let Some(extra) = interface_restrictions.get(iface) {
let restrictions = interface_restrictions.get(iface);
if let Some(extra) = restrictions {
details.extend(extra.iter().cloned());
}
details.sort();
details.dedup();
let eligible = details.is_empty();
let compatibility_shim_eligible = !is_management_interface
&& restrictions.is_none_or(BTreeSet::is_empty)
&& (role_reason.is_none() || is_plain_bond_interface(iface));
InterfaceCandidate {
name: iface.clone(),
details,
bridge_eligible: eligible,
compatibility_shim_eligible,
single_interface_eligible: eligible,
current_selection: selected_set.contains(iface),
}
@@ -862,6 +877,10 @@ pub fn inspect_network_mode_with_paths(
queue_caps: &BTreeMap<String, bool>,
) -> NetworkModeInspection {
let mode = requested_mode(config);
let compatibility_shim_enabled = config
.bridge
.as_ref()
.is_some_and(|bridge| bridge.compatibility_shim_enabled());
let selected = selected_interfaces(&mode);
let (preview, preview_note) = managed_preview_yaml(&mode);
let mut warnings = Vec::new();
@@ -887,10 +906,14 @@ pub fn inspect_network_mode_with_paths(
for iface in &selected {
if !system_ifaces.contains(iface) {
missing.push(iface.clone());
} else if !queue_caps.get(iface).copied().unwrap_or(false) {
} else if !queue_caps.get(iface).copied().unwrap_or(false) && !compatibility_shim_enabled {
warnings.push(format!(
"Interface {iface} does not appear to have multi-queue RX/TX support."
));
} else if !queue_caps.get(iface).copied().unwrap_or(false) {
warnings.push(format!(
"Interface {iface} does not expose multi-queue RX/TX support; the compatibility shim will provide multiqueue veth interfaces to LibreQoS."
));
}
}
@@ -1128,7 +1151,9 @@ pub fn inspect_network_mode(config: &Config) -> NetworkModeInspection {
#[cfg(test)]
mod tests {
use super::{adoption_rewrite_for_path, inspect_network_mode_with_paths};
use super::{
adoption_rewrite_for_path, build_interface_candidates, inspect_network_mode_with_paths,
};
use std::collections::{BTreeMap, BTreeSet};
fn linux_bridge_config() -> lqos_config::Config {
@@ -1138,6 +1163,7 @@ mod tests {
to_internet: "ens19".to_string(),
to_network: "ens20".to_string(),
mtu: None,
compatibility_shim: false,
}),
single_interface: None,
..lqos_config::Config::default()
@@ -1266,6 +1292,89 @@ mod tests {
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn compatibility_shim_allows_bonds_without_multiqueue_support() {
let mut config = linux_bridge_config();
let bridge = config.bridge.as_mut().expect("bridge");
bridge.use_xdp_bridge = true;
bridge.compatibility_shim = true;
bridge.to_internet = "bond0".to_string();
bridge.to_network = "bond1".to_string();
let root = test_env("compatibility-shim-bonds");
let inspection = inspect_network_mode_with_paths(
&config,
&root.join("netplan"),
&root.join("pending"),
&BTreeSet::from(["bond0".to_string(), "bond1".to_string()]),
&BTreeMap::from([("bond0".to_string(), false), ("bond1".to_string(), false)]),
);
assert_eq!(inspection.interface_candidates.len(), 2);
assert!(
inspection
.interface_candidates
.iter()
.all(|candidate| candidate.compatibility_shim_eligible)
);
assert!(
inspection
.interface_candidates
.iter()
.all(|candidate| !candidate.bridge_eligible)
);
assert!(
inspection
.warnings
.iter()
.any(|warning| warning.contains("compatibility shim will provide multiqueue"))
);
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn compatibility_shim_still_rejects_management_interface() {
let candidates = build_interface_candidates(
&BTreeSet::from(["bond0".to_string(), "bond1".to_string()]),
&BTreeMap::from([("bond0".to_string(), false), ("bond1".to_string(), false)]),
&[],
Some("bond0"),
&BTreeMap::new(),
);
let management = candidates
.iter()
.find(|candidate| candidate.name == "bond0")
.expect("bond0 candidate");
assert!(!management.compatibility_shim_eligible);
assert!(
management
.details
.iter()
.any(|detail| detail.contains("management/default-route"))
);
}
#[test]
fn compatibility_shim_rejects_bond_vlan_and_alias_interfaces() {
let candidates = build_interface_candidates(
&BTreeSet::from(["bond0.100".to_string(), "bond0:1".to_string()]),
&BTreeMap::from([
("bond0.100".to_string(), false),
("bond0:1".to_string(), false),
]),
&[],
None,
&BTreeMap::new(),
);
assert!(
candidates
.iter()
.all(|candidate| !candidate.compatibility_shim_eligible)
);
}
#[test]
fn detects_external_compatible_linux_bridge() {
let root = test_env("external-compatible");
@@ -1061,6 +1061,7 @@ esac
to_internet,
to_network,
mtu: None,
compatibility_shim: false,
}),
single_interface: None,
..Config::default()
+51
View File
@@ -43,6 +43,10 @@ pub(crate) enum CommitOutcome {
pub(crate) fn build_candidate_config(existing: Option<lqos_config::Config>) -> lqos_config::Config {
let mut config = existing.unwrap_or_default();
let existing_bridge_mtu = config.bridge.as_ref().and_then(|bridge| bridge.mtu);
let existing_compatibility_shim = config
.bridge
.as_ref()
.is_some_and(|bridge| bridge.compatibility_shim_enabled());
let existing_single_interface_mtu = config
.single_interface
.as_ref()
@@ -61,6 +65,7 @@ pub(crate) fn build_candidate_config(existing: Option<lqos_config::Config>) -> l
to_internet: new_config.to_internet.clone(),
to_network: new_config.to_network.clone(),
mtu: existing_bridge_mtu,
compatibility_shim: false,
});
}
BridgeMode::XDP => {
@@ -70,6 +75,7 @@ pub(crate) fn build_candidate_config(existing: Option<lqos_config::Config>) -> l
to_internet: new_config.to_internet.clone(),
to_network: new_config.to_network.clone(),
mtu: existing_bridge_mtu,
compatibility_shim: existing_compatibility_shim,
});
}
BridgeMode::Single => {
@@ -305,4 +311,49 @@ mod tests {
*CURRENT_CONFIG.lock() = previous_builder;
}
#[test]
fn build_candidate_config_keeps_compatibility_shim_only_in_xdp_mode() {
let previous_builder = {
let mut builder = CURRENT_CONFIG.lock();
let previous = builder.clone();
builder.bridge_mode = BridgeMode::XDP;
builder.to_internet = "bond0".to_string();
builder.to_network = "bond1".to_string();
previous
};
let existing = lqos_config::Config {
bridge: Some(lqos_config::BridgeConfig {
use_xdp_bridge: true,
compatibility_shim: true,
..lqos_config::BridgeConfig::default()
}),
..lqos_config::Config::default()
};
let candidate = build_candidate_config(Some(existing));
assert!(
candidate
.bridge
.as_ref()
.is_some_and(lqos_config::BridgeConfig::compatibility_shim_enabled)
);
CURRENT_CONFIG.lock().bridge_mode = BridgeMode::Linux;
let candidate = build_candidate_config(Some(candidate));
assert!(
candidate
.bridge
.as_ref()
.is_some_and(|bridge| !bridge.compatibility_shim_enabled())
);
CURRENT_CONFIG.lock().bridge_mode = BridgeMode::Single;
let candidate = build_candidate_config(Some(candidate));
assert!(candidate.bridge.is_none());
assert!(candidate.single_interface.is_some());
*CURRENT_CONFIG.lock() = previous_builder;
}
}
+7 -5
View File
@@ -616,19 +616,21 @@ pub fn attach_xdp_and_tc_to_interface(
if let Some(bridge) = &etc.bridge
&& bridge.use_xdp_bridge
{
let internet_interface = etc.internet_interface();
let network_interface = etc.isp_interface();
// Enable "promiscuous" mode on interfaces
debug!("Enabling promiscuous mode on {}", &bridge.to_internet);
debug!("Enabling promiscuous mode on {}", &internet_interface);
std::process::Command::new("/bin/ip")
.args(["link", "set", &bridge.to_internet, "promisc", "on"])
.args(["link", "set", &internet_interface, "promisc", "on"])
.output()?;
debug!("Enabling promiscuous mode on {}", &bridge.to_network);
debug!("Enabling promiscuous mode on {}", &network_interface);
std::process::Command::new("/bin/ip")
.args(["link", "set", &bridge.to_network, "promisc", "on"])
.args(["link", "set", &network_interface, "promisc", "on"])
.output()?;
// Build the interface and vlan map entries
crate::bifrost_maps::clear_bifrost()?;
crate::bifrost_maps::map_multi_interface_mode(&bridge.to_internet, &bridge.to_network)?;
crate::bifrost_maps::map_multi_interface_mode(&internet_interface, &network_interface)?;
// Actually attach the TC ingress program
let error = unsafe { bpf::tc_attach_ingress(interface_index as i32, false, skeleton) };
+616
View File
@@ -0,0 +1,616 @@
//! Builds the veth and Linux-bridge compatibility shim used when the selected
//! physical interfaces cannot host LibreQoS XDP programs directly.
use lqos_config::{
Config, SHIM_INTERNET_BRIDGE, SHIM_INTERNET_LQOS, SHIM_INTERNET_PEER, SHIM_NETWORK_BRIDGE,
SHIM_NETWORK_LQOS, SHIM_NETWORK_PEER,
};
use std::num::{ParseIntError, TryFromIntError};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use thiserror::Error;
use tracing::{debug, info, warn};
const IP_COMMAND: &str = "/bin/ip";
#[derive(Clone, Debug, PartialEq, Eq)]
struct ShimTopology {
physical_to_internet: String,
physical_to_network: String,
queue_count: u32,
mtu: u32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct ShimPhysicalInterfaces {
to_internet: String,
to_network: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct CommandSpec {
program: &'static str,
args: Vec<String>,
}
impl CommandSpec {
fn ip(args: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
program: IP_COMMAND,
args: args.into_iter().map(Into::into).collect(),
}
}
fn display(&self) -> String {
format!("{} {}", self.program, self.args.join(" "))
}
}
/// Errors returned while planning or creating the interface compatibility shim.
#[derive(Debug, Error)]
pub(crate) enum CompatibilityShimError {
/// The bridge configuration cannot form a safe shim topology.
#[error("invalid interface compatibility shim configuration: {0}")]
InvalidConfig(String),
/// The selected shaping CPU count cannot be represented by iproute2.
#[error("interface compatibility shim queue count is too large")]
QueueCount(#[from] TryFromIntError),
/// A physical interface MTU could not be read.
#[error("unable to read MTU from {path}: {source}")]
ReadMtu {
/// Sysfs path that could not be read.
path: PathBuf,
/// Underlying filesystem error.
source: std::io::Error,
},
/// A physical interface MTU was not a valid integer.
#[error("unable to parse MTU from {path}: {source}")]
ParseMtu {
/// Sysfs path containing the invalid value.
path: PathBuf,
/// Integer parsing error.
source: ParseIntError,
},
/// iproute2 could not be started.
#[error("unable to run {command}: {source}")]
Launch {
/// Command that failed to launch.
command: String,
/// Underlying process-launch error.
source: std::io::Error,
},
/// iproute2 returned a failure status.
#[error("{command} failed with status {status}: {stderr}")]
CommandFailed {
/// Command that returned a failure status.
command: String,
/// Process status code, if the process reported one.
status: String,
/// Standard error emitted by iproute2.
stderr: String,
},
}
/// Owns the active compatibility shim and removes it when startup unwinds.
pub(crate) struct CompatibilityShimGuard {
active: bool,
}
impl CompatibilityShimGuard {
/// Returns whether this guard owns an active compatibility shim.
pub(crate) fn is_active(&self) -> bool {
self.active
}
}
impl Drop for CompatibilityShimGuard {
fn drop(&mut self) {
if self.active {
cleanup();
}
}
}
fn read_interface_mtu(interface: &str) -> Result<u32, CompatibilityShimError> {
let path = PathBuf::from(format!("/sys/class/net/{interface}/mtu"));
let raw = std::fs::read_to_string(&path).map_err(|source| CompatibilityShimError::ReadMtu {
path: path.clone(),
source,
})?;
raw.trim()
.parse::<u32>()
.map_err(|source| CompatibilityShimError::ParseMtu { path, source })
}
fn reserved_interface_names() -> [&'static str; 6] {
[
SHIM_INTERNET_LQOS,
SHIM_INTERNET_PEER,
SHIM_NETWORK_LQOS,
SHIM_NETWORK_PEER,
SHIM_INTERNET_BRIDGE,
SHIM_NETWORK_BRIDGE,
]
}
fn planned_queue_count(
config: &Config,
shaping_cpu_count: usize,
) -> Result<u32, CompatibilityShimError> {
let requested_queue_count = config
.queues
.override_available_queues
.map(|count| count as usize)
.unwrap_or(shaping_cpu_count);
Ok(u32::try_from(
requested_queue_count.min(shaping_cpu_count).max(2),
)?)
}
fn physical_interfaces_from_config(
config: &Config,
) -> Result<Option<ShimPhysicalInterfaces>, CompatibilityShimError> {
let Some(bridge) = config.bridge.as_ref() else {
return Ok(None);
};
if !bridge.compatibility_shim_enabled() {
return Ok(None);
}
bridge
.validate_compatibility_shim()
.map_err(|message| CompatibilityShimError::InvalidConfig(message.to_string()))?;
let physical_to_internet = bridge.to_internet.trim();
let physical_to_network = bridge.to_network.trim();
if physical_to_internet.is_empty() || physical_to_network.is_empty() {
return Err(CompatibilityShimError::InvalidConfig(
"both physical interface names are required".to_string(),
));
}
if physical_to_internet == physical_to_network {
return Err(CompatibilityShimError::InvalidConfig(
"physical interface names must be different".to_string(),
));
}
if reserved_interface_names()
.iter()
.any(|reserved| *reserved == physical_to_internet || *reserved == physical_to_network)
{
return Err(CompatibilityShimError::InvalidConfig(
"physical interfaces cannot use LibreQoS shim device names".to_string(),
));
}
Ok(Some(ShimPhysicalInterfaces {
to_internet: physical_to_internet.to_string(),
to_network: physical_to_network.to_string(),
}))
}
fn topology_with_mtu(
config: &Config,
physical: ShimPhysicalInterfaces,
mut read_mtu: impl FnMut(&str) -> Result<u32, CompatibilityShimError>,
) -> Result<ShimTopology, CompatibilityShimError> {
let shaping_cpu_count = lqos_config::detect_shaping_cpus(config)
.shaping
.len()
.max(1);
let queue_count = planned_queue_count(config, shaping_cpu_count)?;
let mtu = read_mtu(&physical.to_internet)?.min(read_mtu(&physical.to_network)?);
Ok(ShimTopology {
physical_to_internet: physical.to_internet,
physical_to_network: physical.to_network,
queue_count,
mtu,
})
}
fn create_veth_commands(name: &str, peer: &str, queues: u32) -> Vec<CommandSpec> {
let queue_count = queues.to_string();
vec![CommandSpec::ip([
"link",
"add",
"name",
name,
"numrxqueues",
queue_count.as_str(),
"numtxqueues",
queue_count.as_str(),
"type",
"veth",
"peer",
"name",
peer,
"numrxqueues",
queue_count.as_str(),
"numtxqueues",
queue_count.as_str(),
])]
}
fn set_link_value(device: &str, field: &str, value: impl ToString) -> CommandSpec {
CommandSpec::ip([
"link".to_string(),
"set".to_string(),
"dev".to_string(),
device.to_string(),
field.to_string(),
value.to_string(),
])
}
fn set_link_up(device: &str) -> CommandSpec {
CommandSpec::ip(["link", "set", "dev", device, "up"])
}
fn create_bridge_commands(name: &str, members: [&str; 2], mtu: u32) -> Vec<CommandSpec> {
let mut commands = vec![
CommandSpec::ip([
"link",
"add",
"name",
name,
"type",
"bridge",
"stp_state",
"0",
"vlan_filtering",
"0",
"mcast_snooping",
"0",
]),
set_link_value(name, "mtu", mtu),
];
for member in members {
commands.push(set_link_value(member, "master", name));
commands.push(set_link_up(member));
}
commands.push(set_link_up(name));
commands
}
fn setup_commands(topology: &ShimTopology) -> Vec<CommandSpec> {
let mut commands = Vec::new();
commands.extend(create_veth_commands(
SHIM_INTERNET_LQOS,
SHIM_INTERNET_PEER,
topology.queue_count,
));
commands.extend(create_veth_commands(
SHIM_NETWORK_LQOS,
SHIM_NETWORK_PEER,
topology.queue_count,
));
for veth in [
SHIM_INTERNET_LQOS,
SHIM_INTERNET_PEER,
SHIM_NETWORK_LQOS,
SHIM_NETWORK_PEER,
] {
commands.push(set_link_value(veth, "mtu", topology.mtu));
commands.push(set_link_up(veth));
}
commands.extend(create_bridge_commands(
SHIM_INTERNET_BRIDGE,
[SHIM_INTERNET_PEER, topology.physical_to_internet.as_str()],
topology.mtu,
));
commands.extend(create_bridge_commands(
SHIM_NETWORK_BRIDGE,
[SHIM_NETWORK_PEER, topology.physical_to_network.as_str()],
topology.mtu,
));
commands
}
fn cleanup_commands() -> Vec<CommandSpec> {
vec![
CommandSpec::ip(["link", "delete", SHIM_INTERNET_BRIDGE, "type", "bridge"]),
CommandSpec::ip(["link", "delete", SHIM_NETWORK_BRIDGE, "type", "bridge"]),
CommandSpec::ip(["link", "delete", SHIM_INTERNET_LQOS, "type", "veth"]),
CommandSpec::ip(["link", "delete", SHIM_NETWORK_LQOS, "type", "veth"]),
]
}
fn run_checked(command: &CommandSpec) -> Result<(), CompatibilityShimError> {
let output = Command::new(command.program)
.args(&command.args)
.stdout(Stdio::null())
.stderr(Stdio::piped())
.output()
.map_err(|source| CompatibilityShimError::Launch {
command: command.display(),
source,
})?;
if output.status.success() {
return Ok(());
}
Err(CompatibilityShimError::CommandFailed {
command: command.display(),
status: output
.status
.code()
.map(|code| code.to_string())
.unwrap_or_else(|| "terminated by signal".to_string()),
stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(),
})
}
/// Removes any LibreQoS interface compatibility shim devices.
///
/// This function changes host network-device state. Missing devices and other
/// cleanup failures are logged and ignored so a later setup can recover.
pub(crate) fn cleanup() {
cleanup_with(&mut run_checked);
}
fn cleanup_with(run_command: &mut impl FnMut(&CommandSpec) -> Result<(), CompatibilityShimError>) {
for command in cleanup_commands() {
match run_command(&command) {
Ok(()) => debug!(command = %command.display(), "Removed compatibility shim device"),
Err(error) => debug!(%error, "Compatibility shim cleanup command did not succeed"),
}
}
}
fn prepare_with(
config: &Config,
read_mtu: impl FnMut(&str) -> Result<u32, CompatibilityShimError>,
mut run_command: impl FnMut(&CommandSpec) -> Result<(), CompatibilityShimError>,
) -> Result<bool, CompatibilityShimError> {
let physical = physical_interfaces_from_config(config)?;
cleanup_with(&mut run_command);
let Some(physical) = physical else {
return Ok(false);
};
let topology = topology_with_mtu(config, physical, read_mtu)?;
info!(
internet_interface = %topology.physical_to_internet,
network_interface = %topology.physical_to_network,
queues = topology.queue_count,
mtu = topology.mtu,
"Creating LibreQoS interface compatibility shim"
);
for command in setup_commands(&topology) {
if let Err(error) = run_command(&command) {
warn!(%error, "Unable to create interface compatibility shim; rolling back");
cleanup_with(&mut run_command);
return Err(error);
}
}
Ok(true)
}
/// Removes stale shim devices, then creates the configured compatibility shim.
///
/// This function changes host network-device state by creating veth pairs,
/// Linux bridges, and bridge membership. The returned guard removes those
/// devices if startup unwinds or the daemon shuts down normally.
pub(crate) fn prepare(config: &Config) -> Result<CompatibilityShimGuard, CompatibilityShimError> {
let active = prepare_with(config, read_interface_mtu, run_checked)?;
Ok(CompatibilityShimGuard { active })
}
#[cfg(test)]
mod tests {
use super::{
CommandSpec, CompatibilityShimError, ShimTopology, cleanup_commands, planned_queue_count,
prepare_with, setup_commands,
};
use lqos_config::{
BridgeConfig, Config, SHIM_INTERNET_BRIDGE, SHIM_INTERNET_LQOS, SHIM_NETWORK_BRIDGE,
SHIM_NETWORK_LQOS,
};
fn topology() -> ShimTopology {
ShimTopology {
physical_to_internet: "bond0".to_string(),
physical_to_network: "enp2s0".to_string(),
queue_count: 8,
mtu: 9000,
}
}
fn command_text(command: &CommandSpec) -> String {
command.display()
}
fn enabled_config() -> Config {
Config {
bridge: Some(BridgeConfig {
use_xdp_bridge: true,
to_internet: "bond0".to_string(),
to_network: "enp2s0".to_string(),
compatibility_shim: true,
..BridgeConfig::default()
}),
..Config::default()
}
}
#[test]
fn setup_plan_uses_two_multiqueue_veth_pairs_and_two_bridges() {
let commands = setup_commands(&topology());
let command_text = commands.iter().map(command_text).collect::<Vec<_>>();
assert_eq!(
command_text
.iter()
.filter(|command| command.contains(" type veth peer "))
.count(),
2
);
assert_eq!(
command_text
.iter()
.filter(|command| command.contains(" type bridge "))
.count(),
2
);
assert!(
command_text
.iter()
.filter(|command| command.contains(" type veth peer "))
.all(|command| command.contains("numrxqueues 8 numtxqueues 8"))
);
assert!(command_text.iter().any(|command| {
command.contains(SHIM_INTERNET_BRIDGE)
&& command.contains("stp_state 0")
&& command.contains("mcast_snooping 0")
}));
assert!(command_text.iter().any(|command| {
command.contains(SHIM_NETWORK_BRIDGE)
&& command.contains("stp_state 0")
&& command.contains("mcast_snooping 0")
}));
assert!(command_text.iter().any(|command| {
command.contains(SHIM_INTERNET_LQOS) && command.ends_with("mtu 9000")
}));
assert!(command_text.iter().any(|command| {
command.contains(SHIM_NETWORK_LQOS) && command.ends_with("mtu 9000")
}));
}
#[test]
fn cleanup_removes_bridges_before_veth_pairs() {
let commands = cleanup_commands();
assert!(commands[0].display().contains(SHIM_INTERNET_BRIDGE));
assert!(commands[1].display().contains(SHIM_NETWORK_BRIDGE));
assert!(commands[2].display().contains(SHIM_INTERNET_LQOS));
assert!(commands[3].display().contains(SHIM_NETWORK_LQOS));
}
#[test]
fn queue_count_honors_the_smaller_of_override_and_shaping_cpus() {
let mut config = enabled_config();
config.queues.override_available_queues = Some(8);
assert_eq!(planned_queue_count(&config, 4).expect("queue count"), 4);
config.queues.override_available_queues = Some(3);
assert_eq!(planned_queue_count(&config, 8).expect("queue count"), 3);
config.queues.override_available_queues = None;
assert_eq!(planned_queue_count(&config, 6).expect("queue count"), 6);
}
#[test]
fn prepare_uses_the_smaller_physical_mtu() {
let mut commands = Vec::new();
let active = prepare_with(
&enabled_config(),
|interface| match interface {
"bond0" => Ok(9000),
"enp2s0" => Ok(1500),
_ => unreachable!("unexpected interface"),
},
|command| {
commands.push(command.display());
Ok(())
},
)
.expect("shim preparation should succeed");
assert!(active);
assert_eq!(
commands[..4],
cleanup_commands()
.iter()
.map(CommandSpec::display)
.collect::<Vec<_>>()
);
assert!(
commands
.iter()
.filter(|command| command.contains(" link set dev ") && command.contains(" mtu "))
.all(|command| command.ends_with("mtu 1500"))
);
}
#[test]
fn prepare_rolls_back_after_partial_setup_failure() {
let mut commands = Vec::new();
let error = prepare_with(
&enabled_config(),
|_| Ok(1500),
|command| {
let display = command.display();
commands.push(display.clone());
if display.contains("link add name v_isp_lq") {
return Err(CompatibilityShimError::CommandFailed {
command: display,
status: "1".to_string(),
stderr: "simulated failure".to_string(),
});
}
Ok(())
},
)
.expect_err("partial setup should fail");
assert!(error.to_string().contains("simulated failure"));
let expected_cleanup = cleanup_commands()
.iter()
.map(CommandSpec::display)
.collect::<Vec<_>>();
assert_eq!(&commands[..4], expected_cleanup.as_slice());
assert_eq!(&commands[commands.len() - 4..], expected_cleanup.as_slice());
}
#[test]
fn invalid_config_is_rejected_before_host_cleanup() {
let mut config = enabled_config();
config.bridge.as_mut().expect("bridge").to_internet = SHIM_INTERNET_LQOS.to_string();
let mut commands = Vec::new();
let error = prepare_with(
&config,
|_| unreachable!("MTU should not be read for invalid config"),
|command| {
commands.push(command.display());
Ok(())
},
)
.expect_err("reserved physical name should fail");
assert!(
error
.to_string()
.contains("cannot use LibreQoS shim device names")
);
assert!(commands.is_empty());
}
#[test]
fn mtu_read_failure_still_removes_stale_devices() {
let mut commands = Vec::new();
let error = prepare_with(
&enabled_config(),
|interface| {
Err(CompatibilityShimError::ReadMtu {
path: format!("/sys/class/net/{interface}/mtu").into(),
source: std::io::Error::other("simulated MTU read failure"),
})
},
|command| {
commands.push(command.display());
Ok(())
},
)
.expect_err("MTU read should fail");
assert!(error.to_string().contains("simulated MTU read failure"));
assert_eq!(
commands,
cleanup_commands()
.iter()
.map(CommandSpec::display)
.collect::<Vec<_>>()
);
}
}
+23 -2
View File
@@ -6,6 +6,7 @@
mod blackboard;
mod dynamic_circuits;
mod file_lock;
mod interface_shim;
mod ip_mapping;
#[cfg(feature = "equinix_tests")]
mod lqos_daht_test;
@@ -243,13 +244,23 @@ fn main() -> Result<()> {
license_cache_ready_tx,
);
// The compatibility shim must exist before interface tuning and preflight.
let mut compatibility_shim = Some(interface_shim::prepare(&config)?);
info!(
active = compatibility_shim
.as_ref()
.is_some_and(interface_shim::CompatibilityShimGuard::is_active),
"Interface compatibility shim state prepared"
);
// Apply Tunings
tuning::tune_lqosd_from_config_file()?;
// Spawn tracking sub-systems
let Ok(control_channel) = lts2_sys::control_channel::init_control_channel() else {
error!("Failed to initialize Insight control channel, exiting.");
std::process::exit(1);
return Err(anyhow::anyhow!(
"Failed to initialize the Insight control channel"
));
};
let control_tx_for_lts = control_channel.tx.clone();
let control_tx_for_web = control_channel.tx.clone();
@@ -371,6 +382,15 @@ fn main() -> Result<()> {
},
};
if kernels.is_none()
&& compatibility_shim
.as_ref()
.is_some_and(interface_shim::CompatibilityShimGuard::is_active)
{
warn!("Removing the compatibility shim because shaping did not start");
std::mem::drop(compatibility_shim.take());
}
let control_tx_for_webserver = control_tx_for_web.clone();
let system_usage_tx_for_web = system_usage_tx.clone();
let shaping_started = stormguard_bakery_sender.is_some();
@@ -492,6 +512,7 @@ fn main() -> Result<()> {
std::mem::drop(kernels);
// Give kernel/driver a moment to finalize detach
thread::sleep(Duration::from_millis(50));
std::mem::drop(compatibility_shim);
UnixSocketServer::signal_cleanup();
std::mem::drop(file_lock);
std::process::exit(0);
@@ -0,0 +1,112 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
bridgeEligibilityField,
normalizedBridgeFlags,
saveManualXdpConfiguration,
usesManualXdpWorkflow,
} from "./network_mode_shim.mjs";
test("compatibility shim selects relaxed bridge eligibility", () => {
assert.equal(bridgeEligibilityField("bridge", false), "bridge_eligible");
assert.equal(bridgeEligibilityField("bridge", true), "compatibility_shim_eligible");
assert.equal(bridgeEligibilityField("single", true), "single_interface_eligible");
});
test("compatibility shim always enables the XDP bridge", () => {
assert.deepEqual(normalizedBridgeFlags(false, true), {
use_xdp_bridge: true,
compatibility_shim: true,
});
assert.deepEqual(normalizedBridgeFlags(false, false), {
use_xdp_bridge: false,
compatibility_shim: false,
});
});
test("XDP and compatibility-shim configs use config-only saves", () => {
assert.equal(usesManualXdpWorkflow({ bridge: { use_xdp_bridge: true } }), true);
assert.equal(usesManualXdpWorkflow({ bridge: { use_xdp_bridge: false } }), false);
assert.equal(usesManualXdpWorkflow({ single_interface: {} }), false);
});
test("manual XDP save completes the page workflow and keeps the saved candidate", async () => {
const previousConfig = { bridge: { use_xdp_bridge: false } };
const candidate = { bridge: { use_xdp_bridge: true, compatibility_shim: true } };
const configState = { config: previousConfig };
let savedConfig = null;
const effects = [];
const result = await saveManualXdpConfiguration({
candidate,
configState,
persistConfig: (onSuccess) => {
savedConfig = configState.config;
effects.push("persist");
onSuccess({ ok: true });
},
clearDraft: () => effects.push("clear-draft"),
notifySaved: () => effects.push("notify"),
refresh: async () => effects.push("refresh"),
});
assert.equal(savedConfig, candidate);
assert.equal(configState.config, candidate);
assert.deepEqual(effects, ["persist", "clear-draft", "notify", "refresh"]);
assert.equal(result.refreshError, null);
});
test("manual XDP save restores config and skips success effects after failure", async () => {
const previousConfig = { bridge: { use_xdp_bridge: false } };
const candidate = { bridge: { use_xdp_bridge: true, compatibility_shim: true } };
const expectedError = new Error("save failed");
const configState = { config: previousConfig };
const effects = [];
await assert.rejects(
saveManualXdpConfiguration({
candidate,
configState,
persistConfig: (_onSuccess, onError) => {
assert.equal(configState.config, candidate);
effects.push("persist");
onError(expectedError);
},
clearDraft: () => effects.push("clear-draft"),
notifySaved: () => effects.push("notify"),
refresh: async () => effects.push("refresh"),
}),
expectedError,
);
assert.equal(configState.config, previousConfig);
assert.deepEqual(effects, ["persist"]);
});
test("manual XDP save keeps saved config when the status refresh fails", async () => {
const previousConfig = { bridge: { use_xdp_bridge: false } };
const candidate = { bridge: { use_xdp_bridge: true, compatibility_shim: true } };
const configState = { config: previousConfig };
const refreshError = new Error("refresh failed");
const effects = [];
const result = await saveManualXdpConfiguration({
candidate,
configState,
persistConfig: (onSuccess) => {
effects.push("persist");
onSuccess({ ok: true });
},
clearDraft: () => effects.push("clear-draft"),
notifySaved: () => effects.push("notify"),
refresh: async () => {
effects.push("refresh");
throw refreshError;
},
});
assert.equal(configState.config, candidate);
assert.equal(result.refreshError, refreshError);
assert.deepEqual(effects, ["persist", "clear-draft", "notify", "refresh"]);
});
@@ -0,0 +1,42 @@
export function bridgeEligibilityField(modeKey, compatibilityShimEnabled) {
if (modeKey === "single") return "single_interface_eligible";
return compatibilityShimEnabled ? "compatibility_shim_eligible" : "bridge_eligible";
}
export function normalizedBridgeFlags(useXdpBridge, compatibilityShim) {
const shimEnabled = Boolean(compatibilityShim);
return {
use_xdp_bridge: shimEnabled || Boolean(useXdpBridge),
compatibility_shim: shimEnabled,
};
}
export function usesManualXdpWorkflow(config) {
return Boolean(config?.bridge?.use_xdp_bridge);
}
export async function saveManualXdpConfiguration({
candidate,
configState,
persistConfig,
clearDraft,
notifySaved,
refresh,
}) {
const previousConfig = configState.config;
configState.config = candidate;
try {
await new Promise((resolve, reject) => persistConfig(resolve, reject));
} catch (error) {
configState.config = previousConfig;
throw error;
}
clearDraft();
notifySaved();
try {
await refresh();
return { refreshError: null };
} catch (refreshError) {
return { refreshError };
}
}
@@ -1,5 +1,11 @@
import {loadConfig, renderConfigMenu} from "./config/config_helper";
import {loadConfig, renderConfigMenu, saveConfig} from "./config/config_helper";
import {hydrateDraftMtu, MTU_MAX, MTU_MIN, parseOptionalMtu} from "./config/network_mode_mtu.mjs";
import {
bridgeEligibilityField,
normalizedBridgeFlags,
saveManualXdpConfiguration,
usesManualXdpWorkflow,
} from "./config/network_mode_shim.mjs";
import {
clearNetworkModeState,
DRAFT_KEY,
@@ -151,6 +157,20 @@ function updateBridgeMtuState() {
}
}
function compatibilityShimEnabled() {
return document.getElementById("compatibilityShim")?.checked ?? false;
}
function updateCompatibilityShimState() {
const useXdpBridge = document.getElementById("useXdpBridge");
if (!useXdpBridge) return;
const shimEnabled = compatibilityShimEnabled();
if (shimEnabled) {
useXdpBridge.checked = true;
}
useXdpBridge.disabled = shimEnabled || Boolean(currentInspection?.editing_locked);
}
function updateSingleInterfaceMtuState() {
const singleInterfaceMtu = document.getElementById("singleInterfaceMtu");
const singleInterfaceMtuHelp = document.getElementById("singleInterfaceMtuHelp");
@@ -167,6 +187,16 @@ function updateMtuState() {
updateSingleInterfaceMtuState();
}
function updateApplyButtonState() {
const applyButton = document.getElementById("applyButton");
if (!applyButton) return;
const manualXdpSave = usesManualXdpWorkflow(buildCandidateConfig());
applyButton.disabled = manualXdpSave
? Boolean(currentInspection?.editing_locked)
: !currentInspection?.can_apply;
applyButton.textContent = manualXdpSave ? "Save XDP Configuration" : "Apply Network Changes";
}
function validateConfig() {
clearMtuErrors();
if (document.getElementById("bridgeMode").checked) {
@@ -217,8 +247,12 @@ function buildCandidateConfig() {
next.single_interface = null;
if (document.getElementById("bridgeMode").checked) {
const bridgeFlags = normalizedBridgeFlags(
document.getElementById("useXdpBridge").checked,
compatibilityShimEnabled(),
);
next.bridge = {
use_xdp_bridge: document.getElementById("useXdpBridge").checked,
...bridgeFlags,
to_internet: document.getElementById("toInternet").value.trim(),
to_network: document.getElementById("toNetwork").value.trim(),
mtu: currentBridgeMtuValue(),
@@ -239,9 +273,9 @@ function interfaceCandidates() {
return Array.isArray(currentInspection?.interface_candidates) ? currentInspection.interface_candidates : [];
}
function optionLabel(candidate, selectedValue) {
function optionLabel(candidate, selectedValue, eligibilityField) {
if (!candidate) return selectedValue;
if (candidate.bridge_eligible || candidate.single_interface_eligible) {
if (candidate[eligibilityField]) {
return candidate.name;
}
if (candidate.current_selection || candidate.name === selectedValue) {
@@ -252,7 +286,7 @@ function optionLabel(candidate, selectedValue) {
function buildSelectOptions(selectElement, modeKey, selectedValue, excludedValue = null) {
if (!selectElement) return;
const eligibilityField = modeKey === "single" ? "single_interface_eligible" : "bridge_eligible";
const eligibilityField = bridgeEligibilityField(modeKey, compatibilityShimEnabled());
const candidates = interfaceCandidates();
const options = [`<option value="">Select an eligible interface</option>`];
const seen = new Set();
@@ -263,7 +297,7 @@ function buildSelectOptions(selectElement, modeKey, selectedValue, excludedValue
if (!eligible && !selected) return;
if (excludedValue && candidate.name === excludedValue && !selected) return;
options.push(
`<option value="${escapeHtml(candidate.name)}">${escapeHtml(optionLabel(candidate, selectedValue))}</option>`
`<option value="${escapeHtml(candidate.name)}">${escapeHtml(optionLabel(candidate, selectedValue, eligibilityField))}</option>`
);
seen.add(candidate.name);
});
@@ -282,7 +316,7 @@ function renderInterfaceHelp(helpElementId, modeKey, selectedValues = {}) {
const element = document.getElementById(helpElementId);
if (!element) return;
const eligibilityField = modeKey === "single" ? "single_interface_eligible" : "bridge_eligible";
const eligibilityField = bridgeEligibilityField(modeKey, compatibilityShimEnabled());
const selectedSet = new Set(
Object.values(selectedValues)
.map((value) => String(value || "").trim())
@@ -348,10 +382,10 @@ function renderInterfaceSelectors(config = null) {
}
function populateFormFromConfig(config) {
renderInterfaceSelectors(config);
if (config?.bridge) {
document.getElementById("bridgeMode").checked = true;
document.getElementById("useXdpBridge").checked = config.bridge.use_xdp_bridge ?? true;
document.getElementById("compatibilityShim").checked = config.bridge.compatibility_shim ?? false;
document.getElementById("toInternet").value = config.bridge.to_internet ?? "";
document.getElementById("toNetwork").value = config.bridge.to_network ?? "";
document.getElementById("bridgeMtu").value = config.bridge.mtu ?? "";
@@ -363,9 +397,12 @@ function populateFormFromConfig(config) {
document.getElementById("singleInterfaceMtu").value = config.single_interface.mtu ?? "";
}
updateCompatibilityShimState();
renderInterfaceSelectors(config);
const event = new Event("change");
document.querySelector('input[name="networkMode"]:checked')?.dispatchEvent(event);
updateMtuState();
updateApplyButtonState();
}
function loadDraft() {
@@ -686,6 +723,7 @@ function renderInspection(inspection) {
"bridgeMode",
"singleInterfaceMode",
"useXdpBridge",
"compatibilityShim",
"toInternet",
"toNetwork",
"bridgeMtu",
@@ -700,10 +738,11 @@ function renderInspection(inspection) {
element.disabled = Boolean(inspection?.editing_locked);
}
});
applyButton.disabled = !inspection?.can_apply;
adoptButton.disabled = !inspection?.can_adopt;
takeoverButton.disabled = !inspection?.can_take_over;
updateCompatibilityShimState();
updateMtuState();
updateApplyButtonState();
}
function renderHelperStatus(status) {
@@ -810,6 +849,39 @@ function confirmDangerousChange(actionLabel, candidate) {
function applyNetworkChanges(mode = "Apply") {
if (!validateConfig()) return;
const candidate = buildCandidateConfig();
if (usesManualXdpWorkflow(candidate)) {
if (mode !== "Apply") return;
const confirmed = window.confirm(
"Save this XDP configuration to lqos.conf? LibreQoS will not change Netplan. Restart lqosd to rebuild the shaping path with these settings."
);
if (!confirmed) return;
const button = document.getElementById("applyButton");
if (button) {
button.disabled = true;
button.textContent = "Saving...";
}
saveManualXdpConfiguration({
candidate,
configState: window,
persistConfig: saveConfig,
clearDraft: () => clearNetworkModeState(DRAFT_KEY),
notifySaved: () => {
alert("XDP configuration saved. Netplan was not changed. Restart lqosd to apply the interface configuration.");
},
refresh: () => {
updateApplyButtonState();
return inspectCandidate();
},
}).then(({ refreshError }) => {
if (refreshError) {
alert("XDP configuration was saved, but the current network status could not be refreshed.");
}
}).catch((err) => {
updateApplyButtonState();
alert(err?.message || "Unable to save XDP configuration");
});
return;
}
const actionLabel = mode === "Adopt" ? "Adopt into libreqos.yaml" : mode === "TakeOver" ? "Take Over libreqos.yaml" : "Apply Network Changes";
if (!confirmDangerousChange(actionLabel, candidate)) {
return;
@@ -934,9 +1006,19 @@ function wireActions() {
].forEach((id) => {
document.getElementById(id).addEventListener("change", () => {
renderInterfaceSelectors();
updateApplyButtonState();
});
});
document.getElementById("useXdpBridge").addEventListener("change", updateMtuState);
document.getElementById("useXdpBridge").addEventListener("change", () => {
updateMtuState();
updateApplyButtonState();
});
document.getElementById("compatibilityShim").addEventListener("change", () => {
updateCompatibilityShimState();
updateMtuState();
renderInterfaceSelectors();
updateApplyButtonState();
});
[
"bridgeMtu",
"singleInterfaceMtu",
@@ -953,9 +1035,13 @@ function wireActions() {
});
document.getElementById("saveButton").addEventListener("click", () => {
if (!validateConfig()) return;
saveDraft();
const draft = saveDraft();
inspectCandidate();
alert("Network mode draft saved for this browser tab. Use Apply Network Changes to commit both lqos.conf and netplan together.");
if (usesManualXdpWorkflow(draft)) {
alert("XDP draft saved for this browser tab. Use Save XDP Configuration to update lqos.conf; Netplan remains unchanged.");
} else {
alert("Network mode draft saved for this browser tab. Use Apply Network Changes to commit both lqos.conf and netplan together.");
}
});
document.getElementById("inspectButton").addEventListener("click", inspectCandidate);
document.getElementById("applyButton").addEventListener("click", () => applyNetworkChanges("Apply"));
@@ -20,6 +20,7 @@ node --test \
"${SCRIPT_DIR}/src/config/shaped_device_wire.test.mjs" \
"${SCRIPT_DIR}/src/config_radius_accounting_contract.test.mjs" \
"${SCRIPT_DIR}/src/config/config_interface_mtu.test.mjs" \
"${SCRIPT_DIR}/src/config/config_interface_shim.test.mjs" \
"${SCRIPT_DIR}/src/config/local_api_token.test.mjs" \
"${SCRIPT_DIR}/src/config/network_mode_storage.test.mjs" \
"${SCRIPT_DIR}/src/config/ssl_redirect.test.mjs" \
@@ -55,7 +55,7 @@
<section class="lqos-config-panel">
<div class="lqos-config-section">
<h6 class="lqos-config-section-title">Bridge Mode Configuration</h6>
<div class="lqos-config-section-subtitle">Choose the two shaping interfaces. Linux bridge mode can manage Netplan; XDP mode leaves Netplan alone.</div>
<div class="lqos-config-section-subtitle">Choose the two shaping interfaces. Linux bridge mode can manage Netplan; XDP and its interface compatibility shim leave Netplan alone.</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="useXdpBridge" aria-describedby="useXdpBridgeHelp">
@@ -63,12 +63,18 @@
<div id="useXdpBridgeHelp" class="form-text">XDP does not write Netplan or apply Bridge MTU.</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="compatibilityShim" aria-describedby="compatibilityShimHelp">
<label class="form-check-label" for="compatibilityShim">Interface Compatibility Shim (veth)</label>
<div id="compatibilityShimHelp" class="form-text">Use this only for bonded interfaces or NIC drivers that cannot host LibreQoS XDP directly. LibreQoS inserts two multiqueue veth pairs and two small Linux bridges, which adds CPU overhead.</div>
</div>
<div class="mb-3">
<label for="toInternet" class="form-label">Internet-Facing Interface</label>
<select class="form-select" id="toInternet">
<option value="">Select an eligible interface</option>
</select>
<div class="form-text">Eligible non-management interfaces with LibreQoS-friendly queue support.</div>
<div class="form-text">Eligible non-management interface for the selected direct or compatibility-shim path.</div>
</div>
<div class="mb-2">
+62 -2
View File
@@ -4,6 +4,24 @@ use lqos_bus::{BusRequest, BusResponse};
use lqos_config::{Config, Tunables};
use lqos_queue_tracker::set_queue_refresh_interval;
#[derive(Debug, PartialEq, Eq)]
struct InterfaceTuningPlan {
full: [String; 2],
coalescing_only: Option<[String; 2]>,
}
fn interface_tuning_plan(config: &Config) -> InterfaceTuningPlan {
let coalescing_only = config
.bridge
.as_ref()
.filter(|bridge| bridge.compatibility_shim_enabled())
.map(|bridge| [bridge.to_internet.clone(), bridge.to_network.clone()]);
InterfaceTuningPlan {
full: [config.internet_interface(), config.isp_interface()],
coalescing_only,
}
}
fn apply_non_interface_tuning(tuning: &Tunables) {
offloads::bpf_sysctls();
if tuning.set_cpu_governor_performance {
@@ -16,8 +34,17 @@ fn apply_non_interface_tuning(tuning: &Tunables) {
}
fn apply_interface_tuning(config: &Config, tuning: &Tunables) {
offloads::ethtool_tweaks(&config.internet_interface(), tuning);
offloads::ethtool_tweaks(&config.isp_interface(), tuning);
let plan = interface_tuning_plan(config);
for interface in &plan.full {
offloads::ethtool_tweaks(interface, tuning);
}
if let Some(physical_interfaces) = plan.coalescing_only {
// Keep checksum, segmentation, and VLAN offloads enabled on the
// physical path, but retain the configured interrupt-coalescing tune.
for interface in &physical_interfaces {
offloads::ethtool_coalescing_tweaks(interface, tuning);
}
}
}
pub fn tune_lqosd_from_config_file() -> Result<()> {
@@ -41,3 +68,36 @@ pub fn tune_lqosd_from_bus(request: &BusRequest) -> BusResponse {
_ => BusResponse::Fail("That wasn't a tuning request".to_string()),
}
}
#[cfg(test)]
mod tests {
use super::interface_tuning_plan;
use lqos_config::{BridgeConfig, Config, SHIM_INTERNET_LQOS, SHIM_NETWORK_LQOS};
fn bridge_config(compatibility_shim: bool) -> Config {
Config {
bridge: Some(BridgeConfig {
use_xdp_bridge: true,
to_internet: "bond0".to_string(),
to_network: "enp2s0".to_string(),
compatibility_shim,
..BridgeConfig::default()
}),
..Config::default()
}
}
#[test]
fn compatibility_shim_keeps_full_offload_tuning_on_veths() {
let direct = interface_tuning_plan(&bridge_config(false));
assert_eq!(direct.full, ["bond0", "enp2s0"]);
assert_eq!(direct.coalescing_only, None);
let shim = interface_tuning_plan(&bridge_config(true));
assert_eq!(shim.full, [SHIM_INTERNET_LQOS, SHIM_NETWORK_LQOS]);
assert_eq!(
shim.coalescing_only,
Some(["bond0".to_string(), "enp2s0".to_string()])
);
}
}
+19 -12
View File
@@ -78,6 +78,25 @@ pub fn ethtool_tweaks(interface: &str, tuning: &Tunables) {
disable_individual_offload(interface, feature);
}
ethtool_coalescing_tweaks(interface, tuning);
if tuning.disable_rxvlan {
let _ = Command::new("/sbin/ethtool")
.args(["-K", interface, "rxvlan", "off"])
.output();
}
if tuning.disable_txvlan {
let _ = Command::new("/sbin/ethtool")
.args(["-K", interface, "txvlan", "off"])
.output();
}
}
/// Applies interrupt-coalescing settings without changing interface offloads.
///
/// This function invokes `ethtool` and changes host interface state.
pub(super) fn ethtool_coalescing_tweaks(interface: &str, tuning: &Tunables) {
let _ = Command::new("/sbin/ethtool")
.args([
"-C",
@@ -95,16 +114,4 @@ pub fn ethtool_tweaks(interface: &str, tuning: &Tunables) {
&format!("\"{}\"", tuning.tx_usecs),
])
.output();
if tuning.disable_rxvlan {
let _ = Command::new("/sbin/ethtool")
.args(["-K", interface, "rxvlan", "off"])
.output();
}
if tuning.disable_txvlan {
let _ = Command::new("/sbin/ethtool")
.args(["-K", interface, "txvlan", "off"])
.output();
}
}