use crate::{ config::Config, docker, lock::FileLock, netns::{NsTransport, build_transport}, wg, }; use std::{ collections::{BTreeMap, BTreeSet}, net::Ipv4Addr, }; #[derive(Debug, Clone)] struct TrafficDrift { has_drift: bool, reason: String, } #[derive(Debug, Clone)] struct FilterInspect { map: BTreeMap, duplicate_ips: BTreeSet, duplicate_classes: BTreeSet, } #[derive(Debug, Clone, PartialEq, Eq)] struct ClassRateCeil { rate: String, ceil: String, } #[derive(Debug, Clone)] pub struct PeerTrafficStatus { pub ip: String, pub class_id: u16, pub egress_filter_present: bool, pub ingress_filter_present: bool, pub egress_rate: Option, pub egress_ceil: Option, pub ingress_rate: Option, pub ingress_ceil: Option, pub drift_reason: Option, } pub fn inspect_drift_summary(cfg: &Config) -> anyhow::Result> { if !cfg.traffic.enabled { return Ok(None); } let target = docker::discover_target(cfg)?; let transport = build_transport(target.container_pid, &target.container_name); let peers = wg::discover_peers( target.container_pid, &target.container_name, &cfg.amnezia.peer_ipv4_cidr, )?; if peers.is_empty() { return Ok(Some("no peers discovered".to_string())); } let drift = inspect_and_diff( transport.as_ref(), &target.interface_name, &cfg.traffic.ifb_name, cfg.traffic.class_start_id, &cfg.traffic.root_rate, &cfg.traffic.peer_limit, &peers, )?; if drift.has_drift { Ok(Some(drift.reason)) } else { Ok(None) } } pub fn reconcile_traffic(cfg: &Config) -> anyhow::Result<()> { if !cfg.traffic.enabled { return Ok(()); } let _lock = FileLock::acquire("/run/awg2-harness/traffic.lock")?; let target = docker::discover_target(cfg)?; let transport = build_transport(target.container_pid, &target.container_name); let peers = wg::discover_peers( target.container_pid, &target.container_name, &cfg.amnezia.peer_ipv4_cidr, )?; if peers.is_empty() { println!("no peers found, skip tc apply"); return Ok(()); } let drift = inspect_and_diff( transport.as_ref(), &target.interface_name, &cfg.traffic.ifb_name, cfg.traffic.class_start_id, &cfg.traffic.root_rate, &cfg.traffic.peer_limit, &peers, )?; if !drift.has_drift { println!("traffic rules up-to-date, skip apply"); return Ok(()); } println!("traffic drift detected: {}", 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(()) } pub fn peer_status_table(cfg: &Config) -> anyhow::Result>> { if !cfg.traffic.enabled { return Ok(None); } let target = docker::discover_target(cfg)?; let transport = build_transport(target.container_pid, &target.container_name); let peers = wg::discover_peers( target.container_pid, &target.container_name, &cfg.amnezia.peer_ipv4_cidr, )?; if peers.is_empty() { return Ok(Some(Vec::new())); } let filters_egress = transport .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:", ]) .unwrap_or_default(); let classes_egress_full = transport .run(&["tc", "class", "show", "dev", &target.interface_name]) .unwrap_or_default(); let classes_ingress_full = transport .run(&["tc", "class", "show", "dev", &cfg.traffic.ifb_name]) .unwrap_or_default(); 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); let expected_limit = normalize_tc_rate(&cfg.traffic.peer_limit); let mut out = Vec::with_capacity(peers.len()); for p in peers { let ip = p.ipv4.to_string(); let class_id = *expected_map.get(&ip).unwrap_or(&cfg.traffic.class_start_id); let class_egress = format!("1:{class_id}"); let class_ingress = format!("2:{class_id}"); let egress_filter_present = actual_map_egress.get(&ip).copied() == Some(class_id); let ingress_filter_present = actual_map_ingress.get(&ip).copied() == Some(class_id); let e = egress_rates.get(&class_egress); let i = ingress_rates.get(&class_ingress); let mut reasons = Vec::new(); if !egress_filter_present { reasons.push("missing/mismatched egress filter".to_string()); } if !ingress_filter_present { reasons.push("missing/mismatched ingress filter".to_string()); } if e.is_none() { reasons.push("missing egress class".to_string()); } if i.is_none() { reasons.push("missing ingress class".to_string()); } if let Some(v) = e && !rate_ceil_matches_limit(v, &expected_limit) { reasons.push("egress rate/ceil mismatch".to_string()); } if let Some(v) = i && !rate_ceil_matches_limit(v, &expected_limit) { reasons.push("ingress rate/ceil mismatch".to_string()); } if inspect_egress.duplicate_ips.contains(&ip) { reasons.push("duplicate egress filters".to_string()); } if inspect_ingress.duplicate_ips.contains(&ip) { reasons.push("duplicate ingress filters".to_string()); } out.push(PeerTrafficStatus { ip, class_id, egress_filter_present, ingress_filter_present, egress_rate: e.map(|v| v.rate.clone()), egress_ceil: e.map(|v| v.ceil.clone()), ingress_rate: i.map(|v| v.rate.clone()), ingress_ceil: i.map(|v| v.ceil.clone()), drift_reason: if reasons.is_empty() { None } else { Some(reasons.join(", ")) }, }); } Ok(Some(out)) } fn inspect_and_diff( transport: &dyn NsTransport, iface: &str, ifb: &str, class_start_id: u16, root_rate: &str, peer_limit: &str, peers: &[wg::Peer], ) -> anyhow::Result { let root_qdisc = inspect_cmd( transport, &["tc", "qdisc", "show", "dev", iface], "egress qdisc", )?; if !root_qdisc.contains("htb") { return Ok(TrafficDrift { has_drift: true, reason: "missing egress root htb qdisc".to_string(), }); } let ingress_qdisc = inspect_cmd( transport, &["tc", "qdisc", "show", "dev", iface], "ingress qdisc", )?; if !ingress_qdisc.contains("ingress") { return Ok(TrafficDrift { has_drift: true, reason: "missing ingress qdisc".to_string(), }); } 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 = 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 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)", inspect_egress.duplicate_ips.len() ), }); } 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)", 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 = actual_map_egress.keys().cloned().collect(); let actual_ips_ingress: BTreeSet = actual_map_ingress.keys().cloned().collect(); let expected_ips: BTreeSet = peers.iter().map(|p| p.ipv4.to_string()).collect(); if actual_ips_egress != expected_ips { return Ok(TrafficDrift { has_drift: true, reason: format!( "egress peer filter mismatch (actual={}, expected={})", actual_ips_egress.len(), expected_ips.len() ), }); } if actual_ips_ingress != expected_ips { return Ok(TrafficDrift { has_drift: true, reason: format!( "ingress peer filter mismatch (actual={}, expected={})", actual_ips_ingress.len(), expected_ips.len() ), }); } let expected_map = expected_peer_class_map(peers, class_start_id); if actual_map_egress != expected_map { return Ok(TrafficDrift { has_drift: true, reason: format!( "egress class mapping mismatch (actual={}, expected={})", actual_map_egress.len(), expected_map.len() ), }); } if actual_map_ingress != expected_map { return Ok(TrafficDrift { has_drift: true, reason: format!( "ingress class mapping mismatch (actual={}, expected={})", actual_map_ingress.len(), expected_map.len() ), }); } let expected_limit = normalize_tc_rate(peer_limit); 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:"); 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}"); let e = egress_rates.get(&class_egress); let i = ingress_rates.get(&class_ingress); if e.is_none() || i.is_none() { return Ok(TrafficDrift { has_drift: true, 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) { return Ok(TrafficDrift { has_drift: true, reason: format!("rate/ceil mismatch for peer class_id={class_id}"), }); } } 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 = 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 set mismatch".to_string(), }); } if collect_leaf_class_ids(&classes_ingress) != expected_set { return Ok(TrafficDrift { has_drift: true, reason: "ingress per-peer class set mismatch".to_string(), }); } 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}"); 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 inspect_cmd(transport: &dyn NsTransport, args: &[&str], what: &str) -> anyhow::Result { 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 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 = None; let mut class_id: Option = None; for line in block.lines() { let t = line.trim(); if t.contains("match") && ip.is_none() { ip = extract_u32_match_hex(t).and_then(|hex| hex_to_ipv4_string(&hex)); } if t.contains("flowid") && class_id.is_none() { class_id = extract_flowid_class_id(t); } } if let (Some(ip), Some(class_id)) = (ip, class_id) { if out.contains_key(&ip) { 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); } } FilterInspect { map: out, duplicate_ips, duplicate_classes, } } 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 { 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 { let mut blocks = Vec::new(); let mut cur = Vec::new(); for line in raw.lines() { let t = line.trim(); if t.starts_with("filter") && !cur.is_empty() { blocks.push(cur.join("\n")); cur.clear(); } if !t.is_empty() { cur.push(t.to_string()); } } if !cur.is_empty() { blocks.push(cur.join("\n")); } blocks } fn extract_flowid_class_id(line: &str) -> Option { let toks: Vec<&str> = line.split_whitespace().collect(); let flowid = toks .iter() .position(|v| *v == "flowid") .and_then(|i| toks.get(i + 1))?; let class_tail = flowid.split(':').nth(1)?; class_tail.parse::().ok() } fn expected_peer_class_map(peers: &[wg::Peer], class_start_id: u16) -> BTreeMap { let mut map = BTreeMap::new(); for (idx, p) in peers.iter().enumerate() { map.insert(p.ipv4.to_string(), class_start_id + idx as u16); } map } fn extract_u32_match_hex(line: &str) -> Option { let toks: Vec<&str> = line.split_whitespace().collect(); let pos = toks.iter().position(|v| *v == "match")?; let match_val = *toks.get(pos + 1)?; let (hex, mask) = match_val.split_once('/')?; if mask.eq_ignore_ascii_case("ffffffff") && hex.len() == 8 { return Some(hex.to_ascii_lowercase()); } None } fn hex_to_ipv4_string(hex: &str) -> Option { if hex.len() != 8 { return None; } let b1 = u8::from_str_radix(&hex[0..2], 16).ok()?; let b2 = u8::from_str_radix(&hex[2..4], 16).ok()?; let b3 = u8::from_str_radix(&hex[4..6], 16).ok()?; let b4 = u8::from_str_radix(&hex[6..8], 16).ok()?; Some(Ipv4Addr::new(b1, b2, b3, b4).to_string()) } fn collect_leaf_class_ids(raw: &str) -> BTreeSet { 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::().ok()) && class_id != 1 { out.insert(class_id); } } out } fn teardown_owned_topology( transport: &dyn NsTransport, iface: &str, ifb: &str, ) -> anyhow::Result<()> { run_allow_notfound(transport, &["tc", "qdisc", "del", "dev", iface, "root"])?; run_allow_notfound(transport, &["tc", "qdisc", "del", "dev", iface, "ingress"])?; run_allow_notfound(transport, &["ip", "link", "del", ifb])?; Ok(()) } fn apply_full_topology( cfg: &Config, transport: &dyn NsTransport, iface: &str, ifb: &str, peers: &[wg::Peer], ) -> anyhow::Result<()> { let root = cfg.traffic.root_rate.as_str(); run_allow_exists(transport, &["modprobe", "ifb"])?; run_allow_exists( transport, &["ip", "link", "add", "name", ifb, "type", "ifb"], )?; run_step(transport, &["ip", "link", "set", "dev", ifb, "up"])?; run_allow_exists( transport, &[ "tc", "qdisc", "add", "dev", iface, "root", "handle", "1:", "htb", ], )?; run_allow_exists( transport, &[ "tc", "class", "add", "dev", iface, "parent", "1:", "classid", "1:1", "htb", "rate", root, "ceil", root, ], )?; run_allow_exists( transport, &[ "tc", "qdisc", "add", "dev", iface, "handle", "ffff:", "ingress", ], )?; run_allow_exists( transport, &[ "tc", "filter", "add", "dev", iface, "parent", "ffff:", "protocol", "all", "u32", "match", "u32", "0", "0", "action", "mirred", "egress", "redirect", "dev", ifb, ], )?; run_allow_exists( transport, &[ "tc", "qdisc", "add", "dev", ifb, "root", "handle", "2:", "htb", ], )?; run_allow_exists( transport, &[ "tc", "class", "add", "dev", ifb, "parent", "2:", "classid", "2:1", "htb", "rate", root, "ceil", root, ], )?; for (idx, p) in peers.iter().enumerate() { let class_id = cfg.traffic.class_start_id + idx as u16; let ip = p.ipv4.to_string(); let class_egress = format!("1:{class_id}"); let class_ingress = format!("2:{class_id}"); let limit = cfg.traffic.peer_limit.as_str(); run_allow_exists( transport, &[ "tc", "class", "add", "dev", iface, "parent", "1:1", "classid", class_egress.as_str(), "htb", "rate", limit, "ceil", limit, ], )?; run_allow_exists( transport, &[ "tc", "qdisc", "add", "dev", iface, "parent", class_egress.as_str(), "fq_codel", ], )?; run_allow_exists( transport, &[ "tc", "filter", "add", "dev", iface, "parent", "1:", "protocol", "ip", "u32", "match", "ip", "dst", ip.as_str(), "flowid", class_egress.as_str(), ], )?; run_allow_exists( transport, &[ "tc", "class", "add", "dev", ifb, "parent", "2:1", "classid", class_ingress.as_str(), "htb", "rate", limit, "ceil", limit, ], )?; run_allow_exists( transport, &[ "tc", "qdisc", "add", "dev", ifb, "parent", class_ingress.as_str(), "fq_codel", ], )?; run_allow_exists( transport, &[ "tc", "filter", "add", "dev", ifb, "parent", "2:", "protocol", "ip", "u32", "match", "ip", "src", ip.as_str(), "flowid", class_ingress.as_str(), ], )?; } let verify = inspect_and_diff( transport, iface, ifb, cfg.traffic.class_start_id, &cfg.traffic.root_rate, &cfg.traffic.peer_limit, peers, )?; if verify.has_drift { anyhow::bail!("verify-after-apply failed: {}", verify.reason); } Ok(()) } fn run_step(transport: &dyn NsTransport, args: &[&str]) -> anyhow::Result<()> { let _ = transport.run(args)?; Ok(()) } fn run_allow_exists(transport: &dyn NsTransport, args: &[&str]) -> anyhow::Result<()> { match transport.run(args) { Ok(_) => Ok(()), Err(e) => { let msg = e.to_string(); if msg.contains("File exists") || msg.contains("EEXIST") { return Ok(()); } Err(e) } } } fn run_allow_notfound(transport: &dyn NsTransport, args: &[&str]) -> anyhow::Result<()> { match transport.run(args) { Ok(_) => Ok(()), Err(e) => { let msg = e.to_string(); if msg.contains("No such file") || msg.contains("No such") || msg.contains("not found") || msg.contains("Cannot find") { return Ok(()); } Err(e) } } } fn parse_class_rate_ceil_map(raw: &str, prefix: &str) -> BTreeMap { let mut out = BTreeMap::new(); for line in raw.lines() { let t = line.trim(); if !t.contains("class htb") || !t.contains(" rate ") || !t.contains(" ceil ") { continue; } let toks: Vec<&str> = t.split_whitespace().collect(); let class = toks .iter() .position(|v| *v == "htb") .and_then(|i| toks.get(i + 1)) .map(|s| s.to_string()); 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()); if let (Some(class), Some(rate), Some(ceil)) = (class, rate, ceil) && class.starts_with(prefix) && !class.ends_with(":1") { out.insert(class, ClassRateCeil { rate, ceil }); } } out } fn rate_ceil_matches_limit(v: &ClassRateCeil, limit: &str) -> bool { normalize_tc_rate(&v.rate) == limit && normalize_tc_rate(&v.ceil) == limit } fn normalize_tc_rate(v: &str) -> String { v.trim().to_ascii_lowercase().replace(' ', "") } #[cfg(test)] mod tests { use super::*; #[test] fn parse_tc_u32_hex_ipv4() { let raw = " match 0a080105/ffffffff at 16\n flowid 1: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 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 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] 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"; let m = parse_class_rate_ceil_map(raw, "1:"); let rc = m.get("1:10").expect("class exists"); assert_eq!(normalize_tc_rate(&rc.rate), "70mbit"); assert_eq!(normalize_tc_rate(&rc.ceil), "70mbit"); } #[test] fn rate_limit_match_normalized() { let rc = ClassRateCeil { rate: "70Mbit".to_string(), ceil: "70mbit".to_string(), }; assert!(rate_ceil_matches_limit(&rc, "70mbit")); assert!(!rate_ceil_matches_limit(&rc, "50mbit")); } }