Реализован 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:
+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());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user