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},
};
pub(crate) const DEFAULT_CLASS_MINOR: u16 = 0xFFFF;
pub(crate) const DEFAULT_LEAF_MAJOR: u16 = 0xFFFF;
#[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 configured_root_qdisc(&self) -> Option<&TcMessage> {
self.root_qdisc.as_ref().filter(|q| q.handle_raw() != 0)
}
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;
}
if !percent_matches(desired.delay_correlation, live_opts.delay_correlation()) {
return false;
}
if !percent_matches(desired.loss_correlation, live_opts.loss_correlation()) {
return false;
}
if !percent_matches(desired.duplicate_correlation, live_opts.duplicate_correlation()) {
return false;
}
if !percent_matches(desired.corrupt_correlation, live_opts.corrupt_correlation()) {
return false;
}
if !percent_matches(desired.reorder_correlation, live_opts.reorder_correlation()) {
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) => codel_round_trip_us(want) == opts.target_us,
}
}
pub(crate) fn codel_round_trip_us(us: u32) -> u32 {
const CODEL_SHIFT: u32 = 10;
const NSEC_PER_USEC: u64 = 1_000;
let ticks = (us as u64 * NSEC_PER_USEC) >> CODEL_SHIFT;
((ticks << CODEL_SHIFT) / NSEC_PER_USEC) as u32
}
fn split_attrs(mut input: &[u8]) -> BTreeMap<u16, &[u8]> {
let mut out = BTreeMap::new();
while input.len() >= 4 {
let Ok(len_bytes) = input[..2].try_into() else {
break;
};
let Ok(type_bytes) = input[2..4].try_into() else {
break;
};
let len = u16::from_ne_bytes(len_bytes) as usize;
let attr_type = u16::from_ne_bytes(type_bytes) & 0x3FFF;
if len < 4 || input.len() < len {
break;
}
out.insert(attr_type, &input[4..len]);
let aligned = (len + 3) & !3;
if input.len() <= aligned {
break;
}
input = &input[aligned..];
}
out
}
const RECIPE_FLOWER_VALUE_KEYS: &[u16] = {
use super::types::tc::filter::flower::*;
&[
TCA_FLOWER_KEY_IP_PROTO,
TCA_FLOWER_KEY_IPV4_SRC,
TCA_FLOWER_KEY_IPV4_DST,
TCA_FLOWER_KEY_IPV6_SRC,
TCA_FLOWER_KEY_IPV6_DST,
TCA_FLOWER_KEY_TCP_SRC,
TCA_FLOWER_KEY_TCP_DST,
TCA_FLOWER_KEY_UDP_SRC,
TCA_FLOWER_KEY_UDP_DST,
]
};
pub(crate) fn flower_matches(
desired: &super::filter::FlowerFilter,
desired_protocol: u16,
live: &TcMessage,
) -> bool {
use super::filter::FilterConfig;
if live.kind() != Some("flower") {
return false;
}
if live.protocol() != desired_protocol {
return false;
}
let Some(live_raw) = live.raw_options() else {
return false;
};
let live_attrs = split_attrs(live_raw);
let mut builder = crate::netlink::builder::MessageBuilder::new(0, 0);
let start = builder.len();
if desired.write_options(&mut builder).is_err() {
return false;
}
let end = builder.len();
let desired_blob = builder.as_bytes()[start..end].to_vec();
let desired_attrs = split_attrs(&desired_blob);
for (id, want) in &desired_attrs {
match live_attrs.get(id) {
Some(have) if have == want => {}
_ => return false,
}
}
for id in RECIPE_FLOWER_VALUE_KEYS {
if live_attrs.contains_key(id) && !desired_attrs.contains_key(id) {
return false;
}
}
true
}
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 codel_round_trip_matches_the_kernels_truncation() {
assert_eq!(codel_round_trip_us(20000), 19999);
assert_eq!(codel_round_trip_us(0), 0);
for us in [1u32, 999, 5_000, 20_000, 100_000, 1_000_000] {
assert!(codel_round_trip_us(us) <= us, "round trip grew {us}");
}
assert_eq!(codel_round_trip_us(1_048_576), 1_048_576);
}
#[test]
fn fq_codel_target_matches_accepts_the_kernels_echo() {
let live = TcMessage {
kind: Some("fq_codel".to_string()),
options: Some(fq_codel_options(19999)),
..TcMessage::default()
};
assert!(fq_codel_target_matches(Some(20_000), &live));
assert!(!fq_codel_target_matches(Some(50_000), &live));
assert!(fq_codel_target_matches(None, &live));
}
fn fq_codel_options(target_us: u32) -> Vec<u8> {
const TCA_FQ_CODEL_TARGET: u16 = 1;
let mut out = Vec::new();
out.extend_from_slice(&8u16.to_ne_bytes());
out.extend_from_slice(&TCA_FQ_CODEL_TARGET.to_ne_bytes());
out.extend_from_slice(&target_us.to_ne_bytes());
out
}
#[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));
}
fn live_flower(filter: &crate::netlink::filter::FlowerFilter, protocol: u16) -> TcMessage {
use crate::netlink::filter::FilterConfig;
let mut builder = crate::netlink::builder::MessageBuilder::new(0, 0);
let start = builder.len();
filter.write_options(&mut builder).expect("write options");
let end = builder.len();
let blob = builder.as_bytes()[start..end].to_vec();
let mut msg = TcMessage {
kind: Some("flower".to_string()),
options: Some(blob),
..TcMessage::default()
};
msg.header.tcm_info = protocol.to_be() as u32;
msg
}
const ETH_P_IP: u16 = 0x0800;
const ETH_P_IPV6: u16 = 0x86DD;
fn dst_v4(addr: &str, prefix: u8, classid: TcHandle) -> crate::netlink::filter::FlowerFilter {
crate::netlink::filter::FlowerFilter::new()
.classid(classid)
.priority(1)
.dst_ipv4(addr.parse().unwrap(), prefix)
.build()
}
#[test]
fn flower_matches_accepts_an_identical_filter() {
let cid = TcHandle::new(1, 2);
let f = dst_v4("10.0.0.1", 32, cid);
assert!(flower_matches(&f, ETH_P_IP, &live_flower(&f, ETH_P_IP)));
}
#[test]
fn flower_matches_rejects_a_different_address() {
let cid = TcHandle::new(1, 2);
let live = live_flower(&dst_v4("10.0.0.1", 32, cid), ETH_P_IP);
assert!(!flower_matches(&dst_v4("10.0.0.2", 32, cid), ETH_P_IP, &live));
}
#[test]
fn flower_matches_rejects_a_different_prefix() {
let cid = TcHandle::new(1, 2);
let live = live_flower(&dst_v4("10.0.0.0", 24, cid), ETH_P_IP);
assert!(!flower_matches(&dst_v4("10.0.0.0", 16, cid), ETH_P_IP, &live));
}
#[test]
fn flower_matches_rejects_src_where_dst_was_asked_for() {
let cid = TcHandle::new(1, 2);
let live = live_flower(
&crate::netlink::filter::FlowerFilter::new()
.classid(cid)
.priority(1)
.src_ipv4("10.0.0.1".parse().unwrap(), 32)
.build(),
ETH_P_IP,
);
assert!(!flower_matches(&dst_v4("10.0.0.1", 32, cid), ETH_P_IP, &live));
}
#[test]
fn flower_matches_rejects_a_v4_rule_turned_v6() {
let cid = TcHandle::new(1, 2);
let live = live_flower(&dst_v4("10.0.0.1", 32, cid), ETH_P_IP);
let want = crate::netlink::filter::FlowerFilter::new()
.classid(cid)
.priority(1)
.dst_ipv6("fd00::1".parse().unwrap(), 128)
.build();
assert!(!flower_matches(&want, ETH_P_IPV6, &live));
}
#[test]
fn flower_matches_rejects_a_different_classid() {
let live = live_flower(&dst_v4("10.0.0.1", 32, TcHandle::new(1, 2)), ETH_P_IP);
let want = dst_v4("10.0.0.1", 32, TcHandle::new(1, 3));
assert!(!flower_matches(&want, ETH_P_IP, &live));
}
#[test]
fn flower_matches_rejects_a_live_filter_that_matches_on_more() {
let cid = TcHandle::new(1, 2);
let live = live_flower(
&crate::netlink::filter::FlowerFilter::new()
.classid(cid)
.priority(1)
.dst_ipv4("10.0.0.1".parse().unwrap(), 32)
.ipv4()
.ip_proto_tcp()
.dst_port(443)
.build(),
ETH_P_IP,
);
assert!(!flower_matches(&dst_v4("10.0.0.1", 32, cid), ETH_P_IP, &live));
}
#[test]
fn flower_matches_rejects_a_different_port() {
let cid = TcHandle::new(1, 2);
let mk = |port: u16| {
crate::netlink::filter::FlowerFilter::new()
.classid(cid)
.priority(1)
.ipv4()
.ip_proto_tcp()
.dst_port(port)
.build()
};
let live = live_flower(&mk(443), ETH_P_IP);
assert!(flower_matches(&mk(443), ETH_P_IP, &live));
assert!(!flower_matches(&mk(8443), ETH_P_IP, &live));
}
#[test]
fn flower_matches_rejects_tcp_where_udp_was_asked_for() {
let cid = TcHandle::new(1, 2);
let tcp = crate::netlink::filter::FlowerFilter::new()
.classid(cid)
.priority(1)
.ipv4()
.ip_proto_tcp()
.dst_port(53)
.build();
let udp = crate::netlink::filter::FlowerFilter::new()
.classid(cid)
.priority(1)
.ipv4()
.ip_proto_udp()
.dst_port(53)
.build();
assert!(!flower_matches(&udp, ETH_P_IP, &live_flower(&tcp, ETH_P_IP)));
}
#[test]
fn flower_matches_rejects_a_non_flower_filter() {
let cid = TcHandle::new(1, 2);
let mut live = live_flower(&dst_v4("10.0.0.1", 32, cid), ETH_P_IP);
live.kind = Some("u32".to_string());
assert!(!flower_matches(&dst_v4("10.0.0.1", 32, cid), ETH_P_IP, &live));
}
#[test]
fn split_attrs_stops_on_a_pathological_length() {
assert!(split_attrs(&[0, 0, 0, 0]).is_empty());
assert!(split_attrs(&[2, 0, 1, 0]).is_empty());
assert!(split_attrs(&[0xFF, 0xFF, 1, 0, 9, 9]).is_empty());
}
}