Merge pull request #1141 from LibreQoE/revert-818-meatball-sub

Revert "Add XDP interface compatibility shim"
This commit is contained in:
Robert Chacón
2026-08-11 07:53:16 -06:00
committed by GitHub
29 changed files with 218 additions and 1762 deletions
+3 -24
View File
@@ -2,22 +2,21 @@
## Elegir el Tipo de Puente
LibreQoS admite estas rutas entre las dos interfaces de regulación:
Hay dos opciones para que el puente pase datos entre las dos interfaces:
- Opción A: Puente Regular de Linux (Recomendado)
- Opción B: Puente Bifrost Acelerado con XDP
- Opción C: Capa de compatibilidad para interfaces con bonding y controladores que no pueden alojar XDP
El puente regular de Linux es recomendado para la mayoría de las instalaciones. El puente de Linux continúa transfiriendo datos incluso si el servicio lqosd entra en un estado fallido, lo que lo hace una opción generalmente más segura para escenarios donde no hay una ruta de respaldo disponible. Funciona mejor con tarjetas de red Nvidia/Mellanox como las de la serie ConnectX-5 (que ofrecen un rendimiento superior en puentes) y configuraciones de máquinas virtuales con NICs virtualizadas. El puente Bifrost con XDP está recomendado para tarjetas de red Intel de 40G100G que soportan XDP.
Los siguientes ejemplos de Netplan abarcan el puente normal de Linux y la ruta directa de Bifrost con XDP. La capa de compatibilidad se describe por separado más adelante.
A continuación, se encuentran las instrucciones para configurar Netplan, ya sea usando el puente de Linux o el puente Bifrost con XDP:
```{note}
La página Network Mode en la interfaz web de LibreQoS ahora inspecciona los archivos actuales de Netplan, ofrece en menús desplegables las interfaces elegibles que no forman parte de la ruta de gestión, prepara cambios administrados para `libreqos.yaml` en los modos puente de Linux y de interfaz única, los aplica con una ventana temporizada de reversión manejada por LibreQoS y le permite confirmar o revertir el cambio pendiente. También puede restaurar desde esa página la copia de seguridad administrada anterior. El modo puente XDP sigue siendo un flujo manual de Netplan.
```
```{note}
La configuración inicial ofrece Puente Linux, Capa de compatibilidad de interfaces e Interfaz Única. Use la capa solo para interfaces con bonding o controladores que no puedan alojar XDP de LibreQoS directamente. Si LibreQoS detecta un despliegue existente con XDP directo, también conserva ese modo heredado y le avisa antes de migrar fuera de él.
La configuración inicial actual solo ofrece Puente Linux e Interfaz Única para instalaciones nuevas. Si LibreQoS detecta un despliegue XDP existente, la configuración preserva ese modo XDP heredado y le avisa antes de migrar fuera de él.
```
```{note}
@@ -100,23 +99,3 @@ sudo netplan apply
```
Para usar el puente XDP, asegurese de establecer `use_xdp_bridge` como `true` en el archivo lqos.conf dentro de la sección [Configuración](configuration-es.md).
## Capa de compatibilidad de interfaces
Use la capa de compatibilidad solo cuando LibreQoS no pueda adjuntar XDP directamente a las interfaces seleccionadas. Los casos más comunes son las interfaces enlazadas mediante bonding y los controladores de NIC que no ofrecen la compatibilidad con XDP que LibreQoS necesita. Si funciona XDP directo o el puente normal de Linux, esa opción sigue siendo preferible.
La capa conecta cada interfaz física a una interfaz veth multiqueue mediante un pequeño puente de Linux. LibreQoS adjunta a las interfaces veth su ruta existente de XDP y colas. Esto consume más CPU, pero mantiene activas en las interfaces físicas las descargas de sumas de comprobación, segmentación y VLAN. LibreQoS sigue aplicando los valores configurados de moderación de interrupciones cuando el controlador físico los acepta.
Seleccione `Interface Compatibility Shim` durante la configuración inicial, actívela en la página `Bridge & Interface Mode`, o configure:
```toml
[bridge]
use_xdp_bridge = true
compatibility_shim = true
to_internet = "bond0"
to_network = "bond1"
```
Reinicie `lqosd` después de cambiar esta opción. LibreQoS elige la cantidad de colas veth según las CPU activas para shaping y cualquier override de colas configurado. Para la ruta de la capa usa el menor MTU de las dos interfaces físicas.
La capa de compatibilidad no añade a las interfaces físicas un límite de velocidad, un limitador HTB ni `fq_codel`. Configure el shaping de suscriptores mediante los ajustes normales de colas de LibreQoS.
@@ -37,8 +37,6 @@ En la sección ```[bridge]```, cambie `to_internet` y `to_network` para que coin
En la sección `[bridge]` del archivo lqos.conf, puede habilitar o deshabilitar el puente XDP con la opción `use_xdp_bridge`. El valor predeterminado es `false` - ya que la configuración por defecto asume un [Puente Linux](prereq-es.md). Si eligió usar el puente XDP durante la configuración de requisitos previos, establezca `use_xdp_bridge = true`.
Si una interfaz enlazada mediante bonding o el controlador de una NIC no permite adjuntar XDP de LibreQoS directamente, establezca `compatibility_shim = true` en la misma sección `[bridge]`. La capa requiere `use_xdp_bridge = true`. Proporciona interfaces veth multiqueue a LibreQoS sin añadir límites de velocidad ni qdiscs a las interfaces físicas. Consulte [Capa de compatibilidad de interfaces](bridge-es.md#capa-de-compatibilidad-de-interfaces).
- Configure downlink_bandwidth_mbps y uplink_bandwidth_mbps para que coincidan con el ancho de banda en Mbps de la conexión WAN/Upstream de su red. Lo mismo puede hacerse para generated_pn_download_mbps y generated_pn_upload_mbps.
- to_internet es la interfaz que apunta hacia su router de borde (edge router) y el internet.
- to_network es la interfaz que apunta hacia su router interno (core router) (o la red interna puenteada, si su red está configurada de esa manera).
+3 -24
View File
@@ -2,22 +2,21 @@
## Choose Bridge Type
LibreQoS supports these paths between the two shaping interfaces:
There are two options for the bridge to pass data through your two interfaces:
- Option A: Regular Linux Bridge (Recommended)
- Option B: Bifrost XDP-Accelerated Bridge
- Option C: Interface Compatibility Shim for bonds and drivers that cannot host XDP
The regular Linux bridge is recommended for most installations. The Linux Bridge continues to move data even if the lqosd service is in a failed state, making this a generally safer option in scenarios where a backup route is not in place. It works best with Nvidia/Mellanox NICs such as the ConnectX-5 series (which have superior bridge performance), and VM setups using virtualized NICs. The Bifrost XDP Bridge is recommended for 40G-100G Intel NICs with XDP support.
The Netplan examples below cover the regular Linux bridge and direct Bifrost XDP paths. The compatibility shim is described separately afterward.
Below are the instructions to configure Netplan, whether using the Linux Bridge or Bifrost XDP bridge:
```{note}
The Network Mode page in the LibreQoS web UI now inspects the current Netplan files, offers eligible non-management interfaces in dropdowns, stages managed `libreqos.yaml` changes for Linux bridge and single-interface modes, applies them with a timed LibreQoS rollback window, and lets you confirm or revert the pending change. You can also restore the previous managed backup from that page. XDP bridge mode remains a manual Netplan workflow.
```
```{note}
First-run setup offers Linux Bridge, Interface Compatibility Shim, and Single Interface. Use the shim only for bonds or drivers that cannot host LibreQoS XDP directly. If LibreQoS detects an existing direct-XDP deployment, setup also preserves that legacy mode and warns before you migrate away from it.
Current first-run setup only offers Linux Bridge and Single Interface for new installs. If LibreQoS detects an existing XDP deployment, setup preserves that legacy XDP mode and warns before you migrate away from it.
```
```{note}
@@ -100,23 +99,3 @@ sudo netplan apply
```
To use the XDP bridge, please be sure to set `use_xdp_bridge` to `true` in lqos.conf in the [Configuration](configuration.md) section.
## Interface Compatibility Shim
Use the interface compatibility shim only when LibreQoS cannot attach XDP directly to the selected interfaces. Common examples are bonded interfaces and NIC drivers without the XDP support LibreQoS requires. Direct XDP or the regular Linux bridge remains preferable when either works.
The shim connects each physical interface to a multiqueue veth through a small Linux bridge. LibreQoS attaches its existing XDP and queueing path to the veth interfaces. This adds CPU overhead, but leaves checksum, segmentation, and VLAN offloads enabled on the physical interfaces. LibreQoS still applies the configured interrupt-coalescing values where the physical driver accepts them.
Choose `Interface Compatibility Shim` during first-run setup, enable it on the `Bridge & Interface Mode` page, or set:
```toml
[bridge]
use_xdp_bridge = true
compatibility_shim = true
to_internet = "bond0"
to_network = "bond1"
```
Restart `lqosd` after changing this setting. LibreQoS chooses the veth queue count from the active shaping CPU count and any configured queue override. It uses the smaller physical-interface MTU for the shim path.
The compatibility shim does not add a link-speed cap, HTB limiter, or `fq_codel` to the physical interfaces. Configure subscriber shaping through the normal LibreQoS queue settings.
-2
View File
@@ -39,8 +39,6 @@ In the ```[bridge]``` section, change `to_internet` and `to_network` to match yo
In the `[bridge]` section of the lqos.conf file, you can enable or disable the XDP Bridge with the setting `use_xdp_bridge`. The default value is `false` - because the default setup assumes a [Linux Bridge](prereq.md). If you chose to use the XDP Bridge during that pre-requisites setup, please set `use_xdp_bridge = true` instead.
If a bonded interface or NIC driver cannot host LibreQoS XDP directly, set `compatibility_shim = true` in the same `[bridge]` section. The shim requires `use_xdp_bridge = true`. It supplies multiqueue veth interfaces to LibreQoS without adding rate limits or qdiscs to the physical interfaces. See [Interface Compatibility Shim](bridge.md#interface-compatibility-shim).
- Set downlink_bandwidth_mbps and uplink_bandwidth_mbps to match the bandwidth in Mbps of your network's upstream / WAN internet connection. The same can be done for generated_pn_download_mbps and generated_pn_upload_mbps.
- to_internet would be the interface facing your edge router and the broader internet
- to_network would be the interface facing your core router (or bridged internal network if your network is bridged)
-3
View File
@@ -11987,7 +11987,6 @@ mod tests {
to_internet: "internet0".to_string(),
to_network: "isp0".to_string(),
mtu: None,
compatibility_shim: false,
}),
..Config::default()
});
@@ -12266,7 +12265,6 @@ mod tests {
to_internet: "lo".to_string(),
to_network: "lo".to_string(),
mtu: None,
compatibility_shim: false,
}),
..lqos_config::Config::default()
};
@@ -14122,7 +14120,6 @@ mod tests {
to_internet: "__bakery-missing-wan__".to_string(),
to_network: "__bakery-missing-lan__".to_string(),
mtu: None,
compatibility_shim: false,
}),
..Config::default()
};
@@ -195,7 +195,6 @@ fn migrate_bridge(
to_internet: python_config.interface_b.clone(),
to_network: python_config.interface_a.clone(),
mtu: None,
compatibility_shim: false,
});
}
Ok(())
@@ -20,15 +20,6 @@ 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 {
@@ -38,26 +29,10 @@ 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 {
+2 -43
View File
@@ -301,9 +301,6 @@ 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)?;
@@ -628,11 +625,7 @@ impl Config {
/// Calculate the unterface facing the Internet
pub fn internet_interface(&self) -> String {
if let Some(bridge) = &self.bridge {
if bridge.compatibility_shim_enabled() {
crate::SHIM_INTERNET_LQOS.to_string()
} else {
bridge.to_internet.clone()
}
bridge.to_internet.clone()
} else if let Some(single_interface) = &self.single_interface {
single_interface.interface.clone()
} else {
@@ -643,11 +636,7 @@ impl Config {
/// Calculate the interface facing the ISP
pub fn isp_interface(&self) -> String {
if let Some(bridge) = &self.bridge {
if bridge.compatibility_shim_enabled() {
crate::SHIM_NETWORK_LQOS.to_string()
} else {
bridge.to_network.clone()
}
bridge.to_network.clone()
} else if let Some(single_interface) = &self.single_interface {
single_interface.interface.clone()
} else {
@@ -716,36 +705,6 @@ 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,21 +157,3 @@ 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";
+21 -130
View File
@@ -58,9 +58,6 @@ 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,
@@ -499,31 +496,27 @@ 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 is_plain_bond_interface(iface)
|| [
"br",
"docker",
"veth",
"virbr",
"ifb",
"wg",
"tun",
"tap",
"tailscale",
"zt",
"vmnet",
]
.iter()
.any(|prefix| iface.starts_with(prefix))
} else if [
"br",
"bond",
"docker",
"veth",
"virbr",
"ifb",
"wg",
"tun",
"tap",
"tailscale",
"zt",
"vmnet",
]
.iter()
.any(|prefix| iface.starts_with(prefix))
{
Some("Virtual, bridge, or tunnel interface.")
} else {
@@ -544,34 +537,26 @@ fn build_interface_candidates(
.map(|iface| {
let mut details = Vec::new();
let role_reason = interface_name_role_reason(iface);
if let Some(reason) = role_reason {
if let Some(reason) = interface_name_role_reason(iface) {
details.push(reason.to_string());
}
let is_management_interface =
default_interface_name.is_some_and(|default_iface| default_iface == iface);
if is_management_interface {
if default_interface_name.is_some_and(|default_iface| default_iface == iface) {
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());
}
let restrictions = interface_restrictions.get(iface);
if let Some(extra) = restrictions {
if let Some(extra) = interface_restrictions.get(iface) {
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),
}
@@ -877,10 +862,6 @@ 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();
@@ -906,13 +887,9 @@ 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) && !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."
"Interface {iface} does not appear to have multi-queue RX/TX support."
));
}
}
@@ -1151,9 +1128,7 @@ pub fn inspect_network_mode(config: &Config) -> NetworkModeInspection {
#[cfg(test)]
mod tests {
use super::{
adoption_rewrite_for_path, build_interface_candidates, inspect_network_mode_with_paths,
};
use super::{adoption_rewrite_for_path, inspect_network_mode_with_paths};
use std::collections::{BTreeMap, BTreeSet};
fn linux_bridge_config() -> lqos_config::Config {
@@ -1163,7 +1138,6 @@ mod tests {
to_internet: "ens19".to_string(),
to_network: "ens20".to_string(),
mtu: None,
compatibility_shim: false,
}),
single_interface: None,
..lqos_config::Config::default()
@@ -1292,89 +1266,6 @@ 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,7 +1061,6 @@ esac
to_internet,
to_network,
mtu: None,
compatibility_shim: false,
}),
single_interface: None,
..Config::default()
+1 -1
View File
@@ -10,7 +10,7 @@ The LibreQoS setup tool guides users through the essential configuration steps r
The tool assists in configuring the following components:
- **Bridge Mode**: Select Linux Bridge, Interface Compatibility Shim, or Single Interface. Existing direct-XDP installs can preserve their legacy mode.
- **Bridge Mode**: Select between Linux, XDP, or Single bridge modes to match your deployment scenario.
- **Network Interfaces**: Choose and configure the network interfaces that LibreQoS will manage.
- **Bandwidth Settings**: Set bandwidth limits and parameters for your network environment.
- **IP Ranges**: Define the IP address ranges that LibreQoS should monitor and shape.
+2 -10
View File
@@ -4,11 +4,11 @@ use cursive::{
views::{Dialog, LinearLayout, RadioGroup, TextView},
};
use crate::config_builder::{BridgeMode, CURRENT_CONFIG, existing_config_uses_direct_xdp};
use crate::config_builder::{BridgeMode, CURRENT_CONFIG, existing_config_uses_xdp};
pub fn bridge_mode(s: &mut Cursive) {
let current_mode = CURRENT_CONFIG.lock().bridge_mode;
let show_legacy_xdp = existing_config_uses_direct_xdp() || current_mode == BridgeMode::XDP;
let show_legacy_xdp = existing_config_uses_xdp() || current_mode == BridgeMode::XDP;
// create the group and buttons
let mut group = RadioGroup::new().on_change(|_s, mode| {
@@ -26,10 +26,6 @@ pub fn bridge_mode(s: &mut Cursive) {
"Legacy XDP Bridge (existing installs only - keep only if you already run it)",
)
});
let mut shim_btn = group.button(
BridgeMode::CompatibilityShim,
"Interface Compatibility Shim (2 interfaces) - for bonds and drivers that cannot host XDP",
);
let mut single_btn = group.button(BridgeMode::Single, "Single Interface (1 interface)");
// mark the one we want as selected
@@ -42,9 +38,6 @@ pub fn bridge_mode(s: &mut Cursive) {
button.select();
}
}
BridgeMode::CompatibilityShim => {
shim_btn.select();
}
_ => {
linux_btn.select();
}
@@ -60,7 +53,6 @@ pub fn bridge_mode(s: &mut Cursive) {
));
layout.add_child(button);
}
layout.add_child(shim_btn);
layout.add_child(single_btn);
s.add_layer(
+4 -77
View File
@@ -12,16 +12,9 @@ pub static CURRENT_CONFIG: Lazy<Mutex<ConfigBuilder>> =
pub enum BridgeMode {
Linux,
XDP,
CompatibilityShim,
Single,
}
impl BridgeMode {
pub(crate) const fn uses_netplan_helper(self) -> bool {
matches!(self, Self::Linux | Self::Single)
}
}
#[derive(Clone, Debug)]
pub struct ConfigBuilder {
pub bridge_mode: BridgeMode,
@@ -45,11 +38,7 @@ impl ConfigBuilder {
let mut internet_vlan = 0;
let mut network_vlan = 0;
let mode = if let Some(bridge) = &cfg.bridge {
if bridge.compatibility_shim_enabled() {
to_internet = bridge.to_internet.clone();
to_network = bridge.to_network.clone();
BridgeMode::CompatibilityShim
} else if bridge.use_xdp_bridge {
if bridge.use_xdp_bridge {
to_internet = bridge.to_internet.clone();
to_network = bridge.to_network.clone();
BridgeMode::XDP
@@ -121,21 +110,16 @@ pub fn existing_config_load_error() -> Option<String> {
existing_config_load_error_for_path(&current_config_path())
}
pub fn existing_config_uses_direct_xdp() -> bool {
pub fn existing_config_uses_xdp() -> bool {
lqos_config::load_config()
.ok()
.and_then(|config| {
config
.bridge
.as_ref()
.map(|bridge| bridge.use_xdp_bridge && !bridge.compatibility_shim_enabled())
})
.and_then(|config| config.bridge.as_ref().map(|bridge| bridge.use_xdp_bridge))
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::{BridgeMode, ConfigBuilder, existing_config_uses_direct_xdp};
use super::ConfigBuilder;
use crate::test_support::ConfigEnvGuard;
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};
@@ -159,61 +143,4 @@ mod tests {
fs::remove_file(path).expect("remove temp config");
}
#[test]
fn compatibility_shim_config_loads_as_its_own_setup_mode() {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock before epoch")
.as_nanos();
let root = std::env::temp_dir().join(format!(
"libreqos-setup-shim-config-{}-{unique}",
std::process::id()
));
fs::create_dir_all(&root).expect("create shim config directory");
let path = root.join("lqos.conf");
let _env_guard = ConfigEnvGuard::set_lqos_config(&path);
let mut config = lqos_config::Config {
lqos_directory: root.display().to_string(),
state_directory: Some(root.join("state").display().to_string()),
bridge: Some(lqos_config::BridgeConfig {
use_xdp_bridge: true,
compatibility_shim: true,
to_internet: "bond0".to_string(),
to_network: "bond1".to_string(),
..lqos_config::BridgeConfig::default()
}),
..lqos_config::Config::default()
};
let serialized = toml::to_string_pretty(&config).expect("serialize shim config");
fs::write(&path, serialized).expect("write shim config");
lqos_config::clear_cached_config();
let builder = ConfigBuilder::new();
assert_eq!(builder.bridge_mode, BridgeMode::CompatibilityShim);
assert_eq!(builder.to_internet, "bond0");
assert_eq!(builder.to_network, "bond1");
assert!(!existing_config_uses_direct_xdp());
config
.bridge
.as_mut()
.expect("bridge config")
.compatibility_shim = false;
let serialized = toml::to_string_pretty(&config).expect("serialize direct XDP config");
fs::write(&path, serialized).expect("write direct XDP config");
lqos_config::clear_cached_config();
assert!(existing_config_uses_direct_xdp());
fs::remove_dir_all(root).expect("remove temp config directory");
}
#[test]
fn compatibility_shim_updates_config_without_managing_netplan() {
assert!(!BridgeMode::CompatibilityShim.uses_netplan_helper());
assert!(!BridgeMode::XDP.uses_netplan_helper());
assert!(BridgeMode::Linux.uses_netplan_helper());
assert!(BridgeMode::Single.uses_netplan_helper());
}
}
+65 -136
View File
@@ -9,98 +9,73 @@ use cursive::{
use crate::config_builder::{BridgeMode, CURRENT_CONFIG};
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct InterfaceOption {
pub(crate) name: String,
pub(crate) label: String,
pub(crate) bridge_eligible: bool,
pub(crate) compatibility_shim_eligible: bool,
pub(crate) single_interface_eligible: bool,
pub struct InterfaceOption {
pub name: String,
pub label: String,
}
impl InterfaceOption {
pub(crate) fn is_eligible_for(&self, mode: BridgeMode) -> bool {
match mode {
BridgeMode::Linux | BridgeMode::XDP => self.bridge_eligible,
BridgeMode::CompatibilityShim => self.compatibility_shim_eligible,
BridgeMode::Single => self.single_interface_eligible,
pub fn get_interfaces() -> anyhow::Result<Vec<String>> {
Ok(get_interface_options()?
.into_iter()
.map(|iface| iface.name)
.collect())
}
pub fn get_interface_options() -> anyhow::Result<Vec<InterfaceOption>> {
let mut interfaces = nix::ifaddrs::getifaddrs()?
.filter(|iface| interface_supports_lqos(&iface.interface_name).is_ok())
.map(|iface| iface.interface_name)
.collect::<Vec<_>>();
interfaces.sort();
interfaces.dedup();
Ok(interfaces
.into_iter()
.map(|name| InterfaceOption {
label: interface_label(&name),
name,
})
.collect())
}
pub(crate) fn interface_supports_lqos(interface: &str) -> anyhow::Result<()> {
let path = format!("/sys/class/net/{interface}/queues/");
let sys_path = Path::new(&path);
if !sys_path.exists() {
return Err(anyhow::anyhow!(
"/sys/class/net/{interface}/queues/ does not exist. Does this card only support one queue (not supported)?"
));
}
let mut counts = (0, 0);
let paths = std::fs::read_dir(sys_path)?;
for path in paths {
if let Ok(path) = &path
&& path.path().is_dir()
&& let Some(filename) = path.path().file_name()
&& let Some(filename) = filename.to_str()
{
if filename.starts_with("rx-") {
counts.0 += 1;
} else if filename.starts_with("tx-") {
counts.1 += 1;
}
}
}
}
pub(crate) fn get_interface_options() -> Vec<InterfaceOption> {
let inspection = lqos_netplan_helper::inspect_network_mode(&lqos_config::Config::default());
inspection
.interface_candidates
.into_iter()
.filter(|candidate| {
candidate.bridge_eligible
|| candidate.compatibility_shim_eligible
|| candidate.single_interface_eligible
})
.map(|candidate| InterfaceOption {
label: interface_label(&candidate.name),
name: candidate.name,
bridge_eligible: candidate.bridge_eligible,
compatibility_shim_eligible: candidate.compatibility_shim_eligible,
single_interface_eligible: candidate.single_interface_eligible,
})
.collect()
}
if counts.0 == 0 || counts.1 == 0 {
return Err(anyhow::anyhow!(
"Interface {} does not have both RX and TX queues.",
interface
));
}
if counts.0 == 1 || counts.1 == 1 {
return Err(anyhow::anyhow!(
"Interface {} only has one RX or TX queue. This is not supported.",
interface
));
}
fn eligible_interface_options(mode: BridgeMode) -> Vec<InterfaceOption> {
get_interface_options()
.into_iter()
.filter(|interface| interface.is_eligible_for(mode))
.collect()
}
pub(crate) fn validate_mode_interfaces(
mode: BridgeMode,
to_internet: &str,
to_network: &str,
) -> anyhow::Result<()> {
validate_mode_interfaces_with_options(&get_interface_options(), mode, to_internet, to_network)
}
pub(crate) fn validate_mode_interfaces_with_options(
options: &[InterfaceOption],
mode: BridgeMode,
to_internet: &str,
to_network: &str,
) -> anyhow::Result<()> {
if mode == BridgeMode::Single {
if to_internet.is_empty() {
anyhow::bail!("A shaping interface is required.");
}
return validate_interface_selection(options, to_internet, mode, "Shaping");
}
if to_internet.is_empty() {
anyhow::bail!("An internet-facing interface is required.");
}
if to_network.is_empty() {
anyhow::bail!("A network-facing interface is required.");
}
if to_internet == to_network {
anyhow::bail!("Internet and network interfaces must be different.");
}
validate_interface_selection(options, to_internet, mode, "Internet-facing")?;
validate_interface_selection(options, to_network, mode, "Network-facing")
}
fn validate_interface_selection(
options: &[InterfaceOption],
interface: &str,
mode: BridgeMode,
role: &str,
) -> anyhow::Result<()> {
if options
.iter()
.any(|option| option.name == interface && option.is_eligible_for(mode))
{
Ok(())
} else {
anyhow::bail!("{role} interface {interface} is not eligible for the selected mode.")
}
Ok(())
}
fn interface_label(interface: &str) -> String {
@@ -211,8 +186,8 @@ fn build_interface_list(
fn build_layout() -> LinearLayout {
let bridge_mode = CURRENT_CONFIG.lock().bridge_mode;
match bridge_mode {
BridgeMode::Linux | BridgeMode::XDP | BridgeMode::CompatibilityShim => {
let interfaces = eligible_interface_options(bridge_mode);
BridgeMode::Linux | BridgeMode::XDP => {
let interfaces = get_interface_options().expect("Failed to get interfaces");
// If the configuration has empty interface fields, set them to the first available interface
{
@@ -266,7 +241,7 @@ fn build_layout() -> LinearLayout {
.child(network_layout)
}
BridgeMode::Single => {
let interfaces = eligible_interface_options(bridge_mode);
let interfaces = get_interface_options().expect("Failed to get interfaces");
// If the configuration has empty interface field, set it to the first available interface
{
@@ -343,6 +318,7 @@ fn build_layout() -> LinearLayout {
}
pub fn interface_menu(s: &mut Cursive) {
get_interfaces().expect("Failed to get interfaces");
s.add_layer(
Dialog::around(build_layout())
.title("Select Interfaces")
@@ -353,50 +329,3 @@ pub fn interface_menu(s: &mut Cursive) {
.full_screen(),
);
}
#[cfg(test)]
mod tests {
use super::{InterfaceOption, validate_mode_interfaces_with_options};
use crate::config_builder::BridgeMode;
#[test]
fn compatibility_shim_uses_its_relaxed_interface_eligibility() {
let bond = InterfaceOption {
name: "bond0".to_string(),
label: "bond0".to_string(),
bridge_eligible: false,
compatibility_shim_eligible: true,
single_interface_eligible: false,
};
assert!(bond.is_eligible_for(BridgeMode::CompatibilityShim));
assert!(!bond.is_eligible_for(BridgeMode::Linux));
assert!(!bond.is_eligible_for(BridgeMode::XDP));
assert!(!bond.is_eligible_for(BridgeMode::Single));
}
#[test]
fn compatibility_shim_rejects_stale_ineligible_selection() {
let options = [InterfaceOption {
name: "bond0".to_string(),
label: "bond0".to_string(),
bridge_eligible: false,
compatibility_shim_eligible: true,
single_interface_eligible: false,
}];
let error = validate_mode_interfaces_with_options(
&options,
BridgeMode::CompatibilityShim,
"bond0",
"stale0",
)
.expect_err("reject stale interface");
assert!(
error
.to_string()
.contains("Network-facing interface stale0")
);
}
}
+3 -2
View File
@@ -3,14 +3,15 @@ use cursive::views::{Dialog, LinearLayout, TextView};
use crate::interfaces;
pub fn preflight() {
if interfaces::get_interface_options().is_empty() {
let interfaces = interfaces::get_interfaces().unwrap();
if interfaces.is_empty() {
let mut ui = cursive::default();
ui.add_layer(
Dialog::new()
.title("No Compatible Network Interfaces Found")
.content(LinearLayout::vertical()
.child(TextView::new("LQOS requires at least one compatible network interface."))
.child(TextView::new("Please ensure that your system has a supported shaping interface. The compatibility shim can use bonded interfaces that cannot host XDP directly."))
.child(TextView::new("Please ensure that your system has network interfaces that support XDP and multiple queues."))
.child(TextView::new("For more information, please refer to the documentation."))
.child(TextView::new("https://libreqos.readthedocs.io/en/latest/docs/v2.0/requirements.html."))
)
+14 -69
View File
@@ -1,9 +1,6 @@
//! Shared setup commit/apply helpers used by both Cursive and the setup WebUI.
use crate::{
config_builder::{BridgeMode, CURRENT_CONFIG, existing_config_load_error},
interfaces,
};
use crate::config_builder::{BridgeMode, CURRENT_CONFIG, existing_config_load_error};
use anyhow::{Context, Result, bail};
use lqos_netplan_helper::protocol::{ApplyMode, ApplyRequest};
use lqos_netplan_helper::transaction::{
@@ -57,16 +54,22 @@ pub(crate) fn build_candidate_config(existing: Option<lqos_config::Config>) -> l
config.queues.generated_pn_download_mbps = new_config.mbps_to_internet;
config.queues.generated_pn_upload_mbps = new_config.mbps_to_network;
match new_config.bridge_mode {
mode @ (BridgeMode::Linux | BridgeMode::XDP | BridgeMode::CompatibilityShim) => {
let use_xdp_bridge = !matches!(mode, BridgeMode::Linux);
let compatibility_shim = matches!(mode, BridgeMode::CompatibilityShim);
BridgeMode::Linux => {
config.single_interface = None;
config.bridge = Some(lqos_config::BridgeConfig {
use_xdp_bridge,
use_xdp_bridge: false,
to_internet: new_config.to_internet.clone(),
to_network: new_config.to_network.clone(),
mtu: existing_bridge_mtu,
});
}
BridgeMode::XDP => {
config.single_interface = None;
config.bridge = Some(lqos_config::BridgeConfig {
use_xdp_bridge: true,
to_internet: new_config.to_internet.clone(),
to_network: new_config.to_network.clone(),
mtu: existing_bridge_mtu,
compatibility_shim,
});
}
BridgeMode::Single => {
@@ -121,20 +124,10 @@ pub(crate) fn prepare_commit() -> Result<CommitOutcome> {
bail!("Setup requires at least one admin user before configuration can be committed.");
}
let (mode, to_internet, to_network) = {
let config = CURRENT_CONFIG.lock();
(
config.bridge_mode,
config.to_internet.clone(),
config.to_network.clone(),
)
};
interfaces::validate_mode_interfaces(mode, &to_internet, &to_network)?;
let mut event_log = Vec::new();
let existing_config = load_existing_or_default(&mut event_log)?;
let config = build_candidate_config(Some(existing_config));
let using_helper = mode.uses_netplan_helper();
let using_helper = !matches!(CURRENT_CONFIG.lock().bridge_mode, BridgeMode::XDP);
if !using_helper {
lqos_config::update_config(&config)?;
@@ -262,7 +255,7 @@ fn load_existing_or_default(event_log: &mut Vec<String>) -> Result<lqos_config::
mod tests {
use super::{build_candidate_config, load_existing_or_default};
use crate::config_builder::{BridgeMode, CURRENT_CONFIG};
use crate::test_support::{ConfigEnvGuard, lock_current_config};
use crate::test_support::ConfigEnvGuard;
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};
@@ -288,7 +281,6 @@ mod tests {
#[test]
fn build_candidate_config_preserves_existing_mtu_for_same_mode() {
let _test_guard = lock_current_config();
let previous_builder = {
let mut builder = CURRENT_CONFIG.lock();
let previous = builder.clone();
@@ -313,51 +305,4 @@ mod tests {
*CURRENT_CONFIG.lock() = previous_builder;
}
#[test]
fn build_candidate_config_sets_compatibility_shim_only_in_shim_mode() {
let _test_guard = lock_current_config();
let previous_builder = {
let mut builder = CURRENT_CONFIG.lock();
let previous = builder.clone();
builder.bridge_mode = BridgeMode::CompatibilityShim;
builder.to_internet = "bond0".to_string();
builder.to_network = "bond1".to_string();
previous
};
let candidate = build_candidate_config(None);
assert!(
candidate
.bridge
.as_ref()
.is_some_and(lqos_config::BridgeConfig::compatibility_shim_enabled)
);
CURRENT_CONFIG.lock().bridge_mode = BridgeMode::XDP;
let candidate = build_candidate_config(Some(candidate));
assert!(
candidate
.bridge
.as_ref()
.is_some_and(|bridge| bridge.use_xdp_bridge && !bridge.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;
}
}
-5
View File
@@ -2,11 +2,6 @@ use once_cell::sync::Lazy;
use parking_lot::{Mutex, MutexGuard};
static CONFIG_ENV_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
static CURRENT_CONFIG_TEST_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
pub(crate) fn lock_current_config() -> MutexGuard<'static, ()> {
CURRENT_CONFIG_TEST_LOCK.lock()
}
pub(crate) struct ConfigEnvGuard {
_lock: MutexGuard<'static, ()>,
+65 -201
View File
@@ -1,7 +1,7 @@
//! Minimal setup-only web server for first-run LibreQoS configuration.
use crate::{
config_builder::{BridgeMode, CURRENT_CONFIG, existing_config_uses_direct_xdp},
config_builder::{BridgeMode, CURRENT_CONFIG, existing_config_uses_xdp},
interfaces, service_handoff,
setup_actions::{self, CommitOutcome},
};
@@ -256,15 +256,14 @@ fn render_setup_page(
let snapshot = bootstrap::status_snapshot()?;
let token = token.ok_or_else(|| anyhow::anyhow!("Setup requires a valid tokenized URL."))?;
ensure_valid_setup_token(token)?;
let interface_options = interfaces::get_interface_options();
let interface_options = interfaces::get_interface_options().unwrap_or_default();
let config = CURRENT_CONFIG.lock().clone();
let mode_label = match config.bridge_mode {
BridgeMode::Single => "single",
BridgeMode::XDP => "xdp",
BridgeMode::CompatibilityShim => "shim",
BridgeMode::Linux => "linux",
};
let persisted_xdp = existing_config_uses_direct_xdp();
let persisted_xdp = existing_config_uses_xdp();
let mut html = page_shell("LibreQoS Setup");
html.push_str("<main class=\"setup-shell\">");
@@ -305,7 +304,7 @@ fn render_setup_page(
if config.bridge_mode == BridgeMode::XDP {
html.push_str("<p class=\"notice\">This system already uses legacy XDP bridge mode. LibreQoS no longer recommends XDP for new installs, but this page will preserve it if you leave the mode unchanged.</p>");
} else {
html.push_str("<p class=\"notice\">This system previously used legacy XDP bridge mode. Saving with Linux Bridge, Compatibility Shim, or Single Interface will migrate the node away from direct XDP.</p>");
html.push_str("<p class=\"notice\">This system previously used legacy XDP bridge mode. Saving with Linux Bridge or Single Interface will migrate the node away from XDP.</p>");
}
html.push_str("<p class=\"muted\">Use Linux Bridge for long-term maintenance unless you intentionally need to keep the existing XDP deployment.</p></section>");
}
@@ -366,36 +365,45 @@ fn render_setup_page(
selected(mode_label == "xdp")
));
}
html.push_str(&format!(
"<option value=\"shim\"{}>Interface Compatibility Shim</option>",
selected(mode_label == "shim")
));
html.push_str(&format!(
"<option value=\"single\"{}>Single Interface</option>",
selected(mode_label == "single")
));
html.push_str("</select></label>");
html.push_str("<div class=\"mode-section\" data-mode-section=\"bridge\">");
html.push_str("<p class=\"muted\">Linux Bridge is recommended for most installs. Choose the compatibility shim only for bonded interfaces or drivers that cannot host LibreQoS XDP directly.</p>");
html.push_str("<label>To Internet<select name=\"to_internet\" aria-describedby=\"interfaceValidationMessage\">");
html.push_str("<option value=\"\">Select an eligible interface</option>");
html.push_str("<p class=\"muted\">Linux Bridge is recommended for most installs. Select the WAN-facing and subscriber-facing interfaces below. Existing XDP installs use the same interface pair if you choose to preserve XDP.</p>");
html.push_str("<label>To Internet<select name=\"to_internet\">");
for iface in &interface_options {
html.push_str(&interface_option_html(iface, &config.to_internet));
html.push_str(&format!(
"<option value=\"{}\"{}>{}</option>",
escape_html(&iface.name),
selected(config.to_internet == iface.name),
escape_html(&iface.label)
));
}
html.push_str("</select></label>");
html.push_str("<label>To Network<select name=\"to_network\" aria-describedby=\"interfaceValidationMessage\">");
html.push_str("<option value=\"\">Select an eligible interface</option>");
html.push_str("<label>To Network<select name=\"to_network\">");
for iface in &interface_options {
html.push_str(&interface_option_html(iface, &config.to_network));
html.push_str(&format!(
"<option value=\"{}\"{}>{}</option>",
escape_html(&iface.name),
selected(config.to_network == iface.name),
escape_html(&iface.label)
));
}
html.push_str("</select></label>");
html.push_str("<p id=\"interfaceValidationMessage\" class=\"validation-message\" hidden>Internet and network interfaces must be different.</p>");
html.push_str("</div>");
html.push_str("<div class=\"mode-section\" data-mode-section=\"single\">");
html.push_str("<p class=\"muted\">Use this when a single interface carries both internet and subscriber traffic with optional VLAN tags.</p>");
html.push_str("<label>Interface<select name=\"single_interface\" aria-describedby=\"interfaceValidationMessage\">");
html.push_str("<option value=\"\">Select an eligible interface</option>");
html.push_str("<label>Interface<select name=\"single_interface\">");
for iface in &interface_options {
html.push_str(&interface_option_html(iface, &config.to_internet));
html.push_str(&format!(
"<option value=\"{}\"{}>{}</option>",
escape_html(&iface.name),
selected(config.to_internet == iface.name),
escape_html(&iface.label)
));
}
html.push_str("</select></label>");
html.push_str(&format!(
@@ -407,7 +415,6 @@ fn render_setup_page(
config.network_vlan
));
html.push_str("</div>");
html.push_str("<p id=\"interfaceValidationMessage\" class=\"validation-message\" aria-live=\"polite\" hidden></p>");
html.push_str(&format!(
"<label>Allowed Subnets<textarea name=\"allow_subnets\" rows=\"6\">{}</textarea></label>",
escape_html(&config.allow_subnets.join("\n"))
@@ -436,14 +443,6 @@ fn render_setup_page(
}
fn apply_form_to_current_config(form: &SaveForm) -> Result<()> {
let interface_options = interfaces::get_interface_options();
apply_form_to_current_config_with_options(form, &interface_options)
}
fn apply_form_to_current_config_with_options(
form: &SaveForm,
interface_options: &[interfaces::InterfaceOption],
) -> Result<()> {
let downlink = form
.downlink_mbps
.trim()
@@ -461,51 +460,48 @@ fn apply_form_to_current_config_with_options(
.filter(|line| !line.is_empty())
.map(ToOwned::to_owned)
.collect::<Vec<_>>();
let allow_legacy_xdp =
CURRENT_CONFIG.lock().bridge_mode == BridgeMode::XDP || existing_config_uses_direct_xdp();
let requested_mode = resolve_requested_bridge_mode(&form.bridge_mode, allow_legacy_xdp)?;
let (to_internet, to_network, internet_vlan, network_vlan) = match requested_mode {
BridgeMode::Single => {
let mut config = CURRENT_CONFIG.lock();
config.node_name = form.node_name.trim().to_string();
config.mbps_to_internet = downlink;
config.mbps_to_network = uplink;
config.allow_subnets = allow_subnets;
let allow_legacy_xdp = config.bridge_mode == BridgeMode::XDP || existing_config_uses_xdp();
match resolve_requested_bridge_mode(&form.bridge_mode, allow_legacy_xdp)? {
"single" => {
config.bridge_mode = BridgeMode::Single;
let interface = form
.single_interface
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| anyhow::anyhow!("A shaping interface is required."))?;
(
interface.to_string(),
String::new(),
parse_vlan(form.internet_vlan.as_deref())?,
parse_vlan(form.network_vlan.as_deref())?,
)
config.to_internet = interface.to_string();
config.to_network.clear();
config.internet_vlan = parse_vlan(form.internet_vlan.as_deref())?;
config.network_vlan = parse_vlan(form.network_vlan.as_deref())?;
}
BridgeMode::Linux | BridgeMode::XDP | BridgeMode::CompatibilityShim => {
let to_internet = form.to_internet.trim();
"linux" | "xdp" => {
let to_network = form
.to_network
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| anyhow::anyhow!("A network-facing interface is required."))?;
(to_internet.to_string(), to_network.to_string(), 0, 0)
if form.to_internet.trim() == to_network {
bail!("Internet and network interfaces must be different.");
}
config.bridge_mode = if form.bridge_mode == "xdp" {
BridgeMode::XDP
} else {
BridgeMode::Linux
};
config.to_internet = form.to_internet.trim().to_string();
config.to_network = to_network.to_string();
config.internet_vlan = 0;
config.network_vlan = 0;
}
};
interfaces::validate_mode_interfaces_with_options(
interface_options,
requested_mode,
&to_internet,
&to_network,
)?;
let mut config = CURRENT_CONFIG.lock();
config.node_name = form.node_name.trim().to_string();
config.mbps_to_internet = downlink;
config.mbps_to_network = uplink;
config.allow_subnets = allow_subnets;
config.bridge_mode = requested_mode;
config.to_internet = to_internet;
config.to_network = to_network;
config.internet_vlan = internet_vlan;
config.network_vlan = network_vlan;
_ => bail!("Unsupported bridge mode."),
}
Ok(())
}
@@ -536,15 +532,10 @@ fn ssl_request_value(
.then(|| external_hostname.unwrap_or("").trim().to_string())
}
fn resolve_requested_bridge_mode(
requested_mode: &str,
allow_legacy_xdp: bool,
) -> Result<BridgeMode> {
fn resolve_requested_bridge_mode(requested_mode: &str, allow_legacy_xdp: bool) -> Result<&str> {
match requested_mode {
"single" => Ok(BridgeMode::Single),
"linux" => Ok(BridgeMode::Linux),
"shim" => Ok(BridgeMode::CompatibilityShim),
"xdp" if allow_legacy_xdp => Ok(BridgeMode::XDP),
"single" | "linux" => Ok(requested_mode),
"xdp" if allow_legacy_xdp => Ok("xdp"),
_ => bail!("Unsupported bridge mode."),
}
}
@@ -776,20 +767,17 @@ fn base_css() -> &'static str {
fn base_js() -> &'static str {
"function bridgeSectionForMode(mode){return mode==='single'?'single':'bridge';}\
function eligibilityAttributeForMode(mode){if(mode==='single')return 'data-single-eligible';if(mode==='shim')return 'data-shim-eligible';return 'data-bridge-eligible';}\
function updateInterfaceEligibility(){const mode=currentBridgeMode();const attribute=eligibilityAttributeForMode(mode);document.querySelectorAll('[data-interface-option]').forEach((option)=>{option.disabled=option.getAttribute(attribute)!=='true';});document.querySelectorAll('select[name=\"to_internet\"],select[name=\"to_network\"],select[name=\"single_interface\"]').forEach((select)=>{if(select.selectedOptions[0]?.disabled){select.value='';}});}\
function syncModeSections(){const modeSelect=document.querySelector('select[name=\"bridge_mode\"]');const mode=modeSelect?.value||'linux';const visibleSection=bridgeSectionForMode(mode);document.querySelectorAll('.mode-section').forEach((section)=>{section.hidden=section.dataset.modeSection!==visibleSection;});if(document.activeElement?.closest('.mode-section[hidden]')){modeSelect?.focus();}updateInterfaceEligibility();updateCoreSetupValidation();}\
function syncModeSections(){const mode=document.querySelector('select[name=\"bridge_mode\"]')?.value||'linux';const visibleSection=bridgeSectionForMode(mode);document.querySelectorAll('.mode-section').forEach((section)=>{section.hidden=section.dataset.modeSection!==visibleSection;});updateCoreSetupValidation();}\
function showBusy(message){const overlay=document.getElementById('busyOverlay');const target=document.getElementById('busyMessage');if(target&&message){target.textContent=message;}if(overlay){overlay.hidden=false;}}\
function hideBusy(){const overlay=document.getElementById('busyOverlay');if(overlay){overlay.hidden=true;}}\
function setSubmitDisabled(form,disabled){const button=form?.querySelector('button[type=\"submit\"]');if(button){button.disabled=!!disabled;}}\
function submitWithProgress(form,message){setSubmitDisabled(form,true);showBusy(message);return true;}\
function currentBridgeMode(){return document.querySelector('select[name=\"bridge_mode\"]')?.value||'linux';}\
function selectedInterfaceIsEligible(select){return Boolean(select?.value&&select.selectedOptions[0]&&!select.selectedOptions[0].disabled);}\
function selectedInterfacesAreValid(){if(currentBridgeMode()==='single'){return selectedInterfaceIsEligible(document.querySelector('select[name=\"single_interface\"]'));}const toInternet=document.querySelector('select[name=\"to_internet\"]');const toNetwork=document.querySelector('select[name=\"to_network\"]');return selectedInterfaceIsEligible(toInternet)&&selectedInterfaceIsEligible(toNetwork)&&toInternet.value!==toNetwork.value;}\
function updateCoreSetupValidation(){const form=document.getElementById('coreSetupForm');if(!form){return true;}const message=document.getElementById('interfaceValidationMessage');const invalid=!selectedInterfacesAreValid();if(message){message.textContent=currentBridgeMode()==='single'?'Select an eligible shaping interface.':'Select two different eligible interfaces.';message.hidden=!invalid;}setSubmitDisabled(form,invalid);return !invalid;}\
function validateAndSubmitSetup(form){if(!updateCoreSetupValidation()){hideBusy();setSubmitDisabled(form,false);return false;}return submitWithProgress(form,'Saving setup...');}\
function linuxInterfacesAreDistinct(){const toInternet=document.querySelector('select[name=\"to_internet\"]')?.value||'';const toNetwork=document.querySelector('select[name=\"to_network\"]')?.value||'';return !toInternet||!toNetwork||toInternet!==toNetwork;}\
function updateCoreSetupValidation(){const form=document.getElementById('coreSetupForm');if(!form){return true;}const message=document.getElementById('interfaceValidationMessage');const invalid=currentBridgeMode()!=='single'&&!linuxInterfacesAreDistinct();if(message){message.hidden=!invalid;}setSubmitDisabled(form,invalid);return !invalid;}\
function validateAndSubmitSetup(form){if(!updateCoreSetupValidation()){hideBusy();setSubmitDisabled(form,false);return false;}return submitWithProgress(form,'Saving setup and applying managed network changes...');}\
function resetBusyState(){hideBusy();document.querySelectorAll('form').forEach((form)=>setSubmitDisabled(form,false));updateCoreSetupValidation();}\
function setupPage(){const mode=document.querySelector('select[name=\"bridge_mode\"]');if(mode){mode.addEventListener('change',syncModeSections);}document.querySelectorAll('select[name=\"to_internet\"],select[name=\"to_network\"],select[name=\"single_interface\"]').forEach((select)=>select.addEventListener('change',updateCoreSetupValidation));window.addEventListener('pageshow',resetBusyState);syncModeSections();resetBusyState();}"
function setupPage(){const mode=document.querySelector('select[name=\"bridge_mode\"]');if(mode){mode.addEventListener('change',syncModeSections);}const toInternet=document.querySelector('select[name=\"to_internet\"]');if(toInternet){toInternet.addEventListener('change',updateCoreSetupValidation);}const toNetwork=document.querySelector('select[name=\"to_network\"]');if(toNetwork){toNetwork.addEventListener('change',updateCoreSetupValidation);}window.addEventListener('pageshow',resetBusyState);syncModeSections();resetBusyState();}"
}
fn escape_html(input: &str) -> String {
@@ -817,63 +805,14 @@ fn selected(value: bool) -> &'static str {
if value { " selected" } else { "" }
}
fn interface_option_html(iface: &interfaces::InterfaceOption, selected_value: &str) -> String {
format!(
"<option value=\"{}\" data-interface-option data-bridge-eligible=\"{}\" data-shim-eligible=\"{}\" data-single-eligible=\"{}\"{}>{}</option>",
escape_html(&iface.name),
iface.bridge_eligible,
iface.compatibility_shim_eligible,
iface.single_interface_eligible,
selected(selected_value == iface.name),
escape_html(&iface.label)
)
}
#[cfg(test)]
mod tests {
use super::{
SaveForm, apply_form_to_current_config_with_options, interface_option_html,
render_pending_page, resolve_requested_bridge_mode, runtime_access_url,
};
use crate::config_builder::{BridgeMode, CURRENT_CONFIG};
use crate::interfaces::InterfaceOption;
use crate::test_support::lock_current_config;
use super::{render_pending_page, resolve_requested_bridge_mode, runtime_access_url};
use lqos_config::{Config, SslConfig};
fn shim_save_form() -> SaveForm {
SaveForm {
token: "test-token".to_string(),
node_name: "Shim Node".to_string(),
downlink_mbps: "10000".to_string(),
uplink_mbps: "5000".to_string(),
bridge_mode: "shim".to_string(),
to_internet: "bond0".to_string(),
to_network: Some("bond1".to_string()),
single_interface: None,
internet_vlan: None,
network_vlan: None,
allow_subnets: "10.0.0.0/8".to_string(),
enable_ssl: None,
external_hostname: None,
}
}
fn shim_interface_option(name: &str, eligible: bool) -> InterfaceOption {
InterfaceOption {
name: name.to_string(),
label: name.to_string(),
bridge_eligible: false,
compatibility_shim_eligible: eligible,
single_interface_eligible: false,
}
}
#[test]
fn xdp_mode_is_preserved_for_existing_xdp_config() {
assert_eq!(
resolve_requested_bridge_mode("xdp", true).unwrap(),
BridgeMode::XDP
);
assert_eq!(resolve_requested_bridge_mode("xdp", true).unwrap(), "xdp");
}
#[test]
@@ -882,81 +821,6 @@ mod tests {
assert!(error.to_string().contains("Unsupported bridge mode"));
}
#[test]
fn compatibility_shim_is_available_for_new_installs() {
assert_eq!(
resolve_requested_bridge_mode("shim", false).unwrap(),
BridgeMode::CompatibilityShim
);
}
#[test]
fn compatibility_shim_form_updates_first_run_config() {
let _test_guard = lock_current_config();
let previous_builder = CURRENT_CONFIG.lock().clone();
let form = shim_save_form();
let options = ["bond0", "bond1"].map(|name| shim_interface_option(name, true));
apply_form_to_current_config_with_options(&form, &options).expect("apply shim form");
let candidate = crate::setup_actions::build_candidate_config(None);
assert_eq!(
CURRENT_CONFIG.lock().bridge_mode,
BridgeMode::CompatibilityShim
);
assert!(candidate.bridge.as_ref().is_some_and(|bridge| {
bridge.use_xdp_bridge
&& bridge.compatibility_shim_enabled()
&& bridge.to_internet == "bond0"
&& bridge.to_network == "bond1"
}));
*CURRENT_CONFIG.lock() = previous_builder;
}
#[test]
fn compatibility_shim_form_rejects_ineligible_interface_without_mutating_config() {
let _test_guard = lock_current_config();
let previous_builder = CURRENT_CONFIG.lock().clone();
let form = shim_save_form();
let options = [
shim_interface_option("bond0", true),
shim_interface_option("bond1", false),
];
let error = apply_form_to_current_config_with_options(&form, &options)
.expect_err("reject ineligible shim interface");
assert!(
error
.to_string()
.contains("Network-facing interface bond1 is not eligible")
);
let current = CURRENT_CONFIG.lock().clone();
*CURRENT_CONFIG.lock() = previous_builder.clone();
assert_eq!(current.bridge_mode, previous_builder.bridge_mode);
assert_eq!(current.to_internet, previous_builder.to_internet);
assert_eq!(current.to_network, previous_builder.to_network);
}
#[test]
fn rendered_interface_option_carries_mode_eligibility() {
let option = InterfaceOption {
name: "bond0".to_string(),
label: "bond0 - virtual".to_string(),
bridge_eligible: false,
compatibility_shim_eligible: true,
single_interface_eligible: false,
};
let html = interface_option_html(&option, "bond0");
assert!(html.contains("data-bridge-eligible=\"false\""));
assert!(html.contains("data-shim-eligible=\"true\""));
assert!(html.contains("data-single-eligible=\"false\""));
assert!(html.contains(" selected>bond0 - virtual</option>"));
}
#[test]
fn pending_page_preserves_ssl_request_fields() {
let html = render_pending_page(
+5 -7
View File
@@ -616,21 +616,19 @@ 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 {}", &internet_interface);
debug!("Enabling promiscuous mode on {}", &bridge.to_internet);
std::process::Command::new("/bin/ip")
.args(["link", "set", &internet_interface, "promisc", "on"])
.args(["link", "set", &bridge.to_internet, "promisc", "on"])
.output()?;
debug!("Enabling promiscuous mode on {}", &network_interface);
debug!("Enabling promiscuous mode on {}", &bridge.to_network);
std::process::Command::new("/bin/ip")
.args(["link", "set", &network_interface, "promisc", "on"])
.args(["link", "set", &bridge.to_network, "promisc", "on"])
.output()?;
// Build the interface and vlan map entries
crate::bifrost_maps::clear_bifrost()?;
crate::bifrost_maps::map_multi_interface_mode(&internet_interface, &network_interface)?;
crate::bifrost_maps::map_multi_interface_mode(&bridge.to_internet, &bridge.to_network)?;
// Actually attach the TC ingress program
let error = unsafe { bpf::tc_attach_ingress(interface_index as i32, false, skeleton) };
-616
View File
@@ -1,616 +0,0 @@
//! 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<_>>()
);
}
}
+2 -23
View File
@@ -6,7 +6,6 @@
mod blackboard;
mod dynamic_circuits;
mod file_lock;
mod interface_shim;
mod ip_mapping;
#[cfg(feature = "equinix_tests")]
mod lqos_daht_test;
@@ -244,23 +243,13 @@ 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 {
return Err(anyhow::anyhow!(
"Failed to initialize the Insight control channel"
));
error!("Failed to initialize Insight control channel, exiting.");
std::process::exit(1);
};
let control_tx_for_lts = control_channel.tx.clone();
let control_tx_for_web = control_channel.tx.clone();
@@ -382,15 +371,6 @@ 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();
@@ -512,7 +492,6 @@ 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);
@@ -1,112 +0,0 @@
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"]);
});
@@ -1,42 +0,0 @@
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,11 +1,5 @@
import {loadConfig, renderConfigMenu, saveConfig} from "./config/config_helper";
import {loadConfig, renderConfigMenu} 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,
@@ -157,20 +151,6 @@ 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");
@@ -187,16 +167,6 @@ 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) {
@@ -247,12 +217,8 @@ function buildCandidateConfig() {
next.single_interface = null;
if (document.getElementById("bridgeMode").checked) {
const bridgeFlags = normalizedBridgeFlags(
document.getElementById("useXdpBridge").checked,
compatibilityShimEnabled(),
);
next.bridge = {
...bridgeFlags,
use_xdp_bridge: document.getElementById("useXdpBridge").checked,
to_internet: document.getElementById("toInternet").value.trim(),
to_network: document.getElementById("toNetwork").value.trim(),
mtu: currentBridgeMtuValue(),
@@ -273,9 +239,9 @@ function interfaceCandidates() {
return Array.isArray(currentInspection?.interface_candidates) ? currentInspection.interface_candidates : [];
}
function optionLabel(candidate, selectedValue, eligibilityField) {
function optionLabel(candidate, selectedValue) {
if (!candidate) return selectedValue;
if (candidate[eligibilityField]) {
if (candidate.bridge_eligible || candidate.single_interface_eligible) {
return candidate.name;
}
if (candidate.current_selection || candidate.name === selectedValue) {
@@ -286,7 +252,7 @@ function optionLabel(candidate, selectedValue, eligibilityField) {
function buildSelectOptions(selectElement, modeKey, selectedValue, excludedValue = null) {
if (!selectElement) return;
const eligibilityField = bridgeEligibilityField(modeKey, compatibilityShimEnabled());
const eligibilityField = modeKey === "single" ? "single_interface_eligible" : "bridge_eligible";
const candidates = interfaceCandidates();
const options = [`<option value="">Select an eligible interface</option>`];
const seen = new Set();
@@ -297,7 +263,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, eligibilityField))}</option>`
`<option value="${escapeHtml(candidate.name)}">${escapeHtml(optionLabel(candidate, selectedValue))}</option>`
);
seen.add(candidate.name);
});
@@ -316,7 +282,7 @@ function renderInterfaceHelp(helpElementId, modeKey, selectedValues = {}) {
const element = document.getElementById(helpElementId);
if (!element) return;
const eligibilityField = bridgeEligibilityField(modeKey, compatibilityShimEnabled());
const eligibilityField = modeKey === "single" ? "single_interface_eligible" : "bridge_eligible";
const selectedSet = new Set(
Object.values(selectedValues)
.map((value) => String(value || "").trim())
@@ -382,10 +348,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 ?? "";
@@ -397,12 +363,9 @@ 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() {
@@ -723,7 +686,6 @@ function renderInspection(inspection) {
"bridgeMode",
"singleInterfaceMode",
"useXdpBridge",
"compatibilityShim",
"toInternet",
"toNetwork",
"bridgeMtu",
@@ -738,11 +700,10 @@ 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) {
@@ -849,39 +810,6 @@ 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;
@@ -1006,19 +934,9 @@ function wireActions() {
].forEach((id) => {
document.getElementById(id).addEventListener("change", () => {
renderInterfaceSelectors();
updateApplyButtonState();
});
});
document.getElementById("useXdpBridge").addEventListener("change", () => {
updateMtuState();
updateApplyButtonState();
});
document.getElementById("compatibilityShim").addEventListener("change", () => {
updateCompatibilityShimState();
updateMtuState();
renderInterfaceSelectors();
updateApplyButtonState();
});
document.getElementById("useXdpBridge").addEventListener("change", updateMtuState);
[
"bridgeMtu",
"singleInterfaceMtu",
@@ -1035,13 +953,9 @@ function wireActions() {
});
document.getElementById("saveButton").addEventListener("click", () => {
if (!validateConfig()) return;
const draft = saveDraft();
saveDraft();
inspectCandidate();
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.");
}
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,7 +20,6 @@ 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 and its interface compatibility shim leave Netplan alone.</div>
<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="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="useXdpBridge" aria-describedby="useXdpBridgeHelp">
@@ -63,18 +63,12 @@
<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 interface for the selected direct or compatibility-shim path.</div>
<div class="form-text">Eligible non-management interfaces with LibreQoS-friendly queue support.</div>
</div>
<div class="mb-2">
+2 -62
View File
@@ -4,24 +4,6 @@ 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 {
@@ -34,17 +16,8 @@ fn apply_non_interface_tuning(tuning: &Tunables) {
}
fn apply_interface_tuning(config: &Config, tuning: &Tunables) {
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);
}
}
offloads::ethtool_tweaks(&config.internet_interface(), tuning);
offloads::ethtool_tweaks(&config.isp_interface(), tuning);
}
pub fn tune_lqosd_from_config_file() -> Result<()> {
@@ -68,36 +41,3 @@ 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()])
);
}
}
+12 -19
View File
@@ -78,25 +78,6 @@ 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",
@@ -114,4 +95,16 @@ pub(super) fn ethtool_coalescing_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();
}
}