fix7: продакшен-реализация полного reconcile swap и интеграций

- Полностью переписан swap-модуль в режиме exclusive ownership\n- Добавлены verify/checks, статус фактического swap-state и drift\n- Интегрирован ensure-swap в daemon и systemd ExecStartPre\n- Усилен lock API (таймауты) и применение в traffic/firewall/swap\n- Усилен parser firewall (JSON-first + устойчивый text fallback)\n- Обновлены config validation, runtime tools и default priority\n- Синхронизированы docs и build/package hygiene\n- Удалены member Cargo.lock в пользу корневого lockfile
This commit is contained in:
2026-05-11 17:30:15 +05:00
parent 819cc9c308
commit 7a4e9764b1
17 changed files with 842 additions and 957 deletions
+82 -10
View File
@@ -1,9 +1,11 @@
use crate::{
config::Config,
docker,
lock::FileLock,
netns::{NsTransport, build_host_transport, build_transport},
};
use serde_json::Value;
use std::time::Duration;
#[derive(Debug, Clone)]
struct FirewallActual {
@@ -20,6 +22,9 @@ struct FirewallActual {
}
pub fn apply_firewall(cfg: &Config) -> anyhow::Result<()> {
let _lock =
FileLock::acquire_timeout("/run/awg2-harness/firewall.lock", Duration::from_secs(10))?;
if !cfg.anti_torrent.enabled {
apply_firewall_disabled(cfg)?;
return Ok(());
@@ -94,6 +99,13 @@ fn apply_firewall_disabled(cfg: &Config) -> anyhow::Result<()> {
}
fn inspect_firewall(transport: &dyn NsTransport, cfg: &Config) -> anyhow::Result<FirewallActual> {
let json = transport.run(&["nft", "-j", "list", "table", "inet", "awg2_harness"]);
if let Ok(raw) = json {
if let Some(actual) = inspect_firewall_json(&raw, cfg)? {
return Ok(actual);
}
}
let table = match transport.run(&["nft", "list", "table", "inet", "awg2_harness"]) {
Ok(out) => out,
Err(e) if is_nft_not_found(&e.to_string()) => {
@@ -113,15 +125,16 @@ fn inspect_firewall(transport: &dyn NsTransport, cfg: &Config) -> anyhow::Result
Err(e) => return Err(anyhow::anyhow!("nft table inspect failed: {e}")),
};
let table_exists = !table.trim().is_empty();
let counter_exists = table.contains("counter antitorrent_dropped_v4");
let chain_exists = table.contains("chain forward_antitorrent");
let hook_forward_present = table.contains("hook forward");
let policy_accept_present = table.contains("policy accept");
let ct_state_new_present = table.contains("ct state new");
let meter_present = table.contains("meter awg2_peer_connlimit_v4");
let counter_name_used = table.contains("counter name antitorrent_dropped_v4");
let drop_present = table.contains(" drop") || table.contains("\ndrop");
let nt = normalize_nft_text(&table);
let table_exists = !nt.trim().is_empty();
let counter_exists = nt.contains("counter antitorrent_dropped_v4");
let chain_exists = nt.contains("chain forward_antitorrent");
let hook_forward_present = nt.contains("hook forward");
let policy_accept_present = nt.contains("policy accept");
let ct_state_new_present = nt.contains("ct state new");
let meter_present = nt.contains("meter awg2_peer_connlimit_v4");
let counter_name_used = nt.contains("counter name antitorrent_dropped_v4");
let drop_present = nt.contains(" drop");
let rule_matches = firewall_rule_contract_matches(
&table,
@@ -292,12 +305,15 @@ fn firewall_drift_reason(actual: &FirewallActual) -> String {
}
fn firewall_rule_contract_matches(table: &str, cidr: &str, max_conn: u32) -> bool {
let table = normalize_nft_text(table);
let needle_cidr = format!("ip saddr {cidr}");
let needle_max = format!("ct count over {max_conn}");
table.contains("table inet awg2_harness")
&& table.contains("counter antitorrent_dropped_v4")
&& table.contains("chain forward_antitorrent")
&& table.contains("type filter hook forward priority filter; policy accept")
&& table.contains("type filter hook forward")
&& (table.contains("priority filter") || table.contains("priority 0"))
&& table.contains("policy accept")
&& table.contains(&needle_cidr)
&& table.contains("ct state new")
&& table.contains("meter awg2_peer_connlimit_v4")
@@ -306,6 +322,48 @@ fn firewall_rule_contract_matches(table: &str, cidr: &str, max_conn: u32) -> boo
&& table.contains("drop")
}
fn normalize_nft_text(input: &str) -> String {
input
.replace(['\n', '\t', '{', '}', ';', ','], " ")
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
}
fn inspect_firewall_json(raw: &str, cfg: &Config) -> anyhow::Result<Option<FirewallActual>> {
let v: Value = serde_json::from_str(raw)?;
let Some(arr) = v.get("nftables").and_then(|x| x.as_array()) else {
return Ok(None);
};
let mut text = String::new();
for item in arr {
text.push_str(&item.to_string());
text.push(' ');
}
let nt = normalize_nft_text(&text);
let table_exists = nt.contains("awg2_harness");
if !table_exists {
return Ok(None);
}
let actual = FirewallActual {
table_exists,
counter_exists: nt.contains("antitorrent_dropped_v4"),
chain_exists: nt.contains("forward_antitorrent"),
rule_matches: firewall_rule_contract_matches(
&nt,
&cfg.anti_torrent.peer_ipv4_cidr,
cfg.anti_torrent.max_connections_per_peer,
),
hook_forward_present: nt.contains("hook") && nt.contains("forward"),
policy_accept_present: nt.contains("policy") && nt.contains("accept"),
ct_state_new_present: nt.contains("ct") && nt.contains("state") && nt.contains("new"),
meter_present: nt.contains("awg2_peer_connlimit_v4"),
counter_name_used: nt.contains("antitorrent_dropped_v4"),
drop_present: nt.contains("drop"),
};
Ok(Some(actual))
}
pub fn antitorrent_drop_counter(cfg: &Config) -> anyhow::Result<Option<(u64, u64)>> {
if !cfg.anti_torrent.enabled {
return Ok(None);
@@ -412,4 +470,18 @@ table inet awg2_harness {
"#;
assert!(!firewall_rule_contract_matches(table, "10.8.1.0/24", 100));
}
#[test]
fn nft_text_parser_tolerates_priority_0_and_spacing() {
let table = r#"
table inet awg2_harness {
counter antitorrent_dropped_v4
chain forward_antitorrent {
type filter hook forward priority 0; policy accept;
ip saddr 10.8.1.0/24 ct state new meter awg2_peer_connlimit_v4 { ip saddr ct count over 100 } counter name antitorrent_dropped_v4 drop
}
}
"#;
assert!(firewall_rule_contract_matches(table, "10.8.1.0/24", 100));
}
}