Production sysctl_tuning hardening and upgrade-safe defaults
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user