From 4f384bcff0ebe4ff279a2924d5085e3bdfeefb8d Mon Sep 17 00:00:00 2001 From: Crimson Date: Thu, 28 May 2026 12:12:42 +0500 Subject: [PATCH] Decouple sysctl runtime validation and modprobe dependencies --- docs/CONFIGURATION.md | 6 +++- traffic_core/src/config.rs | 56 +++++++++++++++++++++++++++-- traffic_core/src/main.rs | 2 +- traffic_core/src/sysctl_tuning.rs | 59 +++++++++++++++++++++---------- 4 files changed, 100 insertions(+), 23 deletions(-) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 1f961db..4208fb8 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -118,7 +118,11 @@ swappiness = 1 - на установке `.deb` модуль не применяется; - на старте daemon модуль применяется, если `enabled=true` и `apply_on_daemon_start=true`; - `tcp_bbr = auto` не валит запуск при недоступном BBR (warning), `require` — валит; -- host runtime tools: `modprobe` обязателен только для `tcp_bbr = "require"`. +- для `sysctl_tuning`: `modprobe` обязателен только для `tcp_bbr = "require"`. + +Отдельно для всего пакета: + +- `traffic`-модуль требует `modprobe` для IFB при `traffic.enabled = true`. Совместимость обновлений: diff --git a/traffic_core/src/config.rs b/traffic_core/src/config.rs index bebc259..bda4635 100644 --- a/traffic_core/src/config.rs +++ b/traffic_core/src/config.rs @@ -287,6 +287,25 @@ impl Config { self.check_host_tools() } + pub fn validate_sysctl_tuning_runtime(&self) -> anyhow::Result<()> { + self.validate_config()?; + + if !self.sysctl_tuning.enabled { + return Ok(()); + } + + if !tool_exists("sysctl") { + anyhow::bail!("required host tool not found in PATH: sysctl"); + } + + let tcp_bbr_mode = self.sysctl_tuning.tcp_bbr.trim().to_ascii_lowercase(); + if tcp_bbr_mode == "require" && !tool_exists("modprobe") { + anyhow::bail!("required host tool not found in PATH: modprobe"); + } + + Ok(()) + } + pub fn validate_all(&self) -> anyhow::Result<()> { self.validate_config()?; self.validate_runtime()?; @@ -299,12 +318,15 @@ impl Config { anyhow::bail!("required host tool not found in PATH: nsenter"); } required.push("nsenter"); + let tcp_bbr_mode = self.sysctl_tuning.tcp_bbr.trim().to_ascii_lowercase(); if self.sysctl_tuning.enabled { required.push("sysctl"); } - if self.sysctl_tuning.enabled - && self.sysctl_tuning.tcp_bbr.trim().to_ascii_lowercase() == "require" - { + if requires_modprobe( + self.traffic.enabled, + self.sysctl_tuning.enabled, + &tcp_bbr_mode, + ) { required.push("modprobe"); } if self.anti_torrent.enabled { @@ -328,6 +350,9 @@ impl Config { ]); } + required.sort_unstable(); + required.dedup(); + for tool in required { if !tool_exists(tool) { anyhow::bail!("required host tool not found in PATH: {tool}"); @@ -383,6 +408,10 @@ impl Config { } } +fn requires_modprobe(traffic_enabled: bool, sysctl_enabled: bool, tcp_bbr_mode: &str) -> bool { + traffic_enabled || (sysctl_enabled && tcp_bbr_mode == "require") +} + fn parse_hhmm(v: &str) -> anyhow::Result<()> { let mut s = v.split(':'); let h = s @@ -707,4 +736,25 @@ swappiness = 1 assert!(cfg.sysctl_tuning.apply_on_daemon_start); assert_eq!(cfg.sysctl_tuning.tcp_bbr, "auto"); } + + #[test] + fn validate_sysctl_tuning_runtime_skips_modprobe_for_auto_when_disabled() { + let mut cfg = base_config(); + cfg.sysctl_tuning.enabled = false; + cfg.sysctl_tuning.tcp_bbr = "auto".into(); + assert!(cfg.validate_sysctl_tuning_runtime().is_ok()); + } + + #[test] + fn requires_modprobe_when_traffic_enabled() { + assert!(requires_modprobe(true, true, "auto")); + assert!(requires_modprobe(true, false, "off")); + } + + #[test] + fn requires_modprobe_for_sysctl_require_mode() { + assert!(requires_modprobe(false, true, "require")); + assert!(!requires_modprobe(false, true, "auto")); + assert!(!requires_modprobe(false, true, "off")); + } } diff --git a/traffic_core/src/main.rs b/traffic_core/src/main.rs index f4e9e1e..595a625 100644 --- a/traffic_core/src/main.rs +++ b/traffic_core/src/main.rs @@ -52,7 +52,7 @@ fn main() -> anyhow::Result<()> { } Command::ApplyNetworkTuning => { let cfg = Config::load(&config_path)?; - cfg.validate_all()?; + cfg.validate_sysctl_tuning_runtime()?; sysctl_tuning::ensure_sysctl_tuning(&cfg)?; println!("network tuning reconciled"); } diff --git a/traffic_core/src/sysctl_tuning.rs b/traffic_core/src/sysctl_tuning.rs index 1ef83f9..3947645 100644 --- a/traffic_core/src/sysctl_tuning.rs +++ b/traffic_core/src/sysctl_tuning.rs @@ -19,6 +19,7 @@ pub struct SysctlTuningStatus { pub bbr_available: Option, pub bbr_active: Option, pub current: Vec, + #[allow(dead_code)] pub desired: Vec, pub drift_reason: Option, pub warnings: Vec, @@ -29,6 +30,7 @@ pub struct SysctlValue { pub key: String, pub desired: Option, pub current: Option, + #[allow(dead_code)] pub managed: bool, } @@ -220,7 +222,10 @@ fn remove_managed_sysctl_file_at(path: &Path) -> anyhow::Result<()> { let raw = fs::read_to_string(path)?; let first = raw.lines().next().unwrap_or(""); if !first.contains(MANAGED_MARKER) { - anyhow::bail!("refusing to remove unmanaged sysctl file: {}", path.display()); + anyhow::bail!( + "refusing to remove unmanaged sysctl file: {}", + path.display() + ); } fs::remove_file(path)?; Ok(()) @@ -258,22 +263,23 @@ fn build_render_plan(cfg: &Config, allow_modprobe: bool) -> anyhow::Result anyhow::Result String { } fn write_atomic(path: &str, content: &str) -> anyhow::Result<()> { + #[cfg(unix)] let p = Path::new(path); + #[cfg(unix)] let dir = p .parent() .ok_or_else(|| anyhow::anyhow!("no parent directory for path: {path}"))?; @@ -517,11 +525,26 @@ mod tests { "awg2_harness_managed_overwrite_test_{}.conf", std::process::id() )); - fs::write(&managed, format!("{MANAGED_MARKER}\nnet.core.rmem_max = 1\n")) - .expect("write managed"); + fs::write( + &managed, + format!("{MANAGED_MARKER}\nnet.core.rmem_max = 1\n"), + ) + .expect("write managed"); let managed_str = managed.to_string_lossy().to_string(); ensure_managed_or_absent(&managed_str).expect("allow managed"); let _ = fs::remove_file(&managed); } + + #[test] + fn build_render_plan_off_mode_skips_bbr_desired_keys() { + let mut c = cfg(); + c.sysctl_tuning.tcp_bbr = "off".into(); + + let plan = build_render_plan(&c, false).expect("render plan"); + assert!(!plan.desired.contains_key("net.ipv4.tcp_congestion_control")); + assert!(!plan.desired.contains_key("net.core.default_qdisc")); + assert_eq!(plan.bbr_available, None); + assert_eq!(plan.bbr_active, None); + } }