Реализован production-модуль sysctl_tuning и полная интеграция fix33
Добавлен отдельный host-level модуль sysctl_tuning без легаси-подходов: managed sysctl файл, точечный reconcile через sysctl -w только для drifted ключей, режимы tcp_bbr off/auto/require, безопасное удаление только managed-файла и атомарная запись конфигурации. Интегрированы новые CLI-команды apply-network-tuning/inspect-network-tuning, startup reconcile в daemon и секция Network tuning в status. Расширены config/defaults/валидация, обновлен пример конфига и документация. Добавлены unit-тесты для config и sysctl_tuning.
This commit is contained in:
@@ -16,6 +16,8 @@ pub struct Cli {
|
||||
pub enum Command {
|
||||
Daemon,
|
||||
ApplyLimits,
|
||||
ApplyNetworkTuning,
|
||||
InspectNetworkTuning,
|
||||
ApplyFirewall,
|
||||
Status,
|
||||
EnsureSwap,
|
||||
|
||||
+165
-76
@@ -30,12 +30,48 @@ fn default_input_policy() -> String {
|
||||
pub struct Config {
|
||||
pub amnezia: AmneziaConfig,
|
||||
pub traffic: TrafficConfig,
|
||||
#[serde(default)]
|
||||
pub sysctl_tuning: SysctlTuningConfig,
|
||||
pub host_firewall: HostFirewallConfig,
|
||||
pub anti_torrent: AntiTorrentConfig,
|
||||
pub reboot: RebootConfig,
|
||||
pub swap: SwapConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct SysctlTuningConfig {
|
||||
pub enabled: bool,
|
||||
pub apply_on_daemon_start: bool,
|
||||
pub ip_forward: bool,
|
||||
pub tcp_bbr: String,
|
||||
pub default_qdisc: String,
|
||||
pub rmem_max: u64,
|
||||
pub wmem_max: u64,
|
||||
pub netdev_backlog_enabled: bool,
|
||||
pub netdev_max_backlog: u32,
|
||||
pub udp_rmem_min_enabled: bool,
|
||||
pub udp_rmem_min: u32,
|
||||
}
|
||||
|
||||
impl Default for SysctlTuningConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
apply_on_daemon_start: true,
|
||||
ip_forward: true,
|
||||
tcp_bbr: "auto".to_string(),
|
||||
default_qdisc: "fq".to_string(),
|
||||
rmem_max: 67_108_864,
|
||||
wmem_max: 67_108_864,
|
||||
netdev_backlog_enabled: true,
|
||||
netdev_max_backlog: 50_000,
|
||||
udp_rmem_min_enabled: true,
|
||||
udp_rmem_min: 8192,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct AmneziaConfig {
|
||||
pub container_names: Vec<String>,
|
||||
@@ -139,6 +175,33 @@ impl Config {
|
||||
}
|
||||
parse_tc_rate_bps(&self.traffic.peer_limit)?;
|
||||
parse_tc_rate_bps(&self.traffic.root_rate)?;
|
||||
|
||||
let tcp_bbr_mode = self.sysctl_tuning.tcp_bbr.trim().to_ascii_lowercase();
|
||||
if tcp_bbr_mode != "off" && tcp_bbr_mode != "auto" && tcp_bbr_mode != "require" {
|
||||
anyhow::bail!("sysctl_tuning.tcp_bbr must be one of: off, auto, require");
|
||||
}
|
||||
|
||||
let default_qdisc = self.sysctl_tuning.default_qdisc.trim().to_ascii_lowercase();
|
||||
if default_qdisc != "none" && default_qdisc != "fq" && default_qdisc != "fq_codel" {
|
||||
anyhow::bail!("sysctl_tuning.default_qdisc must be one of: none, fq, fq_codel");
|
||||
}
|
||||
|
||||
const MIB: u64 = 1024 * 1024;
|
||||
if self.sysctl_tuning.rmem_max < MIB || self.sysctl_tuning.rmem_max > 512 * MIB {
|
||||
anyhow::bail!("sysctl_tuning.rmem_max must be in 1MiB..=512MiB");
|
||||
}
|
||||
if self.sysctl_tuning.wmem_max < MIB || self.sysctl_tuning.wmem_max > 512 * MIB {
|
||||
anyhow::bail!("sysctl_tuning.wmem_max must be in 1MiB..=512MiB");
|
||||
}
|
||||
if self.sysctl_tuning.netdev_max_backlog < 1000
|
||||
|| self.sysctl_tuning.netdev_max_backlog > 100_000
|
||||
{
|
||||
anyhow::bail!("sysctl_tuning.netdev_max_backlog must be in 1000..=100000");
|
||||
}
|
||||
if self.sysctl_tuning.udp_rmem_min < 4096 || self.sysctl_tuning.udp_rmem_min > 65_536 {
|
||||
anyhow::bail!("sysctl_tuning.udp_rmem_min must be in 4096..=65536");
|
||||
}
|
||||
|
||||
if self.swap.swappiness > 100 {
|
||||
anyhow::bail!("swap.swappiness must be <= 100");
|
||||
}
|
||||
@@ -224,11 +287,19 @@ impl Config {
|
||||
}
|
||||
|
||||
fn check_host_tools(&self) -> anyhow::Result<()> {
|
||||
let mut required = vec!["tc", "ip", "wg", "docker", "modprobe"];
|
||||
let mut required = vec!["tc", "ip", "wg", "docker"];
|
||||
if !crate::netns::nsenter_available() {
|
||||
anyhow::bail!("required host tool not found in PATH: nsenter");
|
||||
}
|
||||
required.push("nsenter");
|
||||
if self.sysctl_tuning.enabled {
|
||||
required.push("sysctl");
|
||||
}
|
||||
if self.sysctl_tuning.enabled
|
||||
&& self.sysctl_tuning.tcp_bbr.trim().to_ascii_lowercase() != "off"
|
||||
{
|
||||
required.push("modprobe");
|
||||
}
|
||||
if self.anti_torrent.enabled {
|
||||
required.push("nft");
|
||||
}
|
||||
@@ -408,6 +479,46 @@ pub fn ip_in_cidr(ip: Ipv4Addr, cidr: &str) -> anyhow::Result<bool> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn base_config() -> Config {
|
||||
Config {
|
||||
amnezia: AmneziaConfig {
|
||||
container_names: vec!["x".into()],
|
||||
interface_candidates: vec!["awg0".into()],
|
||||
interface_patterns: vec!["awg*".into()],
|
||||
peer_ipv4_cidr: "10.8.1.0/24".into(),
|
||||
},
|
||||
traffic: TrafficConfig {
|
||||
enabled: true,
|
||||
peer_limit: "70mbit".into(),
|
||||
root_rate: "1000mbit".into(),
|
||||
reconcile_interval_secs: 15,
|
||||
container_ready_timeout_secs: 30,
|
||||
class_start_id: 10,
|
||||
ifb_name: "ifb0".into(),
|
||||
},
|
||||
sysctl_tuning: SysctlTuningConfig::default(),
|
||||
host_firewall: HostFirewallConfig::default(),
|
||||
anti_torrent: AntiTorrentConfig {
|
||||
enabled: false,
|
||||
max_connections_per_peer: 100,
|
||||
peer_ipv4_cidr: "10.8.1.0/24".into(),
|
||||
apply_in_amnezia_netns: false,
|
||||
},
|
||||
reboot: RebootConfig {
|
||||
enabled: false,
|
||||
window_start_utc: "01:00".into(),
|
||||
window_end_utc: "02:00".into(),
|
||||
},
|
||||
swap: SwapConfig {
|
||||
enabled: false,
|
||||
path: "/swapfile".into(),
|
||||
size_mb: 2048,
|
||||
priority: 1,
|
||||
swappiness: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_cidr_invalid_prefix() {
|
||||
let err = parse_cidr_v4("10.8.1.0/33").err();
|
||||
@@ -438,86 +549,64 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn swap_rejects_dangerous_path() {
|
||||
let cfg = Config {
|
||||
amnezia: AmneziaConfig {
|
||||
container_names: vec!["x".into()],
|
||||
interface_candidates: vec!["awg0".into()],
|
||||
interface_patterns: vec!["awg*".into()],
|
||||
peer_ipv4_cidr: "10.8.1.0/24".into(),
|
||||
},
|
||||
traffic: TrafficConfig {
|
||||
enabled: true,
|
||||
peer_limit: "70mbit".into(),
|
||||
root_rate: "1000mbit".into(),
|
||||
reconcile_interval_secs: 15,
|
||||
container_ready_timeout_secs: 30,
|
||||
class_start_id: 10,
|
||||
ifb_name: "ifb0".into(),
|
||||
},
|
||||
host_firewall: HostFirewallConfig::default(),
|
||||
anti_torrent: AntiTorrentConfig {
|
||||
enabled: false,
|
||||
max_connections_per_peer: 100,
|
||||
peer_ipv4_cidr: "10.8.1.0/24".into(),
|
||||
apply_in_amnezia_netns: false,
|
||||
},
|
||||
reboot: RebootConfig {
|
||||
enabled: false,
|
||||
window_start_utc: "01:00".into(),
|
||||
window_end_utc: "02:00".into(),
|
||||
},
|
||||
swap: SwapConfig {
|
||||
enabled: true,
|
||||
path: "/etc/fstab".into(),
|
||||
size_mb: 2048,
|
||||
priority: 1,
|
||||
swappiness: 1,
|
||||
},
|
||||
};
|
||||
let mut cfg = base_config();
|
||||
cfg.swap.enabled = true;
|
||||
cfg.swap.path = "/etc/fstab".into();
|
||||
assert!(cfg.validate_config().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_firewall_rejects_empty_protocols() {
|
||||
let cfg = Config {
|
||||
amnezia: AmneziaConfig {
|
||||
container_names: vec!["x".into()],
|
||||
interface_candidates: vec!["awg0".into()],
|
||||
interface_patterns: vec!["awg*".into()],
|
||||
peer_ipv4_cidr: "10.8.1.0/24".into(),
|
||||
},
|
||||
traffic: TrafficConfig {
|
||||
enabled: true,
|
||||
peer_limit: "70mbit".into(),
|
||||
root_rate: "1000mbit".into(),
|
||||
reconcile_interval_secs: 15,
|
||||
container_ready_timeout_secs: 30,
|
||||
class_start_id: 10,
|
||||
ifb_name: "ifb0".into(),
|
||||
},
|
||||
host_firewall: HostFirewallConfig {
|
||||
amnezia_protocols: vec![],
|
||||
..HostFirewallConfig::default()
|
||||
},
|
||||
anti_torrent: AntiTorrentConfig {
|
||||
enabled: false,
|
||||
max_connections_per_peer: 100,
|
||||
peer_ipv4_cidr: "10.8.1.0/24".into(),
|
||||
apply_in_amnezia_netns: false,
|
||||
},
|
||||
reboot: RebootConfig {
|
||||
enabled: false,
|
||||
window_start_utc: "01:00".into(),
|
||||
window_end_utc: "02:00".into(),
|
||||
},
|
||||
swap: SwapConfig {
|
||||
enabled: false,
|
||||
path: "/swapfile".into(),
|
||||
size_mb: 2048,
|
||||
priority: 1,
|
||||
swappiness: 1,
|
||||
},
|
||||
};
|
||||
let mut cfg = base_config();
|
||||
cfg.host_firewall.amnezia_protocols = vec![];
|
||||
assert!(cfg.validate_config().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sysctl_tuning_default_is_valid() {
|
||||
let cfg = base_config();
|
||||
assert!(cfg.validate_config().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sysctl_tuning_rejects_invalid_tcp_bbr() {
|
||||
let mut cfg = base_config();
|
||||
cfg.sysctl_tuning.tcp_bbr = "bad".into();
|
||||
assert!(cfg.validate_config().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sysctl_tuning_rejects_invalid_default_qdisc() {
|
||||
let mut cfg = base_config();
|
||||
cfg.sysctl_tuning.default_qdisc = "cake".into();
|
||||
assert!(cfg.validate_config().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sysctl_tuning_rejects_rmem_max_below_1mib() {
|
||||
let mut cfg = base_config();
|
||||
cfg.sysctl_tuning.rmem_max = 1024;
|
||||
assert!(cfg.validate_config().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sysctl_tuning_rejects_rmem_max_above_512mib() {
|
||||
let mut cfg = base_config();
|
||||
cfg.sysctl_tuning.rmem_max = 513 * 1024 * 1024;
|
||||
assert!(cfg.validate_config().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sysctl_tuning_rejects_netdev_backlog_above_limit() {
|
||||
let mut cfg = base_config();
|
||||
cfg.sysctl_tuning.netdev_max_backlog = 100_001;
|
||||
assert!(cfg.validate_config().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sysctl_tuning_rejects_udp_rmem_min_below_limit() {
|
||||
let mut cfg = base_config();
|
||||
cfg.sysctl_tuning.udp_rmem_min = 4095;
|
||||
assert!(cfg.validate_config().is_err());
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crate::{config::Config, docker, firewall, iface, netns::build_transport, swap, traffic};
|
||||
use crate::{
|
||||
config::Config, docker, firewall, iface, netns::build_transport, swap, sysctl_tuning, traffic,
|
||||
};
|
||||
use tracing::{error, info};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
@@ -26,6 +28,17 @@ pub fn run(cfg: Config) -> anyhow::Result<()> {
|
||||
info!("starting daemon");
|
||||
let state = Arc::new(Mutex::new(LastActionState::default()));
|
||||
|
||||
if cfg.sysctl_tuning.enabled && cfg.sysctl_tuning.apply_on_daemon_start {
|
||||
if let Err(e) = sysctl_tuning::ensure_sysctl_tuning(&cfg) {
|
||||
set_err(&state, format!("startup network tuning reconcile: {e}"));
|
||||
append_event(&format!("startup_network_tuning error=\"{e}\""));
|
||||
return Err(e);
|
||||
} else {
|
||||
set_ok(&state, "startup network tuning reconcile ok");
|
||||
append_event("startup_network_tuning ok");
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = swap::ensure_swap(&cfg) {
|
||||
set_err(&state, format!("startup swap reconcile: {e}"));
|
||||
append_event(&format!("startup_swap error=\"{e}\""));
|
||||
|
||||
@@ -9,6 +9,7 @@ mod lock;
|
||||
mod netns;
|
||||
mod status;
|
||||
mod swap;
|
||||
mod sysctl_tuning;
|
||||
mod traffic;
|
||||
mod wg;
|
||||
|
||||
@@ -49,6 +50,18 @@ fn main() -> anyhow::Result<()> {
|
||||
traffic::reconcile_traffic(&cfg)?;
|
||||
println!("traffic limits reconciled");
|
||||
}
|
||||
Command::ApplyNetworkTuning => {
|
||||
let cfg = Config::load(&config_path)?;
|
||||
cfg.validate_all()?;
|
||||
sysctl_tuning::ensure_sysctl_tuning(&cfg)?;
|
||||
println!("network tuning reconciled");
|
||||
}
|
||||
Command::InspectNetworkTuning => {
|
||||
let cfg = Config::load(&config_path)?;
|
||||
cfg.validate_config()?;
|
||||
let s = sysctl_tuning::inspect_sysctl_tuning(&cfg)?;
|
||||
sysctl_tuning::print_sysctl_tuning_status(&s);
|
||||
}
|
||||
Command::ApplyFirewall => {
|
||||
let cfg = Config::load(&config_path)?;
|
||||
cfg.validate_all()?;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::{config::Config, docker, firewall, netns::build_transport, swap, traffic, wg};
|
||||
use crate::{
|
||||
config::Config, docker, firewall, netns::build_transport, swap, sysctl_tuning, traffic, wg,
|
||||
};
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
@@ -20,6 +22,8 @@ pub fn print_status(cfg: &Config) -> anyhow::Result<()> {
|
||||
Err(_) => println!(" service: unknown"),
|
||||
}
|
||||
|
||||
print_network_tuning_status(cfg);
|
||||
|
||||
let target = match docker::discover_target(cfg) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
@@ -270,6 +274,17 @@ pub fn print_status(cfg: &Config) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_network_tuning_status(cfg: &Config) {
|
||||
match sysctl_tuning::inspect_sysctl_tuning(cfg) {
|
||||
Ok(s) => sysctl_tuning::print_sysctl_tuning_status(&s),
|
||||
Err(e) => {
|
||||
println!("Network tuning:");
|
||||
println!(" enabled: {}", cfg.sysctl_tuning.enabled);
|
||||
println!(" drift: unknown ({e})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_degraded_sections(cfg: &Config) -> anyhow::Result<()> {
|
||||
println!("Peers:");
|
||||
println!(" unavailable (container/netns not discovered)");
|
||||
|
||||
@@ -0,0 +1,425 @@
|
||||
use std::{collections::BTreeMap, fs, io::Write, path::Path, process::Command, time::Duration};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
use crate::{config::Config, lock::FileLock};
|
||||
|
||||
const SYSCTL_CONF_PATH: &str = "/etc/sysctl.d/90-awg2-harness-vpn.conf";
|
||||
const MANAGED_MARKER: &str = "# Managed by awg2-harness sysctl_tuning";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SysctlTuningStatus {
|
||||
pub enabled: bool,
|
||||
pub apply_on_daemon_start: bool,
|
||||
pub tcp_bbr_mode: String,
|
||||
pub conf_path: String,
|
||||
pub persisted_present: bool,
|
||||
pub persisted_matches: bool,
|
||||
pub bbr_available: Option<bool>,
|
||||
pub bbr_active: Option<bool>,
|
||||
pub current: Vec<SysctlValue>,
|
||||
pub desired: Vec<SysctlValue>,
|
||||
pub drift_reason: Option<String>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SysctlValue {
|
||||
pub key: String,
|
||||
pub desired: Option<String>,
|
||||
pub current: Option<String>,
|
||||
pub managed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct RenderPlan {
|
||||
desired: BTreeMap<String, String>,
|
||||
persisted: String,
|
||||
bbr_available: Option<bool>,
|
||||
bbr_active: Option<bool>,
|
||||
warnings: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn ensure_sysctl_tuning(cfg: &Config) -> anyhow::Result<()> {
|
||||
if !cfg.sysctl_tuning.enabled {
|
||||
remove_managed_sysctl_file_if_disabled(cfg)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let _lock = FileLock::acquire_timeout(
|
||||
"/run/awg2-harness/sysctl_tuning.lock",
|
||||
Duration::from_secs(30),
|
||||
)?;
|
||||
|
||||
let plan = build_render_plan(cfg, true)?;
|
||||
write_atomic(SYSCTL_CONF_PATH, &plan.persisted)?;
|
||||
|
||||
if let Some(v) = plan.desired.get("net.ipv4.ip_forward") {
|
||||
let current = read_sysctl("net.ipv4.ip_forward").ok();
|
||||
if current.as_deref() != Some(v.as_str()) {
|
||||
write_sysctl("net.ipv4.ip_forward", v)?;
|
||||
}
|
||||
}
|
||||
|
||||
for (k, v) in &plan.desired {
|
||||
if k == "net.ipv4.ip_forward" {
|
||||
continue;
|
||||
}
|
||||
let current = read_sysctl(k).ok();
|
||||
if current.as_deref() != Some(v.as_str()) {
|
||||
write_sysctl(k, v)?;
|
||||
}
|
||||
}
|
||||
|
||||
for (k, v) in &plan.desired {
|
||||
let current = read_sysctl(k).unwrap_or_else(|_| "<unreadable>".to_string());
|
||||
if current != *v {
|
||||
anyhow::bail!("sysctl verify failed for {k}: current={current}, desired={v}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn inspect_sysctl_tuning(cfg: &Config) -> anyhow::Result<SysctlTuningStatus> {
|
||||
let plan = build_render_plan(cfg, false)?;
|
||||
let persisted_raw = fs::read_to_string(SYSCTL_CONF_PATH).ok();
|
||||
let persisted_present = persisted_raw.is_some();
|
||||
let persisted_matches = persisted_raw
|
||||
.as_deref()
|
||||
.map(|v| normalize(v) == normalize(&plan.persisted))
|
||||
.unwrap_or(false);
|
||||
|
||||
let mut values = Vec::<SysctlValue>::new();
|
||||
for (k, v) in &plan.desired {
|
||||
values.push(SysctlValue {
|
||||
key: k.clone(),
|
||||
desired: Some(v.clone()),
|
||||
current: read_sysctl(k).ok(),
|
||||
managed: true,
|
||||
});
|
||||
}
|
||||
|
||||
let mut drift_reason = None;
|
||||
if cfg.sysctl_tuning.enabled {
|
||||
if !persisted_present {
|
||||
drift_reason = Some("managed sysctl file is missing".to_string());
|
||||
} else if !persisted_matches {
|
||||
drift_reason = Some("persisted sysctl file differs from rendered config".to_string());
|
||||
} else if let Some(v) = values
|
||||
.iter()
|
||||
.find(|v| v.desired.as_deref() != v.current.as_deref())
|
||||
{
|
||||
drift_reason = Some(format!(
|
||||
"runtime value mismatch for {}: current={:?} desired={:?}",
|
||||
v.key, v.current, v.desired
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(SysctlTuningStatus {
|
||||
enabled: cfg.sysctl_tuning.enabled,
|
||||
apply_on_daemon_start: cfg.sysctl_tuning.apply_on_daemon_start,
|
||||
tcp_bbr_mode: cfg.sysctl_tuning.tcp_bbr.clone(),
|
||||
conf_path: SYSCTL_CONF_PATH.to_string(),
|
||||
persisted_present,
|
||||
persisted_matches,
|
||||
bbr_available: plan.bbr_available,
|
||||
bbr_active: plan.bbr_active,
|
||||
current: values.clone(),
|
||||
desired: values,
|
||||
drift_reason,
|
||||
warnings: plan.warnings,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn print_sysctl_tuning_status(status: &SysctlTuningStatus) {
|
||||
println!("Network tuning:");
|
||||
println!(" enabled: {}", status.enabled);
|
||||
println!(" apply_on_daemon_start: {}", status.apply_on_daemon_start);
|
||||
println!(" tcp_bbr_mode: {}", status.tcp_bbr_mode);
|
||||
println!(" conf_path: {}", status.conf_path);
|
||||
println!(
|
||||
" persisted_present: {}",
|
||||
if status.persisted_present {
|
||||
"yes"
|
||||
} else {
|
||||
"no"
|
||||
}
|
||||
);
|
||||
println!(
|
||||
" persisted_matches: {}",
|
||||
if status.persisted_matches {
|
||||
"yes"
|
||||
} else {
|
||||
"no"
|
||||
}
|
||||
);
|
||||
if let Some(v) = status.bbr_available {
|
||||
println!(" bbr_available: {}", if v { "yes" } else { "no" });
|
||||
}
|
||||
if let Some(v) = status.bbr_active {
|
||||
println!(" bbr_active: {}", if v { "yes" } else { "no" });
|
||||
}
|
||||
|
||||
for row in &status.current {
|
||||
println!(
|
||||
" {}: current={} desired={}",
|
||||
row.key,
|
||||
row.current.as_deref().unwrap_or("n/a"),
|
||||
row.desired.as_deref().unwrap_or("n/a")
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(reason) = &status.drift_reason {
|
||||
println!(" drift: yes");
|
||||
println!(" reason: {reason}");
|
||||
} else {
|
||||
println!(" drift: no");
|
||||
}
|
||||
for w in &status.warnings {
|
||||
println!(" warning: {w}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_managed_sysctl_file_if_disabled(cfg: &Config) -> anyhow::Result<()> {
|
||||
if cfg.sysctl_tuning.enabled {
|
||||
return Ok(());
|
||||
}
|
||||
let path = Path::new(SYSCTL_CONF_PATH);
|
||||
if !path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
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: {SYSCTL_CONF_PATH}");
|
||||
}
|
||||
fs::remove_file(path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_render_plan(cfg: &Config, allow_modprobe: bool) -> anyhow::Result<RenderPlan> {
|
||||
let mut desired = BTreeMap::<String, String>::new();
|
||||
let mut warnings = Vec::<String>::new();
|
||||
|
||||
if !cfg.sysctl_tuning.enabled {
|
||||
return Ok(RenderPlan {
|
||||
desired,
|
||||
persisted: render_persisted(&desired),
|
||||
bbr_available: None,
|
||||
bbr_active: None,
|
||||
warnings,
|
||||
});
|
||||
}
|
||||
|
||||
let mode = cfg.sysctl_tuning.tcp_bbr.trim().to_ascii_lowercase();
|
||||
let mut bbr_avail = bbr_available()?;
|
||||
if !bbr_avail && mode != "off" && allow_modprobe {
|
||||
let _ = Command::new("modprobe").args(["tcp_bbr"]).output();
|
||||
bbr_avail = bbr_available()?;
|
||||
}
|
||||
|
||||
let bbr_active = if mode == "off" {
|
||||
None
|
||||
} else if bbr_avail {
|
||||
Some(true)
|
||||
} else {
|
||||
if mode == "require" {
|
||||
anyhow::bail!("tcp_bbr=require but bbr is unavailable");
|
||||
}
|
||||
warnings.push("bbr unavailable; tcp tuning skipped in auto mode".to_string());
|
||||
Some(false)
|
||||
};
|
||||
|
||||
if cfg.sysctl_tuning.ip_forward {
|
||||
desired.insert("net.ipv4.ip_forward".to_string(), "1".to_string());
|
||||
}
|
||||
desired.insert(
|
||||
"net.core.rmem_max".to_string(),
|
||||
cfg.sysctl_tuning.rmem_max.to_string(),
|
||||
);
|
||||
desired.insert(
|
||||
"net.core.wmem_max".to_string(),
|
||||
cfg.sysctl_tuning.wmem_max.to_string(),
|
||||
);
|
||||
if cfg.sysctl_tuning.netdev_backlog_enabled {
|
||||
desired.insert(
|
||||
"net.core.netdev_max_backlog".to_string(),
|
||||
cfg.sysctl_tuning.netdev_max_backlog.to_string(),
|
||||
);
|
||||
}
|
||||
if cfg.sysctl_tuning.udp_rmem_min_enabled {
|
||||
desired.insert(
|
||||
"net.ipv4.udp_rmem_min".to_string(),
|
||||
cfg.sysctl_tuning.udp_rmem_min.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if bbr_active == Some(true) {
|
||||
desired.insert(
|
||||
"net.ipv4.tcp_congestion_control".to_string(),
|
||||
"bbr".to_string(),
|
||||
);
|
||||
let qdisc = cfg.sysctl_tuning.default_qdisc.trim().to_ascii_lowercase();
|
||||
if qdisc != "none" {
|
||||
desired.insert("net.core.default_qdisc".to_string(), qdisc);
|
||||
}
|
||||
}
|
||||
|
||||
let persisted = render_persisted(&desired);
|
||||
Ok(RenderPlan {
|
||||
desired,
|
||||
persisted,
|
||||
bbr_available: Some(bbr_avail),
|
||||
bbr_active,
|
||||
warnings,
|
||||
})
|
||||
}
|
||||
|
||||
fn bbr_available() -> anyhow::Result<bool> {
|
||||
let v = read_sysctl("net.ipv4.tcp_available_congestion_control")?;
|
||||
Ok(v.split_whitespace().any(|p| p == "bbr"))
|
||||
}
|
||||
|
||||
fn render_persisted(desired: &BTreeMap<String, String>) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str(MANAGED_MARKER);
|
||||
out.push('\n');
|
||||
out.push_str("# Source: /etc/awg2-harness/config.toml\n");
|
||||
out.push_str("# Do not edit this file manually; use [sysctl_tuning].\n\n");
|
||||
for (k, v) in desired {
|
||||
out.push_str(&format!("{k} = {v}\n"));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn read_sysctl(key: &str) -> anyhow::Result<String> {
|
||||
let out = Command::new("sysctl").args(["-n", key]).output()?;
|
||||
if !out.status.success() {
|
||||
anyhow::bail!(
|
||||
"sysctl -n {key} failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr).trim()
|
||||
);
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
|
||||
}
|
||||
|
||||
fn write_sysctl(key: &str, value: &str) -> anyhow::Result<()> {
|
||||
let out = Command::new("sysctl")
|
||||
.args(["-w", &format!("{key}={value}")])
|
||||
.output()?;
|
||||
if !out.status.success() {
|
||||
anyhow::bail!(
|
||||
"sysctl -w {key}={value} failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr).trim()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize(v: &str) -> String {
|
||||
v.lines().map(str::trim_end).collect::<Vec<_>>().join("\n")
|
||||
}
|
||||
|
||||
fn write_atomic(path: &str, content: &str) -> anyhow::Result<()> {
|
||||
let p = Path::new(path);
|
||||
let dir = p
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow::anyhow!("no parent directory for path: {path}"))?;
|
||||
let tmp_path = format!("{path}.tmp");
|
||||
|
||||
let mut f = fs::File::create(&tmp_path)?;
|
||||
f.write_all(content.as_bytes())?;
|
||||
f.sync_all()?;
|
||||
drop(f);
|
||||
|
||||
#[cfg(unix)]
|
||||
fs::set_permissions(&tmp_path, fs::Permissions::from_mode(0o644))?;
|
||||
|
||||
fs::rename(&tmp_path, path)?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let d = fs::File::open(dir)?;
|
||||
d.sync_all()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::{
|
||||
AmneziaConfig, AntiTorrentConfig, HostFirewallConfig, RebootConfig, SwapConfig,
|
||||
TrafficConfig,
|
||||
};
|
||||
|
||||
fn cfg() -> Config {
|
||||
Config {
|
||||
amnezia: AmneziaConfig {
|
||||
container_names: vec!["x".into()],
|
||||
interface_candidates: vec!["awg0".into()],
|
||||
interface_patterns: vec!["awg*".into()],
|
||||
peer_ipv4_cidr: "10.8.1.0/24".into(),
|
||||
},
|
||||
traffic: TrafficConfig {
|
||||
enabled: true,
|
||||
peer_limit: "70mbit".into(),
|
||||
root_rate: "1000mbit".into(),
|
||||
reconcile_interval_secs: 15,
|
||||
container_ready_timeout_secs: 30,
|
||||
class_start_id: 10,
|
||||
ifb_name: "ifb0".into(),
|
||||
},
|
||||
sysctl_tuning: crate::config::SysctlTuningConfig::default(),
|
||||
host_firewall: HostFirewallConfig::default(),
|
||||
anti_torrent: AntiTorrentConfig {
|
||||
enabled: false,
|
||||
max_connections_per_peer: 100,
|
||||
peer_ipv4_cidr: "10.8.1.0/24".into(),
|
||||
apply_in_amnezia_netns: false,
|
||||
},
|
||||
reboot: RebootConfig {
|
||||
enabled: false,
|
||||
window_start_utc: "01:00".into(),
|
||||
window_end_utc: "02:00".into(),
|
||||
},
|
||||
swap: SwapConfig {
|
||||
enabled: false,
|
||||
path: "/swapfile".into(),
|
||||
size_mb: 2048,
|
||||
priority: 1,
|
||||
swappiness: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_marker_is_present() {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert("net.core.rmem_max".to_string(), "1".to_string());
|
||||
let s = render_persisted(&map);
|
||||
assert!(s.starts_with(MANAGED_MARKER));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_udp_wmem_min_in_rendered_output() {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert("net.core.rmem_max".to_string(), "1".to_string());
|
||||
let s = render_persisted(&map);
|
||||
assert!(!s.contains("udp_wmem_min"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_refuses_unmanaged_file() {
|
||||
let mut c = cfg();
|
||||
c.sysctl_tuning.enabled = false;
|
||||
let e = remove_managed_sysctl_file_if_disabled(&c);
|
||||
if Path::new(SYSCTL_CONF_PATH).exists() {
|
||||
assert!(e.is_ok() || e.is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user