Полный production-рефакторинг билдера, drift-reconcile и документации по up5

This commit is contained in:
2026-05-10 16:42:09 +05:00
parent b3dad5974c
commit b8d2c4592b
12 changed files with 571 additions and 266 deletions
+150 -78
View File
@@ -17,6 +17,13 @@ struct TrafficDrift {
reason: String,
}
#[derive(Debug, Clone)]
struct FilterInspect {
map: BTreeMap<String, u16>,
duplicate_ips: BTreeSet<String>,
duplicate_classes: BTreeSet<u16>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ClassRateCeil {
rate: String,
@@ -106,13 +113,6 @@ pub fn reconcile_traffic(cfg: &Config) -> anyhow::Result<()> {
println!("traffic drift detected: {}", drift.reason);
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,
@@ -174,8 +174,10 @@ 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, dup_egress) = extract_filter_ip_class_map(&filters_egress);
let (actual_map_ingress, dup_ingress) = extract_filter_ip_class_map(&filters_ingress);
let inspect_egress = extract_filter_ip_class_map(&filters_egress);
let inspect_ingress = extract_filter_ip_class_map(&filters_ingress);
let actual_map_egress = inspect_egress.map;
let actual_map_ingress = inspect_ingress.map;
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);
@@ -216,10 +218,10 @@ 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) {
if inspect_egress.duplicate_ips.contains(&ip) {
reasons.push("duplicate egress filters".to_string());
}
if dup_ingress.contains(&ip) {
if inspect_ingress.duplicate_ips.contains(&ip) {
reasons.push("duplicate ingress filters".to_string());
}
@@ -252,9 +254,11 @@ fn inspect_and_diff(
peer_limit: &str,
peers: &[wg::Peer],
) -> anyhow::Result<TrafficDrift> {
let root_qdisc = transport
.run(&["tc", "qdisc", "show", "dev", iface])
.unwrap_or_default();
let root_qdisc = inspect_cmd(
transport,
&["tc", "qdisc", "show", "dev", iface],
"egress qdisc",
)?;
if !root_qdisc.contains("htb") {
return Ok(TrafficDrift {
has_drift: true,
@@ -262,9 +266,11 @@ fn inspect_and_diff(
});
}
let ingress_qdisc = transport
.run(&["tc", "qdisc", "show", "dev", iface])
.unwrap_or_default();
let ingress_qdisc = inspect_cmd(
transport,
&["tc", "qdisc", "show", "dev", iface],
"ingress qdisc",
)?;
if !ingress_qdisc.contains("ingress") {
return Ok(TrafficDrift {
has_drift: true,
@@ -272,43 +278,65 @@ fn inspect_and_diff(
});
}
let ifb_link = transport
.run(&["ip", "link", "show", ifb])
.unwrap_or_default();
let ifb_link = inspect_cmd(transport, &["ip", "link", "show", ifb], "ifb link")?;
if ifb_link.trim().is_empty() {
return Ok(TrafficDrift {
has_drift: true,
reason: "missing ifb interface".to_string(),
});
}
if !ifb_link.contains(" UP ") && !ifb_link.contains("state UP") {
return Ok(TrafficDrift {
has_drift: true,
reason: "ifb interface is not UP".to_string(),
});
}
let filters_egress = transport
.run(&["tc", "filter", "show", "dev", iface, "parent", "1:"])
.unwrap_or_default();
let filters_ingress = transport
.run(&["tc", "filter", "show", "dev", ifb, "parent", "2:"])
.unwrap_or_default();
let filters_egress = inspect_cmd(
transport,
&["tc", "filter", "show", "dev", iface, "parent", "1:"],
"egress filters",
)?;
let filters_ingress = inspect_cmd(
transport,
&["tc", "filter", "show", "dev", ifb, "parent", "2:"],
"ingress filters",
)?;
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() {
let inspect_egress = extract_filter_ip_class_map(&filters_egress);
let inspect_ingress = extract_filter_ip_class_map(&filters_ingress);
if !inspect_egress.duplicate_ips.is_empty() {
return Ok(TrafficDrift {
has_drift: true,
reason: format!(
"duplicate egress filters detected for {} peer(s)",
dup_egress.len()
inspect_egress.duplicate_ips.len()
),
});
}
if !dup_ingress.is_empty() {
if !inspect_egress.duplicate_classes.is_empty() {
return Ok(TrafficDrift {
has_drift: true,
reason: "duplicate egress class flowid mapping detected".to_string(),
});
}
if !inspect_ingress.duplicate_ips.is_empty() {
return Ok(TrafficDrift {
has_drift: true,
reason: format!(
"duplicate ingress filters detected for {} peer(s)",
dup_ingress.len()
inspect_ingress.duplicate_ips.len()
),
});
}
if !inspect_ingress.duplicate_classes.is_empty() {
return Ok(TrafficDrift {
has_drift: true,
reason: "duplicate ingress class flowid mapping detected".to_string(),
});
}
let actual_map_egress = inspect_egress.map;
let actual_map_ingress = inspect_ingress.map;
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();
@@ -358,12 +386,16 @@ fn inspect_and_diff(
}
let expected_limit = normalize_tc_rate(peer_limit);
let classes_egress_full = transport
.run(&["tc", "class", "show", "dev", iface])
.unwrap_or_default();
let classes_ingress_full = transport
.run(&["tc", "class", "show", "dev", ifb])
.unwrap_or_default();
let classes_egress_full = inspect_cmd(
transport,
&["tc", "class", "show", "dev", iface],
"egress classes",
)?;
let classes_ingress_full = inspect_cmd(
transport,
&["tc", "class", "show", "dev", ifb],
"ingress classes",
)?;
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:");
@@ -414,32 +446,40 @@ fn inspect_and_diff(
}
}
let classes_egress = transport
.run(&["tc", "class", "show", "dev", iface, "parent", "1:1"])
.unwrap_or_default();
let classes_ingress = transport
.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 {
let classes_egress = inspect_cmd(
transport,
&["tc", "class", "show", "dev", iface, "parent", "1:1"],
"egress leaf classes",
)?;
let classes_ingress = inspect_cmd(
transport,
&["tc", "class", "show", "dev", ifb, "parent", "2:1"],
"ingress leaf classes",
)?;
let expected_set: BTreeSet<u16> = expected_map.values().copied().collect();
if collect_leaf_class_ids(&classes_egress) != expected_set {
return Ok(TrafficDrift {
has_drift: true,
reason: "egress per-peer class count mismatch".to_string(),
reason: "egress per-peer class set mismatch".to_string(),
});
}
if count_leaf_classes(&classes_ingress) != expected_class_count {
if collect_leaf_class_ids(&classes_ingress) != expected_set {
return Ok(TrafficDrift {
has_drift: true,
reason: "ingress per-peer class count mismatch".to_string(),
reason: "ingress per-peer class set 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();
let qdisc_egress = inspect_cmd(
transport,
&["tc", "qdisc", "show", "dev", iface],
"egress leaf qdisc",
)?;
let qdisc_ingress = inspect_cmd(
transport,
&["tc", "qdisc", "show", "dev", ifb],
"ingress leaf qdisc",
)?;
for class_id in expected_map.values() {
let class_egress = format!("1:{class_id}");
let class_ingress = format!("2:{class_id}");
@@ -463,9 +503,17 @@ fn inspect_and_diff(
})
}
fn extract_filter_ip_class_map(filters: &str) -> (BTreeMap<String, u16>, BTreeSet<String>) {
fn inspect_cmd(transport: &dyn NsTransport, args: &[&str], what: &str) -> anyhow::Result<String> {
transport
.run(args)
.map_err(|e| anyhow::anyhow!("{what} inspect failed: {e}"))
}
fn extract_filter_ip_class_map(filters: &str) -> FilterInspect {
let mut out = BTreeMap::new();
let mut duplicates = BTreeSet::new();
let mut duplicate_ips = BTreeSet::new();
let mut class_to_ip = BTreeMap::new();
let mut duplicate_classes = BTreeSet::new();
for block in split_filter_blocks(filters) {
let mut ip: Option<String> = None;
@@ -483,13 +531,20 @@ fn extract_filter_ip_class_map(filters: &str) -> (BTreeMap<String, u16>, BTreeSe
if let (Some(ip), Some(class_id)) = (ip, class_id) {
if out.contains_key(&ip) {
duplicates.insert(ip.clone());
duplicate_ips.insert(ip.clone());
}
if class_to_ip.insert(class_id, ip.clone()).is_some() {
duplicate_classes.insert(class_id);
}
out.insert(ip, class_id);
}
}
(out, duplicates)
FilterInspect {
map: out,
duplicate_ips,
duplicate_classes,
}
}
fn has_fq_codel_for_parent(raw_qdisc: &str, parent_class: &str) -> bool {
@@ -585,16 +640,24 @@ fn hex_to_ipv4_string(hex: &str) -> Option<String> {
Some(Ipv4Addr::new(b1, b2, b3, b4).to_string())
}
fn count_leaf_classes(raw: &str) -> usize {
raw.lines()
.filter_map(|l| {
let line = l.trim();
let pos = line.find("class htb ")?;
let class = &line[(pos + "class htb ".len())..];
let head = class.split_whitespace().next()?;
if head.ends_with(":1") { None } else { Some(()) }
})
.count()
fn collect_leaf_class_ids(raw: &str) -> BTreeSet<u16> {
let mut out = BTreeSet::new();
for l in raw.lines() {
let line = l.trim();
let Some(pos) = line.find("class htb ") else {
continue;
};
let class = &line[(pos + "class htb ".len())..];
let Some(head) = class.split_whitespace().next() else {
continue;
};
if let Some(class_id) = head.split(':').nth(1).and_then(|v| v.parse::<u16>().ok())
&& class_id != 1
{
out.insert(class_id);
}
}
out
}
fn teardown_owned_topology(
@@ -876,25 +939,34 @@ mod tests {
#[test]
fn parse_tc_u32_hex_ipv4() {
let raw = " match 0a080105/ffffffff at 16\n flowid 1:42";
let (m, dup) = extract_filter_ip_class_map(raw);
assert!(dup.is_empty());
assert_eq!(m.get("10.8.1.5").copied(), Some(42));
let inspect = extract_filter_ip_class_map(raw);
assert!(inspect.duplicate_ips.is_empty());
assert!(inspect.duplicate_classes.is_empty());
assert_eq!(inspect.map.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, 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));
let inspect = extract_filter_ip_class_map(raw);
assert!(inspect.duplicate_ips.is_empty());
assert!(inspect.duplicate_classes.is_empty());
assert_eq!(inspect.map.get("10.8.1.2").copied(), Some(10));
assert_eq!(inspect.map.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"));
let inspect = extract_filter_ip_class_map(raw);
assert!(inspect.duplicate_ips.contains("10.8.1.2"));
}
#[test]
fn detect_duplicate_filter_class_flowid() {
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:10";
let inspect = extract_filter_ip_class_map(raw);
assert!(inspect.duplicate_classes.contains(&10));
}
#[test]