Production sysctl_tuning hardening and upgrade-safe defaults

This commit is contained in:
2026-05-28 12:01:08 +05:00
parent 2c27bff800
commit 10b2d0aefb
5 changed files with 191 additions and 15 deletions
+7 -1
View File
@@ -117,7 +117,13 @@ swappiness = 1
- runtime apply: только `sysctl -w` и только для drifted keys; - runtime apply: только `sysctl -w` и только для drifted keys;
- на установке `.deb` модуль не применяется; - на установке `.deb` модуль не применяется;
- на старте daemon модуль применяется, если `enabled=true` и `apply_on_daemon_start=true`; - на старте daemon модуль применяется, если `enabled=true` и `apply_on_daemon_start=true`;
- `tcp_bbr = auto` не валит запуск при недоступном BBR (warning), `require` — валит. - `tcp_bbr = auto` не валит запуск при недоступном BBR (warning), `require` — валит;
- host runtime tools: `modprobe` обязателен только для `tcp_bbr = "require"`.
Совместимость обновлений:
- если в существующем runtime-конфиге отсутствует блок `[sysctl_tuning]`, он не включается автоматически (`enabled=false` по default для missing block);
- для новых установок рекомендуется явный блок `[sysctl_tuning]` из примера, где `enabled=true`.
Ограничения модуля: Ограничения модуля:
+60 -2
View File
@@ -26,11 +26,18 @@ fn default_input_policy() -> String {
"drop".to_string() "drop".to_string()
} }
fn default_sysctl_tuning_for_missing_block() -> SysctlTuningConfig {
SysctlTuningConfig {
enabled: false,
..SysctlTuningConfig::default()
}
}
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Config { pub struct Config {
pub amnezia: AmneziaConfig, pub amnezia: AmneziaConfig,
pub traffic: TrafficConfig, pub traffic: TrafficConfig,
#[serde(default)] #[serde(default = "default_sysctl_tuning_for_missing_block")]
pub sysctl_tuning: SysctlTuningConfig, pub sysctl_tuning: SysctlTuningConfig,
pub host_firewall: HostFirewallConfig, pub host_firewall: HostFirewallConfig,
pub anti_torrent: AntiTorrentConfig, pub anti_torrent: AntiTorrentConfig,
@@ -296,7 +303,7 @@ impl Config {
required.push("sysctl"); required.push("sysctl");
} }
if self.sysctl_tuning.enabled if self.sysctl_tuning.enabled
&& self.sysctl_tuning.tcp_bbr.trim().to_ascii_lowercase() != "off" && self.sysctl_tuning.tcp_bbr.trim().to_ascii_lowercase() == "require"
{ {
required.push("modprobe"); required.push("modprobe");
} }
@@ -649,4 +656,55 @@ swappiness = 1
let parsed = toml::from_str::<Config>(raw); let parsed = toml::from_str::<Config>(raw);
assert!(parsed.is_err()); assert!(parsed.is_err());
} }
#[test]
fn missing_sysctl_tuning_block_defaults_to_disabled() {
let raw = r#"
[amnezia]
container_names = ["amnezia-awg2"]
interface_candidates = ["awg0"]
interface_patterns = ["awg*"]
peer_ipv4_cidr = "10.8.1.0/24"
[traffic]
enabled = true
peer_limit = "70mbit"
root_rate = "1000mbit"
reconcile_interval_secs = 15
container_ready_timeout_secs = 30
class_start_id = 10
ifb_name = "ifb0"
[host_firewall]
enabled = true
ssh_port = 2323
amnezia_port = 443
amnezia_protocols = ["udp"]
allow_icmp = true
default_input_policy = "drop"
[anti_torrent]
enabled = false
max_connections_per_peer = 100
peer_ipv4_cidr = "10.8.1.0/24"
apply_in_amnezia_netns = false
[reboot]
enabled = false
window_start_utc = "01:00"
window_end_utc = "02:00"
[swap]
enabled = false
path = "/swapfile"
size_mb = 2048
priority = 1
swappiness = 1
"#;
let cfg = toml::from_str::<Config>(raw).expect("config parse");
assert!(!cfg.sysctl_tuning.enabled);
assert!(cfg.sysctl_tuning.apply_on_daemon_start);
assert_eq!(cfg.sysctl_tuning.tcp_bbr, "auto");
}
} }
+4
View File
@@ -692,6 +692,7 @@ table inet awg2_host_firewall {
class_start_id: 10, class_start_id: 10,
ifb_name: "ifb0".into(), ifb_name: "ifb0".into(),
}, },
sysctl_tuning: crate::config::SysctlTuningConfig::default(),
host_firewall: crate::config::HostFirewallConfig::default(), host_firewall: crate::config::HostFirewallConfig::default(),
anti_torrent: crate::config::AntiTorrentConfig { anti_torrent: crate::config::AntiTorrentConfig {
enabled: false, enabled: false,
@@ -753,6 +754,7 @@ table inet awg2_host_firewall {
class_start_id: 10, class_start_id: 10,
ifb_name: "ifb0".into(), ifb_name: "ifb0".into(),
}, },
sysctl_tuning: crate::config::SysctlTuningConfig::default(),
host_firewall: crate::config::HostFirewallConfig::default(), host_firewall: crate::config::HostFirewallConfig::default(),
anti_torrent: crate::config::AntiTorrentConfig { anti_torrent: crate::config::AntiTorrentConfig {
enabled: false, enabled: false,
@@ -798,6 +800,7 @@ table inet awg2_host_firewall {
class_start_id: 10, class_start_id: 10,
ifb_name: "ifb0".into(), ifb_name: "ifb0".into(),
}, },
sysctl_tuning: crate::config::SysctlTuningConfig::default(),
host_firewall: crate::config::HostFirewallConfig::default(), host_firewall: crate::config::HostFirewallConfig::default(),
anti_torrent: crate::config::AntiTorrentConfig { anti_torrent: crate::config::AntiTorrentConfig {
enabled: false, enabled: false,
@@ -855,6 +858,7 @@ table inet awg2_host_firewall {
class_start_id: 10, class_start_id: 10,
ifb_name: "ifb0".into(), ifb_name: "ifb0".into(),
}, },
sysctl_tuning: crate::config::SysctlTuningConfig::default(),
host_firewall: crate::config::HostFirewallConfig::default(), host_firewall: crate::config::HostFirewallConfig::default(),
anti_torrent: crate::config::AntiTorrentConfig { anti_torrent: crate::config::AntiTorrentConfig {
enabled: false, enabled: false,
+6
View File
@@ -71,6 +71,7 @@ impl Drop for FileLock {
} }
} }
#[cfg(unix)]
fn is_stale_lock(path: &Path) -> anyhow::Result<bool> { fn is_stale_lock(path: &Path) -> anyhow::Result<bool> {
let pid_raw = fs::read_to_string(path)?; let pid_raw = fs::read_to_string(path)?;
let pid = match pid_raw.trim().parse::<u32>() { let pid = match pid_raw.trim().parse::<u32>() {
@@ -82,6 +83,11 @@ fn is_stale_lock(path: &Path) -> anyhow::Result<bool> {
Ok(!Path::new(&proc_path).exists()) Ok(!Path::new(&proc_path).exists())
} }
#[cfg(not(unix))]
fn is_stale_lock(_path: &Path) -> anyhow::Result<bool> {
Ok(false)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+114 -12
View File
@@ -53,6 +53,10 @@ pub fn ensure_sysctl_tuning(cfg: &Config) -> anyhow::Result<()> {
)?; )?;
let plan = build_render_plan(cfg, true)?; let plan = build_render_plan(cfg, true)?;
for w in &plan.warnings {
tracing::warn!(target: "sysctl_tuning", "{w}");
}
ensure_managed_or_absent(SYSCTL_CONF_PATH)?;
write_atomic(SYSCTL_CONF_PATH, &plan.persisted)?; write_atomic(SYSCTL_CONF_PATH, &plan.persisted)?;
if let Some(v) = plan.desired.get("net.ipv4.ip_forward") { if let Some(v) = plan.desired.get("net.ipv4.ip_forward") {
@@ -83,7 +87,25 @@ pub fn ensure_sysctl_tuning(cfg: &Config) -> anyhow::Result<()> {
} }
pub fn inspect_sysctl_tuning(cfg: &Config) -> anyhow::Result<SysctlTuningStatus> { pub fn inspect_sysctl_tuning(cfg: &Config) -> anyhow::Result<SysctlTuningStatus> {
let plan = build_render_plan(cfg, false)?; let plan = match build_render_plan(cfg, false) {
Ok(plan) => plan,
Err(e) => {
return 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: Path::new(SYSCTL_CONF_PATH).exists(),
persisted_matches: false,
bbr_available: None,
bbr_active: None,
current: Vec::new(),
desired: Vec::new(),
drift_reason: Some(format!("render plan unavailable: {e}")),
warnings: vec![format!("render plan error: {e}")],
});
}
};
let persisted_raw = fs::read_to_string(SYSCTL_CONF_PATH).ok(); let persisted_raw = fs::read_to_string(SYSCTL_CONF_PATH).ok();
let persisted_present = persisted_raw.is_some(); let persisted_present = persisted_raw.is_some();
let persisted_matches = persisted_raw let persisted_matches = persisted_raw
@@ -187,27 +209,48 @@ pub fn remove_managed_sysctl_file_if_disabled(cfg: &Config) -> anyhow::Result<()
if cfg.sysctl_tuning.enabled { if cfg.sysctl_tuning.enabled {
return Ok(()); return Ok(());
} }
let path = Path::new(SYSCTL_CONF_PATH);
remove_managed_sysctl_file_at(Path::new(SYSCTL_CONF_PATH))
}
fn remove_managed_sysctl_file_at(path: &Path) -> anyhow::Result<()> {
if !path.exists() { if !path.exists() {
return Ok(()); return Ok(());
} }
let raw = fs::read_to_string(path)?; let raw = fs::read_to_string(path)?;
let first = raw.lines().next().unwrap_or(""); let first = raw.lines().next().unwrap_or("");
if !first.contains(MANAGED_MARKER) { if !first.contains(MANAGED_MARKER) {
anyhow::bail!("refusing to remove unmanaged sysctl file: {SYSCTL_CONF_PATH}"); anyhow::bail!("refusing to remove unmanaged sysctl file: {}", path.display());
} }
fs::remove_file(path)?; fs::remove_file(path)?;
Ok(()) Ok(())
} }
fn ensure_managed_or_absent(path: &str) -> anyhow::Result<()> {
let p = Path::new(path);
if !p.exists() {
return Ok(());
}
let raw = fs::read_to_string(p)?;
let first = raw.lines().next().unwrap_or("");
if !first.contains(MANAGED_MARKER) {
anyhow::bail!("refusing to overwrite unmanaged sysctl file: {path}");
}
Ok(())
}
fn build_render_plan(cfg: &Config, allow_modprobe: bool) -> anyhow::Result<RenderPlan> { fn build_render_plan(cfg: &Config, allow_modprobe: bool) -> anyhow::Result<RenderPlan> {
let mut desired = BTreeMap::<String, String>::new(); let mut desired = BTreeMap::<String, String>::new();
let mut warnings = Vec::<String>::new(); let mut warnings = Vec::<String>::new();
if !cfg.sysctl_tuning.enabled { if !cfg.sysctl_tuning.enabled {
let persisted = render_persisted(&desired);
return Ok(RenderPlan { return Ok(RenderPlan {
desired, desired,
persisted: render_persisted(&desired), persisted,
bbr_available: None, bbr_available: None,
bbr_active: None, bbr_active: None,
warnings, warnings,
@@ -331,13 +374,14 @@ fn write_atomic(path: &str, content: &str) -> anyhow::Result<()> {
let tmp_path = format!("{path}.tmp"); let tmp_path = format!("{path}.tmp");
let mut f = fs::File::create(&tmp_path)?; let mut f = fs::File::create(&tmp_path)?;
f.write_all(content.as_bytes())?;
f.sync_all()?;
drop(f);
#[cfg(unix)] #[cfg(unix)]
fs::set_permissions(&tmp_path, fs::Permissions::from_mode(0o644))?; fs::set_permissions(&tmp_path, fs::Permissions::from_mode(0o644))?;
f.write_all(content.as_bytes())?;
f.sync_all()?;
drop(f);
fs::rename(&tmp_path, path)?; fs::rename(&tmp_path, path)?;
#[cfg(unix)] #[cfg(unix)]
@@ -415,11 +459,69 @@ mod tests {
#[test] #[test]
fn remove_refuses_unmanaged_file() { fn remove_refuses_unmanaged_file() {
let mut c = cfg(); let path = std::env::temp_dir().join(format!(
c.sysctl_tuning.enabled = false; "awg2_harness_unmanaged_sysctl_test_{}.conf",
let e = remove_managed_sysctl_file_if_disabled(&c); std::process::id()
if Path::new(SYSCTL_CONF_PATH).exists() { ));
assert!(e.is_ok() || e.is_err());
fs::write(&path, "# manual file\nnet.core.rmem_max = 1\n").expect("write unmanaged test");
let e = remove_managed_sysctl_file_at(&path);
assert!(e.is_err());
let _ = fs::remove_file(&path);
}
#[test]
fn remove_deletes_managed_file() {
let path = std::env::temp_dir().join(format!(
"awg2_harness_managed_sysctl_test_{}.conf",
std::process::id()
));
fs::write(&path, format!("{MANAGED_MARKER}\nnet.core.rmem_max = 1\n"))
.expect("write managed test");
remove_managed_sysctl_file_at(&path).expect("remove managed file");
assert!(!path.exists());
}
#[test]
fn ensure_refuses_overwrite_unmanaged_file() {
let path = std::env::temp_dir().join(format!(
"awg2_harness_unmanaged_overwrite_test_{}.conf",
std::process::id()
));
fs::write(&path, "# manual file\nnet.core.wmem_max = 1\n").expect("write unmanaged");
let path_str = path.to_string_lossy().to_string();
let e = ensure_managed_or_absent(&path_str);
assert!(e.is_err());
let _ = fs::remove_file(&path);
}
#[test]
fn ensure_allows_absent_or_managed_file() {
let absent = std::env::temp_dir().join(format!(
"awg2_harness_absent_test_{}.conf",
std::process::id()
));
if absent.exists() {
let _ = fs::remove_file(&absent);
} }
let absent_str = absent.to_string_lossy().to_string();
ensure_managed_or_absent(&absent_str).expect("allow absent");
let managed = std::env::temp_dir().join(format!(
"awg2_harness_managed_overwrite_test_{}.conf",
std::process::id()
));
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);
} }
} }