use std::collections::BTreeMap;
use super::{
Connection,
error::Result,
messages::TcMessage,
protocol::Route,
tc::NetemConfig,
tc_handle::TcHandle,
tc_options::{HtbClassOptions, HtbOptions, QdiscOptions, parse_htb_class_options},
};
#[derive(Debug, Default)]
pub(crate) struct LiveTree {
pub(crate) root_qdisc: Option<TcMessage>,
pub(crate) classes: BTreeMap<TcHandle, TcMessage>,
pub(crate) leaf_qdiscs: BTreeMap<TcHandle, TcMessage>,
pub(crate) root_filters: Vec<TcMessage>,
}
impl LiveTree {
pub(crate) fn class(&self, handle: TcHandle) -> Option<&TcMessage> {
self.classes.get(&handle)
}
pub(crate) fn leaf_for(&self, class_handle: TcHandle) -> Option<&TcMessage> {
self.leaf_qdiscs.get(&class_handle)
}
pub(crate) fn filter_at_priority(&self, priority: u16) -> Option<&TcMessage> {
self.root_filters.iter().find(|f| f.priority() == priority)
}
}
pub(crate) async fn dump_live_tree(conn: &Connection<Route>, ifindex: u32) -> Result<LiveTree> {
let mut tree = LiveTree::default();
let qdiscs = conn.get_qdiscs_by_index(ifindex).await?;
for q in qdiscs {
if q.parent().is_root() {
tree.root_qdisc = Some(q);
} else {
tree.leaf_qdiscs.insert(q.parent(), q);
}
}
let classes = conn.get_classes_by_index(ifindex).await?;
for c in classes {
tree.classes.insert(c.handle(), c);
}
let root_parent = TcHandle::major_only(1);
tree.root_filters = conn
.get_filters_by_parent_index(ifindex, root_parent)
.await?;
Ok(tree)
}
pub(crate) fn root_htb_options(tree: &LiveTree) -> Option<HtbOptions> {
let root = tree.root_qdisc.as_ref()?;
if root.kind()? != "htb" {
return None;
}
match root.options()? {
QdiscOptions::Htb(opts) => Some(opts),
_ => None,
}
}
pub(crate) fn htb_class_options(class: &TcMessage) -> Option<HtbClassOptions> {
if class.kind()? != "htb" {
return None;
}
let raw = class.raw_options()?;
parse_htb_class_options(raw)
}
pub(crate) fn netem_matches(desired: &NetemConfig, live: &TcMessage) -> bool {
if live.kind() != Some("netem") {
return false;
}
let Some(QdiscOptions::Netem(live_opts)) = live.options() else {
return false;
};
if desired.delay != live_opts.delay() {
return false;
}
if desired.jitter != live_opts.jitter() {
return false;
}
let percent_matches = |desired: crate::util::Percent, live: Option<f64>| -> bool {
let live_value = live.unwrap_or(0.0);
let live_kernel = crate::util::Percent::new(live_value).as_kernel_probability();
desired.as_kernel_probability() == live_kernel
};
if !percent_matches(desired.loss, live_opts.loss()) {
return false;
}
if !percent_matches(desired.duplicate, live_opts.duplicate()) {
return false;
}
if !percent_matches(desired.corrupt, live_opts.corrupt()) {
return false;
}
if !percent_matches(desired.reorder, live_opts.reorder()) {
return false;
}
let effective_gap = if !desired.reorder.is_zero() && desired.gap == 0 {
1
} else {
desired.gap
};
if effective_gap != live_opts.gap {
return false;
}
let desired_rate = desired.rate.map(|r| r.as_bytes_per_sec()).unwrap_or(0);
if desired_rate != live_opts.rate {
return false;
}
if desired.limit != live_opts.limit {
return false;
}
true
}
pub(crate) fn fq_codel_target_matches(desired_target_us: Option<u32>, live: &TcMessage) -> bool {
if live.kind() != Some("fq_codel") {
return false;
}
let Some(QdiscOptions::FqCodel(opts)) = live.options() else {
return false;
};
match desired_target_us {
None => true,
Some(want) => opts.target_us == want,
}
}
pub(crate) fn flower_classid(filter: &TcMessage) -> Option<TcHandle> {
use super::types::tc::filter::flower::TCA_FLOWER_CLASSID;
if filter.kind() != Some("flower") {
return None;
}
let mut input = filter.raw_options()?;
while input.len() >= 4 {
let len = u16::from_ne_bytes(input[..2].try_into().ok()?) as usize;
let attr_type = u16::from_ne_bytes(input[2..4].try_into().ok()?);
if len < 4 || input.len() < len {
break;
}
let payload = &input[4..len];
if (attr_type & 0x3FFF) == TCA_FLOWER_CLASSID && payload.len() >= 4 {
let raw = u32::from_ne_bytes(payload[..4].try_into().ok()?);
return Some(TcHandle::from_raw(raw));
}
let aligned = (len + 3) & !3;
if input.len() <= aligned {
break;
}
input = &input[aligned..];
}
None
}
pub(crate) fn htb_class_rates_match(
class: &TcMessage,
desired_rate_bps: u64,
desired_ceil_bps: u64,
) -> bool {
let Some(opts) = htb_class_options(class) else {
return false;
};
opts.rate == desired_rate_bps && opts.ceil == desired_ceil_bps
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::netlink::tc::QdiscConfig;
use crate::util::{Percent, Rate};
fn make_netem_msg(cfg: NetemConfig) -> TcMessage {
let mut builder = crate::netlink::builder::MessageBuilder::new(0, 0);
let start = builder.len();
cfg.write_options(&mut builder).expect("write options");
let end = builder.len();
let blob = builder.as_bytes()[start..end].to_vec();
TcMessage {
kind: Some("netem".to_string()),
options: Some(blob),
..TcMessage::default()
}
}
#[test]
fn netem_matches_round_trips_delay_only() {
let desired = NetemConfig::new().delay(Duration::from_millis(50)).build();
let live = make_netem_msg(desired.clone());
assert!(netem_matches(&desired, &live));
}
#[test]
fn netem_matches_rejects_different_delay() {
let desired = NetemConfig::new().delay(Duration::from_millis(50)).build();
let other = NetemConfig::new().delay(Duration::from_millis(60)).build();
let live = make_netem_msg(other);
assert!(!netem_matches(&desired, &live));
}
#[test]
fn netem_matches_rejects_different_loss() {
let desired = NetemConfig::new()
.delay(Duration::from_millis(50))
.loss(Percent::new(1.0))
.build();
let other = NetemConfig::new()
.delay(Duration::from_millis(50))
.loss(Percent::new(2.0))
.build();
let live = make_netem_msg(other);
assert!(!netem_matches(&desired, &live));
}
#[test]
fn netem_matches_round_trips_complex_config() {
let cfg = NetemConfig::new()
.delay(Duration::from_millis(40))
.jitter(Duration::from_millis(5))
.loss(Percent::new(0.5))
.duplicate(Percent::new(0.1))
.rate(Rate::mbit(100))
.build();
let live = make_netem_msg(cfg.clone());
assert!(netem_matches(&cfg, &live));
}
#[test]
fn netem_matches_handles_reorder_gap_default() {
let cfg = NetemConfig::new()
.delay(Duration::from_millis(20))
.reorder(Percent::new(2.0))
.build();
let live = make_netem_msg(cfg.clone());
assert!(netem_matches(&cfg, &live));
}
#[test]
fn netem_matches_rejects_non_netem_kind() {
let desired = NetemConfig::new().delay(Duration::from_millis(50)).build();
let live = TcMessage::default();
assert!(!netem_matches(&desired, &live));
}
}