diff --git a/Cargo.lock b/Cargo.lock index c2236568c..edcea6e87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2328,7 +2328,6 @@ dependencies = [ "derive_more", "hex-literal", "indexmap", - "ipnetwork", "itertools 0.14.0", "mockall", "nix", @@ -2340,13 +2339,12 @@ dependencies = [ "test-case", "thiserror 2.0.17", "tokio", - "tokio-util", "toml", "tracing", "tracing-subscriber", "trippy-packet", "trippy-privilege", - "tun", + "trippy-sim", "widestring", "windows-sys 0.52.0", ] @@ -2386,6 +2384,25 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "trippy-sim" +version = "0.14.0-dev" +dependencies = [ + "anyhow", + "clap", + "ipnetwork", + "serde", + "tokio", + "tokio-util", + "toml", + "tracing", + "tracing-subscriber", + "trippy-core", + "trippy-packet", + "trippy-privilege", + "tun", +] + [[package]] name = "trippy-tui" version = "0.14.0-dev" diff --git a/Cargo.toml b/Cargo.toml index c014307b2..bfb842b69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "crates/trippy-packet", "crates/trippy-privilege", "crates/trippy-dns", + "crates/trippy-sim", "examples/*", ] @@ -29,6 +30,7 @@ trippy-core = { version = "0.14.0-dev", path = "crates/trippy-core" } trippy-privilege = { version = "0.14.0-dev", path = "crates/trippy-privilege" } trippy-dns = { version = "0.14.0-dev", path = "crates/trippy-dns" } trippy-packet = { version = "0.14.0-dev", path = "crates/trippy-packet" } +trippy-sim = { version = "0.14.0-dev", path = "crates/trippy-sim" } anyhow = "1.0.91" arrayvec = { version = "0.7.6", default-features = false } bitflags = "2.10.0" @@ -99,6 +101,7 @@ cast_precision_loss = "allow" bool_assert_comparison = "allow" missing_const_for_fn = "allow" struct_field_names = "allow" +missing_panics_doc = "allow" cognitive_complexity = "allow" [profile.release] diff --git a/Dockerfile b/Dockerfile index 6e8a2ab23..b42744b83 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,12 +9,14 @@ RUN mkdir -p /app/crates/trippy-core/src RUN mkdir -p /app/crates/trippy-dns/src RUN mkdir -p /app/crates/trippy-packet/src RUN mkdir -p /app/crates/trippy-privilege/src +RUN mkdir -p /app/crates/trippy-sim/src COPY crates/trippy/Cargo.toml /app/crates/trippy/Cargo.toml COPY crates/trippy-tui/Cargo.toml /app/crates/trippy-tui/Cargo.toml COPY crates/trippy-core/Cargo.toml /app/crates/trippy-core/Cargo.toml COPY crates/trippy-dns/Cargo.toml /app/crates/trippy-dns/Cargo.toml COPY crates/trippy-packet/Cargo.toml /app/crates/trippy-packet/Cargo.toml COPY crates/trippy-privilege/Cargo.toml /app/crates/trippy-privilege/Cargo.toml +COPY crates/trippy-sim/Cargo.toml /app/crates/trippy-sim/Cargo.toml COPY examples/ /app/examples/ # dummy build to cache dependencies @@ -24,6 +26,7 @@ RUN touch /app/crates/trippy-core/src/lib.rs RUN touch /app/crates/trippy-dns/src/lib.rs RUN touch /app/crates/trippy-packet/src/lib.rs RUN touch /app/crates/trippy-privilege/src/lib.rs +RUN touch /app/crates/trippy-sim/src/lib.rs RUN cargo build --release --target=x86_64-unknown-linux-musl --package trippy # copy the actual application code and build diff --git a/crates/trippy-core/Cargo.toml b/crates/trippy-core/Cargo.toml index 0f60b3260..d29f24210 100644 --- a/crates/trippy-core/Cargo.toml +++ b/crates/trippy-core/Cargo.toml @@ -34,22 +34,17 @@ widestring.workspace = true windows-sys = { workspace = true, features = ["Win32_Foundation", "Win32_Networking_WinSock", "Win32_System_IO", "Win32_NetworkManagement_IpHelper", "Win32_NetworkManagement_Ndis", "Win32_System_IO", "Win32_System_Threading", "Win32_Security"] } [dev-dependencies] +trippy-sim.workspace = true anyhow.workspace = true hex-literal.workspace = true -ipnetwork.workspace = true mockall.workspace = true rand.workspace = true serde = { workspace = true, default-features = false, features = ["derive"] } test-case.workspace = true -tokio-util.workspace = true tokio = { workspace = true, features = ["full"] } toml = { workspace = true, default-features = false, features = ["parse"] } tracing-subscriber = { workspace = true, default-features = false, features = ["env-filter", "fmt"] } -# see https://github.com/meh/rust-tun/pull/74 -[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dev-dependencies] -tun = { workspace = true, features = ["async"] } - [features] # Enable simulation integration tests sim-tests = [] diff --git a/crates/trippy-core/tests/sim/tests.rs b/crates/trippy-core/tests/sim.rs similarity index 78% rename from crates/trippy-core/tests/sim/tests.rs rename to crates/trippy-core/tests/sim.rs index 7d6562645..26dfcdc72 100644 --- a/crates/trippy-core/tests/sim/tests.rs +++ b/crates/trippy-core/tests/sim.rs @@ -1,12 +1,15 @@ -use crate::simulation::Simulation; -use crate::tun_device::tun; -use crate::{network, tracer}; +#![cfg(all( + feature = "sim-tests", + any(target_os = "linux", target_os = "macos", target_os = "windows") +))] +#![allow(clippy::needless_pass_by_value, clippy::redundant_clone)] + use std::sync::{Arc, Mutex, OnceLock}; use test_case::test_case; use tokio::runtime::Runtime; -use tokio_util::sync::CancellationToken; use tracing::{error, info, warn}; use tracing_subscriber::fmt::format::FmtSpan; +use trippy_sim::Simulation; /// The maximum number of attempts for each test. const MAX_ATTEMPTS: usize = 5; @@ -17,7 +20,9 @@ pub fn runtime() -> &'static Arc> { RUNTIME.get_or_init(|| { tracing_subscriber::fmt() .with_span_events(FmtSpan::NONE) - .with_env_filter("trippy=off,sim=debug") + .with_env_filter( + "sim=debug,trippy_sim::tracer=debug,trippy_sim::network=debug,trippy_core=info", + ) .init(); let runtime = tokio::runtime::Builder::new_multi_thread() @@ -30,7 +35,7 @@ pub fn runtime() -> &'static Arc> { macro_rules! sim { ($path:expr) => {{ - let data = include_str!(concat!("../resources/simulation/", $path)); + let data = include_str!(concat!("resources/simulation/", $path)); toml::from_str(data)? }}; } @@ -63,7 +68,6 @@ fn test_simulation_macos(simulation: Simulation) -> anyhow::Result<()> { fn run_simulation_with_retry(simulation: Simulation) -> anyhow::Result<()> { let runtime = runtime().lock().unwrap(); - let simulation = Arc::new(simulation); let name = simulation.name.clone(); if !trippy_privilege::Privilege::discover()?.has_privileges() { // Skip if the current test as the user cannot create a tun device. @@ -72,7 +76,7 @@ fn run_simulation_with_retry(simulation: Simulation) -> anyhow::Result<()> { } for attempt in 1..=MAX_ATTEMPTS { info!("start simulating {} [attempt #{}]", name, attempt); - if let Err(err) = runtime.block_on(run_simulation(simulation.clone())) { + if let Err(err) = runtime.block_on(trippy_sim::simulate(simulation.clone())) { error!("failed simulating {} {} [attempt #{}]", name, err, attempt); } else { info!("end simulating {} [attempt #{}]", name, attempt); @@ -81,11 +85,3 @@ fn run_simulation_with_retry(simulation: Simulation) -> anyhow::Result<()> { } anyhow::bail!("failed simulating {name} after {MAX_ATTEMPTS} attempts") } - -async fn run_simulation(sim: Arc) -> anyhow::Result<()> { - let tun = tun(); - let token = CancellationToken::new(); - let handle = tokio::spawn(network::run(tun.clone(), sim.clone(), token.clone())); - tokio::task::spawn_blocking(move || tracer::Tracer::new(sim, token).trace()).await??; - handle.await? -} diff --git a/crates/trippy-core/tests/sim/main.rs b/crates/trippy-core/tests/sim/main.rs deleted file mode 100644 index 77134fdb2..000000000 --- a/crates/trippy-core/tests/sim/main.rs +++ /dev/null @@ -1,9 +0,0 @@ -#![cfg(all( - feature = "sim-tests", - any(target_os = "linux", target_os = "macos", target_os = "windows") -))] -mod network; -mod simulation; -mod tests; -mod tracer; -mod tun_device; diff --git a/crates/trippy-sim/Cargo.toml b/crates/trippy-sim/Cargo.toml new file mode 100644 index 000000000..c92a30274 --- /dev/null +++ b/crates/trippy-sim/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "trippy-sim" +description = "A network simulator for Trippy" +version.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +readme.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true +keywords.workspace = true +categories.workspace = true + +[dependencies] +trippy-core.workspace = true +trippy-packet.workspace = true +trippy-privilege.workspace = true +anyhow.workspace = true +clap.workspace = true +ipnetwork.workspace = true +serde = { workspace = true, default-features = false, features = ["derive"] } +tokio = { workspace = true, features = ["full"] } +tokio-util.workspace = true +toml = { workspace = true, default-features = false, features = ["parse"] } +tracing-subscriber = { workspace = true, default-features = false, features = ["env-filter", "fmt"] } +tracing.workspace = true + +# see https://github.com/meh/rust-tun/pull/74 +[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies] +tun = { workspace = true, features = ["async"] } + +[lints] +workspace = true diff --git a/crates/trippy-sim/src/app.rs b/crates/trippy-sim/src/app.rs new file mode 100644 index 000000000..b3c3fa902 --- /dev/null +++ b/crates/trippy-sim/src/app.rs @@ -0,0 +1,31 @@ +#![cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] + +use anyhow::Context; +use clap::Parser; +use tracing_subscriber::fmt::format::FmtSpan; +use trippy_privilege::Privilege; +use trippy_sim::{Simulation, simulate}; + +/// Trace a route to a host and record statistics +#[allow(clippy::doc_markdown)] +#[derive(Parser, Debug)] +#[command(name = "trip", author, version, about, long_about = None, arg_required_else_help(true))] +pub struct Args { + /// A simulation file to run. + pub simulation: String, +} + +pub async fn run() -> anyhow::Result<()> { + let args = Args::parse(); + tracing_subscriber::fmt() + .with_span_events(FmtSpan::NONE) + .with_env_filter("debug") + .init(); + if !Privilege::discover()?.has_privileges() { + return Err(anyhow::anyhow!("Privileges required to run this command")); + } + let simulation_file = + std::fs::read_to_string(&args.simulation).context(args.simulation.clone())?; + let sim: Simulation = toml::from_str(&simulation_file)?; + simulate(sim).await +} diff --git a/crates/trippy-sim/src/lib.rs b/crates/trippy-sim/src/lib.rs new file mode 100644 index 000000000..c526a6f6d --- /dev/null +++ b/crates/trippy-sim/src/lib.rs @@ -0,0 +1,21 @@ +#![cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] + +mod network; +mod simulation; +mod tracer; +mod tun_device; + +use crate::tun_device::tun; +pub use simulation::Simulation; +use std::sync::Arc; +use tokio_util::sync::CancellationToken; + +/// Run a simulation. +pub async fn simulate(simulation: Simulation) -> anyhow::Result<()> { + let sim = Arc::new(simulation); + let tun = tun(); + let token = CancellationToken::new(); + let handle = tokio::spawn(network::run(tun.clone(), sim.clone(), token.clone())); + tokio::task::spawn_blocking(move || tracer::Tracer::new(sim.clone(), token).trace()).await??; + handle.await? +} diff --git a/crates/trippy-sim/src/main.rs b/crates/trippy-sim/src/main.rs new file mode 100644 index 000000000..96668cfc9 --- /dev/null +++ b/crates/trippy-sim/src/main.rs @@ -0,0 +1,8 @@ +mod app; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] + app::run().await?; + Ok(()) +} diff --git a/crates/trippy-core/tests/sim/network.rs b/crates/trippy-sim/src/network.rs similarity index 99% rename from crates/trippy-core/tests/sim/network.rs rename to crates/trippy-sim/src/network.rs index 547cf01cd..bfe97e2d1 100644 --- a/crates/trippy-core/tests/sim/network.rs +++ b/crates/trippy-sim/src/network.rs @@ -98,8 +98,8 @@ pub async fn run( } } - // if the ttl is greater than the largest ttl in our sim we will reply as the last node in - // the sim + // if the ttl is greater than the largest ttl in our simulation we will reply as the last node in + // the simulation let index = std::cmp::min(usize::from(ipv4.get_ttl()) - 1, sim.hops.len() - 1); let (reply_addr, reply_delay_ms) = match sim.hops[index].resp { Response::NoResponse => { diff --git a/crates/trippy-core/tests/sim/simulation.rs b/crates/trippy-sim/src/simulation.rs similarity index 94% rename from crates/trippy-core/tests/sim/simulation.rs rename to crates/trippy-sim/src/simulation.rs index 0edf913b4..d9c7970a0 100644 --- a/crates/trippy-core/tests/sim/simulation.rs +++ b/crates/trippy-sim/src/simulation.rs @@ -3,7 +3,7 @@ use std::net::IpAddr; use trippy_core::Port; /// A simulated trace. -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct Simulation { pub name: String, pub rounds: Option, @@ -28,7 +28,8 @@ pub struct Simulation { } impl Simulation { - pub fn latest_ttl(&self) -> u8 { + #[must_use] + pub(crate) fn latest_ttl(&self) -> u8 { if self.hops.is_empty() { 0 } else { @@ -38,7 +39,7 @@ impl Simulation { } /// A simulated hop. -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct Hop { /// The simulated time-to-live (TTL). pub ttl: u8, @@ -47,7 +48,7 @@ pub struct Hop { } /// A simulated probe response. -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Deserialize)] #[serde(tag = "tag")] pub enum Response { /// Simulate a hop which does not response to probes. @@ -57,7 +58,7 @@ pub enum Response { } /// A simulated probe response with a single addr and fixed ttl. -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct SingleHost { /// The simulated host responding to the probe. pub addr: IpAddr, diff --git a/crates/trippy-core/tests/sim/tracer.rs b/crates/trippy-sim/src/tracer.rs similarity index 97% rename from crates/trippy-core/tests/sim/tracer.rs rename to crates/trippy-sim/src/tracer.rs index 2cee44b6b..4235b2203 100644 --- a/crates/trippy-core/tests/sim/tracer.rs +++ b/crates/trippy-sim/src/tracer.rs @@ -11,7 +11,7 @@ use trippy_core::{ }; // The length of time to wait after the completion of the tracing before -// cancelling the network simulator. This is needed to ensure that all +// canceling the network simulator. This is needed to ensure that all // in-flight packets for the current test are sent or received prior to // ending the round so that they are not incorrectly used in a subsequent // test. @@ -90,7 +90,7 @@ impl Tracer { .map_err(anyhow::Error::from); thread::sleep(CLEANUP_DELAY); self.token.cancel(); - // ensure both the tracer and the validator were successful. + // ensure both the tracer and the validation were successful. tracer_res.and(result.replace(Ok(()))) } diff --git a/crates/trippy-core/tests/sim/tun_device.rs b/crates/trippy-sim/src/tun_device.rs similarity index 100% rename from crates/trippy-core/tests/sim/tun_device.rs rename to crates/trippy-sim/src/tun_device.rs