fix8: продакшн host firewall + dual reconcile firewall

- добавлена секция host_firewall в конфиг и defaults\n- реализован host firewall reconcile в отдельной таблице\n- apply-firewall теперь делает host firewall + anti-torrent\n- убрана JSON-инспекция из full-table drift для anti-torrent\n- расширен status вывод drift по host firewall/anti-torrent
This commit is contained in:
2026-05-11 18:14:46 +05:00
parent 7a4e9764b1
commit 381c106f9d
6 changed files with 466 additions and 55 deletions
+134
View File
@@ -2,10 +2,36 @@ use std::{fs, net::Ipv4Addr, path::Path, process::Command};
use serde::Deserialize;
fn default_host_firewall_enabled() -> bool {
true
}
fn default_ssh_port() -> u16 {
2323
}
fn default_amnezia_port() -> u16 {
443
}
fn default_amnezia_protocols() -> Vec<String> {
vec!["udp".to_string()]
}
fn default_allow_icmp() -> bool {
true
}
fn default_input_policy() -> String {
"drop".to_string()
}
#[derive(Debug, Clone, Deserialize)]
pub struct Config {
pub amnezia: AmneziaConfig,
pub traffic: TrafficConfig,
#[serde(default)]
pub host_firewall: HostFirewallConfig,
pub anti_torrent: AntiTorrentConfig,
pub reboot: RebootConfig,
pub swap: SwapConfig,
@@ -38,6 +64,35 @@ pub struct AntiTorrentConfig {
pub apply_in_amnezia_netns: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HostFirewallConfig {
#[serde(default = "default_host_firewall_enabled")]
pub enabled: bool,
#[serde(default = "default_ssh_port")]
pub ssh_port: u16,
#[serde(default = "default_amnezia_port")]
pub amnezia_port: u16,
#[serde(default = "default_amnezia_protocols")]
pub amnezia_protocols: Vec<String>,
#[serde(default = "default_allow_icmp")]
pub allow_icmp: bool,
#[serde(default = "default_input_policy")]
pub default_input_policy: String,
}
impl Default for HostFirewallConfig {
fn default() -> Self {
Self {
enabled: default_host_firewall_enabled(),
ssh_port: default_ssh_port(),
amnezia_port: default_amnezia_port(),
amnezia_protocols: default_amnezia_protocols(),
allow_icmp: default_allow_icmp(),
default_input_policy: default_input_policy(),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct RebootConfig {
pub enabled: bool,
@@ -115,6 +170,37 @@ impl Config {
if self.anti_torrent.enabled && self.anti_torrent.max_connections_per_peer == 0 {
anyhow::bail!("anti_torrent.max_connections_per_peer must be > 0");
}
if self.host_firewall.ssh_port == 0 {
anyhow::bail!("host_firewall.ssh_port must be in 1..=65535");
}
if self.host_firewall.amnezia_port == 0 {
anyhow::bail!("host_firewall.amnezia_port must be in 1..=65535");
}
if self.host_firewall.amnezia_protocols.is_empty() {
anyhow::bail!(
"host_firewall.amnezia_protocols must not be empty; allowed values: tcp, udp"
);
}
let mut normalized = std::collections::BTreeSet::new();
for p in &self.host_firewall.amnezia_protocols {
let pp = p.trim().to_ascii_lowercase();
if pp != "tcp" && pp != "udp" {
anyhow::bail!(
"host_firewall.amnezia_protocols contains invalid value: {p}; allowed: tcp, udp"
);
}
if !normalized.insert(pp) {
anyhow::bail!("host_firewall.amnezia_protocols contains duplicate protocol: {p}");
}
}
let policy = self
.host_firewall
.default_input_policy
.trim()
.to_ascii_lowercase();
if policy != "drop" && policy != "accept" {
anyhow::bail!("host_firewall.default_input_policy must be one of: drop, accept");
}
if self.reboot.enabled && self.reboot.window_start_utc == self.reboot.window_end_utc {
anyhow::bail!("reboot window start and end must differ");
}
@@ -146,6 +232,9 @@ impl Config {
if self.anti_torrent.enabled {
required.push("nft");
}
if self.host_firewall.enabled {
required.push("nft");
}
if self.reboot.enabled {
required.push("systemctl");
}
@@ -319,6 +408,7 @@ mod tests {
class_start_id: 10,
ifb_name: "ifb0".into(),
},
host_firewall: HostFirewallConfig::default(),
anti_torrent: AntiTorrentConfig {
enabled: false,
max_connections_per_peer: 100,
@@ -340,4 +430,48 @@ mod tests {
};
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,
},
};
assert!(cfg.validate_config().is_err());
}
}