#[cfg(target_os = "linux")]
use std::process::Command;
use std::time::Duration;
#[derive(Default, Clone, Debug)]
pub struct NetemConfig {
pub loss: f64,
pub delay: Duration,
pub jitter: Duration,
pub duplicate: f64,
pub reorder: f64,
pub loss_correlation_pct: u32,
}
#[derive(Debug)]
pub enum TcError {
Unsupported,
NotRoot,
Spawn(std::io::Error),
NonZeroExit {
stderr: String,
code: Option<i32>,
},
}
impl core::fmt::Display for TcError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
TcError::Unsupported => write!(f, "tc-Backend nur auf Linux unterstuetzt"),
TcError::NotRoot => write!(f, "tc benoetigt root oder CAP_NET_ADMIN"),
TcError::Spawn(e) => write!(f, "konnte tc nicht starten: {e}"),
TcError::NonZeroExit { stderr, code } => {
write!(f, "tc fehlgeschlagen (exit {code:?}): {stderr}")
}
}
}
}
impl std::error::Error for TcError {}
#[cfg(target_os = "linux")]
fn require_root() -> Result<(), TcError> {
let euid = unsafe { libc_stub::geteuid() };
if euid != 0 {
Err(TcError::NotRoot)
} else {
Ok(())
}
}
#[cfg(target_os = "linux")]
pub fn apply(interface: &str, cfg: &NetemConfig) -> Result<(), TcError> {
require_root()?;
let _ = Command::new("tc")
.args(["qdisc", "del", "dev", interface, "root"])
.output();
let mut args: Vec<String> = vec![
"qdisc".into(),
"add".into(),
"dev".into(),
interface.into(),
"root".into(),
"netem".into(),
];
if cfg.delay > Duration::ZERO {
args.push("delay".into());
args.push(format_ms(cfg.delay));
if cfg.jitter > Duration::ZERO {
args.push(format_ms(cfg.jitter));
}
}
if cfg.loss > 0.0 {
args.push("loss".into());
args.push(format!("{:.2}%", cfg.loss * 100.0));
if cfg.loss_correlation_pct > 0 {
args.push(format!("{}%", cfg.loss_correlation_pct));
}
}
if cfg.duplicate > 0.0 {
args.push("duplicate".into());
args.push(format!("{:.2}%", cfg.duplicate * 100.0));
}
if cfg.reorder > 0.0 {
args.push("reorder".into());
args.push(format!("{:.2}%", cfg.reorder * 100.0));
}
let out = Command::new("tc")
.args(&args)
.output()
.map_err(TcError::Spawn)?;
if !out.status.success() {
return Err(TcError::NonZeroExit {
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
code: out.status.code(),
});
}
Ok(())
}
#[cfg(not(target_os = "linux"))]
pub fn apply(_interface: &str, _cfg: &NetemConfig) -> Result<(), TcError> {
Err(TcError::Unsupported)
}
#[cfg(target_os = "linux")]
pub fn clear(interface: &str) -> Result<(), TcError> {
require_root()?;
let out = Command::new("tc")
.args(["qdisc", "del", "dev", interface, "root"])
.output()
.map_err(TcError::Spawn)?;
if !out.status.success() {
return Err(TcError::NonZeroExit {
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
code: out.status.code(),
});
}
Ok(())
}
#[cfg(not(target_os = "linux"))]
pub fn clear(_interface: &str) -> Result<(), TcError> {
Err(TcError::Unsupported)
}
#[cfg(target_os = "linux")]
fn format_ms(d: Duration) -> String {
let ms = d.as_secs_f64() * 1_000.0;
format!("{ms:.2}ms")
}
#[cfg(target_os = "linux")]
mod libc_stub {
unsafe extern "C" {
pub fn geteuid() -> u32;
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
#[cfg(not(target_os = "linux"))]
#[test]
fn non_linux_returns_unsupported() {
let cfg = NetemConfig::default();
assert!(matches!(apply("eth0", &cfg), Err(TcError::Unsupported)));
assert!(matches!(clear("eth0"), Err(TcError::Unsupported)));
}
#[cfg(target_os = "linux")]
#[test]
fn non_root_returns_not_root() {
let euid = unsafe { libc_stub::geteuid() };
if euid == 0 {
return;
}
let cfg = NetemConfig::default();
let r = apply("eth0", &cfg);
assert!(matches!(r, Err(TcError::NotRoot)), "got {r:?}");
}
}