fix10: стабилизация host firewall first-run и no-op reconcile
- исправлен first-run дефект: delete table вынесен из nft batch\n- добавлен no-op путь при отсутствии host firewall drift\n- исправлен trim для default_input_policy в drift-check\n- добавлены тесты для batch script и policy trim\n- уточнен release-gate в acceptance checklist по INPUT/FORWARD
This commit is contained in:
@@ -128,6 +128,7 @@
|
|||||||
- [ ] При невалидном `host_firewall.amnezia_protocols` (`icmp`, `foo`, дубли) `validate-config` завершается ошибкой.
|
- [ ] При невалидном `host_firewall.amnezia_protocols` (`icmp`, `foo`, дубли) `validate-config` завершается ошибкой.
|
||||||
- [ ] После `docker restart` anti-torrent правила продолжают восстанавливаться корректно и не ломаются host firewall логикой.
|
- [ ] После `docker restart` anti-torrent правила продолжают восстанавливаться корректно и не ломаются host firewall логикой.
|
||||||
- [ ] Проверено, где реально проходит VPN-трафик в вашей Docker-схеме (`INPUT` vs `FORWARD`) и это отражено в эксплуатационной заметке.
|
- [ ] Проверено, где реально проходит VPN-трафик в вашей Docker-схеме (`INPUT` vs `FORWARD`) и это отражено в эксплуатационной заметке.
|
||||||
|
- [ ] Для release-кандидата зафиксирован итог проверки `INPUT` vs `FORWARD` на реальном сервере (команды/логи/вывод) и подтверждено, что текущие правила firewall покрывают реальный путь трафика.
|
||||||
|
|
||||||
## K. Packaging / release
|
## K. Packaging / release
|
||||||
|
|
||||||
|
|||||||
@@ -121,12 +121,21 @@ fn apply_host_firewall(cfg: &Config) -> anyhow::Result<()> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if host_firewall_drift_reason(cfg)?.is_none() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let policy = cfg
|
let policy = cfg
|
||||||
.host_firewall
|
.host_firewall
|
||||||
.default_input_policy
|
.default_input_policy
|
||||||
.trim()
|
.trim()
|
||||||
.to_ascii_lowercase();
|
.to_ascii_lowercase();
|
||||||
|
|
||||||
|
run_allow_notfound(
|
||||||
|
transport.as_ref(),
|
||||||
|
&["nft", "delete", "table", "inet", HOST_FW_TABLE],
|
||||||
|
)?;
|
||||||
|
|
||||||
apply_host_firewall_batch(cfg, &policy)?;
|
apply_host_firewall_batch(cfg, &policy)?;
|
||||||
|
|
||||||
if let Some(reason) = host_firewall_drift_reason(cfg)? {
|
if let Some(reason) = host_firewall_drift_reason(cfg)? {
|
||||||
@@ -136,8 +145,12 @@ fn apply_host_firewall(cfg: &Config) -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn apply_host_firewall_batch(cfg: &Config, policy: &str) -> anyhow::Result<()> {
|
fn apply_host_firewall_batch(cfg: &Config, policy: &str) -> anyhow::Result<()> {
|
||||||
|
let batch = build_host_firewall_batch_script(cfg, policy);
|
||||||
|
run_nft_batch(&batch)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_host_firewall_batch_script(cfg: &Config, policy: &str) -> String {
|
||||||
let mut batch = String::new();
|
let mut batch = String::new();
|
||||||
batch.push_str(&format!("delete table inet {HOST_FW_TABLE}\n"));
|
|
||||||
batch.push_str(&format!("add table inet {HOST_FW_TABLE}\n"));
|
batch.push_str(&format!("add table inet {HOST_FW_TABLE}\n"));
|
||||||
batch.push_str(&format!(
|
batch.push_str(&format!(
|
||||||
"add chain inet {HOST_FW_TABLE} input {{ type filter hook input priority filter; policy accept; }}\n"
|
"add chain inet {HOST_FW_TABLE} input {{ type filter hook input priority filter; policy accept; }}\n"
|
||||||
@@ -170,7 +183,7 @@ fn apply_host_firewall_batch(cfg: &Config, policy: &str) -> anyhow::Result<()> {
|
|||||||
batch.push_str(&format!(
|
batch.push_str(&format!(
|
||||||
"set chain inet {HOST_FW_TABLE} input {{ policy {policy}; }}\n"
|
"set chain inet {HOST_FW_TABLE} input {{ policy {policy}; }}\n"
|
||||||
));
|
));
|
||||||
run_nft_batch(&batch)
|
batch
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_nft_batch(script: &str) -> anyhow::Result<()> {
|
fn run_nft_batch(script: &str) -> anyhow::Result<()> {
|
||||||
@@ -296,7 +309,10 @@ fn host_firewall_drift_reason_from_text(nt: &str, cfg: &Config) -> Option<String
|
|||||||
}
|
}
|
||||||
let pol = format!(
|
let pol = format!(
|
||||||
"policy {}",
|
"policy {}",
|
||||||
cfg.host_firewall.default_input_policy.to_ascii_lowercase()
|
cfg.host_firewall
|
||||||
|
.default_input_policy
|
||||||
|
.trim()
|
||||||
|
.to_ascii_lowercase()
|
||||||
);
|
);
|
||||||
if !nt.contains(&pol) {
|
if !nt.contains(&pol) {
|
||||||
return Some("wrong input policy".into());
|
return Some("wrong input policy".into());
|
||||||
@@ -669,4 +685,104 @@ table inet awg2_host_firewall {
|
|||||||
let nt = normalize_nft_text(table);
|
let nt = normalize_nft_text(table);
|
||||||
assert_eq!(host_firewall_drift_reason_from_text(&nt, &cfg), None);
|
assert_eq!(host_firewall_drift_reason_from_text(&nt, &cfg), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn host_firewall_batch_script_does_not_include_delete_table() {
|
||||||
|
let cfg = Config {
|
||||||
|
amnezia: crate::config::AmneziaConfig {
|
||||||
|
container_names: vec!["amnezia-awg2".into()],
|
||||||
|
interface_candidates: vec!["awg0".into()],
|
||||||
|
interface_patterns: vec!["awg*".into()],
|
||||||
|
peer_ipv4_cidr: "10.8.1.0/24".into(),
|
||||||
|
},
|
||||||
|
traffic: crate::config::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: crate::config::HostFirewallConfig::default(),
|
||||||
|
anti_torrent: crate::config::AntiTorrentConfig {
|
||||||
|
enabled: false,
|
||||||
|
max_connections_per_peer: 100,
|
||||||
|
peer_ipv4_cidr: "10.8.1.0/24".into(),
|
||||||
|
apply_in_amnezia_netns: false,
|
||||||
|
},
|
||||||
|
reboot: crate::config::RebootConfig {
|
||||||
|
enabled: false,
|
||||||
|
window_start_utc: "01:00".into(),
|
||||||
|
window_end_utc: "02:00".into(),
|
||||||
|
},
|
||||||
|
swap: crate::config::SwapConfig {
|
||||||
|
enabled: false,
|
||||||
|
path: "/swapfile".into(),
|
||||||
|
size_mb: 2048,
|
||||||
|
priority: 1,
|
||||||
|
swappiness: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let script = build_host_firewall_batch_script(&cfg, "drop");
|
||||||
|
assert!(script.contains("add table inet awg2_host_firewall"));
|
||||||
|
assert!(!script.contains("delete table inet awg2_host_firewall"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn host_firewall_policy_drift_check_trims_policy_value() {
|
||||||
|
let table = r#"
|
||||||
|
table inet awg2_host_firewall {
|
||||||
|
chain input {
|
||||||
|
type filter hook input priority filter; policy drop;
|
||||||
|
iif "lo" accept
|
||||||
|
ct state established,related accept
|
||||||
|
tcp dport 2323 accept
|
||||||
|
udp dport 443 accept
|
||||||
|
ip protocol icmp accept
|
||||||
|
ip6 nexthdr icmpv6 accept
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let mut cfg = Config {
|
||||||
|
amnezia: crate::config::AmneziaConfig {
|
||||||
|
container_names: vec!["amnezia-awg2".into()],
|
||||||
|
interface_candidates: vec!["awg0".into()],
|
||||||
|
interface_patterns: vec!["awg*".into()],
|
||||||
|
peer_ipv4_cidr: "10.8.1.0/24".into(),
|
||||||
|
},
|
||||||
|
traffic: crate::config::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: crate::config::HostFirewallConfig::default(),
|
||||||
|
anti_torrent: crate::config::AntiTorrentConfig {
|
||||||
|
enabled: false,
|
||||||
|
max_connections_per_peer: 100,
|
||||||
|
peer_ipv4_cidr: "10.8.1.0/24".into(),
|
||||||
|
apply_in_amnezia_netns: false,
|
||||||
|
},
|
||||||
|
reboot: crate::config::RebootConfig {
|
||||||
|
enabled: false,
|
||||||
|
window_start_utc: "01:00".into(),
|
||||||
|
window_end_utc: "02:00".into(),
|
||||||
|
},
|
||||||
|
swap: crate::config::SwapConfig {
|
||||||
|
enabled: false,
|
||||||
|
path: "/swapfile".into(),
|
||||||
|
size_mb: 2048,
|
||||||
|
priority: 1,
|
||||||
|
swappiness: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
cfg.host_firewall.default_input_policy = " drop ".into();
|
||||||
|
let nt = normalize_nft_text(table);
|
||||||
|
assert_eq!(host_firewall_drift_reason_from_text(&nt, &cfg), None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user