use std::io;
use std::marker::PhantomData;
use std::net::Ipv4Addr;
use std::time::Duration;
use std::sync::{
atomic::{AtomicU16, Ordering},
Mutex,
};
static SUBNET_COUNTER: AtomicU16 = AtomicU16::new(0);
struct LoTcState {
instance_count: u32,
next_minor: u32, }
impl LoTcState {
const fn new() -> Self {
Self { instance_count: 0, next_minor: 2 }
}
fn alloc_minor(&mut self) -> u32 {
let m = self.next_minor;
self.next_minor += 1;
m
}
}
static LO_TC: Mutex<LoTcState> = Mutex::new(LoTcState::new());
#[derive(Clone, Debug)]
pub struct GilbertElliot {
pub p: f64,
pub r: f64,
pub h: f64,
pub k: f64,
}
#[derive(Clone, Debug)]
pub struct BadNetConfig {
#[cfg(feature = "seed")]
pub seed: u64,
pub delay: Duration,
pub loss_rate: f64,
pub corrupt_rate: f64,
pub duplicate_rate: f64,
pub reorder_rate: f64,
pub gap: u32,
pub loss_ge: Option<GilbertElliot>,
}
impl Default for BadNetConfig {
fn default() -> Self {
Self {
#[cfg(feature = "seed")]
seed: 0,
delay: Duration::ZERO,
loss_rate: 0.0,
corrupt_rate: 0.0,
duplicate_rate: 0.0,
reorder_rate: 0.0,
gap: 0,
loss_ge: None,
}
}
}
pub struct BadNet {
left_addr: Ipv4Addr,
right_addr: Ipv4Addr,
class_minor: u32,
filter_prio_fwd: u32,
filter_prio_rev: u32,
config: BadNetConfig,
}
#[doc(hidden)] pub struct NoDelay;
#[doc(hidden)] pub struct WithDelay;
#[doc(hidden)] pub struct NoReorder;
#[doc(hidden)] pub struct WithReorder;
pub struct BadNetBuilder<D = NoDelay, R = NoReorder> {
#[cfg(feature = "seed")]
seed: u64,
delay: Duration,
loss_rate: f64,
corrupt_rate: f64,
duplicate_rate: f64,
reorder_rate: f64,
gap: u32,
ge_model: Option<GilbertElliot>,
_state: PhantomData<(D, R)>,
}
impl Default for BadNetBuilder<NoDelay, NoReorder> {
fn default() -> Self {
Self {
#[cfg(feature = "seed")]
seed: 0,
delay: Duration::ZERO,
loss_rate: 0.0,
corrupt_rate: 0.0,
duplicate_rate: 0.0,
reorder_rate: 0.0,
gap: 0,
ge_model: None,
_state: PhantomData,
}
}
}
impl BadNet {
pub fn builder() -> BadNetBuilder {
BadNetBuilder::default()
}
pub fn left_addr(&self) -> Ipv4Addr { self.left_addr }
pub fn right_addr(&self) -> Ipv4Addr { self.right_addr }
pub fn config(&self) -> &BadNetConfig { &self.config }
pub fn reconfigure(&mut self, config: BadNetConfig) -> io::Result<()> {
netem_tc("change", self.class_minor, &config)?;
self.config = config;
Ok(())
}
}
impl<D, R> BadNetBuilder<D, R> {
fn restate<D2, R2>(self) -> BadNetBuilder<D2, R2> {
BadNetBuilder {
#[cfg(feature = "seed")]
seed: self.seed,
delay: self.delay,
loss_rate: self.loss_rate,
corrupt_rate: self.corrupt_rate,
duplicate_rate: self.duplicate_rate,
reorder_rate: self.reorder_rate,
gap: self.gap,
ge_model: self.ge_model,
_state: PhantomData,
}
}
#[cfg(feature = "seed")]
pub fn seed(mut self, seed: u64) -> Self {
self.seed = seed;
self
}
pub fn loss_ge(mut self, p: f64, r: f64, h: f64, k: f64) -> Self {
assert!((0.0..=1.0).contains(&p), "GilbertElliot p must be in [0.0, 1.0]");
assert!((0.0..=1.0).contains(&r), "GilbertElliot r must be in [0.0, 1.0]");
assert!((0.0..=1.0).contains(&h), "GilbertElliot h must be in [0.0, 1.0]");
assert!((0.0..=1.0).contains(&k), "GilbertElliot k must be in [0.0, 1.0]");
self.ge_model = Some(GilbertElliot { p, r, h, k });
self
}
pub fn loss(mut self, rate: f64) -> Self {
assert!((0.0..=1.0).contains(&rate), "loss rate must be in [0.0, 1.0]");
self.loss_rate = rate;
self
}
pub fn corrupt(mut self, rate: f64) -> Self {
assert!((0.0..=1.0).contains(&rate), "corrupt rate must be in [0.0, 1.0]");
self.corrupt_rate = rate;
self
}
pub fn delay(mut self, d: Duration) -> BadNetBuilder<WithDelay, R> {
self.delay = d;
self.restate()
}
pub fn duplicate(mut self, rate: f64) -> Self {
assert!((0.0..=1.0).contains(&rate), "duplicate rate must be in [0.0, 1.0]");
self.duplicate_rate = rate;
self
}
pub fn build(self) -> io::Result<BadNet> {
let id = SUBNET_COUNTER.fetch_add(1, Ordering::SeqCst);
let hi = (id >> 8) as u8;
let lo = (id & 0xFF) as u8;
let left_addr = Ipv4Addr::new(10, hi, lo, 1);
let right_addr = Ipv4Addr::new(10, hi, lo, 2);
let config = BadNetConfig {
#[cfg(feature = "seed")]
seed: self.seed,
delay: self.delay,
loss_rate: self.loss_rate,
corrupt_rate: self.corrupt_rate,
duplicate_rate: self.duplicate_rate,
reorder_rate: self.reorder_rate,
gap: self.gap,
loss_ge: self.ge_model,
};
ip(&["addr", "add", &format!("{left_addr}/32"), "dev", "lo"])?;
ip(&["addr", "add", &format!("{right_addr}/32"), "dev", "lo"])
.map_err(|e| { let _ = ip_best_effort_del(left_addr); e })?;
let (class_minor, filter_prio_fwd, filter_prio_rev) =
match setup_tc(left_addr, right_addr, &config) {
Ok(v) => v,
Err(e) => {
let _ = ip_best_effort_del(left_addr);
let _ = ip_best_effort_del(right_addr);
return Err(e);
}
};
Ok(BadNet { left_addr, right_addr, class_minor, filter_prio_fwd, filter_prio_rev, config })
}
}
impl<R> BadNetBuilder<WithDelay, R> {
pub fn reorder(mut self, rate: f64) -> BadNetBuilder<WithDelay, WithReorder> {
assert!((0.0..=1.0).contains(&rate), "reorder rate must be in [0.0, 1.0]");
self.reorder_rate = rate;
self.restate()
}
}
impl BadNetBuilder<WithDelay, WithReorder> {
pub fn gap(mut self, n: u32) -> Self {
assert!(n >= 1, "gap must be >= 1");
self.gap = n;
self
}
}
fn netem_tc(subcommand: &str, class_minor: u32, cfg: &BadNetConfig) -> io::Result<()> {
#[cfg(feature = "seed")]
let netem_seed = (cfg.seed & 0xFFFF_FFFF) as u32;
let class_parent = format!("1:{class_minor}");
let class_handle = format!("{}:", class_minor * 100);
let delay_us = format!("{}us", cfg.delay.as_micros());
let reorder_pct = format!("{:.4}%", cfg.reorder_rate * 100.0);
let loss_pct = format!("{:.4}%", cfg.loss_rate * 100.0);
let corrupt_pct = format!("{:.4}%", cfg.corrupt_rate * 100.0);
let duplicate_pct = format!("{:.4}%", cfg.duplicate_rate * 100.0);
let gap_str = cfg.gap.to_string();
#[cfg(feature = "seed")]
let seed_str = netem_seed.to_string();
let mut netem_args: Vec<&str> = vec![
"qdisc", subcommand, "dev", "lo",
"parent", &class_parent,
"handle", &class_handle,
"netem",
"delay", &delay_us,
"reorder", &reorder_pct,
];
if cfg.gap > 0 {
netem_args.extend_from_slice(&["gap", &gap_str]);
}
let ge_p: String;
let ge_r: String;
let ge_h: String;
let ge_k: String;
if let Some(ref ge) = cfg.loss_ge {
ge_p = format!("{:.4}%", ge.p * 100.0);
ge_r = format!("{:.4}%", ge.r * 100.0);
ge_h = format!("{:.4}%", (1.0 - ge.h) * 100.0);
ge_k = format!("{:.4}%", (1.0 - ge.k) * 100.0);
netem_args.extend_from_slice(&["loss", "gemodel", &ge_p, &ge_r, &ge_h, &ge_k]);
} else {
netem_args.extend_from_slice(&["loss", &loss_pct]);
}
netem_args.extend_from_slice(&[
"corrupt", &corrupt_pct,
"duplicate", &duplicate_pct,
]);
#[cfg(feature = "seed")]
netem_args.extend_from_slice(&["seed", &seed_str]);
tc(&netem_args)
}
fn setup_tc(
left: Ipv4Addr,
right: Ipv4Addr,
cfg: &BadNetConfig,
) -> io::Result<(u32, u32, u32)> {
let mut state = LO_TC.lock().unwrap();
if state.instance_count == 0 {
tc(&["qdisc", "add", "dev", "lo", "root", "handle", "1:", "htb", "default", "1"])?;
tc(&["class", "add", "dev", "lo", "parent", "1:", "classid", "1:1",
"htb", "rate", "1tbit"])?;
}
let class_minor = state.alloc_minor();
state.instance_count += 1;
drop(state);
let filter_prio_fwd = (class_minor - 2) * 2 + 1;
let filter_prio_rev = filter_prio_fwd + 1;
tc(&["class", "add", "dev", "lo", "parent", "1:", "classid", &format!("1:{class_minor}"),
"htb", "rate", "1tbit"])?;
netem_tc("add", class_minor, cfg)?;
add_filter("lo", "1:", filter_prio_fwd, &left, &right, class_minor)?;
add_filter("lo", "1:", filter_prio_rev, &right, &left, class_minor)?;
Ok((class_minor, filter_prio_fwd, filter_prio_rev))
}
fn add_filter(dev: &str, parent: &str, prio: u32, src: &Ipv4Addr, dst: &Ipv4Addr, minor: u32) -> io::Result<()> {
tc(&[
"filter", "add", "dev", dev,
"parent", parent,
"prio", &prio.to_string(),
"protocol", "ip", "u32",
"match", "ip", "src", &format!("{src}/32"),
"match", "ip", "dst", &format!("{dst}/32"),
"flowid", &format!("1:{minor}"),
])
}
impl Drop for BadNet {
fn drop(&mut self) {
let _ = tc(&["filter", "del", "dev", "lo", "parent", "1:",
"prio", &self.filter_prio_fwd.to_string()]);
let _ = tc(&["filter", "del", "dev", "lo", "parent", "1:",
"prio", &self.filter_prio_rev.to_string()]);
let _ = tc(&["qdisc", "del", "dev", "lo",
"parent", &format!("1:{}", self.class_minor)]);
let _ = tc(&["class", "del", "dev", "lo",
"classid", &format!("1:{}", self.class_minor)]);
let mut state = LO_TC.lock().unwrap();
state.instance_count -= 1;
if state.instance_count == 0 {
let _ = tc(&["qdisc", "del", "dev", "lo", "root"]);
}
drop(state);
let _ = ip_best_effort_del(self.left_addr);
let _ = ip_best_effort_del(self.right_addr);
}
}
fn ip_best_effort_del(addr: Ipv4Addr) -> io::Result<()> {
ip(&["addr", "del", &format!("{addr}/32"), "dev", "lo"])
}
fn tc(args: &[&str]) -> io::Result<()> { run("tc", args) }
fn ip(args: &[&str]) -> io::Result<()> { run("ip", args) }
fn run(prog: &str, args: &[&str]) -> io::Result<()> {
let out = std::process::Command::new(prog).args(args).output()?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
return Err(io::Error::new(
io::ErrorKind::Other,
format!("`{prog} {}` failed: {}", args.join(" "), stderr.trim()),
));
}
Ok(())
}