Реализован 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:
2026-05-28 11:31:13 +05:00
parent 4ff2bb2c30
commit 2c27bff800
13 changed files with 719 additions and 79 deletions
+165 -76
View File
@@ -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());
}