Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/v2.0/bridge.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,21 @@ 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.

## Sandwich Mode (Optional)

Sandwich mode inserts a lightweight veth+bridge pair between your two physical shaping ports. It is useful when you need a compatibility layer or a hard/accurate rate cap.

When to use
- Unsupported NICs or environments where the standard Linux or XDP bridge has quirks (handy for testing and evaluation).
- Bonded NICs/LACP on the physical ports where the extra bridge hop simplifies attach points.
- A hard/accurate limiter in one or both directions (for metered bandwidth or strict caps). The limiter uses HTB with an optional fq_codel child.

How to enable
- Web UI: Configuration → Network Mode → Bridge Mode → Enable “Sandwich Bridge (veth pair)”.
- Then choose limiter direction (None / Download / Upload / Both). Optionally set per‑direction Mbps overrides and enable “Use fq_codel under HTB”.
- Or set it in `/etc/lqos.conf` (see details in [Configuration → Sandwich Mode Settings](configuration.md#sandwich-mode-settings)).

Notes
- This adds a small amount of overhead versus a direct bridge/XDP path, but is generally fine for testing and many production needs.
- `to_internet` remains the ISP‑facing physical interface; `to_network` remains the LAN‑facing physical interface. Sandwich wiring is handled automatically by LibreQoS.
35 changes: 35 additions & 0 deletions docs/v2.0/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,41 @@ For direct file editing (`/etc/lqos.conf`, `network.json`, `ShapedDevices.csv`),

- [Advanced Configuration Reference](configuration-advanced.md)

### Sandwich Mode Settings

Sandwich mode is an optional compatibility and rate‑limiting layer for Bridge Mode. Enable and configure it in the Web UI under Configuration → Network Mode → Bridge Mode → “Sandwich Bridge (veth pair)”.

When appropriate
- Compatibility with unsupported NICs or special environments (acceptable performance trade‑off for testing).
- Compatibility when using bonded NICs/LACP.
- Enforcing a hard/accurate rate limit in one or both directions (metered bandwidth).

Key options (under `[bridge]`)
- `to_internet` and `to_network`: existing physical shaping interfaces (unchanged).
- `sandwich.Full.with_rate_limiter`: one of `"None"`, `"Download"`, `"Upload"`, or `"Both"`.
- `sandwich.Full.rate_override_mbps_down`: optional integer; overrides the Download limit if set.
- `sandwich.Full.rate_override_mbps_up`: optional integer; overrides the Upload limit if set.
- `sandwich.Full.queue_override`: optional integer; sets veth TX queue count (default is number of CPU cores).
- `sandwich.Full.use_fq_codel`: optional boolean; attach `fq_codel` under the HTB class for better queueing.

Example (TOML)
```
[bridge]
to_internet = "enp1s0f1"
to_network = "enp1s0f2"

[bridge.sandwich.Full]
with_rate_limiter = "Both"
rate_override_mbps_down = 500
rate_override_mbps_up = 100
queue_override = 8
use_fq_codel = true
```

Rate limiting details
- Sandwich rate limiting uses an HTB class for the cap; `fq_codel` (if enabled) is attached as a child qdisc to improve queueing behavior.
- Choose the limiter direction based on the need: Download (ISP→LAN), Upload (LAN→ISP), or Both.

## Related Pages

- [Quickstart](quickstart.md)
Expand Down
2 changes: 1 addition & 1 deletion src/rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ edition = "2024"
license = "GPL-2.0-only"

[profile.release]
strip = yes
strip = true
lto = "thin"
incremental = false

Expand Down
1 change: 1 addition & 0 deletions src/rust/lqos_config/src/etc/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ fn migrate_bridge(
.use_xdp_bridge,
to_internet: python_config.interface_b.clone(),
to_network: python_config.interface_a.clone(),
sandwich: None,
});
}
Ok(())
Expand Down
7 changes: 4 additions & 3 deletions src/rust/lqos_config/src/etc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ mod python_migration;
pub mod test_data;
mod v15;
pub use v15::{
BridgeConfig, LazyQueueMode, RttThresholds, SingleInterfaceConfig, StormguardConfig,
StormguardStrategy, TreeguardCircuitsConfig, TreeguardConfig, TreeguardCpuConfig,
TreeguardCpuMode, TreeguardLinksConfig, TreeguardQooConfig, Tunables,
BridgeConfig, LazyQueueMode, RttThresholds, SandwichMode, SandwichRateLimiter,
SingleInterfaceConfig, StormguardConfig, StormguardStrategy, TreeguardCircuitsConfig,
TreeguardConfig, TreeguardCpuConfig, TreeguardCpuMode, TreeguardLinksConfig,
TreeguardQooConfig, Tunables,
};

static CONFIG: Lazy<ArcSwap<Option<Arc<Config>>>> = Lazy::new(|| ArcSwap::from_pointee(None));
Expand Down
67 changes: 67 additions & 0 deletions src/rust/lqos_config/src/etc/v15/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,50 @@ pub struct BridgeConfig {

/// The name of the second interface, facing the LAN
pub to_network: String,

/// The sandwich mode, if any. Sandwich mode enables a veth bridge pair,
/// both for compatibility (e.g. if one interface doesn't support XDP),
/// and for attaching an absolute rate limiter to the bridge.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sandwich: Option<SandwichMode>,
}

/// The sandwich mode to use, if any.
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Allocative)]
pub enum SandwichMode {
/// No sandwich mode - direct interfaces
None,
/// Use a veth pair as the LibreQoS interface set, and attach a bridge
/// on each end to the physical interfaces.
Full {
/// Whether to attach an absolute rate limiter to the bridge
with_rate_limiter: SandwichRateLimiter,
/// Normally, the rate limiter is set to the bandwidth of the
/// connection. This allows overriding that for download traffic.
rate_override_mbps_down: Option<u64>,
/// Normally, the rate limiter is set to the bandwidth of the
/// connection. This allows overriding that for upload traffic.
rate_override_mbps_up: Option<u64>,
/// Number of TX queues to use on the veth interfaces
/// (Defaults to the available CPU cores)
queue_override: Option<usize>,
/// Attach an fq_codel child qdisc under the HTB class for better queueing behavior
#[serde(default)]
use_fq_codel: bool,
},
}

/// The type of rate limiting to apply in sandwich mode
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Allocative)]
pub enum SandwichRateLimiter {
/// No rate limiter
None,
/// Rate limit only download traffic
Download,
/// Rate limit only upload traffic
Upload,
/// Rate limit both download and upload traffic
Both,
}

impl Default for BridgeConfig {
Expand All @@ -24,6 +68,29 @@ impl Default for BridgeConfig {
use_xdp_bridge: true,
to_internet: "eth0".to_string(),
to_network: "eth1".to_string(),
sandwich: None,
}
}
}

impl BridgeConfig {
/// Returns the active sandwich mode, filtering out legacy explicit `None`.
pub fn sandwich_mode(&self) -> Option<&SandwichMode> {
self.sandwich.as_ref().and_then(SandwichMode::as_active)
}

/// Returns `true` when sandwich mode is actively enabled.
pub fn sandwich_enabled(&self) -> bool {
self.sandwich_mode().is_some()
}
}

impl SandwichMode {
/// Treats the legacy explicit `None` variant the same as an absent sandwich config.
pub fn as_active(&self) -> Option<&Self> {
match self {
Self::None => None,
Self::Full { .. } => Some(self),
}
}
}
Expand Down
121 changes: 119 additions & 2 deletions src/rust/lqos_config/src/etc/v15/top_config.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//! Top-level configuration file for LibreQoS.

use super::tuning::Tunables;
use crate::etc::v15::stormguard;
use crate::etc::v15::treeguard;
use crate::etc::v15::{stormguard, treeguard};
use crate::{SANDWICH_TO_INTERNET, SANDWICH_TO_NETWORK};
use allocative::Allocative;
use serde::{Deserialize, Serialize};
use sha2::Digest;
Expand Down Expand Up @@ -210,6 +210,32 @@ impl Config {
if self.node_id.is_empty() {
return Err("Node ID must be set".to_string());
}
if let Some(bridge) = &self.bridge {
if let Some(super::bridge::SandwichMode::Full {
rate_override_mbps_down,
rate_override_mbps_up,
queue_override,
..
}) = bridge.sandwich_mode()
{
if !bridge.use_xdp_bridge {
return Err("Sandwich mode requires bridge.use_xdp_bridge = true.".to_string());
}
if rate_override_mbps_down.is_some_and(|rate| rate == 0) {
return Err(
"bridge.sandwich.Full.rate_override_mbps_down must be > 0".to_string()
);
}
if rate_override_mbps_up.is_some_and(|rate| rate == 0) {
return Err(
"bridge.sandwich.Full.rate_override_mbps_up must be > 0".to_string()
);
}
if queue_override.is_some_and(|queues| queues == 0) {
return Err("bridge.sandwich.Full.queue_override must be > 0".to_string());
}
}
}
if let Some(rtt) = &self.rtt_thresholds {
if rtt.red_ms == 0 {
return Err("rtt_thresholds.red_ms must be > 0".to_string());
Expand Down Expand Up @@ -328,6 +354,22 @@ impl Default for Config {
impl Config {
/// Calculate the unterface facing the Internet
pub fn internet_interface(&self) -> String {
if let Some(bridge) = &self.bridge {
if bridge.sandwich_enabled() {
// In sandwich mode, the internet interface is the veth pair
SANDWICH_TO_INTERNET.to_string()
} else {
bridge.to_internet.clone()
}
} else if let Some(single_interface) = &self.single_interface {
single_interface.interface.clone()
} else {
panic!("No internet interface configured")
}
}

/// Calculate the physical interface facing the Internet (ignoring sandwich mode)
pub fn internet_interface_physical(&self) -> String {
if let Some(bridge) = &self.bridge {
bridge.to_internet.clone()
} else if let Some(single_interface) = &self.single_interface {
Expand All @@ -339,6 +381,22 @@ impl Config {

/// Calculate the interface facing the ISP
pub fn isp_interface(&self) -> String {
if let Some(bridge) = &self.bridge {
if bridge.sandwich_enabled() {
// In sandwich mode, the ISP interface is the veth pair
SANDWICH_TO_NETWORK.to_string()
} else {
bridge.to_network.clone()
}
} else if let Some(single_interface) = &self.single_interface {
single_interface.interface.clone()
} else {
panic!("No ISP interface configured")
}
}

/// Calculate the physical interface facing the ISP (ignoring sandwich mode)
pub fn isp_interface_physical(&self) -> String {
if let Some(bridge) = &self.bridge {
bridge.to_network.clone()
} else if let Some(single_interface) = &self.single_interface {
Expand Down Expand Up @@ -538,6 +596,65 @@ mod test {
assert!(config.stormguard.is_none());
}

#[test]
fn load_example_without_sandwich_section_uses_physical_bridge_interfaces() {
let config = Config::load_from_string(include_str!("example.toml"))
.expect("Cannot read example toml file");
assert_eq!(config.internet_interface(), "eth0");
assert_eq!(config.isp_interface(), "eth1");
}

#[test]
fn sandwich_mode_switches_effective_interfaces() {
let mut raw = include_str!("example.toml").to_string();
raw.push_str(
r#"

[bridge.sandwich.Full]
with_rate_limiter = "Both"
rate_override_mbps_down = 500
rate_override_mbps_up = 100
queue_override = 8
use_fq_codel = true
"#,
);

let config = Config::load_from_string(&raw).expect("Sandwich config should deserialize");
assert_eq!(config.internet_interface(), crate::SANDWICH_TO_INTERNET);
assert_eq!(config.isp_interface(), crate::SANDWICH_TO_NETWORK);
assert_eq!(config.internet_interface_physical(), "eth0");
assert_eq!(config.isp_interface_physical(), "eth1");
}

#[test]
fn sandwich_mode_requires_xdp_bridge() {
let mut raw =
include_str!("example.toml").replace("use_xdp_bridge = true", "use_xdp_bridge = false");
raw.push_str(
r#"

[bridge.sandwich.Full]
with_rate_limiter = "None"
"#,
);

let err = Config::load_from_string(&raw).expect_err("Sandwich mode should require XDP");
assert!(err.contains("bridge.use_xdp_bridge = true"));
}

#[test]
fn explicit_legacy_sandwich_none_is_treated_as_disabled() {
let raw = include_str!("example.toml").replace(
"[bridge]\nuse_xdp_bridge = true\nto_internet = \"eth0\"\nto_network = \"eth1\"\n",
"[bridge]\nuse_xdp_bridge = true\nto_internet = \"eth0\"\nto_network = \"eth1\"\nsandwich = \"None\"\n",
);

let config = Config::load_from_string(&raw)
.expect("Legacy explicit sandwich none should deserialize");
assert_eq!(config.internet_interface(), "eth0");
assert_eq!(config.isp_interface(), "eth1");
}

#[test]
fn treeguard_validation_rejects_invalid_thresholds() {
let mut cfg = Config::default();
Expand Down
27 changes: 23 additions & 4 deletions src/rust/lqos_config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ pub use cpu_topology::{
CpuListParseError, ShapingCpuDetection, ShapingCpuSource, detect_shaping_cpus,
};
pub use etc::{
BridgeConfig, Config, LazyQueueMode, RttThresholds, SingleInterfaceConfig, StormguardConfig,
StormguardStrategy, TreeguardCircuitsConfig, TreeguardConfig, TreeguardCpuConfig,
TreeguardCpuMode, TreeguardLinksConfig, TreeguardQooConfig, Tunables, disable_xdp_bridge,
enable_long_term_stats, load_config, update_config,
BridgeConfig, Config, LazyQueueMode, RttThresholds, SandwichMode, SandwichRateLimiter,
SingleInterfaceConfig, StormguardConfig, StormguardStrategy, TreeguardCircuitsConfig,
TreeguardConfig, TreeguardCpuConfig, TreeguardCpuMode, TreeguardLinksConfig,
TreeguardQooConfig, Tunables, disable_xdp_bridge, enable_long_term_stats, load_config,
update_config,
};
pub use network_json::{NetworkJson, NetworkJsonNode, NetworkJsonTransport};
pub use program_control::load_libreqos;
Expand All @@ -35,3 +36,21 @@ pub use shaped_devices::{ConfigShapedDevices, ShapedDevice};

/// Used as a constant in determining buffer preallocation
pub const SUPPORTED_CUSTOMERS: usize = 100_000;

/// The name of the veth interface facing the Internet in sandwich mode
pub const SANDWICH_TO_INTERNET: &str = "v_inet_lq";

/// The name of the other half of the veth facing the Internet in sandwich mode
pub const SANDWICH_TO_INTERNET2: &str = "v_inet_phy";

/// The name of the veth interface facing the ISP in sandwich mode
pub const SANDWICH_TO_NETWORK: &str = "v_isp_lq";

/// The name of the other half of veth interface facing the ISP in sandwich mode
pub const SANDWICH_TO_NETWORK2: &str = "v_isp_phy";

/// The name of the bridge facing the Internet in sandwich mode
pub const BRIDGE_TO_INTERNET: &str = "br_lq_inet";

/// The name of the bridge facing the ISP in sandwich mode
pub const BRIDGE_TO_NETWORK: &str = "br_lq_isp";
2 changes: 2 additions & 0 deletions src/rust/lqos_setup/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ fn continue_finalize(ui: &mut cursive::Cursive) {
use_xdp_bridge: false,
to_internet: new_config.to_internet.clone(),
to_network: new_config.to_network.clone(),
sandwich: None,
});
}
config_builder::BridgeMode::XDP => {
Expand All @@ -242,6 +243,7 @@ fn continue_finalize(ui: &mut cursive::Cursive) {
use_xdp_bridge: true,
to_internet: new_config.to_internet.clone(),
to_network: new_config.to_network.clone(),
sandwich: None,
});
}
config_builder::BridgeMode::Single => {
Expand Down
Loading
Loading