Fix21: прод-фиксы swap/firewall/wg без легаси

- Swap: допуск небольшой дельты usable size при verify/compare

- Firewall: поддержка Debian 13 dynamic set/add @set вместо только literal meter

- WG: приоритет docker exec для peer/runtime discovery с безопасным fallback
This commit is contained in:
2026-05-12 13:37:37 +05:00
parent c463e64205
commit ff2ae98d30
3 changed files with 83 additions and 19 deletions
+31 -3
View File
@@ -234,7 +234,7 @@ fn inspect_firewall(transport: &dyn NsTransport, cfg: &Config) -> anyhow::Result
let hook_forward_present = nt.contains("hook forward"); let hook_forward_present = nt.contains("hook forward");
let policy_accept_present = nt.contains("policy accept"); let policy_accept_present = nt.contains("policy accept");
let ct_state_new_present = nt.contains("ct state new"); let ct_state_new_present = nt.contains("ct state new");
let meter_present = nt.contains("meter awg2_peer_connlimit_v4"); let meter_present = connlimit_state_present(&nt);
let counter_name_used = nt.contains("counter name antitorrent_dropped_v4"); let counter_name_used = nt.contains("counter name antitorrent_dropped_v4");
let drop_present = nt.contains(" drop"); let drop_present = nt.contains(" drop");
@@ -470,7 +470,7 @@ fn firewall_drift_reason(actual: &FirewallActual) -> String {
return "missing ct state new".to_string(); return "missing ct state new".to_string();
} }
if !actual.meter_present { if !actual.meter_present {
return "missing nft meter".to_string(); return "missing nft meter/dynamic set".to_string();
} }
if !actual.counter_name_used { if !actual.counter_name_used {
return "missing nft counter name usage".to_string(); return "missing nft counter name usage".to_string();
@@ -496,12 +496,19 @@ fn firewall_rule_contract_matches(table: &str, cidr: &str, max_conn: u32) -> boo
&& table.contains("policy accept") && table.contains("policy accept")
&& table.contains(&needle_cidr) && table.contains(&needle_cidr)
&& table.contains("ct state new") && table.contains("ct state new")
&& table.contains("meter awg2_peer_connlimit_v4") && connlimit_state_present(&table)
&& table.contains(&needle_max) && table.contains(&needle_max)
&& table.contains("counter name antitorrent_dropped_v4") && table.contains("counter name antitorrent_dropped_v4")
&& table.contains("drop") && table.contains("drop")
} }
fn connlimit_state_present(table: &str) -> bool {
table.contains("meter awg2_peer_connlimit_v4")
|| (table.contains("set awg2_peer_connlimit_v4")
&& table.contains("flags dynamic")
&& table.contains("add @awg2_peer_connlimit_v4"))
}
fn normalize_nft_text(input: &str) -> String { fn normalize_nft_text(input: &str) -> String {
input input
.replace(['\n', '\t', '{', '}', ';', ',', '"'], " ") .replace(['\n', '\t', '{', '}', ';', ',', '"'], " ")
@@ -633,6 +640,27 @@ table inet awg2_harness {
assert!(firewall_rule_contract_matches(table, "10.8.1.0/24", 100)); assert!(firewall_rule_contract_matches(table, "10.8.1.0/24", 100));
} }
#[test]
fn nft_rule_contract_accepts_debian13_dynamic_set_rendering() {
let table = r#"
table inet awg2_harness {
counter antitorrent_dropped_v4 {
packets 0 bytes 0
}
set awg2_peer_connlimit_v4 {
type ipv4_addr
size 65535
flags dynamic
}
chain forward_antitorrent {
type filter hook forward priority filter; policy accept;
ip saddr 10.8.1.0/24 ct state new add @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));
}
#[test] #[test]
fn host_firewall_parser_tolerates_quoted_lo_and_ipv6_icmp_alias() { fn host_firewall_parser_tolerates_quoted_lo_and_ipv6_icmp_alias() {
let table = r#" let table = r#"
+17 -2
View File
@@ -195,7 +195,9 @@ fn disable_foreign_swaps(desired: &DesiredSwap, active: &[ActiveSwap]) -> anyhow
if entry.name != desired.path { if entry.name != desired.path {
run("swapoff", &[entry.name.to_string_lossy().as_ref()])?; run("swapoff", &[entry.name.to_string_lossy().as_ref()])?;
maybe_delete_foreign_swapfile(entry, &desired.path)?; maybe_delete_foreign_swapfile(entry, &desired.path)?;
} else if entry.size_bytes != desired.size_bytes || entry.priority != desired.priority { } else if !active_swap_size_matches(entry.size_bytes, desired.size_bytes)
|| entry.priority != desired.priority
{
run("swapoff", &[desired.path.to_string_lossy().as_ref()])?; run("swapoff", &[desired.path.to_string_lossy().as_ref()])?;
} }
} }
@@ -428,7 +430,7 @@ fn verify_exclusive_swap_state(desired: &DesiredSwap) -> anyhow::Result<()> {
desired.path.display() desired.path.display()
); );
} }
if only.size_bytes != desired.size_bytes { if !active_swap_size_matches(only.size_bytes, desired.size_bytes) {
anyhow::bail!( anyhow::bail!(
"swap verify failed: size mismatch {} != {}", "swap verify failed: size mismatch {} != {}",
only.size_bytes, only.size_bytes,
@@ -490,6 +492,13 @@ fn normalize_ws(s: &str) -> String {
s.split_whitespace().collect::<Vec<_>>().join(" ") s.split_whitespace().collect::<Vec<_>>().join(" ")
} }
fn active_swap_size_matches(active_bytes: u64, desired_file_bytes: u64) -> bool {
if active_bytes == desired_file_bytes {
return true;
}
active_bytes < desired_file_bytes && desired_file_bytes - active_bytes <= 1024 * 1024
}
fn run(program: &str, args: &[&str]) -> anyhow::Result<()> { fn run(program: &str, args: &[&str]) -> anyhow::Result<()> {
let out = Command::new(program).args(args).output()?; let out = Command::new(program).args(args).output()?;
if !out.status.success() { if !out.status.success() {
@@ -542,6 +551,12 @@ mod tests {
assert_eq!(got[0].priority, 1); assert_eq!(got[0].priority, 1);
} }
#[test]
fn active_swap_size_accepts_mkswap_usable_size_delta() {
assert!(active_swap_size_matches(2147479552, 2147483648));
assert!(!active_swap_size_matches(2146435072, 2147483648));
}
#[test] #[test]
fn parse_swapon_rejects_malformed_size() { fn parse_swapon_rejects_malformed_size() {
let err = parse_swapon_output("/swapfile file abc 1\n"); let err = parse_swapon_output("/swapfile file abc 1\n");
+35 -14
View File
@@ -21,15 +21,25 @@ pub struct PeerRuntime {
} }
pub fn discover_peers(pid: u32, container: &str, peer_cidr: &str) -> anyhow::Result<Vec<Peer>> { pub fn discover_peers(pid: u32, container: &str, peer_cidr: &str) -> anyhow::Result<Vec<Peer>> {
let boxed: Box<dyn NsTransport> = if nsenter_available() { let docker = DockerExecTransport {
Box::new(NsenterTransport { pid }) container: container.to_string(),
} else {
Box::new(DockerExecTransport {
container: container.to_string(),
})
}; };
let docker_result = discover_peers_with_transport(&docker, peer_cidr);
if let Ok(peers) = &docker_result
&& (!peers.is_empty() || !nsenter_available())
{
return docker_result;
}
discover_peers_with_transport(boxed.as_ref(), peer_cidr) if nsenter_available() {
let nsenter = NsenterTransport { pid };
match discover_peers_with_transport(&nsenter, peer_cidr) {
Ok(peers) if !peers.is_empty() => Ok(peers),
_ => docker_result,
}
} else {
docker_result
}
} }
pub fn discover_peers_with_transport( pub fn discover_peers_with_transport(
@@ -57,14 +67,25 @@ pub fn discover_peer_runtime(
container: &str, container: &str,
peer_cidr: &str, peer_cidr: &str,
) -> anyhow::Result<Vec<PeerRuntime>> { ) -> anyhow::Result<Vec<PeerRuntime>> {
let boxed: Box<dyn NsTransport> = if nsenter_available() { let docker = DockerExecTransport {
Box::new(NsenterTransport { pid }) container: container.to_string(),
} else {
Box::new(DockerExecTransport {
container: container.to_string(),
})
}; };
discover_peer_runtime_with_transport(boxed.as_ref(), peer_cidr) let docker_result = discover_peer_runtime_with_transport(&docker, peer_cidr);
if let Ok(peers) = &docker_result
&& (!peers.is_empty() || !nsenter_available())
{
return docker_result;
}
if nsenter_available() {
let nsenter = NsenterTransport { pid };
match discover_peer_runtime_with_transport(&nsenter, peer_cidr) {
Ok(peers) if !peers.is_empty() => Ok(peers),
_ => docker_result,
}
} else {
docker_result
}
} }
pub fn discover_peer_runtime_with_transport( pub fn discover_peer_runtime_with_transport(