Полный продакшн-фикс: hardened build/install, drift/firewall/wg проверки и актуализация документации

This commit is contained in:
2026-05-10 15:41:44 +05:00
parent 535c1dbc33
commit b3dad5974c
11 changed files with 410 additions and 67 deletions
+187 -29
View File
@@ -58,6 +58,7 @@ pub fn inspect_drift_summary(cfg: &Config) -> anyhow::Result<Option<String>> {
&target.interface_name,
&cfg.traffic.ifb_name,
cfg.traffic.class_start_id,
&cfg.traffic.root_rate,
&cfg.traffic.peer_limit,
&peers,
)?;
@@ -93,6 +94,7 @@ pub fn reconcile_traffic(cfg: &Config) -> anyhow::Result<()> {
&target.interface_name,
&cfg.traffic.ifb_name,
cfg.traffic.class_start_id,
&cfg.traffic.root_rate,
&cfg.traffic.peer_limit,
&peers,
)?;
@@ -104,21 +106,26 @@ pub fn reconcile_traffic(cfg: &Config) -> anyhow::Result<()> {
println!("traffic drift detected: {}", drift.reason);
if cfg.traffic.full_rebuild_on_drift {
teardown_owned_topology(
transport.as_ref(),
&target.interface_name,
&cfg.traffic.ifb_name,
)?;
apply_full_topology(
cfg,
transport.as_ref(),
&target.interface_name,
&cfg.traffic.ifb_name,
&peers,
)?;
if !cfg.traffic.full_rebuild_on_drift {
anyhow::bail!(
"traffic drift detected but full_rebuild_on_drift=false: {}",
drift.reason
);
}
teardown_owned_topology(
transport.as_ref(),
&target.interface_name,
&cfg.traffic.ifb_name,
)?;
apply_full_topology(
cfg,
transport.as_ref(),
&target.interface_name,
&cfg.traffic.ifb_name,
&peers,
)?;
Ok(())
}
@@ -139,10 +146,26 @@ pub fn peer_status_table(cfg: &Config) -> anyhow::Result<Option<Vec<PeerTrafficS
}
let filters_egress = transport
.run(&["tc", "filter", "show", "dev", &target.interface_name, "parent", "1:"])
.run(&[
"tc",
"filter",
"show",
"dev",
&target.interface_name,
"parent",
"1:",
])
.unwrap_or_default();
let filters_ingress = transport
.run(&["tc", "filter", "show", "dev", &cfg.traffic.ifb_name, "parent", "2:"])
.run(&[
"tc",
"filter",
"show",
"dev",
&cfg.traffic.ifb_name,
"parent",
"2:",
])
.unwrap_or_default();
let classes_egress_full = transport
.run(&["tc", "class", "show", "dev", &target.interface_name])
@@ -151,8 +174,8 @@ pub fn peer_status_table(cfg: &Config) -> anyhow::Result<Option<Vec<PeerTrafficS
.run(&["tc", "class", "show", "dev", &cfg.traffic.ifb_name])
.unwrap_or_default();
let actual_map_egress = extract_filter_ip_class_map(&filters_egress);
let actual_map_ingress = extract_filter_ip_class_map(&filters_ingress);
let (actual_map_egress, dup_egress) = extract_filter_ip_class_map(&filters_egress);
let (actual_map_ingress, dup_ingress) = extract_filter_ip_class_map(&filters_ingress);
let egress_rates = parse_class_rate_ceil_map(&classes_egress_full, "1:");
let ingress_rates = parse_class_rate_ceil_map(&classes_ingress_full, "2:");
let expected_map = expected_peer_class_map(&peers, cfg.traffic.class_start_id);
@@ -193,6 +216,12 @@ pub fn peer_status_table(cfg: &Config) -> anyhow::Result<Option<Vec<PeerTrafficS
{
reasons.push("ingress rate/ceil mismatch".to_string());
}
if dup_egress.contains(&ip) {
reasons.push("duplicate egress filters".to_string());
}
if dup_ingress.contains(&ip) {
reasons.push("duplicate ingress filters".to_string());
}
out.push(PeerTrafficStatus {
ip,
@@ -219,6 +248,7 @@ fn inspect_and_diff(
iface: &str,
ifb: &str,
class_start_id: u16,
root_rate: &str,
peer_limit: &str,
peers: &[wg::Peer],
) -> anyhow::Result<TrafficDrift> {
@@ -259,8 +289,26 @@ fn inspect_and_diff(
.run(&["tc", "filter", "show", "dev", ifb, "parent", "2:"])
.unwrap_or_default();
let actual_map_egress = extract_filter_ip_class_map(&filters_egress);
let actual_map_ingress = extract_filter_ip_class_map(&filters_ingress);
let (actual_map_egress, dup_egress) = extract_filter_ip_class_map(&filters_egress);
let (actual_map_ingress, dup_ingress) = extract_filter_ip_class_map(&filters_ingress);
if !dup_egress.is_empty() {
return Ok(TrafficDrift {
has_drift: true,
reason: format!(
"duplicate egress filters detected for {} peer(s)",
dup_egress.len()
),
});
}
if !dup_ingress.is_empty() {
return Ok(TrafficDrift {
has_drift: true,
reason: format!(
"duplicate ingress filters detected for {} peer(s)",
dup_ingress.len()
),
});
}
let actual_ips_egress: BTreeSet<String> = actual_map_egress.keys().cloned().collect();
let actual_ips_ingress: BTreeSet<String> = actual_map_ingress.keys().cloned().collect();
let expected_ips: BTreeSet<String> = peers.iter().map(|p| p.ipv4.to_string()).collect();
@@ -316,9 +364,33 @@ fn inspect_and_diff(
let classes_ingress_full = transport
.run(&["tc", "class", "show", "dev", ifb])
.unwrap_or_default();
let expected_root = normalize_tc_rate(root_rate);
let egress_rates = parse_class_rate_ceil_map(&classes_egress_full, "1:");
let ingress_rates = parse_class_rate_ceil_map(&classes_ingress_full, "2:");
let root_egress = parse_root_class_rate_ceil(&classes_egress_full, "1:1");
let root_ingress = parse_root_class_rate_ceil(&classes_ingress_full, "2:1");
if root_egress
.as_ref()
.map(|v| !rate_ceil_matches_limit(v, &expected_root))
.unwrap_or(true)
{
return Ok(TrafficDrift {
has_drift: true,
reason: "egress root class 1:1 rate/ceil mismatch".to_string(),
});
}
if root_ingress
.as_ref()
.map(|v| !rate_ceil_matches_limit(v, &expected_root))
.unwrap_or(true)
{
return Ok(TrafficDrift {
has_drift: true,
reason: "ingress root class 2:1 rate/ceil mismatch".to_string(),
});
}
for class_id in expected_map.values() {
let class_egress = format!("1:{class_id}");
let class_ingress = format!("2:{class_id}");
@@ -330,8 +402,10 @@ fn inspect_and_diff(
reason: format!("missing rate/ceil class details for peer class_id={class_id}"),
});
}
if e.map(|v| !rate_ceil_matches_limit(v, &expected_limit)).unwrap_or(true)
|| i.map(|v| !rate_ceil_matches_limit(v, &expected_limit)).unwrap_or(true)
if e.map(|v| !rate_ceil_matches_limit(v, &expected_limit))
.unwrap_or(true)
|| i.map(|v| !rate_ceil_matches_limit(v, &expected_limit))
.unwrap_or(true)
{
return Ok(TrafficDrift {
has_drift: true,
@@ -347,27 +421,51 @@ fn inspect_and_diff(
.run(&["tc", "class", "show", "dev", ifb, "parent", "2:1"])
.unwrap_or_default();
let expected_class_count = peers.len();
if count_leaf_classes(&classes_egress) < expected_class_count {
if count_leaf_classes(&classes_egress) != expected_class_count {
return Ok(TrafficDrift {
has_drift: true,
reason: "missing egress per-peer classes".to_string(),
reason: "egress per-peer class count mismatch".to_string(),
});
}
if count_leaf_classes(&classes_ingress) < expected_class_count {
if count_leaf_classes(&classes_ingress) != expected_class_count {
return Ok(TrafficDrift {
has_drift: true,
reason: "missing ingress per-peer classes".to_string(),
reason: "ingress per-peer class count mismatch".to_string(),
});
}
let qdisc_egress = transport
.run(&["tc", "qdisc", "show", "dev", iface])
.unwrap_or_default();
let qdisc_ingress = transport
.run(&["tc", "qdisc", "show", "dev", ifb])
.unwrap_or_default();
for class_id in expected_map.values() {
let class_egress = format!("1:{class_id}");
let class_ingress = format!("2:{class_id}");
if !has_fq_codel_for_parent(&qdisc_egress, &class_egress) {
return Ok(TrafficDrift {
has_drift: true,
reason: format!("missing fq_codel for egress class {class_egress}"),
});
}
if !has_fq_codel_for_parent(&qdisc_ingress, &class_ingress) {
return Ok(TrafficDrift {
has_drift: true,
reason: format!("missing fq_codel for ingress class {class_ingress}"),
});
}
}
Ok(TrafficDrift {
has_drift: false,
reason: "no drift".to_string(),
})
}
fn extract_filter_ip_class_map(filters: &str) -> BTreeMap<String, u16> {
fn extract_filter_ip_class_map(filters: &str) -> (BTreeMap<String, u16>, BTreeSet<String>) {
let mut out = BTreeMap::new();
let mut duplicates = BTreeSet::new();
for block in split_filter_blocks(filters) {
let mut ip: Option<String> = None;
@@ -384,11 +482,46 @@ fn extract_filter_ip_class_map(filters: &str) -> BTreeMap<String, u16> {
}
if let (Some(ip), Some(class_id)) = (ip, class_id) {
if out.contains_key(&ip) {
duplicates.insert(ip.clone());
}
out.insert(ip, class_id);
}
}
out
(out, duplicates)
}
fn has_fq_codel_for_parent(raw_qdisc: &str, parent_class: &str) -> bool {
raw_qdisc.lines().map(str::trim).any(|line| {
line.contains("fq_codel") && line.contains("parent") && line.contains(parent_class)
})
}
fn parse_root_class_rate_ceil(raw: &str, class_id: &str) -> Option<ClassRateCeil> {
for line in raw.lines() {
let t = line.trim();
if !t.contains("class htb")
|| !t.contains(class_id)
|| !t.contains(" rate ")
|| !t.contains(" ceil ")
{
continue;
}
let toks: Vec<&str> = t.split_whitespace().collect();
let rate = toks
.iter()
.position(|v| *v == "rate")
.and_then(|i| toks.get(i + 1))
.map(|s| s.to_string())?;
let ceil = toks
.iter()
.position(|v| *v == "ceil")
.and_then(|i| toks.get(i + 1))
.map(|s| s.to_string())?;
return Some(ClassRateCeil { rate, ceil });
}
None
}
fn split_filter_blocks(raw: &str) -> Vec<String> {
@@ -649,6 +782,7 @@ fn apply_full_topology(
iface,
ifb,
cfg.traffic.class_start_id,
&cfg.traffic.root_rate,
&cfg.traffic.peer_limit,
peers,
)?;
@@ -742,18 +876,42 @@ mod tests {
#[test]
fn parse_tc_u32_hex_ipv4() {
let raw = " match 0a080105/ffffffff at 16\n flowid 1:42";
let m = extract_filter_ip_class_map(raw);
let (m, dup) = extract_filter_ip_class_map(raw);
assert!(dup.is_empty());
assert_eq!(m.get("10.8.1.5").copied(), Some(42));
}
#[test]
fn parse_block_based_filters() {
let raw = "filter parent 1: protocol ip pref 1 u32\n match 0a080102/ffffffff at 16\n flowid 1:10\nfilter parent 1: protocol ip pref 2 u32\n match 0a080103/ffffffff at 16\n flowid 1:11";
let m = extract_filter_ip_class_map(raw);
let (m, dup) = extract_filter_ip_class_map(raw);
assert!(dup.is_empty());
assert_eq!(m.get("10.8.1.2").copied(), Some(10));
assert_eq!(m.get("10.8.1.3").copied(), Some(11));
}
#[test]
fn detect_duplicate_filter_ip() {
let raw = "filter parent 1: protocol ip pref 1 u32\n match 0a080102/ffffffff at 16\n flowid 1:10\nfilter parent 1: protocol ip pref 2 u32\n match 0a080102/ffffffff at 16\n flowid 1:10";
let (_m, dup) = extract_filter_ip_class_map(raw);
assert!(dup.contains("10.8.1.2"));
}
#[test]
fn detect_fq_codel_parent() {
let raw = "qdisc fq_codel 8001: dev wg0 parent 1:10 limit 10240p";
assert!(has_fq_codel_for_parent(raw, "1:10"));
assert!(!has_fq_codel_for_parent(raw, "1:11"));
}
#[test]
fn parse_root_rate_ceil() {
let raw = "class htb 1:1 root rate 1000Mbit ceil 1000Mbit";
let rc = parse_root_class_rate_ceil(raw, "1:1").expect("root class exists");
assert_eq!(normalize_tc_rate(&rc.rate), "1000mbit");
assert_eq!(normalize_tc_rate(&rc.ceil), "1000mbit");
}
#[test]
fn parse_class_rate_ceil() {
let raw = "class htb 1:10 parent 1:1 rate 70Mbit ceil 70Mbit burst 1599b cburst 1599b\nclass htb 1:1 root rate 1000Mbit ceil 1000Mbit";