use std::time::Duration;
use nlink::{
Connection, Result, Route, TcHandle,
netlink::{
filter::{FlowerFilter, MatchallFilter, U32Filter},
link::{DummyLink, IfbLink},
tc::{
FqCodelConfig, HtbClassConfig, HtbQdiscConfig, IngressConfig, NetemConfig, PrioConfig,
SfqConfig, TbfConfig,
},
},
};
use crate::common::TestNamespace;
async fn setup_tc_ns(name: &str) -> Result<(TestNamespace, Connection<Route>)> {
let ns = TestNamespace::new(name)?;
let conn = ns.connection()?;
conn.add_link(DummyLink::new("dummy0")).await?;
conn.set_link_up("dummy0").await?;
Ok((ns, conn))
}
#[tokio::test]
async fn test_add_netem_qdisc() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("netem").await?;
let netem = NetemConfig::new()
.delay(Duration::from_millis(100))
.jitter(Duration::from_millis(10))
.build();
conn.add_qdisc("dummy0", netem).await?;
let qdiscs = conn.get_qdiscs_by_name("dummy0").await?;
let netem = qdiscs.iter().find(|q| q.kind() == Some("netem"));
assert!(netem.is_some(), "netem qdisc should exist");
Ok(())
}
#[tokio::test]
async fn test_netem_with_loss() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("netemloss").await?;
let netem = NetemConfig::new().loss(nlink::Percent::new(1.0)).build();
conn.add_qdisc("dummy0", netem).await?;
let qdiscs = conn.get_qdiscs_by_name("dummy0").await?;
let netem = qdiscs.iter().find(|q| q.kind() == Some("netem"));
assert!(netem.is_some());
Ok(())
}
#[tokio::test]
async fn test_del_netem() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("netemrm").await?;
let netem = NetemConfig::new().delay(Duration::from_millis(50)).build();
conn.add_qdisc("dummy0", netem).await?;
conn.del_netem("dummy0").await?;
let qdiscs = conn.get_qdiscs_by_name("dummy0").await?;
assert!(
!qdiscs.iter().any(|q| q.kind() == Some("netem")),
"netem should be removed"
);
Ok(())
}
#[tokio::test]
async fn test_add_htb_qdisc() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("htb").await?;
let htb = HtbQdiscConfig::new().default_class(0x30).build();
conn.add_qdisc_full("dummy0", TcHandle::ROOT, Some(TcHandle::major_only(1)), htb)
.await?;
let qdiscs = conn.get_qdiscs_by_name("dummy0").await?;
let htb = qdiscs.iter().find(|q| q.kind() == Some("htb"));
assert!(htb.is_some(), "htb qdisc should exist");
assert!(htb.unwrap().is_root());
Ok(())
}
#[tokio::test]
async fn test_add_tbf_qdisc() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("tbf").await?;
let tbf = TbfConfig::new()
.rate(nlink::Rate::bytes_per_sec(1_000_000)) .burst(nlink::Bytes::new(10000))
.limit(nlink::Bytes::new(100000))
.build();
conn.add_qdisc("dummy0", tbf).await?;
let qdiscs = conn.get_qdiscs_by_name("dummy0").await?;
let tbf = qdiscs.iter().find(|q| q.kind() == Some("tbf"));
assert!(tbf.is_some(), "tbf qdisc should exist");
Ok(())
}
#[tokio::test]
async fn test_add_fq_codel_qdisc() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("fqcodel").await?;
let fqcodel = FqCodelConfig::new()
.target(Duration::from_micros(5000)) .interval(Duration::from_micros(100000)) .limit(10240)
.build();
conn.add_qdisc("dummy0", fqcodel).await?;
let qdiscs = conn.get_qdiscs_by_name("dummy0").await?;
let fq = qdiscs.iter().find(|q| q.kind() == Some("fq_codel"));
assert!(fq.is_some(), "fq_codel qdisc should exist");
Ok(())
}
#[tokio::test]
async fn test_add_prio_qdisc() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("prio").await?;
let prio = PrioConfig::new().bands(3).build();
conn.add_qdisc_full(
"dummy0",
TcHandle::ROOT,
Some(TcHandle::major_only(1)),
prio,
)
.await?;
let qdiscs = conn.get_qdiscs_by_name("dummy0").await?;
let prio = qdiscs.iter().find(|q| q.kind() == Some("prio"));
assert!(prio.is_some(), "prio qdisc should exist");
Ok(())
}
#[tokio::test]
async fn test_add_sfq_qdisc() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("sfq").await?;
let sfq = SfqConfig::new().perturb(10).quantum(1500).build();
conn.add_qdisc("dummy0", sfq).await?;
let qdiscs = conn.get_qdiscs_by_name("dummy0").await?;
let sfq = qdiscs.iter().find(|q| q.kind() == Some("sfq"));
assert!(sfq.is_some(), "sfq qdisc should exist");
Ok(())
}
#[tokio::test]
async fn test_add_ingress_qdisc() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("ingress").await?;
conn.add_qdisc("dummy0", IngressConfig::new()).await?;
let qdiscs = conn.get_qdiscs_by_name("dummy0").await?;
let ingress = qdiscs.iter().find(|q| q.kind() == Some("ingress"));
assert!(ingress.is_some(), "ingress qdisc should exist");
Ok(())
}
#[tokio::test]
async fn test_delete_qdisc() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("qdiscdel").await?;
let netem = NetemConfig::new().delay(Duration::from_millis(10)).build();
conn.add_qdisc("dummy0", netem).await?;
conn.del_qdisc("dummy0", TcHandle::ROOT).await?;
let qdiscs = conn.get_qdiscs_by_name("dummy0").await?;
assert!(!qdiscs.iter().any(|q| q.kind() == Some("netem")));
Ok(())
}
#[tokio::test]
async fn test_replace_qdisc() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("qdiscrep").await?;
let netem1 = NetemConfig::new().delay(Duration::from_millis(100)).build();
conn.add_qdisc("dummy0", netem1).await?;
let netem2 = NetemConfig::new().delay(Duration::from_millis(50)).build();
conn.replace_qdisc("dummy0", netem2).await?;
let qdiscs = conn.get_qdiscs_by_name("dummy0").await?;
let netem_count = qdiscs.iter().filter(|q| q.kind() == Some("netem")).count();
assert_eq!(netem_count, 1);
Ok(())
}
#[tokio::test]
async fn test_add_htb_class() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("htbclass").await?;
let htb = HtbQdiscConfig::new().default_class(0x30).build();
conn.add_qdisc_full("dummy0", TcHandle::ROOT, Some(TcHandle::major_only(1)), htb)
.await?;
let class = HtbClassConfig::new(nlink::Rate::mbit(10))
.ceil(nlink::Rate::mbit(800)) .build();
conn.add_class(
"dummy0",
TcHandle::major_only(1),
TcHandle::new(1, 10),
class,
)
.await?;
let classes = conn.get_classes_by_name("dummy0").await?;
assert!(
!classes.is_empty(),
"at least one class should exist (may include root)"
);
Ok(())
}
#[tokio::test]
async fn test_htb_class_hierarchy() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("htbhier").await?;
let htb = HtbQdiscConfig::new().default_class(0x30).build();
conn.add_qdisc_full("dummy0", TcHandle::ROOT, Some(TcHandle::major_only(1)), htb)
.await?;
let root_class = HtbClassConfig::new(nlink::Rate::mbit(100)).build();
conn.add_class(
"dummy0",
TcHandle::major_only(1),
TcHandle::new(1, 1),
root_class,
)
.await?;
let child1 = HtbClassConfig::new(nlink::Rate::mbit(50))
.ceil(nlink::Rate::mbit(800))
.build();
conn.add_class("dummy0", TcHandle::new(1, 1), TcHandle::new(1, 10), child1)
.await?;
let child2 = HtbClassConfig::new(nlink::Rate::mbit(30))
.ceil(nlink::Rate::mbit(800))
.build();
conn.add_class("dummy0", TcHandle::new(1, 1), TcHandle::new(1, 20), child2)
.await?;
let classes = conn.get_classes_by_name("dummy0").await?;
assert!(classes.len() >= 3, "should have at least 3 classes");
Ok(())
}
#[tokio::test]
async fn test_delete_class() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("classdel").await?;
let htb = HtbQdiscConfig::new().default_class(0x30).build();
conn.add_qdisc_full("dummy0", TcHandle::ROOT, Some(TcHandle::major_only(1)), htb)
.await?;
let class = HtbClassConfig::new(nlink::Rate::mbit(10)).build();
conn.add_class(
"dummy0",
TcHandle::major_only(1),
TcHandle::new(1, 10),
class,
)
.await?;
conn.del_class("dummy0", TcHandle::major_only(1), TcHandle::new(1, 10))
.await?;
let classes = conn.get_classes_by_name("dummy0").await?;
assert!(!classes.iter().any(|c| c.handle() == TcHandle::new(1, 0x10)));
Ok(())
}
#[tokio::test]
async fn test_add_matchall_filter() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("matchall").await?;
let htb = HtbQdiscConfig::new().default_class(0x30).build();
conn.add_qdisc_full("dummy0", TcHandle::ROOT, Some(TcHandle::major_only(1)), htb)
.await?;
let class = HtbClassConfig::new(nlink::Rate::mbit(10)).build();
conn.add_class(
"dummy0",
TcHandle::major_only(1),
TcHandle::new(1, 10),
class,
)
.await?;
let filter = MatchallFilter::new()
.classid(TcHandle::new(1, 0x10))
.build();
conn.add_filter("dummy0", TcHandle::major_only(1), filter)
.await?;
let filters = conn.get_filters_by_name("dummy0").await?;
let matchall = filters.iter().find(|f| f.kind() == Some("matchall"));
assert!(matchall.is_some(), "matchall filter should exist");
Ok(())
}
#[tokio::test]
async fn test_add_u32_filter() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("u32").await?;
let htb = HtbQdiscConfig::new().default_class(0x30).build();
conn.add_qdisc_full("dummy0", TcHandle::ROOT, Some(TcHandle::major_only(1)), htb)
.await?;
let class = HtbClassConfig::new(nlink::Rate::mbit(10)).build();
conn.add_class(
"dummy0",
TcHandle::major_only(1),
TcHandle::new(1, 10),
class,
)
.await?;
let filter = U32Filter::new()
.classid(TcHandle::new(1, 0x10))
.match_dst_port(80)
.build();
conn.add_filter("dummy0", TcHandle::major_only(1), filter)
.await?;
let filters = conn.get_filters_by_name("dummy0").await?;
let u32 = filters.iter().find(|f| f.kind() == Some("u32"));
assert!(u32.is_some(), "u32 filter should exist");
Ok(())
}
#[tokio::test]
async fn test_add_flower_filter() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("flower").await?;
let htb = HtbQdiscConfig::new().default_class(0x30).build();
conn.add_qdisc_full("dummy0", TcHandle::ROOT, Some(TcHandle::major_only(1)), htb)
.await?;
let class = HtbClassConfig::new(nlink::Rate::mbit(10)).build();
conn.add_class(
"dummy0",
TcHandle::major_only(1),
TcHandle::new(1, 10),
class,
)
.await?;
let filter = FlowerFilter::new()
.classid(TcHandle::new(1, 0x10))
.ip_proto_tcp()
.build();
conn.add_filter("dummy0", TcHandle::major_only(1), filter)
.await?;
let filters = conn.get_filters_by_name("dummy0").await?;
let flower = filters.iter().find(|f| f.kind() == Some("flower"));
assert!(flower.is_some(), "flower filter should exist");
Ok(())
}
#[tokio::test]
async fn test_matchall_on_ingress() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("matchinq").await?;
conn.add_qdisc("dummy0", IngressConfig::new()).await?;
let filter = MatchallFilter::new().build();
conn.add_filter("dummy0", TcHandle::INGRESS, filter).await?;
let filters = conn.get_filters_by_name("dummy0").await?;
assert!(!filters.is_empty(), "filter should exist");
Ok(())
}
#[tokio::test]
async fn test_matchall_goto_chain() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("gotoch").await?;
let htb = HtbQdiscConfig::new().default_class(0x30).build();
conn.add_qdisc_full("dummy0", TcHandle::ROOT, Some(TcHandle::major_only(1)), htb)
.await?;
conn.add_tc_chain("dummy0", TcHandle::major_only(1), 10)
.await?;
let filter = MatchallFilter::new().goto_chain(10).build();
conn.add_filter("dummy0", TcHandle::major_only(1), filter)
.await?;
let filters = conn.get_filters_by_name("dummy0").await?;
assert!(!filters.is_empty(), "filter should exist");
Ok(())
}
#[tokio::test]
async fn test_filter_on_ifb() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("ifbfilt").await?;
conn.add_link(IfbLink::new("ifb0")).await?;
conn.set_link_up("ifb0").await?;
let htb = HtbQdiscConfig::new().default_class(0x30).build();
conn.add_qdisc_full("ifb0", TcHandle::ROOT, Some(TcHandle::major_only(1)), htb)
.await?;
let class = HtbClassConfig::new(nlink::Rate::mbit(10)).build();
conn.add_class("ifb0", TcHandle::major_only(1), TcHandle::new(1, 10), class)
.await?;
let filter = MatchallFilter::new()
.classid(TcHandle::new(1, 0x10))
.build();
conn.add_filter("ifb0", TcHandle::major_only(1), filter)
.await?;
let filters = conn.get_filters_by_name("ifb0").await?;
assert!(!filters.is_empty(), "filter should exist");
Ok(())
}
#[tokio::test]
async fn test_delete_filter() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("filterdel").await?;
let htb = HtbQdiscConfig::new().default_class(0x30).build();
conn.add_qdisc_full("dummy0", TcHandle::ROOT, Some(TcHandle::major_only(1)), htb)
.await?;
let class = HtbClassConfig::new(nlink::Rate::mbit(10)).build();
conn.add_class(
"dummy0",
TcHandle::major_only(1),
TcHandle::new(1, 10),
class,
)
.await?;
let filter = MatchallFilter::new()
.classid(TcHandle::new(1, 0x10))
.build();
conn.add_filter("dummy0", TcHandle::major_only(1), filter)
.await?;
conn.flush_filters("dummy0", TcHandle::major_only(1))
.await?;
let filters = conn.get_filters_by_name("dummy0").await?;
assert!(filters.is_empty(), "filters should be deleted");
Ok(())
}
#[tokio::test]
async fn test_replace_filter() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("filterrep").await?;
let htb = HtbQdiscConfig::new().default_class(0x30).build();
conn.add_qdisc_full("dummy0", TcHandle::ROOT, Some(TcHandle::major_only(1)), htb)
.await?;
let class1 = HtbClassConfig::new(nlink::Rate::mbit(10)).build();
conn.add_class(
"dummy0",
TcHandle::major_only(1),
TcHandle::new(1, 10),
class1,
)
.await?;
let class2 = HtbClassConfig::new(nlink::Rate::mbit(20)).build();
conn.add_class(
"dummy0",
TcHandle::major_only(1),
TcHandle::new(1, 20),
class2,
)
.await?;
let filter = MatchallFilter::new()
.classid(TcHandle::new(1, 0x10))
.build();
conn.add_filter("dummy0", TcHandle::major_only(1), filter)
.await?;
let filter2 = MatchallFilter::new()
.classid(TcHandle::new(1, 0x20))
.build();
conn.replace_filter("dummy0", TcHandle::major_only(1), filter2)
.await?;
let filters = conn.get_filters_by_name("dummy0").await?;
let matchall_count = filters
.iter()
.filter(|f| f.kind() == Some("matchall"))
.count();
assert_eq!(matchall_count, 1);
Ok(())
}
#[tokio::test]
async fn test_qdisc_statistics() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("qdiscstats").await?;
let netem = NetemConfig::new().delay(Duration::from_millis(10)).build();
conn.add_qdisc("dummy0", netem).await?;
let qdiscs = conn.get_qdiscs_by_name("dummy0").await?;
let netem = qdiscs.iter().find(|q| q.kind() == Some("netem")).unwrap();
let _bytes = netem.bytes();
let _packets = netem.packets();
let _drops = netem.drops();
Ok(())
}
#[tokio::test]
async fn test_class_statistics() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("classstats").await?;
let htb = HtbQdiscConfig::new().default_class(0x30).build();
conn.add_qdisc_full("dummy0", TcHandle::ROOT, Some(TcHandle::major_only(1)), htb)
.await?;
let class = HtbClassConfig::new(nlink::Rate::mbit(10)).build();
conn.add_class(
"dummy0",
TcHandle::major_only(1),
TcHandle::new(1, 10),
class,
)
.await?;
let classes = conn.get_classes_by_name("dummy0").await?;
assert!(!classes.is_empty());
for c in &classes {
let _bytes = c.bytes();
let _packets = c.packets();
}
Ok(())
}
#[tokio::test]
async fn test_add_tc_chain() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("chain").await?;
let htb = HtbQdiscConfig::new().default_class(0x30).build();
conn.add_qdisc_full("dummy0", TcHandle::ROOT, Some(TcHandle::major_only(1)), htb)
.await?;
conn.add_tc_chain("dummy0", TcHandle::major_only(1), 10)
.await?;
let chains = conn
.get_tc_chains("dummy0", TcHandle::major_only(1))
.await?;
assert!(chains.contains(&10), "chain 10 should exist");
Ok(())
}
#[tokio::test]
async fn test_delete_tc_chain() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("chaindel").await?;
let htb = HtbQdiscConfig::new().default_class(0x30).build();
conn.add_qdisc_full("dummy0", TcHandle::ROOT, Some(TcHandle::major_only(1)), htb)
.await?;
conn.add_tc_chain("dummy0", TcHandle::major_only(1), 20)
.await?;
conn.del_tc_chain("dummy0", TcHandle::major_only(1), 20)
.await?;
let chains = conn
.get_tc_chains("dummy0", TcHandle::major_only(1))
.await?;
assert!(!chains.contains(&20), "chain 20 should be deleted");
Ok(())
}
#[tokio::test]
async fn test_filter_with_chain() -> Result<()> {
require_root!();
let (_ns, conn) = setup_tc_ns("fchain").await?;
let htb = HtbQdiscConfig::new().default_class(0x30).build();
conn.add_qdisc_full("dummy0", TcHandle::ROOT, Some(TcHandle::major_only(1)), htb)
.await?;
let class = HtbClassConfig::new(nlink::Rate::mbit(10)).build();
conn.add_class(
"dummy0",
TcHandle::major_only(1),
TcHandle::new(1, 10),
class,
)
.await?;
conn.add_tc_chain("dummy0", TcHandle::major_only(1), 5)
.await?;
let filter = MatchallFilter::new()
.classid(TcHandle::new(1, 0x10))
.chain(5)
.build();
conn.add_filter("dummy0", TcHandle::major_only(1), filter)
.await?;
let filters = conn.get_filters_by_name("dummy0").await?;
assert!(!filters.is_empty());
Ok(())
}