use std::time::Duration;
pub use super::types::tc::qdisc::hfsc::TcServiceCurve;
pub use super::types::tc::qdisc::taprio::TaprioSchedEntry;
use super::{
Connection,
builder::MessageBuilder,
connection::{ack_request, create_request, replace_request},
error::{Error, Result},
interface_ref::InterfaceRef,
message::NlMsgType,
protocol::Route,
tc_handle::TcHandle,
types::tc::{
TcMsg, TcaAttr,
qdisc::{TcRateSpec, fq_codel, htb, netem::*, prio, sfq, tbf},
},
};
pub trait QdiscConfig: Send + Sync {
fn kind(&self) -> &'static str;
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()>;
fn default_handle(&self) -> Option<u32> {
None
}
}
#[derive(Debug, Clone, Default)]
pub struct NetemConfig {
pub delay: Option<Duration>,
pub jitter: Option<Duration>,
pub delay_correlation: crate::util::Percent,
pub loss: crate::util::Percent,
pub loss_correlation: crate::util::Percent,
pub duplicate: crate::util::Percent,
pub duplicate_correlation: crate::util::Percent,
pub corrupt: crate::util::Percent,
pub corrupt_correlation: crate::util::Percent,
pub reorder: crate::util::Percent,
pub reorder_correlation: crate::util::Percent,
pub gap: u32,
pub rate: Option<crate::util::Rate>,
pub limit: u32,
}
impl NetemConfig {
pub fn new() -> Self {
Self {
limit: 1000, ..Default::default()
}
}
pub fn delay(mut self, delay: Duration) -> Self {
self.delay = Some(delay);
self
}
pub fn jitter(mut self, jitter: Duration) -> Self {
self.jitter = Some(jitter);
self
}
pub fn delay_correlation(mut self, corr: crate::util::Percent) -> Self {
self.delay_correlation = corr;
self
}
pub fn loss(mut self, percent: crate::util::Percent) -> Self {
self.loss = percent;
self
}
pub fn loss_correlation(mut self, corr: crate::util::Percent) -> Self {
self.loss_correlation = corr;
self
}
pub fn duplicate(mut self, percent: crate::util::Percent) -> Self {
self.duplicate = percent;
self
}
pub fn duplicate_correlation(mut self, corr: crate::util::Percent) -> Self {
self.duplicate_correlation = corr;
self
}
pub fn corrupt(mut self, percent: crate::util::Percent) -> Self {
self.corrupt = percent;
self
}
pub fn corrupt_correlation(mut self, corr: crate::util::Percent) -> Self {
self.corrupt_correlation = corr;
self
}
pub fn reorder(mut self, percent: crate::util::Percent) -> Self {
self.reorder = percent;
self
}
pub fn reorder_correlation(mut self, corr: crate::util::Percent) -> Self {
self.reorder_correlation = corr;
self
}
pub fn gap(mut self, gap: u32) -> Self {
self.gap = gap;
self
}
pub fn rate(mut self, rate: crate::util::Rate) -> Self {
self.rate = Some(rate);
self
}
pub fn limit(mut self, packets: u32) -> Self {
self.limit = packets;
self
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut cfg = Self::new();
let mut i = 0;
while i < params.len() {
let key = params[i];
match key {
"delay" | "latency" => {
let time_str = params.get(i + 1).copied().ok_or_else(|| {
Error::InvalidMessage(format!("netem: `{key}` requires a value"))
})?;
cfg.delay = Some(crate::util::parse::get_time(time_str).map_err(|_| {
Error::InvalidMessage(format!(
"netem: invalid {key} `{time_str}` (expected tc-style time)"
))
})?);
i += 2;
if let Some(j) = params.get(i)
&& !is_netem_keyword(j)
{
cfg.jitter = Some(crate::util::parse::get_time(j).map_err(|_| {
Error::InvalidMessage(format!(
"netem: invalid jitter `{j}` (expected tc-style time)"
))
})?);
i += 1;
if let Some(c) = params.get(i)
&& !is_netem_keyword(c)
{
cfg.delay_correlation = parse_netem_percent(c, "delay correlation")?;
i += 1;
}
}
}
"loss" | "drop" => {
i += 1;
if key == "loss" && params.get(i) == Some(&"random") {
i += 1;
}
if let Some(next) = params.get(i)
&& (*next == "state" || *next == "gemodel")
{
return Err(Error::InvalidMessage(format!(
"netem: `loss {next}` (Markov model) is not supported by the typed parser yet — file an issue if you need this"
)));
}
let pct_str = params.get(i).copied().ok_or_else(|| {
Error::InvalidMessage(format!("netem: `{key}` requires a percent value"))
})?;
cfg.loss = parse_netem_percent(pct_str, key)?;
i += 1;
if let Some(c) = params.get(i)
&& !is_netem_keyword(c)
{
cfg.loss_correlation = parse_netem_percent(c, "loss correlation")?;
i += 1;
}
}
"duplicate" => {
let pct_str = params.get(i + 1).copied().ok_or_else(|| {
Error::InvalidMessage("netem: `duplicate` requires a percent value".into())
})?;
cfg.duplicate = parse_netem_percent(pct_str, "duplicate")?;
i += 2;
if let Some(c) = params.get(i)
&& !is_netem_keyword(c)
{
cfg.duplicate_correlation =
parse_netem_percent(c, "duplicate correlation")?;
i += 1;
}
}
"corrupt" => {
let pct_str = params.get(i + 1).copied().ok_or_else(|| {
Error::InvalidMessage("netem: `corrupt` requires a percent value".into())
})?;
cfg.corrupt = parse_netem_percent(pct_str, "corrupt")?;
i += 2;
if let Some(c) = params.get(i)
&& !is_netem_keyword(c)
{
cfg.corrupt_correlation = parse_netem_percent(c, "corrupt correlation")?;
i += 1;
}
}
"reorder" => {
let pct_str = params.get(i + 1).copied().ok_or_else(|| {
Error::InvalidMessage("netem: `reorder` requires a percent value".into())
})?;
cfg.reorder = parse_netem_percent(pct_str, "reorder")?;
i += 2;
if let Some(c) = params.get(i)
&& !is_netem_keyword(c)
{
cfg.reorder_correlation = parse_netem_percent(c, "reorder correlation")?;
i += 1;
}
}
"gap" => {
let n_str = params.get(i + 1).copied().ok_or_else(|| {
Error::InvalidMessage("netem: `gap` requires a value".into())
})?;
cfg.gap = n_str.parse().map_err(|_| {
Error::InvalidMessage(format!(
"netem: invalid gap `{n_str}` (expected unsigned integer)"
))
})?;
i += 2;
}
"rate" => {
let rate_str = params.get(i + 1).copied().ok_or_else(|| {
Error::InvalidMessage("netem: `rate` requires a value".into())
})?;
cfg.rate = Some(crate::util::Rate::parse(rate_str).map_err(|_| {
Error::InvalidMessage(format!(
"netem: invalid rate `{rate_str}` (expected tc-style rate like `100mbit`)"
))
})?);
i += 2;
if let Some(extra) = params.get(i)
&& !is_netem_keyword(extra)
{
return Err(Error::InvalidMessage(format!(
"netem: positional `rate` extras (packet_overhead/cell_size/cell_overhead) are not modelled by NetemConfig — file an issue if you need them, got `{extra}`"
)));
}
}
"limit" => {
let n_str = params.get(i + 1).copied().ok_or_else(|| {
Error::InvalidMessage("netem: `limit` requires a value".into())
})?;
cfg.limit = n_str.parse().map_err(|_| {
Error::InvalidMessage(format!(
"netem: invalid limit `{n_str}` (expected unsigned integer)"
))
})?;
i += 2;
}
"slot" | "ecn" | "distribution" => {
return Err(Error::InvalidMessage(format!(
"netem: `{key}` is not modelled by NetemConfig yet — file an issue if you need this token"
)));
}
other => {
return Err(Error::InvalidMessage(format!(
"netem: unknown token `{other}`"
)));
}
}
}
Ok(cfg)
}
}
fn is_netem_keyword(s: &str) -> bool {
matches!(
s,
"delay"
| "latency"
| "loss"
| "drop"
| "duplicate"
| "corrupt"
| "reorder"
| "gap"
| "rate"
| "limit"
| "slot"
| "ecn"
| "distribution"
| "random"
)
}
fn parse_netem_percent(s: &str, label: &str) -> Result<crate::util::Percent> {
s.parse::<crate::util::Percent>()
.map_err(|_| Error::InvalidMessage(format!("netem: invalid {label} `{s}`")))
}
impl QdiscConfig for NetemConfig {
fn kind(&self) -> &'static str {
"netem"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
if !self.reorder.is_zero() && self.delay.is_none() {
return Err(Error::InvalidMessage(
"netem: reorder requires delay to be set".into(),
));
}
let mut qopt = TcNetemQopt::new();
qopt.limit = self.limit;
if let Some(delay) = self.delay {
qopt.latency = delay.as_micros() as u32;
}
if let Some(jitter) = self.jitter {
qopt.jitter = jitter.as_micros() as u32;
}
if !self.loss.is_zero() {
qopt.loss = self.loss.as_kernel_probability();
}
if !self.duplicate.is_zero() {
qopt.duplicate = self.duplicate.as_kernel_probability();
}
if !self.reorder.is_zero() && self.gap == 0 {
qopt.gap = 1; } else {
qopt.gap = self.gap;
}
builder.append(&qopt);
if let Some(delay) = self.delay {
let latency_ns = delay.as_nanos() as i64;
builder.append_attr(TCA_NETEM_LATENCY64, &latency_ns.to_ne_bytes());
}
if let Some(jitter) = self.jitter {
let jitter_ns = jitter.as_nanos() as i64;
builder.append_attr(TCA_NETEM_JITTER64, &jitter_ns.to_ne_bytes());
}
if !self.delay_correlation.is_zero()
|| !self.loss_correlation.is_zero()
|| !self.duplicate_correlation.is_zero()
{
let corr = TcNetemCorr {
delay_corr: self.delay_correlation.as_kernel_probability(),
loss_corr: self.loss_correlation.as_kernel_probability(),
dup_corr: self.duplicate_correlation.as_kernel_probability(),
};
builder.append_attr(TCA_NETEM_CORR, corr.as_bytes());
}
if !self.reorder.is_zero() {
let reorder = TcNetemReorder {
probability: self.reorder.as_kernel_probability(),
correlation: self.reorder_correlation.as_kernel_probability(),
};
builder.append_attr(TCA_NETEM_REORDER, reorder.as_bytes());
}
if !self.corrupt.is_zero() {
let corrupt = TcNetemCorrupt {
probability: self.corrupt.as_kernel_probability(),
correlation: self.corrupt_correlation.as_kernel_probability(),
};
builder.append_attr(TCA_NETEM_CORRUPT, corrupt.as_bytes());
}
if let Some(rate) = self.rate {
let bytes_per_sec = rate.as_bytes_per_sec();
let mut rate_struct = TcNetemRate::default();
if bytes_per_sec > u32::MAX as u64 {
rate_struct.rate = u32::MAX;
builder.append_attr(TCA_NETEM_RATE, rate_struct.as_bytes());
builder.append_attr(TCA_NETEM_RATE64, &bytes_per_sec.to_ne_bytes());
} else {
rate_struct.rate = bytes_per_sec as u32;
builder.append_attr(TCA_NETEM_RATE, rate_struct.as_bytes());
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct FqCodelConfig {
pub target: Option<Duration>,
pub interval: Option<Duration>,
pub limit: Option<u32>,
pub flows: Option<u32>,
pub quantum: Option<u32>,
pub ecn: bool,
pub ce_threshold: Option<Duration>,
pub memory_limit: Option<u32>,
}
impl Default for FqCodelConfig {
fn default() -> Self {
Self::new()
}
}
impl FqCodelConfig {
pub fn new() -> Self {
Self {
target: None,
interval: None,
limit: None,
flows: None,
quantum: None,
ecn: false,
ce_threshold: None,
memory_limit: None,
}
}
pub fn target(mut self, target: Duration) -> Self {
self.target = Some(target);
self
}
pub fn interval(mut self, interval: Duration) -> Self {
self.interval = Some(interval);
self
}
pub fn limit(mut self, packets: u32) -> Self {
self.limit = Some(packets);
self
}
pub fn flows(mut self, flows: u32) -> Self {
self.flows = Some(flows);
self
}
pub fn quantum(mut self, bytes: u32) -> Self {
self.quantum = Some(bytes);
self
}
pub fn ecn(mut self, enable: bool) -> Self {
self.ecn = enable;
self
}
pub fn ce_threshold(mut self, threshold: Duration) -> Self {
self.ce_threshold = Some(threshold);
self
}
pub fn memory_limit(mut self, bytes: u32) -> Self {
self.memory_limit = Some(bytes);
self
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut cfg = Self::new();
let mut i = 0;
while i < params.len() {
let key = params[i];
let need_value = || {
params.get(i + 1).copied().ok_or_else(|| {
Error::InvalidMessage(format!("fq_codel: `{key}` requires a value"))
})
};
match key {
"limit" => {
let s = need_value()?;
cfg.limit = Some(s.parse().map_err(|_| {
Error::InvalidMessage(format!("fq_codel: invalid limit `{s}`"))
})?);
i += 2;
}
"target" => {
let s = need_value()?;
cfg.target = Some(crate::util::parse::get_time(s).map_err(|_| {
Error::InvalidMessage(format!(
"fq_codel: invalid target `{s}` (expected tc-style time)"
))
})?);
i += 2;
}
"interval" => {
let s = need_value()?;
cfg.interval = Some(crate::util::parse::get_time(s).map_err(|_| {
Error::InvalidMessage(format!(
"fq_codel: invalid interval `{s}` (expected tc-style time)"
))
})?);
i += 2;
}
"flows" => {
let s = need_value()?;
cfg.flows = Some(s.parse().map_err(|_| {
Error::InvalidMessage(format!("fq_codel: invalid flows `{s}`"))
})?);
i += 2;
}
"quantum" => {
let s = need_value()?;
cfg.quantum = Some(s.parse().map_err(|_| {
Error::InvalidMessage(format!(
"fq_codel: invalid quantum `{s}` (expected unsigned integer bytes)"
))
})?);
i += 2;
}
"ce_threshold" => {
let s = need_value()?;
cfg.ce_threshold = Some(crate::util::parse::get_time(s).map_err(|_| {
Error::InvalidMessage(format!(
"fq_codel: invalid ce_threshold `{s}` (expected tc-style time)"
))
})?);
i += 2;
}
"memory_limit" => {
let s = need_value()?;
let bytes = crate::util::parse::get_size(s).map_err(|_| {
Error::InvalidMessage(format!(
"fq_codel: invalid memory_limit `{s}` (expected tc-style size)"
))
})?;
cfg.memory_limit = Some(bytes.try_into().map_err(|_| {
Error::InvalidMessage(format!("fq_codel: memory_limit `{s}` exceeds u32"))
})?);
i += 2;
}
"ecn" => {
cfg.ecn = true;
i += 1;
}
"noecn" => {
cfg.ecn = false;
i += 1;
}
other => {
return Err(Error::InvalidMessage(format!(
"fq_codel: unknown token `{other}`"
)));
}
}
}
Ok(cfg)
}
}
impl QdiscConfig for FqCodelConfig {
fn kind(&self) -> &'static str {
"fq_codel"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
if let Some(target) = self.target {
builder.append_attr_u32(fq_codel::TCA_FQ_CODEL_TARGET, target.as_micros() as u32);
}
if let Some(interval) = self.interval {
builder.append_attr_u32(fq_codel::TCA_FQ_CODEL_INTERVAL, interval.as_micros() as u32);
}
if let Some(limit) = self.limit {
builder.append_attr_u32(fq_codel::TCA_FQ_CODEL_LIMIT, limit);
}
if let Some(flows) = self.flows {
builder.append_attr_u32(fq_codel::TCA_FQ_CODEL_FLOWS, flows);
}
if let Some(quantum) = self.quantum {
builder.append_attr_u32(fq_codel::TCA_FQ_CODEL_QUANTUM, quantum);
}
if self.ecn {
builder.append_attr_u32(fq_codel::TCA_FQ_CODEL_ECN, 1);
}
if let Some(ce) = self.ce_threshold {
builder.append_attr_u32(fq_codel::TCA_FQ_CODEL_CE_THRESHOLD, ce.as_micros() as u32);
}
if let Some(mem) = self.memory_limit {
builder.append_attr_u32(fq_codel::TCA_FQ_CODEL_MEMORY_LIMIT, mem);
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct TbfConfig {
pub rate: crate::util::Rate,
pub peakrate: Option<crate::util::Rate>,
pub burst: crate::util::Bytes,
pub mtu: u32,
pub limit: crate::util::Bytes,
}
impl Default for TbfConfig {
fn default() -> Self {
Self::new()
}
}
impl TbfConfig {
pub fn new() -> Self {
Self {
rate: crate::util::Rate::ZERO,
peakrate: None,
burst: crate::util::Bytes::ZERO,
mtu: 1514,
limit: crate::util::Bytes::ZERO,
}
}
pub fn rate(mut self, rate: crate::util::Rate) -> Self {
self.rate = rate;
self
}
pub fn peakrate(mut self, rate: crate::util::Rate) -> Self {
self.peakrate = Some(rate);
self
}
pub fn burst(mut self, b: crate::util::Bytes) -> Self {
self.burst = b;
self
}
pub fn mtu(mut self, mtu: u32) -> Self {
self.mtu = mtu;
self
}
pub fn limit(mut self, b: crate::util::Bytes) -> Self {
self.limit = b;
self
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut cfg = Self::new();
let mut i = 0;
while i < params.len() {
let key = params[i];
let need_value = || {
params
.get(i + 1)
.copied()
.ok_or_else(|| Error::InvalidMessage(format!("tbf: `{key}` requires a value")))
};
match key {
"rate" => {
let s = need_value()?;
cfg.rate = crate::util::Rate::parse(s).map_err(|_| {
Error::InvalidMessage(format!(
"tbf: invalid rate `{s}` (expected tc-style rate like `1mbit`)"
))
})?;
i += 2;
}
"peakrate" => {
let s = need_value()?;
cfg.peakrate = Some(crate::util::Rate::parse(s).map_err(|_| {
Error::InvalidMessage(format!(
"tbf: invalid peakrate `{s}` (expected tc-style rate)"
))
})?);
i += 2;
}
"burst" | "buffer" | "maxburst" => {
let s = need_value()?;
let bytes = crate::util::parse::get_size(s).map_err(|_| {
Error::InvalidMessage(format!(
"tbf: invalid {key} `{s}` (expected tc-style size)"
))
})?;
cfg.burst = crate::util::Bytes::new(bytes);
i += 2;
}
"limit" => {
let s = need_value()?;
let bytes = crate::util::parse::get_size(s).map_err(|_| {
Error::InvalidMessage(format!(
"tbf: invalid limit `{s}` (expected tc-style size)"
))
})?;
cfg.limit = crate::util::Bytes::new(bytes);
i += 2;
}
"mtu" | "minburst" => {
let s = need_value()?;
let bytes = crate::util::parse::get_size(s).map_err(|_| {
Error::InvalidMessage(format!(
"tbf: invalid {key} `{s}` (expected tc-style size)"
))
})?;
cfg.mtu = bytes.try_into().map_err(|_| {
Error::InvalidMessage(format!("tbf: {key} `{s}` exceeds u32 (max ~4GB)"))
})?;
i += 2;
}
"latency" => {
return Err(Error::InvalidMessage(
"tbf: `latency` is a derived form (limit = rate * latency) and is not modelled by TbfConfig — compute the limit yourself".into(),
));
}
other => {
return Err(Error::InvalidMessage(format!(
"tbf: unknown token `{other}`"
)));
}
}
}
Ok(cfg)
}
}
impl QdiscConfig for TbfConfig {
fn kind(&self) -> &'static str {
"tbf"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
let rate_bps = self.rate.as_bytes_per_sec();
let peakrate_bps = self.peakrate.map(|p| p.as_bytes_per_sec());
let qopt = tbf::TcTbfQopt {
rate: TcRateSpec::new(rate_bps.min(u32::MAX as u64) as u32),
peakrate: peakrate_bps
.map(|pr| TcRateSpec::new(pr.min(u32::MAX as u64) as u32))
.unwrap_or_default(),
limit: self.limit.as_u32_saturating(),
buffer: self.burst.as_u32_saturating(),
mtu: self.mtu,
};
builder.append_attr(tbf::TCA_TBF_PARMS, qopt.as_bytes());
if rate_bps > u32::MAX as u64 {
builder.append_attr(tbf::TCA_TBF_RATE64, &rate_bps.to_ne_bytes());
}
if let Some(pr) = peakrate_bps
&& pr > u32::MAX as u64
{
builder.append_attr(tbf::TCA_TBF_PRATE64, &pr.to_ne_bytes());
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct HtbQdiscConfig {
pub default_class: u32,
pub r2q: u32,
pub direct_qlen: Option<u32>,
}
impl Default for HtbQdiscConfig {
fn default() -> Self {
Self::new()
}
}
impl HtbQdiscConfig {
pub fn new() -> Self {
Self {
default_class: 0,
r2q: 10,
direct_qlen: None,
}
}
pub fn default_class(mut self, classid: u32) -> Self {
self.default_class = classid;
self
}
pub fn r2q(mut self, r2q: u32) -> Self {
self.r2q = r2q;
self
}
pub fn direct_qlen(mut self, qlen: u32) -> Self {
self.direct_qlen = Some(qlen);
self
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut cfg = Self::new();
let mut i = 0;
while i < params.len() {
let key = params[i];
let value = || {
params
.get(i + 1)
.copied()
.ok_or_else(|| Error::InvalidMessage(format!("htb: `{key}` requires a value")))
};
match key {
"default" => {
cfg.default_class = parse_default_class(value()?)?;
i += 2;
}
"r2q" => {
cfg.r2q = value()?.parse().map_err(|_| {
Error::InvalidMessage(format!(
"htb: invalid r2q `{}` (expected unsigned integer)",
params[i + 1]
))
})?;
i += 2;
}
"direct_qlen" => {
cfg.direct_qlen = Some(value()?.parse().map_err(|_| {
Error::InvalidMessage(format!(
"htb: invalid direct_qlen `{}` (expected unsigned integer)",
params[i + 1]
))
})?);
i += 2;
}
other => {
return Err(Error::InvalidMessage(format!(
"htb: unknown token `{other}` (expected default, r2q, or direct_qlen)"
)));
}
}
}
Ok(cfg)
}
}
fn parse_default_class(s: &str) -> Result<u32> {
if s.contains(':') {
s.parse::<crate::netlink::tc_handle::TcHandle>()
.map(|h| h.as_raw())
.map_err(|e| Error::InvalidMessage(format!("htb: invalid default class `{s}`: {e}")))
} else {
u32::from_str_radix(s, 16).map_err(|_| {
Error::InvalidMessage(format!(
"htb: invalid default class `{s}` (expected hex minor or tc handle)"
))
})
}
}
impl QdiscConfig for HtbQdiscConfig {
fn kind(&self) -> &'static str {
"htb"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
let glob = htb::TcHtbGlob::new().with_default(self.default_class);
builder.append_attr(htb::TCA_HTB_INIT, glob.as_bytes());
if let Some(qlen) = self.direct_qlen {
builder.append_attr_u32(htb::TCA_HTB_DIRECT_QLEN, qlen);
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct PrioConfig {
pub bands: i32,
pub priomap: [u8; 16],
}
impl Default for PrioConfig {
fn default() -> Self {
Self::new()
}
}
impl PrioConfig {
pub fn new() -> Self {
Self {
bands: 3,
priomap: [1, 2, 2, 2, 1, 2, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],
}
}
pub fn bands(mut self, bands: i32) -> Self {
self.bands = bands;
self
}
pub fn priomap(mut self, map: [u8; 16]) -> Self {
self.priomap = map;
self
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut cfg = Self::new();
let mut i = 0;
while i < params.len() {
let key = params[i];
match key {
"bands" => {
let s = params.get(i + 1).copied().ok_or_else(|| {
Error::InvalidMessage("prio: `bands` requires a value".into())
})?;
cfg.bands = s.parse().map_err(|_| {
Error::InvalidMessage(format!(
"prio: invalid bands `{s}` (expected signed integer)"
))
})?;
i += 2;
}
"priomap" => {
if params.len() < i + 1 + 16 {
return Err(Error::InvalidMessage(format!(
"prio: `priomap` requires exactly 16 values, got {}",
params.len().saturating_sub(i + 1)
)));
}
for j in 0..16 {
let s = params[i + 1 + j];
cfg.priomap[j] = s.parse().map_err(|_| {
Error::InvalidMessage(format!(
"prio: invalid priomap[{j}] `{s}` (expected 0-255)"
))
})?;
}
i += 17;
}
other => {
return Err(Error::InvalidMessage(format!(
"prio: unknown token `{other}`"
)));
}
}
}
Ok(cfg)
}
}
impl QdiscConfig for PrioConfig {
fn kind(&self) -> &'static str {
"prio"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
let qopt = prio::TcPrioQopt {
bands: self.bands,
priomap: self.priomap,
};
builder.append(&qopt);
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct SfqConfig {
pub perturb: i32,
pub limit: u32,
pub quantum: u32,
}
impl Default for SfqConfig {
fn default() -> Self {
Self::new()
}
}
impl SfqConfig {
pub fn new() -> Self {
Self {
perturb: 0,
limit: 127,
quantum: 0,
}
}
pub fn perturb(mut self, seconds: i32) -> Self {
self.perturb = seconds;
self
}
pub fn limit(mut self, limit: u32) -> Self {
self.limit = limit;
self
}
pub fn quantum(mut self, bytes: u32) -> Self {
self.quantum = bytes;
self
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut cfg = Self::new();
let mut i = 0;
while i < params.len() {
let key = params[i];
let need_value = || {
params
.get(i + 1)
.copied()
.ok_or_else(|| Error::InvalidMessage(format!("sfq: `{key}` requires a value")))
};
match key {
"quantum" => {
let s = need_value()?;
let bytes = crate::util::parse::get_size(s).map_err(|_| {
Error::InvalidMessage(format!(
"sfq: invalid quantum `{s}` (expected tc-style size)"
))
})?;
cfg.quantum = bytes.try_into().map_err(|_| {
Error::InvalidMessage(format!("sfq: quantum `{s}` exceeds u32"))
})?;
i += 2;
}
"perturb" => {
let s = need_value()?;
cfg.perturb = s.parse().map_err(|_| {
Error::InvalidMessage(format!(
"sfq: invalid perturb `{s}` (expected signed integer seconds)"
))
})?;
i += 2;
}
"limit" => {
let s = need_value()?;
cfg.limit = s.parse().map_err(|_| {
Error::InvalidMessage(format!(
"sfq: invalid limit `{s}` (expected unsigned integer packets)"
))
})?;
i += 2;
}
"divisor" => {
return Err(Error::InvalidMessage(
"sfq: `divisor` is not modelled by SfqConfig — file an issue if you need it"
.into(),
));
}
other => {
return Err(Error::InvalidMessage(format!(
"sfq: unknown token `{other}`"
)));
}
}
}
Ok(cfg)
}
}
impl QdiscConfig for SfqConfig {
fn kind(&self) -> &'static str {
"sfq"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
let qopt = sfq::TcSfqQopt {
quantum: self.quantum,
perturb_period: self.perturb,
limit: self.limit,
divisor: 0,
flows: 0,
};
builder.append(&qopt);
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct RedConfig {
pub limit: u32,
pub min: u32,
pub max: u32,
pub max_p: u8,
pub ecn: bool,
pub harddrop: bool,
pub adaptive: bool,
}
impl Default for RedConfig {
fn default() -> Self {
Self::new()
}
}
impl RedConfig {
pub fn new() -> Self {
Self {
limit: 0,
min: 0,
max: 0,
max_p: 5, ecn: false,
harddrop: false,
adaptive: false,
}
}
pub fn limit(mut self, bytes: u32) -> Self {
self.limit = bytes;
self
}
pub fn min(mut self, bytes: u32) -> Self {
self.min = bytes;
self
}
pub fn max(mut self, bytes: u32) -> Self {
self.max = bytes;
self
}
pub fn max_probability(mut self, percent: f64) -> Self {
self.max_p = ((percent / 100.0) * 255.0).clamp(0.0, 255.0) as u8;
self
}
pub fn ecn(mut self, enable: bool) -> Self {
self.ecn = enable;
self
}
pub fn harddrop(mut self, enable: bool) -> Self {
self.harddrop = enable;
self
}
pub fn adaptive(mut self, enable: bool) -> Self {
self.adaptive = enable;
self
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut cfg = Self::new();
let mut i = 0;
while i < params.len() {
let key = params[i];
let need_value = || {
params
.get(i + 1)
.copied()
.ok_or_else(|| Error::InvalidMessage(format!("red: `{key}` requires a value")))
};
match key {
"limit" | "min" | "max" => {
let s = need_value()?;
let bytes = crate::util::parse::get_size(s).map_err(|_| {
Error::InvalidMessage(format!(
"red: invalid {key} `{s}` (expected tc-style size)"
))
})?;
let val: u32 = bytes.try_into().map_err(|_| {
Error::InvalidMessage(format!("red: {key} `{s}` exceeds u32"))
})?;
match key {
"limit" => cfg.limit = val,
"min" => cfg.min = val,
"max" => cfg.max = val,
_ => unreachable!(),
}
i += 2;
}
"probability" => {
let s = need_value()?;
let pct: f64 = s.parse().map_err(|_| {
Error::InvalidMessage(format!(
"red: invalid probability `{s}` (expected percentage 0-100)"
))
})?;
cfg = cfg.max_probability(pct);
i += 2;
}
"ecn" => {
cfg.ecn = true;
i += 1;
}
"noecn" => {
cfg.ecn = false;
i += 1;
}
"harddrop" => {
cfg.harddrop = true;
i += 1;
}
"noharddrop" => {
cfg.harddrop = false;
i += 1;
}
"adaptive" => {
cfg.adaptive = true;
i += 1;
}
"noadaptive" => {
cfg.adaptive = false;
i += 1;
}
"avpkt" | "burst" | "bandwidth" => {
return Err(Error::InvalidMessage(format!(
"red: `{key}` is not modelled by RedConfig — drop to a hand-rolled MessageBuilder if needed"
)));
}
other => {
return Err(Error::InvalidMessage(format!(
"red: unknown token `{other}`"
)));
}
}
}
Ok(cfg)
}
}
impl QdiscConfig for RedConfig {
fn kind(&self) -> &'static str {
"red"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
use super::types::tc::qdisc::red;
let mut flags: u8 = 0;
if self.ecn {
flags |= red::TC_RED_ECN as u8;
}
if self.harddrop {
flags |= red::TC_RED_HARDDROP as u8;
}
if self.adaptive {
flags |= red::TC_RED_ADAPTATIVE as u8;
}
let qopt = red::TcRedQopt {
limit: self.limit,
qth_min: self.min,
qth_max: self.max,
wlog: 9, plog: 13, scell_log: 0, flags,
};
builder.append_attr(red::TCA_RED_PARMS, qopt.as_bytes());
let max_p = (self.max_p as u32) << 24;
builder.append_attr_u32(red::TCA_RED_MAX_P, max_p);
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct PieConfig {
pub target: Option<Duration>,
pub limit: Option<u32>,
pub tupdate: Option<Duration>,
pub alpha: Option<u32>,
pub beta: Option<u32>,
pub ecn: bool,
pub bytemode: bool,
}
impl Default for PieConfig {
fn default() -> Self {
Self::new()
}
}
impl PieConfig {
pub fn new() -> Self {
Self {
target: None,
limit: None,
tupdate: None,
alpha: None,
beta: None,
ecn: false,
bytemode: false,
}
}
pub fn target(mut self, target: Duration) -> Self {
self.target = Some(target);
self
}
pub fn limit(mut self, packets: u32) -> Self {
self.limit = Some(packets);
self
}
pub fn tupdate(mut self, interval: Duration) -> Self {
self.tupdate = Some(interval);
self
}
pub fn alpha(mut self, alpha: u32) -> Self {
self.alpha = Some(alpha);
self
}
pub fn beta(mut self, beta: u32) -> Self {
self.beta = Some(beta);
self
}
pub fn ecn(mut self, enable: bool) -> Self {
self.ecn = enable;
self
}
pub fn bytemode(mut self, enable: bool) -> Self {
self.bytemode = enable;
self
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut cfg = Self::new();
let mut i = 0;
while i < params.len() {
let key = params[i];
let need_value = || {
params
.get(i + 1)
.copied()
.ok_or_else(|| Error::InvalidMessage(format!("pie: `{key}` requires a value")))
};
match key {
"target" => {
let s = need_value()?;
cfg.target = Some(crate::util::parse::get_time(s).map_err(|_| {
Error::InvalidMessage(format!(
"pie: invalid target `{s}` (expected tc-style time)"
))
})?);
i += 2;
}
"limit" => {
let s = need_value()?;
cfg.limit =
Some(s.parse().map_err(|_| {
Error::InvalidMessage(format!("pie: invalid limit `{s}`"))
})?);
i += 2;
}
"tupdate" => {
let s = need_value()?;
cfg.tupdate = Some(crate::util::parse::get_time(s).map_err(|_| {
Error::InvalidMessage(format!(
"pie: invalid tupdate `{s}` (expected tc-style time)"
))
})?);
i += 2;
}
"alpha" => {
let s = need_value()?;
cfg.alpha =
Some(s.parse().map_err(|_| {
Error::InvalidMessage(format!("pie: invalid alpha `{s}`"))
})?);
i += 2;
}
"beta" => {
let s = need_value()?;
cfg.beta =
Some(s.parse().map_err(|_| {
Error::InvalidMessage(format!("pie: invalid beta `{s}`"))
})?);
i += 2;
}
"ecn" => {
cfg.ecn = true;
i += 1;
}
"noecn" => {
cfg.ecn = false;
i += 1;
}
"bytemode" => {
cfg.bytemode = true;
i += 1;
}
"nobytemode" => {
cfg.bytemode = false;
i += 1;
}
other => {
return Err(Error::InvalidMessage(format!(
"pie: unknown token `{other}`"
)));
}
}
}
Ok(cfg)
}
}
impl QdiscConfig for PieConfig {
fn kind(&self) -> &'static str {
"pie"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
use super::types::tc::qdisc::pie;
if let Some(target) = self.target {
builder.append_attr_u32(pie::TCA_PIE_TARGET, target.as_micros() as u32);
}
if let Some(limit) = self.limit {
builder.append_attr_u32(pie::TCA_PIE_LIMIT, limit);
}
if let Some(tupdate) = self.tupdate {
builder.append_attr_u32(pie::TCA_PIE_TUPDATE, tupdate.as_micros() as u32);
}
if let Some(alpha) = self.alpha {
builder.append_attr_u32(pie::TCA_PIE_ALPHA, alpha);
}
if let Some(beta) = self.beta {
builder.append_attr_u32(pie::TCA_PIE_BETA, beta);
}
if self.ecn {
builder.append_attr_u32(pie::TCA_PIE_ECN, 1);
}
if self.bytemode {
builder.append_attr_u32(pie::TCA_PIE_BYTEMODE, 1);
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct FqPieConfig {
pub limit: Option<u32>,
pub flows: Option<u32>,
pub target: Option<Duration>,
pub tupdate: Option<Duration>,
pub alpha: Option<u32>,
pub beta: Option<u32>,
pub quantum: Option<crate::util::Bytes>,
pub memory_limit: Option<crate::util::Bytes>,
pub ecn_prob: Option<crate::util::Percent>,
pub ecn: bool,
pub bytemode: bool,
pub dq_rate_estimator: bool,
}
impl Default for FqPieConfig {
fn default() -> Self {
Self::new()
}
}
impl FqPieConfig {
pub fn new() -> Self {
Self {
limit: None,
flows: None,
target: None,
tupdate: None,
alpha: None,
beta: None,
quantum: None,
memory_limit: None,
ecn_prob: None,
ecn: false,
bytemode: false,
dq_rate_estimator: false,
}
}
pub fn limit(mut self, packets: u32) -> Self {
self.limit = Some(packets);
self
}
pub fn flows(mut self, n: u32) -> Self {
self.flows = Some(n);
self
}
pub fn target(mut self, target: Duration) -> Self {
self.target = Some(target);
self
}
pub fn tupdate(mut self, interval: Duration) -> Self {
self.tupdate = Some(interval);
self
}
pub fn alpha(mut self, alpha: u32) -> Self {
self.alpha = Some(alpha);
self
}
pub fn beta(mut self, beta: u32) -> Self {
self.beta = Some(beta);
self
}
pub fn quantum(mut self, quantum: crate::util::Bytes) -> Self {
self.quantum = Some(quantum);
self
}
pub fn memory_limit(mut self, limit: crate::util::Bytes) -> Self {
self.memory_limit = Some(limit);
self
}
pub fn ecn_prob(mut self, prob: crate::util::Percent) -> Self {
self.ecn_prob = Some(prob);
self
}
pub fn ecn(mut self, enable: bool) -> Self {
self.ecn = enable;
self
}
pub fn bytemode(mut self, enable: bool) -> Self {
self.bytemode = enable;
self
}
pub fn dq_rate_estimator(mut self, enable: bool) -> Self {
self.dq_rate_estimator = enable;
self
}
pub fn build(self) -> Self {
self
}
}
impl QdiscConfig for FqPieConfig {
fn kind(&self) -> &'static str {
"fq_pie"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
use super::types::tc::qdisc::fq_pie;
if let Some(limit) = self.limit {
builder.append_attr_u32(fq_pie::TCA_FQ_PIE_LIMIT, limit);
}
if let Some(flows) = self.flows {
builder.append_attr_u32(fq_pie::TCA_FQ_PIE_FLOWS, flows);
}
if let Some(target) = self.target {
builder.append_attr_u32(fq_pie::TCA_FQ_PIE_TARGET, target.as_micros() as u32);
}
if let Some(tupdate) = self.tupdate {
builder.append_attr_u32(fq_pie::TCA_FQ_PIE_TUPDATE, tupdate.as_micros() as u32);
}
if let Some(alpha) = self.alpha {
builder.append_attr_u32(fq_pie::TCA_FQ_PIE_ALPHA, alpha);
}
if let Some(beta) = self.beta {
builder.append_attr_u32(fq_pie::TCA_FQ_PIE_BETA, beta);
}
if let Some(quantum) = self.quantum {
builder.append_attr_u32(fq_pie::TCA_FQ_PIE_QUANTUM, quantum.as_u32_saturating());
}
if let Some(memory_limit) = self.memory_limit {
builder.append_attr_u32(
fq_pie::TCA_FQ_PIE_MEMORY_LIMIT,
memory_limit.as_u32_saturating(),
);
}
if let Some(prob) = self.ecn_prob {
let permille = (prob.as_percent() * 10.0) as u32;
builder.append_attr_u32(fq_pie::TCA_FQ_PIE_ECN_PROB, permille);
}
if self.ecn {
builder.append_attr_u32(fq_pie::TCA_FQ_PIE_ECN, 1);
}
if self.bytemode {
builder.append_attr_u32(fq_pie::TCA_FQ_PIE_BYTEMODE, 1);
}
if self.dq_rate_estimator {
builder.append_attr_u32(fq_pie::TCA_FQ_PIE_DQ_RATE_ESTIMATOR, 1);
}
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct IngressConfig;
impl IngressConfig {
pub fn new() -> Self {
Self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
if let Some(token) = params.first() {
return Err(Error::InvalidMessage(format!(
"ingress: takes no parameters (got `{token}`)"
)));
}
Ok(Self::new())
}
}
impl QdiscConfig for IngressConfig {
fn kind(&self) -> &'static str {
"ingress"
}
fn write_options(&self, _builder: &mut MessageBuilder) -> Result<()> {
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct ClsactConfig;
impl ClsactConfig {
pub fn new() -> Self {
Self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
if let Some(token) = params.first() {
return Err(Error::InvalidMessage(format!(
"clsact: takes no parameters (got `{token}`)"
)));
}
Ok(Self::new())
}
}
impl QdiscConfig for ClsactConfig {
fn kind(&self) -> &'static str {
"clsact"
}
fn write_options(&self, _builder: &mut MessageBuilder) -> Result<()> {
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct PfifoConfig {
pub limit: u32,
}
impl Default for PfifoConfig {
fn default() -> Self {
Self::new()
}
}
impl PfifoConfig {
pub fn new() -> Self {
Self { limit: 1000 }
}
pub fn limit(mut self, packets: u32) -> Self {
self.limit = packets;
self
}
pub fn build(self) -> Self {
self
}
}
impl QdiscConfig for PfifoConfig {
fn kind(&self) -> &'static str {
"pfifo"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
use super::types::tc::qdisc::fifo::TcFifoQopt;
let qopt = TcFifoQopt::new(self.limit);
builder.append(&qopt);
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct BfifoConfig {
pub limit: u32,
}
impl Default for BfifoConfig {
fn default() -> Self {
Self::new()
}
}
impl BfifoConfig {
pub fn new() -> Self {
Self {
limit: 100 * 1024, }
}
pub fn limit(mut self, bytes: u32) -> Self {
self.limit = bytes;
self
}
pub fn build(self) -> Self {
self
}
}
impl QdiscConfig for BfifoConfig {
fn kind(&self) -> &'static str {
"bfifo"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
use super::types::tc::qdisc::fifo::TcFifoQopt;
let qopt = TcFifoQopt::new(self.limit);
builder.append(&qopt);
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct DrrConfig {}
impl Default for DrrConfig {
fn default() -> Self {
Self::new()
}
}
impl DrrConfig {
pub fn new() -> Self {
Self {}
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
if let Some(token) = params.first() {
return Err(Error::InvalidMessage(format!(
"drr: qdisc takes no parameters (got `{token}`); per-class quantum belongs on DrrClassConfig"
)));
}
Ok(Self::new())
}
}
impl QdiscConfig for DrrConfig {
fn kind(&self) -> &'static str {
"drr"
}
fn write_options(&self, _builder: &mut MessageBuilder) -> Result<()> {
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct QfqConfig {}
impl Default for QfqConfig {
fn default() -> Self {
Self::new()
}
}
impl QfqConfig {
pub fn new() -> Self {
Self {}
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
if let Some(token) = params.first() {
return Err(Error::InvalidMessage(format!(
"qfq: qdisc takes no parameters (got `{token}`); per-class weight/lmax belongs on QfqClassConfig"
)));
}
Ok(Self::new())
}
}
impl QdiscConfig for QfqConfig {
fn kind(&self) -> &'static str {
"qfq"
}
fn write_options(&self, _builder: &mut MessageBuilder) -> Result<()> {
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CakeDiffserv {
Diffserv3,
Diffserv4,
Diffserv8,
Besteffort,
Precedence,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CakeFlowMode {
Flowblind,
Srchost,
Dsthost,
Hosts,
Flows,
DualSrchost,
DualDsthost,
Triple,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CakeAtmMode {
None,
Atm,
Ptm,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CakeAckFilter {
Disabled,
Filter,
Aggressive,
}
#[derive(Debug, Clone)]
pub struct CakeConfig {
pub bandwidth: Option<crate::util::Rate>,
pub rtt: Option<Duration>,
pub target: Option<Duration>,
pub overhead: Option<i32>,
pub mpu: Option<u32>,
pub memory_limit: Option<crate::util::Bytes>,
pub fwmark: Option<u32>,
pub diffserv_mode: Option<CakeDiffserv>,
pub flow_mode: Option<CakeFlowMode>,
pub atm_mode: Option<CakeAtmMode>,
pub ack_filter: Option<CakeAckFilter>,
pub autorate: bool,
pub nat: bool,
pub raw: bool,
pub wash: bool,
pub ingress: bool,
pub split_gso: bool,
}
impl Default for CakeConfig {
fn default() -> Self {
Self::new()
}
}
impl CakeConfig {
pub fn new() -> Self {
Self {
bandwidth: None,
rtt: None,
target: None,
overhead: None,
mpu: None,
memory_limit: None,
fwmark: None,
diffserv_mode: None,
flow_mode: None,
atm_mode: None,
ack_filter: None,
autorate: false,
nat: false,
raw: false,
wash: false,
ingress: false,
split_gso: false,
}
}
pub fn bandwidth(mut self, rate: crate::util::Rate) -> Self {
self.bandwidth = Some(rate);
self
}
pub fn unlimited(mut self) -> Self {
self.bandwidth = Some(crate::util::Rate::ZERO);
self
}
pub fn rtt(mut self, rtt: Duration) -> Self {
self.rtt = Some(rtt);
self
}
pub fn target(mut self, target: Duration) -> Self {
self.target = Some(target);
self
}
pub fn overhead(mut self, overhead: i32) -> Self {
self.overhead = Some(overhead);
self
}
pub fn mpu(mut self, mpu: u32) -> Self {
self.mpu = Some(mpu);
self
}
pub fn memory_limit(mut self, mem: crate::util::Bytes) -> Self {
self.memory_limit = Some(mem);
self
}
pub fn fwmark(mut self, mask: u32) -> Self {
self.fwmark = Some(mask);
self
}
pub fn diffserv_mode(mut self, mode: CakeDiffserv) -> Self {
self.diffserv_mode = Some(mode);
self
}
pub fn flow_mode(mut self, mode: CakeFlowMode) -> Self {
self.flow_mode = Some(mode);
self
}
pub fn atm_mode(mut self, mode: CakeAtmMode) -> Self {
self.atm_mode = Some(mode);
self
}
pub fn ack_filter(mut self, mode: CakeAckFilter) -> Self {
self.ack_filter = Some(mode);
self
}
pub fn autorate(mut self, enable: bool) -> Self {
self.autorate = enable;
self
}
pub fn nat(mut self, enable: bool) -> Self {
self.nat = enable;
self
}
pub fn raw(mut self, enable: bool) -> Self {
self.raw = enable;
self
}
pub fn wash(mut self, enable: bool) -> Self {
self.wash = enable;
self
}
pub fn ingress(mut self, enable: bool) -> Self {
self.ingress = enable;
self
}
pub fn split_gso(mut self, enable: bool) -> Self {
self.split_gso = enable;
self
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut cfg = Self::new();
let mut i = 0;
while i < params.len() {
let key = params[i];
let need_value = || {
params
.get(i + 1)
.copied()
.ok_or_else(|| Error::InvalidMessage(format!("cake: `{key}` requires a value")))
};
match key {
"bandwidth" => {
let s = need_value()?;
cfg.bandwidth = Some(crate::util::Rate::parse(s).map_err(|_| {
Error::InvalidMessage(format!(
"cake: invalid bandwidth `{s}` (expected tc-style rate like `100mbit`)"
))
})?);
i += 2;
}
"rtt" => {
let s = need_value()?;
cfg.rtt = Some(crate::util::parse::get_time(s).map_err(|_| {
Error::InvalidMessage(format!(
"cake: invalid rtt `{s}` (expected tc-style time)"
))
})?);
i += 2;
}
"target" => {
let s = need_value()?;
cfg.target = Some(crate::util::parse::get_time(s).map_err(|_| {
Error::InvalidMessage(format!(
"cake: invalid target `{s}` (expected tc-style time)"
))
})?);
i += 2;
}
"overhead" => {
let s = need_value()?;
cfg.overhead = Some(s.parse().map_err(|_| {
Error::InvalidMessage(format!(
"cake: invalid overhead `{s}` (expected signed integer bytes)"
))
})?);
i += 2;
}
"mpu" => {
let s = need_value()?;
cfg.mpu = Some(s.parse().map_err(|_| {
Error::InvalidMessage(format!(
"cake: invalid mpu `{s}` (expected unsigned integer bytes)"
))
})?);
i += 2;
}
"memlimit" => {
let s = need_value()?;
let bytes = crate::util::parse::get_size(s).map_err(|_| {
Error::InvalidMessage(format!(
"cake: invalid memlimit `{s}` (expected tc-style size)"
))
})?;
cfg.memory_limit = Some(crate::util::Bytes::new(bytes));
i += 2;
}
"fwmark" => {
let s = need_value()?;
let trimmed = s.strip_prefix("0x").unwrap_or(s);
cfg.fwmark = Some(u32::from_str_radix(trimmed, 16).map_err(|_| {
Error::InvalidMessage(format!(
"cake: invalid fwmark `{s}` (expected hex u32)"
))
})?);
i += 2;
}
"diffserv3" => {
cfg.diffserv_mode = Some(CakeDiffserv::Diffserv3);
i += 1;
}
"diffserv4" => {
cfg.diffserv_mode = Some(CakeDiffserv::Diffserv4);
i += 1;
}
"diffserv8" => {
cfg.diffserv_mode = Some(CakeDiffserv::Diffserv8);
i += 1;
}
"besteffort" => {
cfg.diffserv_mode = Some(CakeDiffserv::Besteffort);
i += 1;
}
"precedence" => {
cfg.diffserv_mode = Some(CakeDiffserv::Precedence);
i += 1;
}
"flowblind" => {
cfg.flow_mode = Some(CakeFlowMode::Flowblind);
i += 1;
}
"srchost" => {
cfg.flow_mode = Some(CakeFlowMode::Srchost);
i += 1;
}
"dsthost" => {
cfg.flow_mode = Some(CakeFlowMode::Dsthost);
i += 1;
}
"hosts" => {
cfg.flow_mode = Some(CakeFlowMode::Hosts);
i += 1;
}
"flows" => {
cfg.flow_mode = Some(CakeFlowMode::Flows);
i += 1;
}
"dual-srchost" => {
cfg.flow_mode = Some(CakeFlowMode::DualSrchost);
i += 1;
}
"dual-dsthost" => {
cfg.flow_mode = Some(CakeFlowMode::DualDsthost);
i += 1;
}
"triple-isolate" => {
cfg.flow_mode = Some(CakeFlowMode::Triple);
i += 1;
}
"noatm" => {
cfg.atm_mode = Some(CakeAtmMode::None);
i += 1;
}
"atm" => {
cfg.atm_mode = Some(CakeAtmMode::Atm);
i += 1;
}
"ptm" => {
cfg.atm_mode = Some(CakeAtmMode::Ptm);
i += 1;
}
"no-ack-filter" => {
cfg.ack_filter = Some(CakeAckFilter::Disabled);
i += 1;
}
"ack-filter" => {
cfg.ack_filter = Some(CakeAckFilter::Filter);
i += 1;
}
"ack-filter-aggressive" => {
cfg.ack_filter = Some(CakeAckFilter::Aggressive);
i += 1;
}
"raw" => {
cfg.raw = true;
i += 1;
}
"nat" => {
cfg.nat = true;
i += 1;
}
"nonat" => {
cfg.nat = false;
i += 1;
}
"wash" => {
cfg.wash = true;
i += 1;
}
"nowash" => {
cfg.wash = false;
i += 1;
}
"ingress" => {
cfg.ingress = true;
i += 1;
}
"egress" => {
cfg.ingress = false;
i += 1;
}
"split-gso" => {
cfg.split_gso = true;
i += 1;
}
"no-split-gso" => {
cfg.split_gso = false;
i += 1;
}
"autorate-ingress" => {
cfg.autorate = true;
i += 1;
}
"unlimited" => {
cfg.bandwidth = Some(crate::util::Rate::ZERO);
i += 1;
}
other => {
return Err(Error::InvalidMessage(format!(
"cake: unknown token `{other}`"
)));
}
}
}
Ok(cfg)
}
}
impl QdiscConfig for CakeConfig {
fn kind(&self) -> &'static str {
"cake"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
use super::types::tc::qdisc::cake::*;
if let Some(bw) = self.bandwidth {
builder.append_attr_u64(TCA_CAKE_BASE_RATE64, bw.as_bytes_per_sec());
}
if let Some(rtt) = self.rtt {
builder.append_attr_u32(TCA_CAKE_RTT, rtt.as_micros() as u32);
}
if let Some(target) = self.target {
builder.append_attr_u32(TCA_CAKE_TARGET, target.as_micros() as u32);
}
if let Some(overhead) = self.overhead {
builder.append_attr(TCA_CAKE_OVERHEAD, &overhead.to_ne_bytes());
}
if let Some(mpu) = self.mpu {
builder.append_attr_u32(TCA_CAKE_MPU, mpu);
}
if let Some(mem) = self.memory_limit {
builder.append_attr_u32(TCA_CAKE_MEMORY, mem.as_u32_saturating());
}
if let Some(mask) = self.fwmark {
builder.append_attr_u32(TCA_CAKE_FWMARK, mask);
}
if let Some(mode) = self.diffserv_mode {
let v = match mode {
CakeDiffserv::Diffserv3 => CAKE_DIFFSERV_DIFFSERV3,
CakeDiffserv::Diffserv4 => CAKE_DIFFSERV_DIFFSERV4,
CakeDiffserv::Diffserv8 => CAKE_DIFFSERV_DIFFSERV8,
CakeDiffserv::Besteffort => CAKE_DIFFSERV_BESTEFFORT,
CakeDiffserv::Precedence => CAKE_DIFFSERV_PRECEDENCE,
};
builder.append_attr_u32(TCA_CAKE_DIFFSERV_MODE, v);
}
if let Some(mode) = self.flow_mode {
let v = match mode {
CakeFlowMode::Flowblind => CAKE_FLOW_NONE,
CakeFlowMode::Srchost => CAKE_FLOW_SRC_IP,
CakeFlowMode::Dsthost => CAKE_FLOW_DST_IP,
CakeFlowMode::Hosts => CAKE_FLOW_HOSTS,
CakeFlowMode::Flows => CAKE_FLOW_FLOWS,
CakeFlowMode::DualSrchost => CAKE_FLOW_DUAL_SRC,
CakeFlowMode::DualDsthost => CAKE_FLOW_DUAL_DST,
CakeFlowMode::Triple => CAKE_FLOW_TRIPLE,
};
builder.append_attr_u32(TCA_CAKE_FLOW_MODE, v);
}
if let Some(mode) = self.atm_mode {
let v = match mode {
CakeAtmMode::None => CAKE_ATM_NONE,
CakeAtmMode::Atm => CAKE_ATM_ATM,
CakeAtmMode::Ptm => CAKE_ATM_PTM,
};
builder.append_attr_u32(TCA_CAKE_ATM, v);
}
if let Some(mode) = self.ack_filter {
let v = match mode {
CakeAckFilter::Disabled => CAKE_ACK_NONE,
CakeAckFilter::Filter => CAKE_ACK_FILTER,
CakeAckFilter::Aggressive => CAKE_ACK_AGGRESSIVE,
};
builder.append_attr_u32(TCA_CAKE_ACK_FILTER, v);
}
if self.autorate {
builder.append_attr_u32(TCA_CAKE_AUTORATE, 1);
}
if self.nat {
builder.append_attr_u32(TCA_CAKE_NAT, 1);
}
if self.raw {
builder.append_attr_u32(TCA_CAKE_RAW, 1);
}
if self.wash {
builder.append_attr_u32(TCA_CAKE_WASH, 1);
}
if self.ingress {
builder.append_attr_u32(TCA_CAKE_INGRESS, 1);
}
if self.split_gso {
builder.append_attr_u32(TCA_CAKE_SPLIT_GSO, 1);
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct PlugConfig {
pub limit: Option<u32>,
}
impl Default for PlugConfig {
fn default() -> Self {
Self::new()
}
}
impl PlugConfig {
pub fn new() -> Self {
Self { limit: None }
}
pub fn limit(mut self, bytes: u32) -> Self {
self.limit = Some(bytes);
self
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut cfg = Self::new();
let mut i = 0;
while i < params.len() {
let key = params[i];
match key {
"limit" => {
let s = params.get(i + 1).copied().ok_or_else(|| {
Error::InvalidMessage("plug: `limit` requires a value".into())
})?;
let bytes = crate::util::parse::get_size(s).map_err(|_| {
Error::InvalidMessage(format!(
"plug: invalid limit `{s}` (expected tc-style size)"
))
})?;
cfg.limit = Some(bytes.try_into().map_err(|_| {
Error::InvalidMessage(format!("plug: limit `{s}` exceeds u32"))
})?);
i += 2;
}
other => {
return Err(Error::InvalidMessage(format!(
"plug: unknown token `{other}`"
)));
}
}
}
Ok(cfg)
}
}
impl QdiscConfig for PlugConfig {
fn kind(&self) -> &'static str {
"plug"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
use super::types::tc::qdisc::plug::TcPlugQopt;
if let Some(limit) = self.limit {
let qopt = TcPlugQopt::limit(limit);
builder.append(&qopt);
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct MqprioConfig {
pub num_tc: u8,
pub prio_tc_map: [u8; 16],
pub hw: bool,
pub count: [u16; 16],
pub offset: [u16; 16],
}
impl Default for MqprioConfig {
fn default() -> Self {
Self::new()
}
}
impl MqprioConfig {
pub fn new() -> Self {
Self {
num_tc: 8,
prio_tc_map: [0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 1, 1, 3, 3, 3, 3],
hw: true,
count: [0; 16],
offset: [0; 16],
}
}
pub fn num_tc(mut self, num_tc: u8) -> Self {
self.num_tc = num_tc.min(16);
self
}
pub fn map(mut self, map: &[u8]) -> Self {
for (i, &tc) in map.iter().enumerate().take(16) {
self.prio_tc_map[i] = tc;
}
self
}
pub fn hw_offload(mut self, enable: bool) -> Self {
self.hw = enable;
self
}
pub fn queues(mut self, queues: &[(u16, u16)]) -> Self {
for (i, &(c, o)) in queues.iter().enumerate().take(16) {
self.count[i] = c;
self.offset[i] = o;
}
self
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut cfg = Self::new();
let mut i = 0;
while i < params.len() {
let key = params[i];
match key {
"num_tc" => {
let s = params.get(i + 1).copied().ok_or_else(|| {
Error::InvalidMessage("mqprio: `num_tc` requires a value".into())
})?;
let n: u8 = s.parse().map_err(|_| {
Error::InvalidMessage(format!(
"mqprio: invalid num_tc `{s}` (expected 1-16)"
))
})?;
if !(1..=16).contains(&n) {
return Err(Error::InvalidMessage(format!(
"mqprio: num_tc `{n}` out of range (1-16)"
)));
}
cfg = cfg.num_tc(n);
i += 2;
}
"map" => {
if params.len() < i + 1 + 16 {
return Err(Error::InvalidMessage(format!(
"mqprio: `map` requires exactly 16 values, got {}",
params.len().saturating_sub(i + 1)
)));
}
let mut map = [0u8; 16];
for j in 0..16 {
let s = params[i + 1 + j];
map[j] = s.parse().map_err(|_| {
Error::InvalidMessage(format!(
"mqprio: invalid map[{j}] `{s}` (expected 0-15)"
))
})?;
}
cfg.prio_tc_map = map;
i += 17;
}
"hw" => {
cfg.hw = true;
i += 1;
}
"nohw" => {
cfg.hw = false;
i += 1;
}
"queues" => {
return Err(Error::InvalidMessage(
"mqprio: `queues` (count@offset list) is not parsed by parse_params yet — use MqprioConfig::queues() on the typed builder".into(),
));
}
other => {
return Err(Error::InvalidMessage(format!(
"mqprio: unknown token `{other}`"
)));
}
}
}
Ok(cfg)
}
}
impl QdiscConfig for MqprioConfig {
fn kind(&self) -> &'static str {
"mqprio"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
use super::types::tc::qdisc::mqprio::TcMqprioQopt;
let mut qopt = TcMqprioQopt::new()
.with_num_tc(self.num_tc)
.with_hw(self.hw);
qopt.prio_tc_map = self.prio_tc_map;
qopt.count = self.count;
qopt.offset = self.offset;
builder.append(&qopt);
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct TaprioConfig {
pub num_tc: u8,
pub prio_tc_map: [u8; 16],
pub count: [u16; 16],
pub offset: [u16; 16],
pub clockid: i32,
pub base_time: i64,
pub cycle_time: i64,
pub cycle_time_extension: i64,
pub entries: Vec<super::types::tc::qdisc::taprio::TaprioSchedEntry>,
pub flags: u32,
pub txtime_delay: u32,
}
impl Default for TaprioConfig {
fn default() -> Self {
Self::new()
}
}
impl TaprioConfig {
pub fn new() -> Self {
Self {
num_tc: 0,
prio_tc_map: [0; 16],
count: [0; 16],
offset: [0; 16],
clockid: -1,
base_time: 0,
cycle_time: 0,
cycle_time_extension: 0,
entries: Vec::new(),
flags: 0,
txtime_delay: 0,
}
}
pub fn num_tc(mut self, num_tc: u8) -> Self {
self.num_tc = num_tc.min(16);
self
}
pub fn map(mut self, map: &[u8]) -> Self {
for (i, &tc) in map.iter().enumerate().take(16) {
self.prio_tc_map[i] = tc;
}
self
}
pub fn queues(mut self, queues: &[(u16, u16)]) -> Self {
for (i, &(c, o)) in queues.iter().enumerate().take(16) {
self.count[i] = c;
self.offset[i] = o;
}
self
}
pub fn clockid(mut self, clockid: i32) -> Self {
self.clockid = clockid;
self
}
pub fn base_time(mut self, base_time: i64) -> Self {
self.base_time = base_time;
self
}
pub fn cycle_time(mut self, cycle_time: i64) -> Self {
self.cycle_time = cycle_time;
self
}
pub fn cycle_time_extension(mut self, extension: i64) -> Self {
self.cycle_time_extension = extension;
self
}
pub fn entry(mut self, entry: super::types::tc::qdisc::taprio::TaprioSchedEntry) -> Self {
self.entries.push(entry);
self
}
pub fn txtime_assist(mut self, enable: bool) -> Self {
use super::types::tc::qdisc::taprio::TAPRIO_ATTR_FLAG_TXTIME_ASSIST;
if enable {
self.flags |= TAPRIO_ATTR_FLAG_TXTIME_ASSIST;
} else {
self.flags &= !TAPRIO_ATTR_FLAG_TXTIME_ASSIST;
}
self
}
pub fn full_offload(mut self, enable: bool) -> Self {
use super::types::tc::qdisc::taprio::TAPRIO_ATTR_FLAG_FULL_OFFLOAD;
if enable {
self.flags |= TAPRIO_ATTR_FLAG_FULL_OFFLOAD;
} else {
self.flags &= !TAPRIO_ATTR_FLAG_FULL_OFFLOAD;
}
self
}
pub fn txtime_delay(mut self, delay: u32) -> Self {
self.txtime_delay = delay;
self
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
use crate::netlink::types::tc::qdisc::taprio::{
TAPRIO_ATTR_FLAG_FULL_OFFLOAD, TAPRIO_ATTR_FLAG_TXTIME_ASSIST,
};
let mut cfg = Self::new();
let mut i = 0;
while i < params.len() {
let key = params[i];
let need_value = || {
params.get(i + 1).copied().ok_or_else(|| {
Error::InvalidMessage(format!("taprio: `{key}` requires a value"))
})
};
match key {
"num_tc" => {
let s = need_value()?;
let n: u8 = s.parse().map_err(|_| {
Error::InvalidMessage(format!(
"taprio: invalid num_tc `{s}` (expected 1-16)"
))
})?;
if !(1..=16).contains(&n) {
return Err(Error::InvalidMessage(format!(
"taprio: num_tc `{n}` out of range (1-16)"
)));
}
cfg = cfg.num_tc(n);
i += 2;
}
"map" => {
if params.len() < i + 1 + 16 {
return Err(Error::InvalidMessage(format!(
"taprio: `map` requires exactly 16 values, got {}",
params.len().saturating_sub(i + 1)
)));
}
let mut map = [0u8; 16];
for j in 0..16 {
let s = params[i + 1 + j];
map[j] = s.parse().map_err(|_| {
Error::InvalidMessage(format!(
"taprio: invalid map[{j}] `{s}` (expected 0-15)"
))
})?;
}
cfg.prio_tc_map = map;
i += 17;
}
"clockid" => {
let s = need_value()?;
cfg.clockid = parse_etf_clockid(s).map_err(|e| {
Error::InvalidMessage(format!("taprio: invalid clockid `{s}` ({e})"))
})?;
i += 2;
}
"base-time" => {
let s = need_value()?;
cfg.base_time = s.parse().map_err(|_| {
Error::InvalidMessage(format!("taprio: invalid base-time `{s}`"))
})?;
i += 2;
}
"cycle-time" => {
let s = need_value()?;
cfg.cycle_time = s.parse().map_err(|_| {
Error::InvalidMessage(format!("taprio: invalid cycle-time `{s}`"))
})?;
i += 2;
}
"cycle-time-extension" => {
let s = need_value()?;
cfg.cycle_time_extension = s.parse().map_err(|_| {
Error::InvalidMessage(format!("taprio: invalid cycle-time-extension `{s}`"))
})?;
i += 2;
}
"txtime-delay" => {
let s = need_value()?;
cfg.txtime_delay = s.parse().map_err(|_| {
Error::InvalidMessage(format!("taprio: invalid txtime-delay `{s}`"))
})?;
i += 2;
}
"txtime-assist" => {
cfg.flags |= TAPRIO_ATTR_FLAG_TXTIME_ASSIST;
i += 1;
}
"notxtime-assist" => {
cfg.flags &= !TAPRIO_ATTR_FLAG_TXTIME_ASSIST;
i += 1;
}
"full-offload" => {
cfg.flags |= TAPRIO_ATTR_FLAG_FULL_OFFLOAD;
i += 1;
}
"nofull-offload" => {
cfg.flags &= !TAPRIO_ATTR_FLAG_FULL_OFFLOAD;
i += 1;
}
"flags" => {
let s = need_value()?;
let trimmed = s.strip_prefix("0x").unwrap_or(s);
cfg.flags = u32::from_str_radix(trimmed, 16)
.or_else(|_| s.parse::<u32>())
.map_err(|_| {
Error::InvalidMessage(format!(
"taprio: invalid flags `{s}` (expected hex u32 or decimal)"
))
})?;
i += 2;
}
"sched-entry" => {
let cmd_s = params.get(i + 1).copied().ok_or_else(|| {
Error::InvalidMessage(
"taprio: `sched-entry` needs <cmd> <gate-mask-hex> <interval-ns>"
.into(),
)
})?;
let mask_s = params.get(i + 2).copied().ok_or_else(|| {
Error::InvalidMessage(
"taprio: `sched-entry` needs <cmd> <gate-mask-hex> <interval-ns>"
.into(),
)
})?;
let interval_s = params.get(i + 3).copied().ok_or_else(|| {
Error::InvalidMessage(
"taprio: `sched-entry` needs <cmd> <gate-mask-hex> <interval-ns>"
.into(),
)
})?;
let entry = parse_taprio_sched_entry(cmd_s, mask_s, interval_s)?;
cfg.entries.push(entry);
i += 4;
}
"queues" => {
return Err(Error::InvalidMessage(
"taprio: `queues` (count@offset list) is not parsed by parse_params yet — use TaprioConfig::queues() on the typed builder".into(),
));
}
other => {
return Err(Error::InvalidMessage(format!(
"taprio: unknown token `{other}`"
)));
}
}
}
Ok(cfg)
}
}
fn parse_taprio_sched_entry(
cmd_s: &str,
mask_s: &str,
interval_s: &str,
) -> Result<super::types::tc::qdisc::taprio::TaprioSchedEntry> {
use super::types::tc::qdisc::taprio::{
TC_TAPRIO_CMD_SET_AND_HOLD, TC_TAPRIO_CMD_SET_AND_RELEASE, TC_TAPRIO_CMD_SET_GATES,
TaprioSchedEntry,
};
let cmd = match cmd_s {
"SET" | "S" | "set" | "s" => TC_TAPRIO_CMD_SET_GATES,
"SET_AND_HOLD" | "HOLD" | "H" | "hold" | "h" => TC_TAPRIO_CMD_SET_AND_HOLD,
"SET_AND_RELEASE" | "RELEASE" | "R" | "release" | "r" => TC_TAPRIO_CMD_SET_AND_RELEASE,
other => {
return Err(Error::InvalidMessage(format!(
"taprio: invalid sched-entry cmd `{other}` (expected SET / HOLD / RELEASE)"
)));
}
};
let mask_trimmed = mask_s.strip_prefix("0x").unwrap_or(mask_s);
let mask = u32::from_str_radix(mask_trimmed, 16).map_err(|_| {
Error::InvalidMessage(format!(
"taprio: invalid sched-entry gate mask `{mask_s}` (expected hex)"
))
})?;
let interval = interval_s.parse::<u32>().map_err(|_| {
Error::InvalidMessage(format!(
"taprio: invalid sched-entry interval `{interval_s}` (expected u32 ns)"
))
})?;
Ok(TaprioSchedEntry::new(cmd, mask, interval))
}
impl QdiscConfig for TaprioConfig {
fn kind(&self) -> &'static str {
"taprio"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
use super::types::tc::qdisc::{mqprio::TcMqprioQopt, taprio::*};
let mut qopt = TcMqprioQopt::new().with_num_tc(self.num_tc).with_hw(false);
qopt.prio_tc_map = self.prio_tc_map;
qopt.count = self.count;
qopt.offset = self.offset;
builder.append_attr(TCA_TAPRIO_ATTR_PRIOMAP, qopt.as_bytes());
if self.clockid >= 0 {
builder.append_attr(TCA_TAPRIO_ATTR_SCHED_CLOCKID, &self.clockid.to_ne_bytes());
}
if self.base_time != 0 {
builder.append_attr(
TCA_TAPRIO_ATTR_SCHED_BASE_TIME,
&self.base_time.to_ne_bytes(),
);
}
if self.cycle_time != 0 {
builder.append_attr(
TCA_TAPRIO_ATTR_SCHED_CYCLE_TIME,
&self.cycle_time.to_ne_bytes(),
);
}
if self.cycle_time_extension != 0 {
builder.append_attr(
TCA_TAPRIO_ATTR_SCHED_CYCLE_TIME_EXTENSION,
&self.cycle_time_extension.to_ne_bytes(),
);
}
if self.flags != 0 {
builder.append_attr(TCA_TAPRIO_ATTR_FLAGS, &self.flags.to_ne_bytes());
}
if self.txtime_delay != 0 {
builder.append_attr(
TCA_TAPRIO_ATTR_TXTIME_DELAY,
&self.txtime_delay.to_ne_bytes(),
);
}
if !self.entries.is_empty() {
let list_token = builder.nest_start(TCA_TAPRIO_ATTR_SCHED_ENTRY_LIST);
for (idx, entry) in self.entries.iter().enumerate() {
let entry_token = builder.nest_start(TCA_TAPRIO_ATTR_SCHED_SINGLE_ENTRY);
builder.append_attr(TCA_TAPRIO_SCHED_ENTRY_INDEX, &(idx as u32).to_ne_bytes());
builder.append_attr(TCA_TAPRIO_SCHED_ENTRY_CMD, &[entry.cmd]);
builder.append_attr(
TCA_TAPRIO_SCHED_ENTRY_GATE_MASK,
&entry.gate_mask.to_ne_bytes(),
);
builder.append_attr(
TCA_TAPRIO_SCHED_ENTRY_INTERVAL,
&entry.interval.to_ne_bytes(),
);
builder.nest_end(entry_token);
}
builder.nest_end(list_token);
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct HfscConfig {
pub default_class: u16,
}
impl Default for HfscConfig {
fn default() -> Self {
Self::new()
}
}
impl HfscConfig {
pub fn new() -> Self {
Self { default_class: 0 }
}
pub fn default_class(mut self, classid: u16) -> Self {
self.default_class = classid;
self
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut cfg = Self::new();
let mut i = 0;
while i < params.len() {
let key = params[i];
match key {
"default" => {
let s = params.get(i + 1).copied().ok_or_else(|| {
Error::InvalidMessage("hfsc: `default` requires a value".into())
})?;
cfg.default_class = u16::from_str_radix(s, 16).map_err(|_| {
Error::InvalidMessage(format!(
"hfsc: invalid default `{s}` (expected hex minor like `10`)"
))
})?;
i += 2;
}
other => {
return Err(Error::InvalidMessage(format!(
"hfsc: unknown token `{other}`"
)));
}
}
}
Ok(cfg)
}
}
impl QdiscConfig for HfscConfig {
fn kind(&self) -> &'static str {
"hfsc"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
use super::types::tc::qdisc::hfsc::TcHfscQopt;
let qopt = TcHfscQopt::new(self.default_class);
builder.append(&qopt);
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct EtfConfig {
pub delta: i32,
pub clockid: i32,
pub deadline_mode: bool,
pub offload: bool,
pub skip_sock_check: bool,
}
impl Default for EtfConfig {
fn default() -> Self {
Self::new()
}
}
impl EtfConfig {
pub fn new() -> Self {
Self {
delta: 0,
clockid: -1, deadline_mode: false,
offload: false,
skip_sock_check: false,
}
}
pub fn clockid(mut self, clockid: i32) -> Self {
self.clockid = clockid;
self
}
pub fn delta_ns(mut self, delta: i32) -> Self {
self.delta = delta;
self
}
pub fn deadline_mode(mut self, enable: bool) -> Self {
self.deadline_mode = enable;
self
}
pub fn offload(mut self, enable: bool) -> Self {
self.offload = enable;
self
}
pub fn skip_sock_check(mut self, enable: bool) -> Self {
self.skip_sock_check = enable;
self
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut cfg = Self::new();
let mut i = 0;
while i < params.len() {
let key = params[i];
let need_value = || {
params
.get(i + 1)
.copied()
.ok_or_else(|| Error::InvalidMessage(format!("etf: `{key}` requires a value")))
};
match key {
"delta" => {
let s = need_value()?;
cfg.delta = s.parse().map_err(|_| {
Error::InvalidMessage(format!(
"etf: invalid delta `{s}` (expected signed integer ns)"
))
})?;
i += 2;
}
"clockid" => {
let s = need_value()?;
cfg.clockid = parse_etf_clockid(s)?;
i += 2;
}
"deadline_mode" => {
cfg.deadline_mode = true;
i += 1;
}
"nodeadline_mode" => {
cfg.deadline_mode = false;
i += 1;
}
"offload" => {
cfg.offload = true;
i += 1;
}
"nooffload" => {
cfg.offload = false;
i += 1;
}
"skip_sock_check" => {
cfg.skip_sock_check = true;
i += 1;
}
"noskip_sock_check" => {
cfg.skip_sock_check = false;
i += 1;
}
other => {
return Err(Error::InvalidMessage(format!(
"etf: unknown token `{other}`"
)));
}
}
}
Ok(cfg)
}
}
fn parse_etf_clockid(s: &str) -> Result<i32> {
Ok(match s {
"CLOCK_REALTIME" => libc::CLOCK_REALTIME,
"CLOCK_MONOTONIC" => libc::CLOCK_MONOTONIC,
"CLOCK_PROCESS_CPUTIME_ID" => libc::CLOCK_PROCESS_CPUTIME_ID,
"CLOCK_THREAD_CPUTIME_ID" => libc::CLOCK_THREAD_CPUTIME_ID,
"CLOCK_BOOTTIME" => libc::CLOCK_BOOTTIME,
"CLOCK_TAI" => libc::CLOCK_TAI,
other => other.parse::<i32>().map_err(|_| {
Error::InvalidMessage(format!(
"etf: invalid clockid `{other}` (expected name like CLOCK_TAI or bare integer)"
))
})?,
})
}
impl QdiscConfig for EtfConfig {
fn kind(&self) -> &'static str {
"etf"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
use super::types::tc::qdisc::etf::{
TC_ETF_DEADLINE_MODE_ON, TC_ETF_OFFLOAD_ON, TC_ETF_SKIP_SOCK_CHECK, TCA_ETF_PARMS,
TcEtfQopt,
};
let mut flags = 0i32;
if self.deadline_mode {
flags |= TC_ETF_DEADLINE_MODE_ON;
}
if self.offload {
flags |= TC_ETF_OFFLOAD_ON;
}
if self.skip_sock_check {
flags |= TC_ETF_SKIP_SOCK_CHECK;
}
let qopt = TcEtfQopt {
delta: self.delta,
clockid: self.clockid,
flags,
};
builder.append_attr(TCA_ETF_PARMS, qopt.as_bytes());
Ok(())
}
}
pub trait ClassConfig: Send + Sync {
fn kind(&self) -> &'static str;
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()>;
}
#[derive(Debug, Clone)]
pub struct HtbClassConfig {
rate: crate::util::Rate,
ceil: Option<crate::util::Rate>,
burst: Option<crate::util::Bytes>,
cburst: Option<crate::util::Bytes>,
prio: Option<u32>,
quantum: Option<u32>,
mtu: u32,
mpu: u16,
overhead: u16,
}
impl HtbClassConfig {
pub fn new(rate: crate::util::Rate) -> Self {
Self {
rate,
ceil: None,
burst: None,
cburst: None,
prio: None,
quantum: None,
mtu: 1600,
mpu: 0,
overhead: 0,
}
}
pub fn ceil(mut self, ceil: crate::util::Rate) -> Self {
self.ceil = Some(ceil);
self
}
pub fn burst(mut self, burst: crate::util::Bytes) -> Self {
self.burst = Some(burst);
self
}
pub fn cburst(mut self, cburst: crate::util::Bytes) -> Self {
self.cburst = Some(cburst);
self
}
pub fn prio(mut self, prio: u32) -> Self {
self.prio = Some(prio.min(7));
self
}
pub fn quantum(mut self, quantum: u32) -> Self {
self.quantum = Some(quantum);
self
}
pub fn mtu(mut self, mtu: u32) -> Self {
self.mtu = mtu;
self
}
pub fn mpu(mut self, mpu: u16) -> Self {
self.mpu = mpu;
self
}
pub fn overhead(mut self, overhead: u16) -> Self {
self.overhead = overhead;
self
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
use crate::util::Rate;
let mut rate: Option<Rate> = None;
let mut ceil: Option<Rate> = None;
let mut burst: Option<crate::util::Bytes> = None;
let mut cburst: Option<crate::util::Bytes> = None;
let mut prio: Option<u32> = None;
let mut quantum: Option<u32> = None;
let mut mtu: Option<u32> = None;
let mut mpu: Option<u16> = None;
let mut overhead: Option<u16> = None;
let mut i = 0;
while i < params.len() {
let tok = params[i];
let val = || {
params
.get(i + 1)
.copied()
.ok_or_else(|| Error::InvalidMessage(format!("htb: `{tok}` requires a value")))
};
match tok {
"rate" => {
let v = val()?;
rate = Some(v.parse::<Rate>().map_err(|e| {
Error::InvalidMessage(format!("htb: invalid rate `{v}`: {e}"))
})?);
i += 2;
}
"ceil" => {
let v = val()?;
ceil = Some(v.parse::<Rate>().map_err(|e| {
Error::InvalidMessage(format!("htb: invalid ceil `{v}`: {e}"))
})?);
i += 2;
}
"burst" | "buffer" | "maxburst" => {
let v = val()?;
burst = Some(v.parse::<crate::util::Bytes>().map_err(|e| {
Error::InvalidMessage(format!("htb: invalid burst `{v}`: {e}"))
})?);
i += 2;
}
"cburst" | "cbuffer" | "cmaxburst" => {
let v = val()?;
cburst = Some(v.parse::<crate::util::Bytes>().map_err(|e| {
Error::InvalidMessage(format!("htb: invalid cburst `{v}`: {e}"))
})?);
i += 2;
}
"prio" => {
let v = val()?;
prio = Some(v.parse::<u32>().map_err(|_| {
Error::InvalidMessage(format!(
"htb: invalid prio `{v}` (expected unsigned integer)"
))
})?);
i += 2;
}
"quantum" => {
let v = val()?;
let bytes = v.parse::<crate::util::Bytes>().map_err(|e| {
Error::InvalidMessage(format!("htb: invalid quantum `{v}`: {e}"))
})?;
quantum = Some(bytes.as_u32_saturating());
i += 2;
}
"mtu" => {
let v = val()?;
mtu = Some(v.parse::<u32>().map_err(|_| {
Error::InvalidMessage(format!(
"htb: invalid mtu `{v}` (expected unsigned integer)"
))
})?);
i += 2;
}
"mpu" => {
let v = val()?;
mpu = Some(v.parse::<u16>().map_err(|_| {
Error::InvalidMessage(format!("htb: invalid mpu `{v}` (expected u16)"))
})?);
i += 2;
}
"overhead" => {
let v = val()?;
overhead = Some(v.parse::<u16>().map_err(|_| {
Error::InvalidMessage(format!("htb: invalid overhead `{v}` (expected u16)"))
})?);
i += 2;
}
other => {
return Err(Error::InvalidMessage(format!(
"htb: unknown token `{other}` (recognised: rate, ceil, burst, cburst, prio, quantum, mtu, mpu, overhead)"
)));
}
}
}
let rate = rate.ok_or_else(|| Error::InvalidMessage("htb: `rate` is required".into()))?;
let mut cfg = HtbClassConfig::new(rate);
if let Some(c) = ceil {
cfg = cfg.ceil(c);
}
if let Some(b) = burst {
cfg = cfg.burst(b);
}
if let Some(c) = cburst {
cfg = cfg.cburst(c);
}
if let Some(p) = prio {
cfg = cfg.prio(p);
}
if let Some(q) = quantum {
cfg = cfg.quantum(q);
}
if let Some(m) = mtu {
cfg = cfg.mtu(m);
}
if let Some(m) = mpu {
cfg = cfg.mpu(m);
}
if let Some(o) = overhead {
cfg = cfg.overhead(o);
}
Ok(cfg)
}
}
impl ClassConfig for HtbClassConfig {
fn kind(&self) -> &'static str {
"htb"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
let cfg = self;
let rate = cfg.rate.as_bytes_per_sec();
let ceil = cfg.ceil.unwrap_or(cfg.rate).as_bytes_per_sec();
let hz: u64 = 1000;
let burst = cfg
.burst
.map(|b| b.as_u32_saturating())
.unwrap_or_else(|| (rate / hz + cfg.mtu as u64) as u32);
let cburst = cfg
.cburst
.map(|b| b.as_u32_saturating())
.unwrap_or_else(|| (ceil / hz + cfg.mtu as u64) as u32);
let buffer = (burst as u64 * 1_000_000)
.checked_div(rate)
.map(|v| v as u32)
.unwrap_or(burst);
let cbuffer = (cburst as u64 * 1_000_000)
.checked_div(ceil)
.map(|v| v as u32)
.unwrap_or(cburst);
let opt = htb::TcHtbOpt {
rate: TcRateSpec {
rate: if rate >= (1u64 << 32) {
u32::MAX
} else {
rate as u32
},
mpu: cfg.mpu,
overhead: cfg.overhead,
..Default::default()
},
ceil: TcRateSpec {
rate: if ceil >= (1u64 << 32) {
u32::MAX
} else {
ceil as u32
},
mpu: cfg.mpu,
overhead: cfg.overhead,
..Default::default()
},
buffer,
cbuffer,
quantum: cfg.quantum.unwrap_or(0),
prio: cfg.prio.unwrap_or(0),
..Default::default()
};
if rate >= (1u64 << 32) {
builder.append_attr(htb::TCA_HTB_RATE64, &rate.to_ne_bytes());
}
if ceil >= (1u64 << 32) {
builder.append_attr(htb::TCA_HTB_CEIL64, &ceil.to_ne_bytes());
}
builder.append_attr(htb::TCA_HTB_PARMS, opt.as_bytes());
let rtab = compute_htb_rate_table(rate, cfg.mtu);
let ctab = compute_htb_rate_table(ceil, cfg.mtu);
builder.append_attr(htb::TCA_HTB_RTAB, &rtab);
builder.append_attr(htb::TCA_HTB_CTAB, &ctab);
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct HfscClassConfig {
rsc: Option<TcServiceCurve>,
fsc: Option<TcServiceCurve>,
usc: Option<TcServiceCurve>,
}
impl HfscClassConfig {
pub fn new() -> Self {
Self::default()
}
pub fn rt_curve(mut self, curve: TcServiceCurve) -> Self {
self.rsc = Some(curve);
self
}
pub fn rt_rate(mut self, rate: crate::util::Rate) -> Self {
self.rsc = Some(TcServiceCurve::rate(rate.as_u32_bytes_per_sec_saturating()));
self
}
pub fn ls_curve(mut self, curve: TcServiceCurve) -> Self {
self.fsc = Some(curve);
self
}
pub fn ls_rate(mut self, rate: crate::util::Rate) -> Self {
self.fsc = Some(TcServiceCurve::rate(rate.as_u32_bytes_per_sec_saturating()));
self
}
pub fn ul_curve(mut self, curve: TcServiceCurve) -> Self {
self.usc = Some(curve);
self
}
pub fn ul_rate(mut self, rate: crate::util::Rate) -> Self {
self.usc = Some(TcServiceCurve::rate(rate.as_u32_bytes_per_sec_saturating()));
self
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
use crate::util::Rate;
let mut cfg = HfscClassConfig::new();
let mut i = 0;
while i < params.len() {
let curve = params[i];
let kind = match curve {
"rt" | "ls" | "ul" => curve,
"sc" => {
return Err(Error::InvalidMessage(
"hfsc: `sc` (real-time + link-share combined) not modelled by parse_params — use HfscClassConfig::rt_curve + ls_curve on the typed builder".into(),
));
}
other => {
return Err(Error::InvalidMessage(format!(
"hfsc: unknown token `{other}` (recognised: rt, ls, ul; each followed by `rate <rate>`)"
)));
}
};
let next = params.get(i + 1).copied().ok_or_else(|| {
Error::InvalidMessage(format!("hfsc: `{kind}` requires `rate <rate>`"))
})?;
if next != "rate" {
return Err(Error::InvalidMessage(format!(
"hfsc: `{kind}` followed by `{next}` — only the `rate <rate>` form is parsed (use the typed builder for full m1/d/m2 curves)"
)));
}
let v = params.get(i + 2).copied().ok_or_else(|| {
Error::InvalidMessage(format!("hfsc: `{kind} rate` requires a value"))
})?;
let rate = v
.parse::<Rate>()
.map_err(|e| Error::InvalidMessage(format!("hfsc: invalid rate `{v}`: {e}")))?;
cfg = match kind {
"rt" => cfg.rt_rate(rate),
"ls" => cfg.ls_rate(rate),
"ul" => cfg.ul_rate(rate),
_ => unreachable!(),
};
i += 3;
}
Ok(cfg)
}
}
impl ClassConfig for HfscClassConfig {
fn kind(&self) -> &'static str {
"hfsc"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
use super::types::tc::qdisc::hfsc::{TCA_HFSC_FSC, TCA_HFSC_RSC, TCA_HFSC_USC};
let cfg = self;
if let Some(ref rsc) = cfg.rsc {
builder.append_attr(TCA_HFSC_RSC, rsc.as_bytes());
}
if let Some(ref fsc) = cfg.fsc {
builder.append_attr(TCA_HFSC_FSC, fsc.as_bytes());
}
if let Some(ref usc) = cfg.usc {
builder.append_attr(TCA_HFSC_USC, usc.as_bytes());
}
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct DrrClassConfig {
quantum: Option<crate::util::Bytes>,
}
impl DrrClassConfig {
pub fn new() -> Self {
Self::default()
}
pub fn quantum(mut self, q: crate::util::Bytes) -> Self {
self.quantum = Some(q);
self
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut cfg = DrrClassConfig::new();
let mut i = 0;
while i < params.len() {
let tok = params[i];
match tok {
"quantum" => {
let v = params.get(i + 1).copied().ok_or_else(|| {
Error::InvalidMessage("drr: `quantum` requires a value".into())
})?;
let q = v.parse::<crate::util::Bytes>().map_err(|e| {
Error::InvalidMessage(format!("drr: invalid quantum `{v}`: {e}"))
})?;
cfg = cfg.quantum(q);
i += 2;
}
other => {
return Err(Error::InvalidMessage(format!(
"drr: unknown token `{other}` (recognised: quantum)"
)));
}
}
}
Ok(cfg)
}
}
impl ClassConfig for DrrClassConfig {
fn kind(&self) -> &'static str {
"drr"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
use super::types::tc::qdisc::drr::TCA_DRR_QUANTUM;
if let Some(quantum) = self.quantum {
builder.append_attr_u32(TCA_DRR_QUANTUM, quantum.as_u32_saturating());
}
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct QfqClassConfig {
weight: Option<u32>,
lmax: Option<crate::util::Bytes>,
}
impl QfqClassConfig {
pub fn new() -> Self {
Self::default()
}
pub fn weight(mut self, weight: u32) -> Self {
self.weight = Some(weight.clamp(1, 1023));
self
}
pub fn lmax(mut self, b: crate::util::Bytes) -> Self {
self.lmax = Some(b);
self
}
pub fn build(self) -> Self {
self
}
pub fn parse_params(params: &[&str]) -> Result<Self> {
let mut cfg = QfqClassConfig::new();
let mut i = 0;
while i < params.len() {
let tok = params[i];
match tok {
"weight" => {
let v = params.get(i + 1).copied().ok_or_else(|| {
Error::InvalidMessage("qfq: `weight` requires a value".into())
})?;
let w = v.parse::<u32>().map_err(|_| {
Error::InvalidMessage(format!(
"qfq: invalid weight `{v}` (expected unsigned integer)"
))
})?;
cfg = cfg.weight(w);
i += 2;
}
"lmax" => {
let v = params.get(i + 1).copied().ok_or_else(|| {
Error::InvalidMessage("qfq: `lmax` requires a value".into())
})?;
let b = v.parse::<crate::util::Bytes>().map_err(|e| {
Error::InvalidMessage(format!("qfq: invalid lmax `{v}`: {e}"))
})?;
cfg = cfg.lmax(b);
i += 2;
}
other => {
return Err(Error::InvalidMessage(format!(
"qfq: unknown token `{other}` (recognised: weight, lmax)"
)));
}
}
}
Ok(cfg)
}
}
impl ClassConfig for QfqClassConfig {
fn kind(&self) -> &'static str {
"qfq"
}
fn write_options(&self, builder: &mut MessageBuilder) -> Result<()> {
use super::types::tc::qdisc::qfq::{TCA_QFQ_LMAX, TCA_QFQ_WEIGHT};
if let Some(weight) = self.weight {
builder.append_attr_u32(TCA_QFQ_WEIGHT, weight);
}
if let Some(lmax) = self.lmax {
builder.append_attr_u32(TCA_QFQ_LMAX, lmax.as_u32_saturating());
}
Ok(())
}
}
fn compute_htb_rate_table(rate: u64, mtu: u32) -> [u8; 1024] {
let mut table = [0u8; 1024];
if rate == 0 {
return table;
}
let cell_log: u32 = 3;
let cell_size = 1u32 << cell_log;
let time_units_per_sec: u64 = 1_000_000;
for i in 0..256 {
let size = ((i + 1) as u32) * cell_size;
let size = size.min(mtu);
let time = (size as u64 * time_units_per_sec) / rate;
let time = time.min(u32::MAX as u64) as u32;
let offset = i * 4;
table[offset..offset + 4].copy_from_slice(&time.to_ne_bytes());
}
table
}
impl Connection<Route> {
#[tracing::instrument(level = "debug", skip_all, fields(method = "add_qdisc"))]
pub async fn add_qdisc(
&self,
dev: impl Into<InterfaceRef>,
config: impl QdiscConfig,
) -> Result<()> {
self.add_qdisc_full(dev, TcHandle::ROOT, None, config).await
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "add_qdisc_full"))]
pub async fn add_qdisc_full(
&self,
dev: impl Into<InterfaceRef>,
parent: TcHandle,
handle: Option<TcHandle>,
config: impl QdiscConfig,
) -> Result<()> {
let ifindex = self.resolve_interface(&dev.into()).await?;
self.add_qdisc_by_index_full(ifindex, parent, handle, config)
.await
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "add_qdisc_by_index"))]
pub async fn add_qdisc_by_index(&self, ifindex: u32, config: impl QdiscConfig) -> Result<()> {
self.add_qdisc_by_index_full(ifindex, TcHandle::ROOT, None, config)
.await
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "add_qdisc_by_index_full"))]
pub async fn add_qdisc_by_index_full(
&self,
ifindex: u32,
parent: TcHandle,
handle: Option<TcHandle>,
config: impl QdiscConfig,
) -> Result<()> {
let parent_handle = parent.as_raw();
let qdisc_handle = handle.map(|h| h.as_raw()).unwrap_or(0);
let tcmsg = TcMsg::new()
.with_ifindex(ifindex as i32)
.with_parent(parent_handle)
.with_handle(qdisc_handle);
let mut builder = create_request(NlMsgType::RTM_NEWQDISC);
builder.append(&tcmsg);
builder.append_attr_str(TcaAttr::Kind as u16, config.kind());
let options_token = builder.nest_start(TcaAttr::Options as u16);
config.write_options(&mut builder)?;
builder.nest_end(options_token);
self.send_ack(builder)
.await
.map_err(|e| e.with_context("add_qdisc"))
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "del_qdisc"))]
pub async fn del_qdisc(&self, dev: impl Into<InterfaceRef>, parent: TcHandle) -> Result<()> {
self.del_qdisc_full(dev, parent, None).await
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "del_qdisc_full"))]
pub async fn del_qdisc_full(
&self,
dev: impl Into<InterfaceRef>,
parent: TcHandle,
handle: Option<TcHandle>,
) -> Result<()> {
let ifindex = self.resolve_interface(&dev.into()).await?;
self.del_qdisc_by_index_full(ifindex, parent, handle).await
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "del_qdisc_by_index"))]
pub async fn del_qdisc_by_index(&self, ifindex: u32, parent: TcHandle) -> Result<()> {
self.del_qdisc_by_index_full(ifindex, parent, None).await
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "del_qdisc_by_index_full"))]
pub async fn del_qdisc_by_index_full(
&self,
ifindex: u32,
parent: TcHandle,
handle: Option<TcHandle>,
) -> Result<()> {
let parent_handle = parent.as_raw();
let qdisc_handle = handle.map(|h| h.as_raw()).unwrap_or(0);
let tcmsg = TcMsg::new()
.with_ifindex(ifindex as i32)
.with_parent(parent_handle)
.with_handle(qdisc_handle);
let mut builder = ack_request(NlMsgType::RTM_DELQDISC);
builder.append(&tcmsg);
self.send_ack(builder).await.map_err(|e| {
if e.is_not_found() {
Error::QdiscNotFound {
kind: handle.unwrap_or(parent).to_string(),
interface: format!("ifindex {ifindex}"),
}
} else {
e.with_context(format!("del_qdisc(ifindex {ifindex}, parent={})", parent))
}
})
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "replace_qdisc"))]
pub async fn replace_qdisc(
&self,
dev: impl Into<InterfaceRef>,
config: impl QdiscConfig,
) -> Result<()> {
self.replace_qdisc_full(dev, TcHandle::ROOT, None, config)
.await
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "replace_qdisc_full"))]
pub async fn replace_qdisc_full(
&self,
dev: impl Into<InterfaceRef>,
parent: TcHandle,
handle: Option<TcHandle>,
config: impl QdiscConfig,
) -> Result<()> {
let ifindex = self.resolve_interface(&dev.into()).await?;
self.replace_qdisc_by_index_full(ifindex, parent, handle, config)
.await
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "replace_qdisc_by_index"))]
pub async fn replace_qdisc_by_index(
&self,
ifindex: u32,
config: impl QdiscConfig,
) -> Result<()> {
self.replace_qdisc_by_index_full(ifindex, TcHandle::ROOT, None, config)
.await
}
#[tracing::instrument(
level = "debug",
skip_all,
fields(method = "replace_qdisc_by_index_full")
)]
pub async fn replace_qdisc_by_index_full(
&self,
ifindex: u32,
parent: TcHandle,
handle: Option<TcHandle>,
config: impl QdiscConfig,
) -> Result<()> {
let parent_handle = parent.as_raw();
let qdisc_handle = handle.map(|h| h.as_raw()).unwrap_or(0);
let tcmsg = TcMsg::new()
.with_ifindex(ifindex as i32)
.with_parent(parent_handle)
.with_handle(qdisc_handle);
let mut builder = replace_request(NlMsgType::RTM_NEWQDISC);
builder.append(&tcmsg);
builder.append_attr_str(TcaAttr::Kind as u16, config.kind());
let options_token = builder.nest_start(TcaAttr::Options as u16);
config.write_options(&mut builder)?;
builder.nest_end(options_token);
self.send_ack(builder)
.await
.map_err(|e| e.with_context("replace_qdisc"))
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "change_qdisc"))]
pub async fn change_qdisc(
&self,
dev: impl Into<InterfaceRef>,
parent: TcHandle,
config: impl QdiscConfig,
) -> Result<()> {
self.change_qdisc_full(dev, parent, None, config).await
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "change_qdisc_full"))]
pub async fn change_qdisc_full(
&self,
dev: impl Into<InterfaceRef>,
parent: TcHandle,
handle: Option<TcHandle>,
config: impl QdiscConfig,
) -> Result<()> {
let ifindex = self.resolve_interface(&dev.into()).await?;
self.change_qdisc_by_index_full(ifindex, parent, handle, config)
.await
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "change_qdisc_by_index"))]
pub async fn change_qdisc_by_index(
&self,
ifindex: u32,
parent: TcHandle,
config: impl QdiscConfig,
) -> Result<()> {
self.change_qdisc_by_index_full(ifindex, parent, None, config)
.await
}
#[tracing::instrument(
level = "debug",
skip_all,
fields(method = "change_qdisc_by_index_full")
)]
pub async fn change_qdisc_by_index_full(
&self,
ifindex: u32,
parent: TcHandle,
handle: Option<TcHandle>,
config: impl QdiscConfig,
) -> Result<()> {
let parent_handle = parent.as_raw();
let qdisc_handle = handle.map(|h| h.as_raw()).unwrap_or(0);
let tcmsg = TcMsg::new()
.with_ifindex(ifindex as i32)
.with_parent(parent_handle)
.with_handle(qdisc_handle);
let kind = config.kind().to_string();
let mut builder = ack_request(NlMsgType::RTM_NEWQDISC);
builder.append(&tcmsg);
builder.append_attr_str(TcaAttr::Kind as u16, &kind);
let options_token = builder.nest_start(TcaAttr::Options as u16);
config.write_options(&mut builder)?;
builder.nest_end(options_token);
self.send_ack(builder).await.map_err(|e| {
if e.is_not_found() {
Error::QdiscNotFound {
kind,
interface: format!("ifindex {ifindex}"),
}
} else {
e.with_context(format!(
"change_qdisc(ifindex {ifindex}, parent={})",
parent
))
}
})
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "apply_netem"))]
pub async fn apply_netem(
&self,
dev: impl Into<InterfaceRef>,
config: NetemConfig,
) -> Result<()> {
let dev = dev.into();
match self.replace_qdisc(dev.clone(), config.clone()).await {
Ok(()) => Ok(()),
Err(e) if e.is_not_found() => self.add_qdisc(dev, config).await,
Err(e) => Err(e),
}
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "apply_netem_by_index"))]
pub async fn apply_netem_by_index(&self, ifindex: u32, config: NetemConfig) -> Result<()> {
match self.replace_qdisc_by_index(ifindex, config.clone()).await {
Ok(()) => Ok(()),
Err(e) if e.is_not_found() => self.add_qdisc_by_index(ifindex, config).await,
Err(e) => Err(e),
}
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "del_netem"))]
pub async fn del_netem(&self, dev: impl Into<InterfaceRef>) -> Result<()> {
self.del_qdisc(dev, TcHandle::ROOT).await
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "del_netem_by_index"))]
pub async fn del_netem_by_index(&self, ifindex: u32) -> Result<()> {
self.del_qdisc_by_index(ifindex, TcHandle::ROOT).await
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "del_class"))]
pub async fn del_class(
&self,
dev: impl Into<InterfaceRef>,
parent: TcHandle,
classid: TcHandle,
) -> Result<()> {
let ifindex = self.resolve_interface(&dev.into()).await?;
self.del_class_by_index(ifindex, parent, classid).await
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "del_class_by_index"))]
pub async fn del_class_by_index(
&self,
ifindex: u32,
parent: TcHandle,
classid: TcHandle,
) -> Result<()> {
let parent_handle = parent.as_raw();
let class_handle = classid.as_raw();
let tcmsg = TcMsg::new()
.with_ifindex(ifindex as i32)
.with_parent(parent_handle)
.with_handle(class_handle);
let mut builder = ack_request(NlMsgType::RTM_DELTCLASS);
builder.append(&tcmsg);
self.send_ack(builder)
.await
.map_err(|e| e.with_context("del_class"))
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "add_class", kind = %config.kind()))]
pub async fn add_class<C: ClassConfig>(
&self,
dev: impl Into<InterfaceRef>,
parent: TcHandle,
classid: TcHandle,
config: C,
) -> Result<()> {
let ifindex = self.resolve_interface(&dev.into()).await?;
self.add_class_by_index(ifindex, parent, classid, config)
.await
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "add_class_by_index", kind = %config.kind()))]
pub async fn add_class_by_index<C: ClassConfig>(
&self,
ifindex: u32,
parent: TcHandle,
classid: TcHandle,
config: C,
) -> Result<()> {
let parent_handle = parent.as_raw();
let class_handle = classid.as_raw();
let tcmsg = TcMsg::new()
.with_ifindex(ifindex as i32)
.with_parent(parent_handle)
.with_handle(class_handle);
let mut builder = create_request(NlMsgType::RTM_NEWTCLASS);
builder.append(&tcmsg);
builder.append_attr_str(TcaAttr::Kind as u16, config.kind());
let options_token = builder.nest_start(TcaAttr::Options as u16);
config.write_options(&mut builder)?;
builder.nest_end(options_token);
self.send_ack(builder)
.await
.map_err(|e| e.with_context("add_class"))
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "change_class", kind = %config.kind()))]
pub async fn change_class<C: ClassConfig>(
&self,
dev: impl Into<InterfaceRef>,
parent: TcHandle,
classid: TcHandle,
config: C,
) -> Result<()> {
let ifindex = self.resolve_interface(&dev.into()).await?;
self.change_class_by_index(ifindex, parent, classid, config)
.await
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "change_class_by_index", kind = %config.kind()))]
pub async fn change_class_by_index<C: ClassConfig>(
&self,
ifindex: u32,
parent: TcHandle,
classid: TcHandle,
config: C,
) -> Result<()> {
let parent_handle = parent.as_raw();
let class_handle = classid.as_raw();
let tcmsg = TcMsg::new()
.with_ifindex(ifindex as i32)
.with_parent(parent_handle)
.with_handle(class_handle);
let mut builder = ack_request(NlMsgType::RTM_NEWTCLASS);
builder.append(&tcmsg);
builder.append_attr_str(TcaAttr::Kind as u16, config.kind());
let options_token = builder.nest_start(TcaAttr::Options as u16);
config.write_options(&mut builder)?;
builder.nest_end(options_token);
self.send_ack(builder)
.await
.map_err(|e| e.with_context("change_class"))
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "replace_class", kind = %config.kind()))]
pub async fn replace_class<C: ClassConfig>(
&self,
dev: impl Into<InterfaceRef>,
parent: TcHandle,
classid: TcHandle,
config: C,
) -> Result<()> {
let ifindex = self.resolve_interface(&dev.into()).await?;
self.replace_class_by_index(ifindex, parent, classid, config)
.await
}
#[tracing::instrument(level = "debug", skip_all, fields(method = "replace_class_by_index", kind = %config.kind()))]
pub async fn replace_class_by_index<C: ClassConfig>(
&self,
ifindex: u32,
parent: TcHandle,
classid: TcHandle,
config: C,
) -> Result<()> {
let parent_handle = parent.as_raw();
let class_handle = classid.as_raw();
let tcmsg = TcMsg::new()
.with_ifindex(ifindex as i32)
.with_parent(parent_handle)
.with_handle(class_handle);
let mut builder = replace_request(NlMsgType::RTM_NEWTCLASS);
builder.append(&tcmsg);
builder.append_attr_str(TcaAttr::Kind as u16, config.kind());
let options_token = builder.nest_start(TcaAttr::Options as u16);
config.write_options(&mut builder)?;
builder.nest_end(options_token);
self.send_ack(builder)
.await
.map_err(|e| e.with_context("replace_class"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_netem_builder() {
use crate::util::Percent;
let config = NetemConfig::new()
.delay(Duration::from_millis(100))
.jitter(Duration::from_millis(10))
.delay_correlation(Percent::new(25.0))
.loss(Percent::new(1.0))
.build();
assert_eq!(config.delay, Some(Duration::from_millis(100)));
assert_eq!(config.jitter, Some(Duration::from_millis(10)));
assert_eq!(config.delay_correlation.as_percent(), 25.0);
assert_eq!(config.loss.as_percent(), 1.0);
assert_eq!(config.kind(), "netem");
}
#[test]
fn test_fq_codel_builder() {
let config = FqCodelConfig::new()
.target(Duration::from_millis(5))
.interval(Duration::from_millis(100))
.limit(10000)
.ecn(true)
.build();
assert_eq!(config.target, Some(Duration::from_millis(5)));
assert_eq!(config.interval, Some(Duration::from_millis(100)));
assert_eq!(config.limit, Some(10000));
assert!(config.ecn);
assert_eq!(config.kind(), "fq_codel");
}
#[test]
fn test_tbf_builder() {
use crate::util::{Bytes, Rate};
let config = TbfConfig::new()
.rate(Rate::bytes_per_sec(1_000_000))
.burst(Bytes::kib(32))
.limit(Bytes::kib(100))
.build();
assert_eq!(config.rate, Rate::bytes_per_sec(1_000_000));
assert_eq!(config.burst, Bytes::kib(32));
assert_eq!(config.limit, Bytes::kib(100));
assert_eq!(config.kind(), "tbf");
}
#[test]
fn test_netem_clamp() {
use crate::util::Percent;
let config = NetemConfig::new()
.loss(Percent::new(150.0)) .delay_correlation(Percent::new(-10.0)) .build();
assert_eq!(config.loss.as_percent(), 100.0);
assert_eq!(config.delay_correlation.as_percent(), 0.0);
}
#[test]
fn test_drr_builder() {
let config = DrrConfig::new().build();
assert_eq!(config.kind(), "drr");
}
#[test]
fn test_qfq_builder() {
let config = QfqConfig::new().build();
assert_eq!(config.kind(), "qfq");
}
#[test]
fn test_plug_builder() {
let config = PlugConfig::new().limit(10000).build();
assert_eq!(config.limit, Some(10000));
assert_eq!(config.kind(), "plug");
}
#[test]
fn test_mqprio_builder() {
let config = MqprioConfig::new()
.num_tc(4)
.map(&[0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3])
.hw_offload(true)
.queues(&[(2, 0), (2, 2), (2, 4), (2, 6)])
.build();
assert_eq!(config.num_tc, 4);
assert!(config.hw);
assert_eq!(config.prio_tc_map[0], 0);
assert_eq!(config.prio_tc_map[3], 3);
assert_eq!(config.count[0], 2);
assert_eq!(config.offset[1], 2);
assert_eq!(config.kind(), "mqprio");
}
#[test]
fn test_etf_builder() {
let config = EtfConfig::new()
.clockid(1) .delta_ns(300000)
.deadline_mode(true)
.offload(true)
.skip_sock_check(false)
.build();
assert_eq!(config.clockid, 1);
assert_eq!(config.delta, 300000);
assert!(config.deadline_mode);
assert!(config.offload);
assert!(!config.skip_sock_check);
assert_eq!(config.kind(), "etf");
}
#[test]
fn test_hfsc_builder() {
let config = HfscConfig::new().default_class(0x10).build();
assert_eq!(config.default_class, 0x10);
assert_eq!(config.kind(), "hfsc");
}
#[test]
fn test_taprio_builder() {
let config = TaprioConfig::new()
.num_tc(2)
.map(&[0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
.queues(&[(1, 0), (1, 1)])
.clockid(11) .base_time(1000000000)
.cycle_time(1000000)
.entry(TaprioSchedEntry::set_gates(0x1, 500000))
.entry(TaprioSchedEntry::set_gates(0x2, 500000))
.full_offload(true)
.build();
assert_eq!(config.num_tc, 2);
assert_eq!(config.clockid, 11);
assert_eq!(config.base_time, 1000000000);
assert_eq!(config.cycle_time, 1000000);
assert_eq!(config.entries.len(), 2);
assert_eq!(config.entries[0].gate_mask, 0x1);
assert_eq!(config.entries[0].interval, 500000);
assert_eq!(config.entries[1].gate_mask, 0x2);
assert!(config.flags & 2 != 0); assert_eq!(config.kind(), "taprio");
}
#[test]
fn test_htb_class_config_typed() {
use crate::util::Rate;
let config = HtbClassConfig::new(Rate::mbit(100))
.ceil(Rate::mbit(500))
.prio(1)
.quantum(1500)
.build();
assert_eq!(config.rate, Rate::mbit(100));
assert_eq!(config.ceil, Some(Rate::mbit(500)));
assert_eq!(config.prio, Some(1));
assert_eq!(config.quantum, Some(1500));
assert_eq!(config.kind(), "htb");
}
#[test]
fn test_htb_class_config_from_string() {
use crate::util::Rate;
let config = HtbClassConfig::new("100mbit".parse::<Rate>().unwrap())
.ceil("500mbit".parse::<Rate>().unwrap())
.prio(2)
.build();
assert_eq!(config.rate, Rate::mbit(100));
assert_eq!(config.ceil, Some(Rate::mbit(500)));
assert_eq!(config.prio, Some(2));
assert_eq!(config.kind(), "htb");
}
#[test]
fn test_htb_class_config_burst() {
use crate::util::{Bytes, Rate};
let config = HtbClassConfig::new(Rate::bytes_per_sec(1_000_000))
.burst(Bytes::new(16384))
.cburst(Bytes::new(32768))
.mtu(9000)
.mpu(64)
.overhead(14)
.build();
assert_eq!(config.burst, Some(Bytes::new(16384)));
assert_eq!(config.cburst, Some(Bytes::new(32768)));
assert_eq!(config.mtu, 9000);
assert_eq!(config.mpu, 64);
assert_eq!(config.overhead, 14);
}
#[test]
fn test_htb_class_config_prio_clamp() {
use crate::util::Rate;
let config = HtbClassConfig::new(Rate::bytes_per_sec(1_000_000))
.prio(100) .build();
assert_eq!(config.prio, Some(7));
}
#[test]
fn test_htb_class_config_defaults() {
use crate::util::Rate;
let config = HtbClassConfig::new(Rate::bytes_per_sec(1_000_000)).build();
assert_eq!(config.ceil, None);
assert_eq!(config.burst, None);
assert_eq!(config.cburst, None);
assert_eq!(config.prio, None);
assert_eq!(config.quantum, None);
assert_eq!(config.mtu, 1600);
assert_eq!(config.mpu, 0);
assert_eq!(config.overhead, 0);
}
#[test]
fn htb_qdisc_parse_params_empty_yields_defaults() {
let cfg = HtbQdiscConfig::parse_params(&[]).unwrap();
assert_eq!(cfg.default_class, 0);
assert_eq!(cfg.r2q, 10);
assert_eq!(cfg.direct_qlen, None);
}
#[test]
fn htb_qdisc_parse_params_default_as_handle() {
let cfg = HtbQdiscConfig::parse_params(&["default", "1:10"]).unwrap();
assert_eq!(
cfg.default_class,
crate::netlink::tc_handle::TcHandle::new(1, 0x10).as_raw(),
);
}
#[test]
fn htb_qdisc_parse_params_default_as_bare_hex() {
let cfg = HtbQdiscConfig::parse_params(&["default", "10"]).unwrap();
assert_eq!(cfg.default_class, 0x10);
let cfg = HtbQdiscConfig::parse_params(&["default", "ff"]).unwrap();
assert_eq!(cfg.default_class, 0xff);
}
#[test]
fn htb_qdisc_parse_params_all_three() {
let cfg =
HtbQdiscConfig::parse_params(&["default", "1:10", "r2q", "5", "direct_qlen", "1000"])
.unwrap();
assert_eq!(
cfg.default_class,
crate::netlink::tc_handle::TcHandle::new(1, 0x10).as_raw(),
);
assert_eq!(cfg.r2q, 5);
assert_eq!(cfg.direct_qlen, Some(1000));
}
#[test]
fn htb_qdisc_parse_params_unknown_token_errors() {
let err = HtbQdiscConfig::parse_params(&["default_class", "1:10"]).unwrap_err();
assert!(
err.to_string().contains("unknown token"),
"expected unknown-token error, got: {err}"
);
}
#[test]
fn htb_qdisc_parse_params_missing_value_errors() {
let err = HtbQdiscConfig::parse_params(&["default"]).unwrap_err();
assert!(
err.to_string().contains("requires a value"),
"expected missing-value error, got: {err}"
);
let err = HtbQdiscConfig::parse_params(&["r2q"]).unwrap_err();
assert!(err.to_string().contains("requires a value"));
}
#[test]
fn htb_qdisc_parse_params_invalid_number_errors() {
let err = HtbQdiscConfig::parse_params(&["r2q", "not-a-number"]).unwrap_err();
assert!(
err.to_string().contains("invalid r2q"),
"expected invalid-r2q error, got: {err}"
);
let err = HtbQdiscConfig::parse_params(&["direct_qlen", "x"]).unwrap_err();
assert!(err.to_string().contains("invalid direct_qlen"));
}
#[test]
fn htb_qdisc_parse_params_invalid_default_errors() {
let err = HtbQdiscConfig::parse_params(&["default", "zzzz"]).unwrap_err();
assert!(
err.to_string().contains("invalid default class"),
"got: {err}"
);
}
#[test]
fn netem_parse_params_empty_yields_defaults() {
let cfg = NetemConfig::parse_params(&[]).unwrap();
assert_eq!(cfg.delay, None);
assert_eq!(cfg.jitter, None);
assert!(cfg.loss.is_zero());
assert_eq!(cfg.limit, 1000);
}
#[test]
fn netem_parse_params_delay_only() {
let cfg = NetemConfig::parse_params(&["delay", "100ms"]).unwrap();
assert_eq!(cfg.delay, Some(Duration::from_millis(100)));
assert_eq!(cfg.jitter, None);
}
#[test]
fn netem_parse_params_delay_with_jitter_and_corr() {
let cfg = NetemConfig::parse_params(&["delay", "100ms", "10ms", "25%"]).unwrap();
assert_eq!(cfg.delay, Some(Duration::from_millis(100)));
assert_eq!(cfg.jitter, Some(Duration::from_millis(10)));
assert!(!cfg.delay_correlation.is_zero());
}
#[test]
fn netem_parse_params_loss_with_random_qualifier() {
let cfg = NetemConfig::parse_params(&["loss", "random", "1.5%"]).unwrap();
assert!(!cfg.loss.is_zero());
}
#[test]
fn netem_parse_params_drop_alias() {
let cfg = NetemConfig::parse_params(&["drop", "0.5%"]).unwrap();
assert!(!cfg.loss.is_zero());
}
#[test]
fn netem_parse_params_multiple_groups() {
let cfg = NetemConfig::parse_params(&[
"delay",
"100ms",
"10ms",
"loss",
"1%",
"duplicate",
"0.1%",
"limit",
"5000",
])
.unwrap();
assert_eq!(cfg.delay, Some(Duration::from_millis(100)));
assert_eq!(cfg.jitter, Some(Duration::from_millis(10)));
assert!(!cfg.loss.is_zero());
assert!(!cfg.duplicate.is_zero());
assert_eq!(cfg.limit, 5000);
}
#[test]
fn netem_parse_params_rate_no_extras() {
let cfg = NetemConfig::parse_params(&["rate", "100mbit"]).unwrap();
assert_eq!(
cfg.rate.map(|r| r.as_bytes_per_sec()),
Some(12_500_000),
"100mbit should round-trip to 12.5 MB/sec"
);
}
#[test]
fn netem_parse_params_rate_extras_rejected() {
let err = NetemConfig::parse_params(&["rate", "100mbit", "20"]).unwrap_err();
assert!(
err.to_string().contains("packet_overhead"),
"expected rate-extras error, got: {err}"
);
}
#[test]
fn netem_parse_params_unknown_token_errors() {
let err = NetemConfig::parse_params(&["nonsense"]).unwrap_err();
assert!(err.to_string().contains("unknown token"), "got: {err}");
}
#[test]
fn netem_parse_params_unsupported_features_rejected() {
for unsup in ["slot", "ecn", "distribution"] {
let err = NetemConfig::parse_params(&[unsup]).unwrap_err();
assert!(
err.to_string().contains("not modelled"),
"expected not-modelled error for `{unsup}`, got: {err}"
);
}
}
#[test]
fn netem_parse_params_loss_state_rejected() {
let err = NetemConfig::parse_params(&["loss", "state", "0.1"]).unwrap_err();
assert!(err.to_string().contains("Markov"), "got: {err}");
}
#[test]
fn netem_parse_params_missing_value_errors() {
let err = NetemConfig::parse_params(&["delay"]).unwrap_err();
assert!(err.to_string().contains("requires a value"), "got: {err}");
let err = NetemConfig::parse_params(&["loss"]).unwrap_err();
assert!(err.to_string().contains("percent value"), "got: {err}");
}
#[test]
fn netem_parse_params_invalid_time_errors() {
let err = NetemConfig::parse_params(&["delay", "fast"]).unwrap_err();
assert!(err.to_string().contains("invalid delay"), "got: {err}");
}
#[test]
fn netem_parse_params_invalid_percent_errors() {
let err = NetemConfig::parse_params(&["loss", "lots"]).unwrap_err();
assert!(err.to_string().contains("invalid loss"), "got: {err}");
}
#[test]
fn cake_parse_params_empty_yields_default() {
let cfg = CakeConfig::parse_params(&[]).unwrap();
assert!(cfg.bandwidth.is_none());
assert!(cfg.rtt.is_none());
assert!(cfg.diffserv_mode.is_none());
assert!(cfg.flow_mode.is_none());
assert!(!cfg.nat);
assert!(!cfg.wash);
}
#[test]
fn cake_parse_params_bandwidth_and_rtt() {
let cfg = CakeConfig::parse_params(&["bandwidth", "100mbit", "rtt", "20ms"]).unwrap();
assert_eq!(
cfg.bandwidth.map(|r| r.as_bytes_per_sec()),
Some(12_500_000)
);
assert_eq!(cfg.rtt, Some(Duration::from_millis(20)));
}
#[test]
fn cake_parse_params_unlimited_flag() {
let cfg = CakeConfig::parse_params(&["unlimited"]).unwrap();
assert_eq!(cfg.bandwidth, Some(crate::util::Rate::ZERO));
}
#[test]
fn cake_parse_params_diffserv_modes() {
for (token, expected) in [
("diffserv3", CakeDiffserv::Diffserv3),
("diffserv4", CakeDiffserv::Diffserv4),
("diffserv8", CakeDiffserv::Diffserv8),
("besteffort", CakeDiffserv::Besteffort),
("precedence", CakeDiffserv::Precedence),
] {
let cfg = CakeConfig::parse_params(&[token]).unwrap();
assert_eq!(cfg.diffserv_mode, Some(expected), "diffserv {token}");
}
}
#[test]
fn cake_parse_params_flow_modes() {
for (token, expected) in [
("flowblind", CakeFlowMode::Flowblind),
("srchost", CakeFlowMode::Srchost),
("dsthost", CakeFlowMode::Dsthost),
("hosts", CakeFlowMode::Hosts),
("flows", CakeFlowMode::Flows),
("dual-srchost", CakeFlowMode::DualSrchost),
("dual-dsthost", CakeFlowMode::DualDsthost),
("triple-isolate", CakeFlowMode::Triple),
] {
let cfg = CakeConfig::parse_params(&[token]).unwrap();
assert_eq!(cfg.flow_mode, Some(expected), "flow {token}");
}
}
#[test]
fn cake_parse_params_atm_modes() {
for (token, expected) in [
("noatm", CakeAtmMode::None),
("atm", CakeAtmMode::Atm),
("ptm", CakeAtmMode::Ptm),
] {
let cfg = CakeConfig::parse_params(&[token]).unwrap();
assert_eq!(cfg.atm_mode, Some(expected), "atm {token}");
}
}
#[test]
fn cake_parse_params_ack_filter() {
let cfg = CakeConfig::parse_params(&["ack-filter"]).unwrap();
assert_eq!(cfg.ack_filter, Some(CakeAckFilter::Filter));
let cfg = CakeConfig::parse_params(&["ack-filter-aggressive"]).unwrap();
assert_eq!(cfg.ack_filter, Some(CakeAckFilter::Aggressive));
let cfg = CakeConfig::parse_params(&["no-ack-filter"]).unwrap();
assert_eq!(cfg.ack_filter, Some(CakeAckFilter::Disabled));
}
#[test]
fn cake_parse_params_boolean_flags_with_negations() {
let cfg =
CakeConfig::parse_params(&["nat", "wash", "ingress", "split-gso", "raw"]).unwrap();
assert!(cfg.nat);
assert!(cfg.wash);
assert!(cfg.ingress);
assert!(cfg.split_gso);
assert!(cfg.raw);
let cfg =
CakeConfig::parse_params(&["nat", "nonat", "wash", "nowash", "ingress", "egress"])
.unwrap();
assert!(!cfg.nat);
assert!(!cfg.wash);
assert!(!cfg.ingress);
}
#[test]
fn cake_parse_params_overhead_signed() {
let cfg = CakeConfig::parse_params(&["overhead", "-4"]).unwrap();
assert_eq!(cfg.overhead, Some(-4));
let cfg = CakeConfig::parse_params(&["overhead", "38"]).unwrap();
assert_eq!(cfg.overhead, Some(38));
}
#[test]
fn cake_parse_params_memlimit_size() {
let cfg = CakeConfig::parse_params(&["memlimit", "32k"]).unwrap();
assert_eq!(cfg.memory_limit.map(|b| b.as_u64()), Some(32 * 1024));
}
#[test]
fn cake_parse_params_fwmark_hex() {
let cfg = CakeConfig::parse_params(&["fwmark", "0xff"]).unwrap();
assert_eq!(cfg.fwmark, Some(0xff));
let cfg = CakeConfig::parse_params(&["fwmark", "ff"]).unwrap();
assert_eq!(cfg.fwmark, Some(0xff));
}
#[test]
fn cake_parse_params_realistic_combo() {
let cfg = CakeConfig::parse_params(&[
"bandwidth",
"100mbit",
"rtt",
"20ms",
"diffserv4",
"triple-isolate",
"ack-filter",
"nat",
"wash",
])
.unwrap();
assert!(cfg.bandwidth.is_some());
assert_eq!(cfg.rtt, Some(Duration::from_millis(20)));
assert_eq!(cfg.diffserv_mode, Some(CakeDiffserv::Diffserv4));
assert_eq!(cfg.flow_mode, Some(CakeFlowMode::Triple));
assert_eq!(cfg.ack_filter, Some(CakeAckFilter::Filter));
assert!(cfg.nat);
assert!(cfg.wash);
}
#[test]
fn cake_parse_params_unknown_token_errors() {
let err = CakeConfig::parse_params(&["nonsense"]).unwrap_err();
assert!(err.to_string().contains("unknown token"));
let err = CakeConfig::parse_params(&["dual_srchost"]).unwrap_err();
assert!(err.to_string().contains("unknown token"));
}
#[test]
fn cake_parse_params_missing_value_errors() {
let err = CakeConfig::parse_params(&["bandwidth"]).unwrap_err();
assert!(err.to_string().contains("requires a value"));
let err = CakeConfig::parse_params(&["rtt"]).unwrap_err();
assert!(err.to_string().contains("requires a value"));
}
#[test]
fn cake_parse_params_invalid_value_errors() {
let err = CakeConfig::parse_params(&["bandwidth", "fast"]).unwrap_err();
assert!(err.to_string().contains("invalid bandwidth"));
let err = CakeConfig::parse_params(&["overhead", "lots"]).unwrap_err();
assert!(err.to_string().contains("invalid overhead"));
let err = CakeConfig::parse_params(&["fwmark", "zzzz"]).unwrap_err();
assert!(err.to_string().contains("invalid fwmark"));
}
#[test]
fn tbf_parse_params_empty_yields_default() {
let cfg = TbfConfig::parse_params(&[]).unwrap();
assert_eq!(cfg.rate, crate::util::Rate::ZERO);
assert!(cfg.peakrate.is_none());
assert_eq!(cfg.burst, crate::util::Bytes::ZERO);
assert_eq!(cfg.mtu, 1514);
}
#[test]
fn tbf_parse_params_typical_set() {
let cfg =
TbfConfig::parse_params(&["rate", "1mbit", "burst", "32kb", "limit", "10kb"]).unwrap();
assert_eq!(cfg.rate.as_bytes_per_sec(), 125_000);
assert_eq!(cfg.burst.as_u64(), 32 * 1024);
assert_eq!(cfg.limit.as_u64(), 10 * 1024);
}
#[test]
fn tbf_parse_params_burst_aliases() {
for alias in ["burst", "buffer", "maxburst"] {
let cfg = TbfConfig::parse_params(&[alias, "16kb"]).unwrap();
assert_eq!(cfg.burst.as_u64(), 16 * 1024, "alias {alias}");
}
}
#[test]
fn tbf_parse_params_mtu_alias() {
for alias in ["mtu", "minburst"] {
let cfg = TbfConfig::parse_params(&[alias, "9000"]).unwrap();
assert_eq!(cfg.mtu, 9000, "alias {alias}");
}
}
#[test]
fn tbf_parse_params_peakrate() {
let cfg = TbfConfig::parse_params(&["rate", "1mbit", "peakrate", "2mbit"]).unwrap();
assert_eq!(cfg.rate.as_bytes_per_sec(), 125_000);
assert_eq!(cfg.peakrate.unwrap().as_bytes_per_sec(), 250_000);
}
#[test]
fn tbf_parse_params_latency_rejected() {
let err = TbfConfig::parse_params(&["latency", "50ms"]).unwrap_err();
assert!(
err.to_string().contains("derived form"),
"expected derived-form rejection, got: {err}"
);
}
#[test]
fn tbf_parse_params_unknown_token_errors() {
let err = TbfConfig::parse_params(&["nonsense"]).unwrap_err();
assert!(err.to_string().contains("unknown token"));
}
#[test]
fn tbf_parse_params_missing_value_errors() {
let err = TbfConfig::parse_params(&["rate"]).unwrap_err();
assert!(err.to_string().contains("requires a value"));
}
#[test]
fn tbf_parse_params_invalid_rate_errors() {
let err = TbfConfig::parse_params(&["rate", "fast"]).unwrap_err();
assert!(err.to_string().contains("invalid rate"));
}
#[test]
fn sfq_parse_params_empty_yields_default() {
let cfg = SfqConfig::parse_params(&[]).unwrap();
assert_eq!(cfg.perturb, 0);
assert_eq!(cfg.limit, 127);
assert_eq!(cfg.quantum, 0);
}
#[test]
fn sfq_parse_params_typical_set() {
let cfg = SfqConfig::parse_params(&["perturb", "10", "limit", "1000", "quantum", "1500"])
.unwrap();
assert_eq!(cfg.perturb, 10);
assert_eq!(cfg.limit, 1000);
assert_eq!(cfg.quantum, 1500);
}
#[test]
fn sfq_parse_params_quantum_with_size_suffix() {
let cfg = SfqConfig::parse_params(&["quantum", "1k"]).unwrap();
assert_eq!(cfg.quantum, 1024);
}
#[test]
fn sfq_parse_params_divisor_rejected() {
let err = SfqConfig::parse_params(&["divisor", "1024"]).unwrap_err();
assert!(err.to_string().contains("not modelled"), "got: {err}");
}
#[test]
fn sfq_parse_params_unknown_token_errors() {
let err = SfqConfig::parse_params(&["nonsense"]).unwrap_err();
assert!(err.to_string().contains("unknown token"));
}
#[test]
fn sfq_parse_params_missing_value_errors() {
let err = SfqConfig::parse_params(&["perturb"]).unwrap_err();
assert!(err.to_string().contains("requires a value"));
}
#[test]
fn prio_parse_params_empty_yields_default() {
let cfg = PrioConfig::parse_params(&[]).unwrap();
assert_eq!(cfg.bands, 3);
assert_eq!(
cfg.priomap,
[1, 2, 2, 2, 1, 2, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]
);
}
#[test]
fn prio_parse_params_bands() {
let cfg = PrioConfig::parse_params(&["bands", "5"]).unwrap();
assert_eq!(cfg.bands, 5);
}
#[test]
fn prio_parse_params_priomap_full() {
let mut params = vec!["priomap"];
for n in 0u8..16 {
params.push(if n < 8 { "0" } else { "1" });
}
let cfg = PrioConfig::parse_params(¶ms).unwrap();
let expected: [u8; 16] = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1];
assert_eq!(cfg.priomap, expected);
}
#[test]
fn prio_parse_params_priomap_short_errors() {
let err = PrioConfig::parse_params(&["priomap", "1", "2", "3", "4", "5"]).unwrap_err();
assert!(
err.to_string().contains("requires exactly 16 values"),
"got: {err}"
);
}
#[test]
fn prio_parse_params_unknown_token_errors() {
let err = PrioConfig::parse_params(&["nonsense"]).unwrap_err();
assert!(err.to_string().contains("unknown token"));
}
#[test]
fn fq_codel_parse_params_empty_yields_default() {
let cfg = FqCodelConfig::parse_params(&[]).unwrap();
assert!(cfg.target.is_none());
assert!(cfg.interval.is_none());
assert!(cfg.limit.is_none());
assert!(!cfg.ecn);
}
#[test]
fn fq_codel_parse_params_typical_set() {
let cfg = FqCodelConfig::parse_params(&[
"limit",
"10240",
"target",
"5ms",
"interval",
"100ms",
"flows",
"1024",
"quantum",
"1500",
"memory_limit",
"32m",
"ecn",
])
.unwrap();
assert_eq!(cfg.limit, Some(10240));
assert_eq!(cfg.target, Some(Duration::from_millis(5)));
assert_eq!(cfg.interval, Some(Duration::from_millis(100)));
assert_eq!(cfg.flows, Some(1024));
assert_eq!(cfg.quantum, Some(1500));
assert_eq!(cfg.memory_limit, Some(32 * 1024 * 1024));
assert!(cfg.ecn);
}
#[test]
fn fq_codel_parse_params_ecn_noecn_toggle() {
let cfg = FqCodelConfig::parse_params(&["ecn"]).unwrap();
assert!(cfg.ecn);
let cfg = FqCodelConfig::parse_params(&["ecn", "noecn"]).unwrap();
assert!(!cfg.ecn);
}
#[test]
fn fq_codel_parse_params_ce_threshold() {
let cfg = FqCodelConfig::parse_params(&["ce_threshold", "20ms"]).unwrap();
assert_eq!(cfg.ce_threshold, Some(Duration::from_millis(20)));
}
#[test]
fn fq_codel_parse_params_unknown_token_errors() {
let err = FqCodelConfig::parse_params(&["nonsense"]).unwrap_err();
assert!(err.to_string().contains("unknown token"));
}
#[test]
fn fq_codel_parse_params_invalid_time_errors() {
let err = FqCodelConfig::parse_params(&["target", "fast"]).unwrap_err();
assert!(err.to_string().contains("invalid target"));
}
#[test]
fn red_parse_params_empty_yields_default() {
let cfg = RedConfig::parse_params(&[]).unwrap();
assert_eq!(cfg.limit, 0);
assert_eq!(cfg.min, 0);
assert_eq!(cfg.max, 0);
assert_eq!(cfg.max_p, 5);
assert!(!cfg.ecn);
assert!(!cfg.adaptive);
}
#[test]
fn red_parse_params_thresholds_with_size_suffixes() {
let cfg = RedConfig::parse_params(&["limit", "100k", "min", "10k", "max", "30k"]).unwrap();
assert_eq!(cfg.limit, 100 * 1024);
assert_eq!(cfg.min, 10 * 1024);
assert_eq!(cfg.max, 30 * 1024);
}
#[test]
fn red_parse_params_probability() {
let cfg = RedConfig::parse_params(&["probability", "50"]).unwrap();
assert!(
cfg.max_p >= 126 && cfg.max_p <= 128,
"expected ~127, got {}",
cfg.max_p
);
}
#[test]
fn red_parse_params_flags_with_negations() {
let cfg = RedConfig::parse_params(&["ecn", "harddrop", "adaptive"]).unwrap();
assert!(cfg.ecn);
assert!(cfg.harddrop);
assert!(cfg.adaptive);
let cfg = RedConfig::parse_params(&[
"ecn",
"noecn",
"harddrop",
"noharddrop",
"adaptive",
"noadaptive",
])
.unwrap();
assert!(!cfg.ecn);
assert!(!cfg.harddrop);
assert!(!cfg.adaptive);
}
#[test]
fn red_parse_params_unsupported_features_rejected() {
for unsup in ["avpkt", "burst", "bandwidth"] {
let err = RedConfig::parse_params(&[unsup, "1"]).unwrap_err();
assert!(
err.to_string().contains("not modelled"),
"expected not-modelled for `{unsup}`, got: {err}"
);
}
}
#[test]
fn red_parse_params_unknown_token_errors() {
let err = RedConfig::parse_params(&["nonsense"]).unwrap_err();
assert!(err.to_string().contains("unknown token"));
}
#[test]
fn pie_parse_params_empty_yields_default() {
let cfg = PieConfig::parse_params(&[]).unwrap();
assert!(cfg.target.is_none());
assert!(cfg.limit.is_none());
assert!(cfg.alpha.is_none());
assert!(!cfg.ecn);
assert!(!cfg.bytemode);
}
#[test]
fn pie_parse_params_typical_set() {
let cfg = PieConfig::parse_params(&[
"target", "15ms", "limit", "1000", "tupdate", "30ms", "alpha", "2", "beta", "20",
])
.unwrap();
assert_eq!(cfg.target, Some(Duration::from_millis(15)));
assert_eq!(cfg.limit, Some(1000));
assert_eq!(cfg.tupdate, Some(Duration::from_millis(30)));
assert_eq!(cfg.alpha, Some(2));
assert_eq!(cfg.beta, Some(20));
}
#[test]
fn pie_parse_params_flags_with_negations() {
let cfg = PieConfig::parse_params(&["ecn", "bytemode"]).unwrap();
assert!(cfg.ecn);
assert!(cfg.bytemode);
let cfg = PieConfig::parse_params(&["ecn", "noecn", "bytemode", "nobytemode"]).unwrap();
assert!(!cfg.ecn);
assert!(!cfg.bytemode);
}
#[test]
fn pie_parse_params_unknown_token_errors() {
let err = PieConfig::parse_params(&["nonsense"]).unwrap_err();
assert!(err.to_string().contains("unknown token"));
}
#[test]
fn pie_parse_params_invalid_time_errors() {
let err = PieConfig::parse_params(&["target", "fast"]).unwrap_err();
assert!(err.to_string().contains("invalid target"));
}
#[test]
fn hfsc_parse_params_empty_yields_default() {
let cfg = HfscConfig::parse_params(&[]).unwrap();
assert_eq!(cfg.default_class, 0);
}
#[test]
fn hfsc_parse_params_default_hex() {
let cfg = HfscConfig::parse_params(&["default", "10"]).unwrap();
assert_eq!(cfg.default_class, 0x10);
let cfg = HfscConfig::parse_params(&["default", "ff"]).unwrap();
assert_eq!(cfg.default_class, 0xff);
}
#[test]
fn hfsc_parse_params_unknown_token_errors() {
let err = HfscConfig::parse_params(&["nonsense"]).unwrap_err();
assert!(err.to_string().contains("unknown token"));
}
#[test]
fn hfsc_parse_params_missing_value_errors() {
let err = HfscConfig::parse_params(&["default"]).unwrap_err();
assert!(err.to_string().contains("requires a value"));
}
#[test]
fn ingress_clsact_parse_params_empty_succeeds() {
IngressConfig::parse_params(&[]).unwrap();
ClsactConfig::parse_params(&[]).unwrap();
}
#[test]
fn ingress_clsact_parse_params_reject_any_token() {
let err = IngressConfig::parse_params(&["foo"]).unwrap_err();
assert!(err.to_string().contains("takes no parameters"));
let err = ClsactConfig::parse_params(&["bar"]).unwrap_err();
assert!(err.to_string().contains("takes no parameters"));
}
#[test]
fn drr_qfq_parse_params_empty_succeeds() {
DrrConfig::parse_params(&[]).unwrap();
QfqConfig::parse_params(&[]).unwrap();
}
#[test]
fn drr_qfq_parse_params_reject_any_token() {
let err = DrrConfig::parse_params(&["quantum", "1500"]).unwrap_err();
assert!(
err.to_string().contains("DrrClassConfig"),
"expected per-class hint, got: {err}"
);
let err = QfqConfig::parse_params(&["weight", "10"]).unwrap_err();
assert!(err.to_string().contains("QfqClassConfig"), "got: {err}");
}
#[test]
fn plug_parse_params_empty_yields_default() {
let cfg = PlugConfig::parse_params(&[]).unwrap();
assert!(cfg.limit.is_none());
}
#[test]
fn plug_parse_params_limit_with_size_suffix() {
let cfg = PlugConfig::parse_params(&["limit", "10k"]).unwrap();
assert_eq!(cfg.limit, Some(10 * 1024));
}
#[test]
fn plug_parse_params_unknown_token_errors() {
let err = PlugConfig::parse_params(&["nonsense"]).unwrap_err();
assert!(err.to_string().contains("unknown token"));
}
#[test]
fn mqprio_parse_params_num_tc_and_hw_flag() {
let cfg = MqprioConfig::parse_params(&["num_tc", "4", "nohw"]).unwrap();
assert_eq!(cfg.num_tc, 4);
assert!(!cfg.hw);
let cfg = MqprioConfig::parse_params(&["num_tc", "8", "hw"]).unwrap();
assert!(cfg.hw);
}
#[test]
fn mqprio_parse_params_map_full() {
let mut params = vec!["map"];
for n in 0u8..16 {
params.push(if n < 8 { "0" } else { "1" });
}
let cfg = MqprioConfig::parse_params(¶ms).unwrap();
let expected: [u8; 16] = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1];
assert_eq!(cfg.prio_tc_map, expected);
}
#[test]
fn mqprio_parse_params_map_short_errors() {
let err = MqprioConfig::parse_params(&["map", "1", "2", "3"]).unwrap_err();
assert!(err.to_string().contains("requires exactly 16 values"));
}
#[test]
fn mqprio_parse_params_num_tc_out_of_range() {
let err = MqprioConfig::parse_params(&["num_tc", "20"]).unwrap_err();
assert!(err.to_string().contains("out of range"));
}
#[test]
fn mqprio_parse_params_queues_rejected() {
let err = MqprioConfig::parse_params(&["queues", "1@0"]).unwrap_err();
assert!(err.to_string().contains("not parsed by parse_params yet"));
}
#[test]
fn etf_parse_params_empty_yields_default() {
let cfg = EtfConfig::parse_params(&[]).unwrap();
assert_eq!(cfg.delta, 0);
assert_eq!(cfg.clockid, -1);
assert!(!cfg.deadline_mode);
assert!(!cfg.offload);
}
#[test]
fn etf_parse_params_typical() {
let cfg = EtfConfig::parse_params(&[
"delta",
"300000",
"clockid",
"CLOCK_TAI",
"deadline_mode",
"offload",
])
.unwrap();
assert_eq!(cfg.delta, 300_000);
assert_eq!(cfg.clockid, libc::CLOCK_TAI);
assert!(cfg.deadline_mode);
assert!(cfg.offload);
}
#[test]
fn etf_parse_params_clockid_named_and_integer() {
for (name, expected) in [
("CLOCK_REALTIME", libc::CLOCK_REALTIME),
("CLOCK_MONOTONIC", libc::CLOCK_MONOTONIC),
("CLOCK_BOOTTIME", libc::CLOCK_BOOTTIME),
("CLOCK_TAI", libc::CLOCK_TAI),
] {
let cfg = EtfConfig::parse_params(&["clockid", name]).unwrap();
assert_eq!(cfg.clockid, expected, "name {name}");
}
let cfg = EtfConfig::parse_params(&["clockid", "11"]).unwrap();
assert_eq!(cfg.clockid, 11);
}
#[test]
fn etf_parse_params_unknown_clockid_errors() {
let err = EtfConfig::parse_params(&["clockid", "CLOCK_NONSENSE"]).unwrap_err();
assert!(err.to_string().contains("invalid clockid"));
}
#[test]
fn etf_parse_params_unknown_token_errors() {
let err = EtfConfig::parse_params(&["nonsense"]).unwrap_err();
assert!(err.to_string().contains("unknown token"));
}
#[test]
fn taprio_parse_params_empty_yields_default() {
let cfg = TaprioConfig::parse_params(&[]).unwrap();
assert_eq!(cfg.entries.len(), 0);
assert_eq!(cfg.cycle_time, 0);
}
#[test]
fn taprio_parse_params_typical() {
let cfg = TaprioConfig::parse_params(&[
"num_tc",
"2",
"clockid",
"CLOCK_TAI",
"base-time",
"0",
"cycle-time",
"1000000",
"sched-entry",
"SET",
"0x1",
"500000",
"sched-entry",
"SET",
"0x2",
"500000",
])
.unwrap();
assert_eq!(cfg.num_tc, 2);
assert_eq!(cfg.clockid, libc::CLOCK_TAI);
assert_eq!(cfg.cycle_time, 1_000_000);
assert_eq!(cfg.entries.len(), 2);
assert_eq!(cfg.entries[0].gate_mask, 0x1);
assert_eq!(cfg.entries[0].interval, 500_000);
assert_eq!(cfg.entries[1].gate_mask, 0x2);
}
#[test]
fn taprio_parse_params_sched_entry_cmd_aliases() {
use crate::netlink::types::tc::qdisc::taprio::{
TC_TAPRIO_CMD_SET_AND_HOLD, TC_TAPRIO_CMD_SET_AND_RELEASE, TC_TAPRIO_CMD_SET_GATES,
};
for (cmd, expected) in [
("SET", TC_TAPRIO_CMD_SET_GATES),
("S", TC_TAPRIO_CMD_SET_GATES),
("set", TC_TAPRIO_CMD_SET_GATES),
("HOLD", TC_TAPRIO_CMD_SET_AND_HOLD),
("H", TC_TAPRIO_CMD_SET_AND_HOLD),
("RELEASE", TC_TAPRIO_CMD_SET_AND_RELEASE),
("R", TC_TAPRIO_CMD_SET_AND_RELEASE),
] {
let cfg = TaprioConfig::parse_params(&["sched-entry", cmd, "0x1", "100"]).unwrap();
assert_eq!(cfg.entries[0].cmd, expected, "cmd alias `{cmd}`");
}
}
#[test]
fn taprio_parse_params_sched_entry_short_errors() {
let err = TaprioConfig::parse_params(&["sched-entry", "SET", "0x1"]).unwrap_err();
assert!(err.to_string().contains("sched-entry"));
}
#[test]
fn taprio_parse_params_flags_and_named() {
use crate::netlink::types::tc::qdisc::taprio::{
TAPRIO_ATTR_FLAG_FULL_OFFLOAD, TAPRIO_ATTR_FLAG_TXTIME_ASSIST,
};
let cfg = TaprioConfig::parse_params(&["txtime-assist", "full-offload"]).unwrap();
assert_eq!(
cfg.flags,
TAPRIO_ATTR_FLAG_TXTIME_ASSIST | TAPRIO_ATTR_FLAG_FULL_OFFLOAD
);
let cfg = TaprioConfig::parse_params(&["flags", "0x3"]).unwrap();
assert_eq!(cfg.flags, 0x3);
}
#[test]
fn taprio_parse_params_queues_rejected() {
let err = TaprioConfig::parse_params(&["queues", "1@0"]).unwrap_err();
assert!(err.to_string().contains("not parsed by parse_params yet"));
}
#[test]
fn taprio_parse_params_unknown_token_errors() {
let err = TaprioConfig::parse_params(&["nonsense"]).unwrap_err();
assert!(err.to_string().contains("unknown token"));
}
#[test]
fn taprio_parse_params_invalid_sched_entry_cmd() {
let err = TaprioConfig::parse_params(&["sched-entry", "BOGUS", "0x1", "100"]).unwrap_err();
assert!(err.to_string().contains("invalid sched-entry cmd"));
}
#[test]
fn htb_class_parse_params_rate_only() {
let cfg = HtbClassConfig::parse_params(&["rate", "100mbit"]).unwrap();
assert_eq!(
cfg.rate.as_bytes_per_sec(),
crate::util::Rate::mbit(100).as_bytes_per_sec()
);
assert!(cfg.ceil.is_none());
}
#[test]
fn htb_class_parse_params_full_aliases() {
let cfg = HtbClassConfig::parse_params(&[
"rate", "10mbit", "ceil", "100mbit", "buffer", "32kb", "cbuffer", "64kb", "prio", "1",
"quantum", "1500", "mtu", "1500", "mpu", "64", "overhead", "14",
])
.unwrap();
assert!(cfg.ceil.is_some());
assert!(cfg.burst.is_some());
assert!(cfg.cburst.is_some());
assert_eq!(cfg.prio, Some(1));
assert_eq!(cfg.quantum, Some(1500));
assert_eq!(cfg.mtu, 1500);
assert_eq!(cfg.mpu, 64);
assert_eq!(cfg.overhead, 14);
}
#[test]
fn htb_class_parse_params_missing_rate_errors() {
let err = HtbClassConfig::parse_params(&[]).unwrap_err();
assert!(err.to_string().contains("htb:"), "kind-prefixed: {err}");
assert!(err.to_string().contains("`rate` is required"), "got: {err}");
}
#[test]
fn htb_class_parse_params_unknown_token_errors() {
let err = HtbClassConfig::parse_params(&["rate", "100mbit", "nonsense"]).unwrap_err();
assert!(
err.to_string().contains("htb: unknown token `nonsense`"),
"got: {err}"
);
}
#[test]
fn htb_class_parse_params_missing_value_errors() {
let err = HtbClassConfig::parse_params(&["rate"]).unwrap_err();
assert!(err.to_string().contains("requires a value"), "got: {err}");
}
#[test]
fn htb_class_parse_params_invalid_rate_errors() {
let err = HtbClassConfig::parse_params(&["rate", "fast"]).unwrap_err();
assert!(err.to_string().contains("htb: invalid rate"), "got: {err}");
}
#[test]
fn hfsc_class_parse_params_empty_yields_empty_config() {
let cfg = HfscClassConfig::parse_params(&[]).unwrap();
assert!(cfg.rsc.is_none() && cfg.fsc.is_none() && cfg.usc.is_none());
}
#[test]
fn hfsc_class_parse_params_three_curves() {
let cfg = HfscClassConfig::parse_params(&[
"rt", "rate", "10mbit", "ls", "rate", "100mbit", "ul", "rate", "200mbit",
])
.unwrap();
assert!(cfg.rsc.is_some());
assert!(cfg.fsc.is_some());
assert!(cfg.usc.is_some());
}
#[test]
fn hfsc_class_parse_params_unknown_curve_errors() {
let err = HfscClassConfig::parse_params(&["xt", "rate", "10mbit"]).unwrap_err();
assert!(
err.to_string().contains("hfsc: unknown token `xt`"),
"got: {err}"
);
}
#[test]
fn hfsc_class_parse_params_non_rate_form_errors() {
let err = HfscClassConfig::parse_params(&["rt", "rate", "10mbit", "ls", "m1", "100"])
.unwrap_err();
assert!(err.to_string().contains("hfsc:"), "kind-prefixed: {err}");
assert!(
err.to_string().contains("only the `rate <rate>` form"),
"got: {err}"
);
}
#[test]
fn drr_class_parse_params_empty_and_quantum() {
let cfg = DrrClassConfig::parse_params(&[]).unwrap();
assert!(cfg.quantum.is_none());
let cfg = DrrClassConfig::parse_params(&["quantum", "1500"]).unwrap();
assert!(cfg.quantum.is_some());
}
#[test]
fn drr_class_parse_params_unknown_token_errors() {
let err = DrrClassConfig::parse_params(&["rate", "100mbit"]).unwrap_err();
assert!(
err.to_string().contains("drr: unknown token `rate`"),
"got: {err}"
);
}
#[test]
fn qfq_class_parse_params_weight_and_lmax() {
let cfg = QfqClassConfig::parse_params(&["weight", "5", "lmax", "9000"]).unwrap();
assert_eq!(cfg.weight, Some(5));
assert!(cfg.lmax.is_some());
}
#[test]
fn qfq_class_parse_params_weight_clamped() {
let cfg = QfqClassConfig::parse_params(&["weight", "9999"]).unwrap();
assert_eq!(cfg.weight, Some(1023));
}
#[test]
fn qfq_class_parse_params_unknown_token_errors() {
let err = QfqClassConfig::parse_params(&["nonsense"]).unwrap_err();
assert!(
err.to_string().contains("qfq: unknown token `nonsense`"),
"got: {err}"
);
}
#[test]
fn class_configs_dispatch_through_parse_params_trait() {
use crate::ParseParams;
let _: HtbClassConfig =
<HtbClassConfig as ParseParams>::parse_params(&["rate", "100mbit"]).unwrap();
let _: HfscClassConfig = <HfscClassConfig as ParseParams>::parse_params(&[]).unwrap();
let _: DrrClassConfig = <DrrClassConfig as ParseParams>::parse_params(&[]).unwrap();
let _: QfqClassConfig = <QfqClassConfig as ParseParams>::parse_params(&[]).unwrap();
}
}