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
+60 -2
View File
@@ -26,11 +26,18 @@ fn default_input_policy() -> String {
"drop".to_string()
}
fn default_sysctl_tuning_for_missing_block() -> SysctlTuningConfig {
SysctlTuningConfig {
enabled: false,
..SysctlTuningConfig::default()
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct Config {
pub amnezia: AmneziaConfig,
pub traffic: TrafficConfig,
#[serde(default)]
#[serde(default = "default_sysctl_tuning_for_missing_block")]
pub sysctl_tuning: SysctlTuningConfig,
pub host_firewall: HostFirewallConfig,
pub anti_torrent: AntiTorrentConfig,
@@ -296,7 +303,7 @@ impl Config {
required.push("sysctl");
}
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");
}
@@ -649,4 +656,55 @@ swappiness = 1
let parsed = toml::from_str::<Config>(raw);
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,
ifb_name: "ifb0".into(),
},
sysctl_tuning: crate::config::SysctlTuningConfig::default(),
host_firewall: crate::config::HostFirewallConfig::default(),
anti_torrent: crate::config::AntiTorrentConfig {
enabled: false,
@@ -753,6 +754,7 @@ table inet awg2_host_firewall {
class_start_id: 10,
ifb_name: "ifb0".into(),
},
sysctl_tuning: crate::config::SysctlTuningConfig::default(),
host_firewall: crate::config::HostFirewallConfig::default(),
anti_torrent: crate::config::AntiTorrentConfig {
enabled: false,
@@ -798,6 +800,7 @@ table inet awg2_host_firewall {
class_start_id: 10,
ifb_name: "ifb0".into(),
},
sysctl_tuning: crate::config::SysctlTuningConfig::default(),
host_firewall: crate::config::HostFirewallConfig::default(),
anti_torrent: crate::config::AntiTorrentConfig {
enabled: false,
@@ -855,6 +858,7 @@ table inet awg2_host_firewall {
class_start_id: 10,
ifb_name: "ifb0".into(),
},
sysctl_tuning: crate::config::SysctlTuningConfig::default(),
host_firewall: crate::config::HostFirewallConfig::default(),
anti_torrent: crate::config::AntiTorrentConfig {
enabled: false,
+6
View File
@@ -71,6 +71,7 @@ impl Drop for FileLock {
}
}
#[cfg(unix)]
fn is_stale_lock(path: &Path) -> anyhow::Result<bool> {
let pid_raw = fs::read_to_string(path)?;
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())
}
#[cfg(not(unix))]
fn is_stale_lock(_path: &Path) -> anyhow::Result<bool> {
Ok(false)
}
#[cfg(test)]
mod tests {
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)?;
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)?;
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> {
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_present = persisted_raw.is_some();
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 {
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() {
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}");
anyhow::bail!("refusing to remove unmanaged sysctl file: {}", path.display());
}
fs::remove_file(path)?;
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> {
let mut desired = BTreeMap::<String, String>::new();
let mut warnings = Vec::<String>::new();
if !cfg.sysctl_tuning.enabled {
let persisted = render_persisted(&desired);
return Ok(RenderPlan {
desired,
persisted: render_persisted(&desired),
persisted,
bbr_available: None,
bbr_active: None,
warnings,
@@ -331,13 +374,14 @@ fn write_atomic(path: &str, content: &str) -> anyhow::Result<()> {
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))?;
f.write_all(content.as_bytes())?;
f.sync_all()?;
drop(f);
fs::rename(&tmp_path, path)?;
#[cfg(unix)]
@@ -415,11 +459,69 @@ mod tests {
#[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());
let path = std::env::temp_dir().join(format!(
"awg2_harness_unmanaged_sysctl_test_{}.conf",
std::process::id()
));
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);
}
}