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:
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user