From 39bbe0ce2b70f215a183d54a9a1ba73c110eefb5 Mon Sep 17 00:00:00 2001 From: FujiApple Date: Wed, 12 Jun 2024 23:13:17 +0800 Subject: [PATCH] refactor: define the Trippy public api (#1192) --- .github/workflows/ci.yml | 2 + Cargo.lock | 23 +- Cargo.toml | 6 +- crates/trippy-core/Cargo.toml | 5 +- crates/trippy-core/src/builder.rs | 501 +++-- crates/trippy-core/src/config.rs | 483 +---- crates/trippy-core/src/constants.rs | 3 + crates/trippy-core/src/error.rs | 4 + .../src/backend => trippy-core/src}/flows.rs | 0 crates/trippy-core/src/lib.rs | 39 +- crates/trippy-core/src/net/channel.rs | 6 +- crates/trippy-core/src/net/platform/unix.rs | 2 +- crates/trippy-core/src/net/socket.rs | 3 +- .../trace.rs => trippy-core/src/state.rs} | 126 +- crates/trippy-core/src/strategy.rs | 1231 ++++++++++++ crates/trippy-core/src/tracer.rs | 1701 ++++++----------- .../backend/ipv4_3probes_3hops_completed.yaml | 0 .../ipv4_3probes_3hops_mixed_multi.yaml | 0 .../backend/ipv4_4probes_0latency.yaml | 0 .../backend/ipv4_4probes_all_status.yaml | 0 .../resources/simulation/ipv4_icmp_wrap.yaml | 98 +- crates/trippy-core/tests/sim/tracer.rs | 32 +- crates/trippy/Cargo.toml | 8 +- crates/trippy/src/app.rs | 220 +-- crates/trippy/src/backend.rs | 63 - crates/trippy/src/config.rs | 14 +- crates/trippy/src/config/constants.rs | 6 - crates/trippy/src/frontend/config.rs | 7 + crates/trippy/src/frontend/render/body.rs | 8 +- crates/trippy/src/frontend/render/header.rs | 47 +- crates/trippy/src/frontend/render/settings.rs | 44 +- crates/trippy/src/frontend/render/table.rs | 2 +- crates/trippy/src/frontend/render/world.rs | 4 +- crates/trippy/src/frontend/tui_app.rs | 21 +- crates/trippy/src/lib.rs | 18 +- crates/trippy/src/main.rs | 1 - crates/trippy/src/report.rs | 15 +- crates/trippy/src/report/csv.rs | 14 +- crates/trippy/src/report/dot.rs | 4 +- crates/trippy/src/report/flows.rs | 2 +- crates/trippy/src/report/json.rs | 6 +- crates/trippy/src/report/stream.rs | 14 +- crates/trippy/src/report/table.rs | 4 +- crates/trippy/src/report/types.rs | 5 +- examples/hello-world/Cargo.toml | 10 + examples/hello-world/src/main.rs | 10 + examples/traceroute/Cargo.toml | 12 + examples/traceroute/src/main.rs | 119 ++ 48 files changed, 2803 insertions(+), 2140 deletions(-) rename crates/{trippy/src/backend => trippy-core/src}/flows.rs (100%) rename crates/{trippy/src/backend/trace.rs => trippy-core/src/state.rs} (90%) create mode 100644 crates/trippy-core/src/strategy.rs rename crates/{trippy => trippy-core}/tests/resources/backend/ipv4_3probes_3hops_completed.yaml (100%) rename crates/{trippy => trippy-core}/tests/resources/backend/ipv4_3probes_3hops_mixed_multi.yaml (100%) rename crates/{trippy => trippy-core}/tests/resources/backend/ipv4_4probes_0latency.yaml (100%) rename crates/{trippy => trippy-core}/tests/resources/backend/ipv4_4probes_all_status.yaml (100%) delete mode 100644 crates/trippy/src/backend.rs create mode 100644 examples/hello-world/Cargo.toml create mode 100644 examples/hello-world/src/main.rs create mode 100644 examples/traceroute/Cargo.toml create mode 100644 examples/traceroute/src/main.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 682056a0f..3abfb4083 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -200,6 +200,8 @@ jobs: run: cargo msrv verify --output-format json --manifest-path crates/trippy/Cargo.toml -- cargo check - name: check msrv for trippy-core run: cargo msrv verify --output-format json --manifest-path crates/trippy-core/Cargo.toml -- cargo check + - name: check msrv for trippy-packet + run: cargo msrv verify --output-format json --manifest-path crates/trippy-packet/Cargo.toml -- cargo check - name: check msrv for trippy-dns run: cargo msrv verify --output-format json --manifest-path crates/trippy-dns/Cargo.toml -- cargo check - name: check msrv for trippy-privilege diff --git a/Cargo.lock b/Cargo.lock index f7d0fcf22..7eb6a779b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -746,6 +746,14 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hello-world" +version = "0.1.0" +dependencies = [ + "anyhow", + "trippy", +] + [[package]] name = "hermit-abi" version = "0.3.9" @@ -2071,6 +2079,16 @@ dependencies = [ "winnow", ] +[[package]] +name = "traceroute" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "itertools 0.13.0", + "trippy", +] + [[package]] name = "tracing" version = "0.1.40" @@ -2172,11 +2190,9 @@ dependencies = [ "encoding_rs_io", "etcetera", "humantime", - "indexmap 2.2.6", "insta", "itertools 0.13.0", "maxminddb", - "parking_lot", "petgraph", "pretty_assertions", "ratatui", @@ -2205,10 +2221,12 @@ dependencies = [ "bitflags", "derive_more", "hex-literal", + "indexmap 2.2.6", "ipnetwork", "itertools 0.13.0", "mockall", "nix", + "parking_lot", "paste", "rand", "serde", @@ -2221,6 +2239,7 @@ dependencies = [ "tracing", "tracing-subscriber", "trippy-packet", + "trippy-privilege", "tun2", "widestring", "windows-sys 0.52.0", diff --git a/Cargo.toml b/Cargo.toml index ffbf44d3b..cdd78454d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["crates/trippy", "crates/trippy-core", "crates/trippy-packet", "crates/trippy-privilege", "crates/trippy-dns"] +members = ["crates/trippy", "crates/trippy-core", "crates/trippy-packet", "crates/trippy-privilege", "crates/trippy-dns", "examples/*"] [workspace.package] version = "0.11.0-dev" @@ -14,6 +14,10 @@ edition = "2021" rust-version = "1.75" [workspace.dependencies] +trippy-core = { version = "0.11.0-dev", path = "crates/trippy-core" } +trippy-privilege = { version = "0.11.0-dev", path = "crates/trippy-privilege" } +trippy-dns = { version = "0.11.0-dev", path = "crates/trippy-dns" } +trippy-packet = { version = "0.11.0-dev", path = "crates/trippy-packet" } thiserror = "1.0.60" anyhow = "1.0.83" itertools = "0.13.0" diff --git a/crates/trippy-core/Cargo.toml b/crates/trippy-core/Cargo.toml index 1d920a52a..0d9e7ac75 100644 --- a/crates/trippy-core/Cargo.toml +++ b/crates/trippy-core/Cargo.toml @@ -14,11 +14,14 @@ keywords = ["traceroute", "ping", "icmp"] categories = ["network-programming"] [dependencies] -trippy-packet = { version = "0.11.0-dev", path = "../trippy-packet" } +trippy-packet.workspace = true +trippy-privilege.workspace = true derive_more.workspace = true thiserror.workspace = true tracing.workspace = true itertools.workspace = true +parking_lot.workspace = true +indexmap = { version = "2.2.6", default-features = false, features = [ "std" ] } arrayvec = { version = "0.7.4", default-features = false } socket2 = { version = "0.5.7", features = [ "all" ] } bitflags = "2.5.0" diff --git a/crates/trippy-core/src/builder.rs b/crates/trippy-core/src/builder.rs index 1e3a4e88a..0c0539f78 100644 --- a/crates/trippy-core/src/builder.rs +++ b/crates/trippy-core/src/builder.rs @@ -1,79 +1,134 @@ +use crate::config::{ChannelConfig, StateConfig, StrategyConfig}; +use crate::constants::MAX_INITIAL_SEQUENCE; use crate::error::TraceResult; -use crate::net::PlatformImpl; use crate::{ - ChannelConfig, Config, IcmpExtensionParseMode, MaxInflight, MaxRounds, MultipathStrategy, - PacketSize, PayloadPattern, PortDirection, PrivilegeMode, Protocol, Sequence, SocketImpl, - SourceAddr, TimeToLive, TraceId, Tracer, TracerChannel, TracerRound, TypeOfService, + IcmpExtensionParseMode, MaxInflight, MaxRounds, MultipathStrategy, PacketSize, PayloadPattern, + PortDirection, PrivilegeMode, Protocol, Sequence, TimeToLive, TraceId, Tracer, TracerError, + TypeOfService, MAX_TTL, }; use std::net::IpAddr; +use std::num::NonZeroUsize; use std::time::Duration; -/// Build and run a tracer. +/// Build a tracer. /// /// This is a convenience builder to simplify the creation of execution of a /// tracer. /// -/// The builder exposes all configuration items from`trippy::tracing::Config` -/// and `trippy::tracing::TracerChannel`. -/// -/// # Examples: +/// # Examples /// /// ```no_run +/// # fn main() -> anyhow::Result<()> { /// use trippy_core::{Builder, MultipathStrategy, Port, PortDirection, PrivilegeMode, Protocol}; /// /// let addr = std::net::IpAddr::from([1, 2, 3, 4]); -/// Builder::new(addr, |round| println!("{:?}", round)) +/// let tracer = Builder::new(addr) /// .privilege_mode(PrivilegeMode::Unprivileged) /// .protocol(Protocol::Udp) /// .multipath_strategy(MultipathStrategy::Dublin) /// .port_direction(PortDirection::FixedBoth(Port(33000), Port(3500))) -/// .start() -/// .unwrap(); +/// .build()?; +/// # Ok(()) +/// # } /// ``` -pub struct Builder { - target_addr: IpAddr, - on_round_handler: F, - channel_config: ChannelConfig, - tracer_config: Config, - trace_identifier: Option, +/// +/// # See Also +/// +/// - [`Tracer`] - A traceroute implementation. +#[derive(Debug)] +pub struct Builder { interface: Option, + source_addr: Option, + target_addr: IpAddr, + privilege_mode: PrivilegeMode, + protocol: Protocol, + packet_size: PacketSize, + payload_pattern: PayloadPattern, + tos: TypeOfService, + icmp_extension_parse_mode: IcmpExtensionParseMode, + read_timeout: Duration, + tcp_connect_timeout: Duration, + trace_identifier: TraceId, + max_rounds: Option, + first_ttl: TimeToLive, + max_ttl: TimeToLive, + grace_duration: Duration, + max_inflight: MaxInflight, + initial_sequence: Sequence, + multipath_strategy: MultipathStrategy, + port_direction: PortDirection, + min_round_duration: Duration, + max_round_duration: Duration, + max_samples: usize, + max_flows: usize, + drop_privileges: bool, +} + +impl Default for Builder { + fn default() -> Self { + Self { + interface: None, + source_addr: None, + target_addr: ChannelConfig::default().target_addr, + privilege_mode: ChannelConfig::default().privilege_mode, + protocol: ChannelConfig::default().protocol, + packet_size: ChannelConfig::default().packet_size, + payload_pattern: ChannelConfig::default().payload_pattern, + tos: ChannelConfig::default().tos, + icmp_extension_parse_mode: ChannelConfig::default().icmp_extension_parse_mode, + read_timeout: ChannelConfig::default().read_timeout, + tcp_connect_timeout: ChannelConfig::default().tcp_connect_timeout, + trace_identifier: StrategyConfig::default().trace_identifier, + max_rounds: StrategyConfig::default().max_rounds, + first_ttl: StrategyConfig::default().first_ttl, + max_ttl: StrategyConfig::default().max_ttl, + grace_duration: StrategyConfig::default().grace_duration, + max_inflight: StrategyConfig::default().max_inflight, + initial_sequence: StrategyConfig::default().initial_sequence, + multipath_strategy: StrategyConfig::default().multipath_strategy, + port_direction: StrategyConfig::default().port_direction, + min_round_duration: StrategyConfig::default().min_round_duration, + max_round_duration: StrategyConfig::default().max_round_duration, + max_samples: StateConfig::default().max_samples, + max_flows: StateConfig::default().max_flows, + drop_privileges: false, + } + } } -impl)> Builder { +impl Builder { /// Build a tracer builder for a given target. - pub fn new(target_addr: IpAddr, on_round_handler: F) -> Self { + #[must_use] + pub fn new(target_addr: IpAddr) -> Self { Self { target_addr, - on_round_handler, - channel_config: ChannelConfig::default(), - tracer_config: Config::default(), - trace_identifier: None, - interface: None, + ..Default::default() } } - /// Set the trace identifier. + /// Set the source address. /// - /// If not set then 0 will be used as the trace identifier. + /// If not set then the source address will be discovered based on the + /// target address and the interface. #[must_use] - pub fn trace_identifier(self, trace_id: TraceId) -> Self { + pub fn source_addr(self, source_addr: Option) -> Self { Self { - trace_identifier: Some(trace_id), + source_addr, ..self } } /// Set the source interface. /// - /// If the source interface is provided it will be used to lookup the IPv4 + /// If the source interface is provided it will be used to look up the IPv4 /// or IPv6 source address. /// /// If not provided the source address will be determined by OS based on /// the target IPv4 or IPv6 address. #[must_use] - pub fn interface(self, interface: &str) -> Self { + pub fn interface>(self, interface: Option) -> Self { Self { - interface: Some(String::from(interface)), + interface: interface.map(Into::into), ..self } } @@ -81,15 +136,16 @@ impl)> Builder { /// Set the protocol. #[must_use] pub fn protocol(self, protocol: Protocol) -> Self { + Self { protocol, ..self } + } + + /// Set the trace identifier. + /// + /// If not set then 0 will be used as the trace identifier. + #[must_use] + pub fn trace_identifier(self, trace_id: u16) -> Self { Self { - channel_config: ChannelConfig { - protocol, - ..self.channel_config - }, - tracer_config: Config { - protocol, - ..self.tracer_config - }, + trace_identifier: TraceId(trace_id), ..self } } @@ -98,10 +154,7 @@ impl)> Builder { #[must_use] pub fn privilege_mode(self, privilege_mode: PrivilegeMode) -> Self { Self { - channel_config: ChannelConfig { - privilege_mode, - ..self.channel_config - }, + privilege_mode, ..self } } @@ -110,58 +163,46 @@ impl)> Builder { #[must_use] pub fn multipath_strategy(self, multipath_strategy: MultipathStrategy) -> Self { Self { - tracer_config: Config { - multipath_strategy, - ..self.tracer_config - }, + multipath_strategy, ..self } } /// Set the packet size. #[must_use] - pub fn packet_size(self, packet_size: PacketSize) -> Self { + pub fn packet_size(self, packet_size: u16) -> Self { Self { - channel_config: ChannelConfig { - packet_size, - ..self.channel_config - }, + packet_size: PacketSize(packet_size), ..self } } /// Set the payload pattern. #[must_use] - pub fn payload_pattern(self, payload_pattern: PayloadPattern) -> Self { + pub fn payload_pattern(self, payload_pattern: u8) -> Self { Self { - channel_config: ChannelConfig { - payload_pattern, - ..self.channel_config - }, + payload_pattern: PayloadPattern(payload_pattern), ..self } } /// Set the type of service. #[must_use] - pub fn tos(self, tos: TypeOfService) -> Self { + pub fn tos(self, tos: u8) -> Self { Self { - channel_config: ChannelConfig { - tos, - ..self.channel_config - }, + tos: TypeOfService(tos), ..self } } /// Set the ICMP extensions mode. #[must_use] - pub fn icmp_extension_mode(self, icmp_extension_mode: IcmpExtensionParseMode) -> Self { + pub fn icmp_extension_parse_mode( + self, + icmp_extension_parse_mode: IcmpExtensionParseMode, + ) -> Self { Self { - channel_config: ChannelConfig { - icmp_extension_mode, - ..self.channel_config - }, + icmp_extension_parse_mode, ..self } } @@ -170,10 +211,7 @@ impl)> Builder { #[must_use] pub fn read_timeout(self, read_timeout: Duration) -> Self { Self { - channel_config: ChannelConfig { - read_timeout, - ..self.channel_config - }, + read_timeout, ..self } } @@ -182,46 +220,35 @@ impl)> Builder { #[must_use] pub fn tcp_connect_timeout(self, tcp_connect_timeout: Duration) -> Self { Self { - channel_config: ChannelConfig { - tcp_connect_timeout, - ..self.channel_config - }, + tcp_connect_timeout, ..self } } /// Set the maximum number of rounds. #[must_use] - pub fn max_rounds(self, max_rounds: MaxRounds) -> Self { + pub fn max_rounds(self, max_rounds: Option) -> Self { Self { - tracer_config: Config { - max_rounds: Some(max_rounds), - ..self.tracer_config - }, + max_rounds: max_rounds + .and_then(|max_rounds| NonZeroUsize::new(max_rounds).map(MaxRounds)), ..self } } /// Set the first ttl. #[must_use] - pub fn first_ttl(self, first_ttl: TimeToLive) -> Self { + pub fn first_ttl(self, first_ttl: u8) -> Self { Self { - tracer_config: Config { - first_ttl, - ..self.tracer_config - }, + first_ttl: TimeToLive(first_ttl), ..self } } /// Set the maximum ttl. #[must_use] - pub fn max_ttl(self, max_ttl: TimeToLive) -> Self { + pub fn max_ttl(self, max_ttl: u8) -> Self { Self { - tracer_config: Config { - max_ttl, - ..self.tracer_config - }, + max_ttl: TimeToLive(max_ttl), ..self } } @@ -230,34 +257,25 @@ impl)> Builder { #[must_use] pub fn grace_duration(self, grace_duration: Duration) -> Self { Self { - tracer_config: Config { - grace_duration, - ..self.tracer_config - }, + grace_duration, ..self } } /// Set the max inflight. #[must_use] - pub fn max_inflight(self, max_inflight: MaxInflight) -> Self { + pub fn max_inflight(self, max_inflight: u8) -> Self { Self { - tracer_config: Config { - max_inflight, - ..self.tracer_config - }, + max_inflight: MaxInflight(max_inflight), ..self } } /// Set the initial sequence. #[must_use] - pub fn initial_sequence(self, initial_sequence: Sequence) -> Self { + pub fn initial_sequence(self, initial_sequence: u16) -> Self { Self { - tracer_config: Config { - initial_sequence, - ..self.tracer_config - }, + initial_sequence: Sequence(initial_sequence), ..self } } @@ -266,10 +284,7 @@ impl)> Builder { #[must_use] pub fn port_direction(self, port_direction: PortDirection) -> Self { Self { - tracer_config: Config { - port_direction, - ..self.tracer_config - }, + port_direction, ..self } } @@ -278,10 +293,7 @@ impl)> Builder { #[must_use] pub fn min_round_duration(self, min_round_duration: Duration) -> Self { Self { - tracer_config: Config { - min_round_duration, - ..self.tracer_config - }, + min_round_duration, ..self } } @@ -290,35 +302,248 @@ impl)> Builder { #[must_use] pub fn max_round_duration(self, max_round_duration: Duration) -> Self { Self { - tracer_config: Config { - max_round_duration, - ..self.tracer_config - }, + max_round_duration, + ..self + } + } + + /// Set the maximum number of samples to record. + #[must_use] + pub fn max_samples(self, max_samples: usize) -> Self { + Self { + max_samples, + ..self + } + } + + /// Set the maximum number of flows to record. + #[must_use] + pub fn max_flows(self, max_flows: usize) -> Self { + Self { max_flows, ..self } + } + + /// Drop privileges after connection is established. + #[must_use] + pub fn drop_privileges(self, drop_privileges: bool) -> Self { + Self { + drop_privileges, ..self } } - /// Start the tracer. - pub fn start(self) -> TraceResult<()> { - let trace_identifier = self.trace_identifier.unwrap_or_default(); - let source_addr = SourceAddr::discover::( + /// Build the `Tracer`. + pub fn build(self) -> TraceResult { + match (self.protocol, self.port_direction) { + (Protocol::Udp, PortDirection::None) => { + return Err(TracerError::BadConfig( + "port_direction may not be None for udp protocol".to_string(), + )); + } + (Protocol::Tcp, PortDirection::None) => { + return Err(TracerError::BadConfig( + "port_direction may not be None for tcp protocol".to_string(), + )); + } + _ => (), + } + if self.first_ttl.0 > MAX_TTL { + return Err(TracerError::BadConfig(format!( + "first_ttl {} > {MAX_TTL}", + self.first_ttl.0 + ))); + } + if self.max_ttl.0 > MAX_TTL { + return Err(TracerError::BadConfig(format!( + "max_ttl {} > {MAX_TTL}", + self.max_ttl.0 + ))); + } + if self.initial_sequence.0 > MAX_INITIAL_SEQUENCE { + return Err(TracerError::BadConfig(format!( + "initial_sequence {} > {MAX_INITIAL_SEQUENCE}", + self.initial_sequence.0 + ))); + } + Ok(Tracer::new( + self.interface, + self.source_addr, self.target_addr, - self.tracer_config.port_direction, - self.interface.as_deref(), - )?; - let channel_config = ChannelConfig { - source_addr, - target_addr: self.target_addr, - ..self.channel_config - }; - let channel = TracerChannel::::connect(&channel_config)?; - let tracer_config = Config { - trace_identifier, - target_addr: self.target_addr, - ..self.tracer_config - }; - let tracer = Tracer::new(&tracer_config, self.on_round_handler); - tracer.trace(channel)?; - Ok(()) + self.privilege_mode, + self.protocol, + self.packet_size, + self.payload_pattern, + self.tos, + self.icmp_extension_parse_mode, + self.read_timeout, + self.tcp_connect_timeout, + self.trace_identifier, + self.max_rounds, + self.first_ttl, + self.max_ttl, + self.grace_duration, + self.max_inflight, + self.initial_sequence, + self.multipath_strategy, + self.port_direction, + self.min_round_duration, + self.max_round_duration, + self.max_samples, + self.max_flows, + self.drop_privileges, + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{config, Port}; + use config::defaults; + use std::net::Ipv4Addr; + use std::num::NonZeroUsize; + + const SOURCE_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); + const TARGET_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::new(2, 2, 2, 2)); + + #[test] + fn test_builder_minimal() { + let tracer = Builder::new(TARGET_ADDR).build().unwrap(); + assert_eq!(TARGET_ADDR, tracer.target_addr()); + assert_eq!(None, tracer.source_addr()); + assert_eq!(None, tracer.interface()); + assert_eq!(defaults::DEFAULT_MAX_SAMPLES, tracer.max_samples()); + assert_eq!(defaults::DEFAULT_MAX_FLOWS, tracer.max_flows()); + assert_eq!(defaults::DEFAULT_STRATEGY_PROTOCOL, tracer.protocol()); + assert_eq!(TraceId::default(), tracer.trace_identifier()); + assert_eq!(defaults::DEFAULT_PRIVILEGE_MODE, tracer.privilege_mode()); + assert_eq!( + defaults::DEFAULT_STRATEGY_MULTIPATH, + tracer.multipath_strategy() + ); + assert_eq!( + defaults::DEFAULT_STRATEGY_PACKET_SIZE, + tracer.packet_size().0 + ); + assert_eq!( + defaults::DEFAULT_STRATEGY_PAYLOAD_PATTERN, + tracer.payload_pattern().0 + ); + assert_eq!(defaults::DEFAULT_STRATEGY_TOS, tracer.tos().0); + assert_eq!( + defaults::DEFAULT_ICMP_EXTENSION_PARSE_MODE, + tracer.icmp_extension_parse_mode() + ); + assert_eq!( + defaults::DEFAULT_STRATEGY_READ_TIMEOUT, + tracer.read_timeout() + ); + assert_eq!( + defaults::DEFAULT_STRATEGY_TCP_CONNECT_TIMEOUT, + tracer.tcp_connect_timeout() + ); + assert_eq!(None, tracer.max_rounds()); + assert_eq!(defaults::DEFAULT_STRATEGY_FIRST_TTL, tracer.first_ttl().0); + assert_eq!(defaults::DEFAULT_STRATEGY_MAX_TTL, tracer.max_ttl().0); + assert_eq!( + defaults::DEFAULT_STRATEGY_GRACE_DURATION, + tracer.grace_duration() + ); + assert_eq!( + defaults::DEFAULT_STRATEGY_MAX_INFLIGHT, + tracer.max_inflight().0 + ); + assert_eq!( + defaults::DEFAULT_STRATEGY_INITIAL_SEQUENCE, + tracer.initial_sequence().0 + ); + assert_eq!(PortDirection::None, tracer.port_direction()); + assert_eq!( + defaults::DEFAULT_STRATEGY_MIN_ROUND_DURATION, + tracer.min_round_duration() + ); + assert_eq!( + defaults::DEFAULT_STRATEGY_MAX_ROUND_DURATION, + tracer.max_round_duration() + ); + } + + #[test] + fn test_builder_full() { + let tracer = Builder::new(TARGET_ADDR) + .source_addr(Some(SOURCE_ADDR)) + .interface(Some("eth0")) + .max_samples(10) + .max_flows(20) + .protocol(Protocol::Udp) + .trace_identifier(101) + .privilege_mode(PrivilegeMode::Unprivileged) + .multipath_strategy(MultipathStrategy::Paris) + .packet_size(128) + .payload_pattern(0xff) + .tos(0x1a) + .icmp_extension_parse_mode(IcmpExtensionParseMode::Enabled) + .read_timeout(Duration::from_millis(50)) + .tcp_connect_timeout(Duration::from_millis(100)) + .max_rounds(Some(10)) + .first_ttl(2) + .max_ttl(16) + .grace_duration(Duration::from_millis(100)) + .max_inflight(22) + .initial_sequence(35000) + .port_direction(PortDirection::FixedSrc(Port(8080))) + .min_round_duration(Duration::from_millis(500)) + .max_round_duration(Duration::from_millis(1500)) + .build() + .unwrap(); + + assert_eq!(TARGET_ADDR, tracer.target_addr()); + // note that source_addr is not set until the tracer is run + assert_eq!(None, tracer.source_addr()); + assert_eq!(Some("eth0"), tracer.interface()); + assert_eq!(10, tracer.max_samples()); + assert_eq!(20, tracer.max_flows()); + assert_eq!(Protocol::Udp, tracer.protocol()); + assert_eq!(TraceId(101), tracer.trace_identifier()); + assert_eq!(PrivilegeMode::Unprivileged, tracer.privilege_mode()); + assert_eq!(MultipathStrategy::Paris, tracer.multipath_strategy()); + assert_eq!(PacketSize(128), tracer.packet_size()); + assert_eq!(PayloadPattern(0xff), tracer.payload_pattern()); + assert_eq!(TypeOfService(0x1a), tracer.tos()); + assert_eq!( + IcmpExtensionParseMode::Enabled, + tracer.icmp_extension_parse_mode() + ); + assert_eq!(Duration::from_millis(50), tracer.read_timeout()); + assert_eq!(Duration::from_millis(100), tracer.tcp_connect_timeout()); + assert_eq!( + Some(MaxRounds(NonZeroUsize::new(10).unwrap())), + tracer.max_rounds() + ); + assert_eq!(TimeToLive(2), tracer.first_ttl()); + assert_eq!(TimeToLive(16), tracer.max_ttl()); + assert_eq!(Duration::from_millis(100), tracer.grace_duration()); + assert_eq!(MaxInflight(22), tracer.max_inflight()); + assert_eq!(Sequence(35000), tracer.initial_sequence()); + assert_eq!(PortDirection::FixedSrc(Port(8080)), tracer.port_direction()); + assert_eq!(Duration::from_millis(500), tracer.min_round_duration()); + assert_eq!(Duration::from_millis(1500), tracer.max_round_duration()); + } + + #[test] + fn test_zero_max_rounds() { + let tracer = Builder::new(IpAddr::from([1, 2, 3, 4])) + .max_rounds(Some(0)) + .build() + .unwrap(); + assert_eq!(None, tracer.max_rounds()); + } + + #[test] + fn test_invalid_initial_sequence() { + let err = Builder::new(IpAddr::from([1, 2, 3, 4])) + .initial_sequence(u16::MAX) + .build() + .unwrap_err(); + assert!(matches!(err, TracerError::BadConfig(s) if s == "initial_sequence 65535 > 64511")); } } diff --git a/crates/trippy-core/src/config.rs b/crates/trippy-core/src/config.rs index 031315c00..f5a97b7ae 100644 --- a/crates/trippy-core/src/config.rs +++ b/crates/trippy-core/src/config.rs @@ -1,12 +1,10 @@ -use crate::constants::{MAX_INITIAL_SEQUENCE, MAX_TTL}; -use crate::error::{TraceResult, TracerError}; -use crate::types::{ - MaxInflight, MaxRounds, PacketSize, PayloadPattern, Port, Sequence, TimeToLive, TraceId, +use crate::types::Port; +use crate::{ + MaxInflight, MaxRounds, PacketSize, PayloadPattern, Sequence, TimeToLive, TraceId, TypeOfService, }; use std::fmt::{Display, Formatter}; use std::net::{IpAddr, Ipv4Addr}; -use std::num::NonZeroUsize; use std::time::Duration; /// Default values for configuration. @@ -158,15 +156,15 @@ pub enum MultipathStrategy { Classic, /// The UDP `checksum` field is used to store the sequence number. /// - /// a.k.a [`paris`](https://github.com/libparistraceroute/libparistraceroute/wiki/Checksum) traceroute approach. + /// a.k.a. [`paris`](https://github.com/libparistraceroute/libparistraceroute/wiki/Checksum) traceroute approach. /// - /// This requires that the UDP payload contains a well chosen value to ensure the UDP checksum + /// This requires that the UDP payload contains a well-chosen value to ensure the UDP checksum /// remains valid for the packet and therefore this cannot be used along with a custom /// payload pattern. Paris, /// The IP `identifier` field is used to store the sequence number. /// - /// a.k.a [`dublin`](https://github.com/insomniacslk/dublin-traceroute) traceroute approach. + /// a.k.a. [`dublin`](https://github.com/insomniacslk/dublin-traceroute) traceroute approach. /// /// The allow either the src or dest or both ports to be fixed. /// @@ -208,7 +206,7 @@ pub enum PortDirection { /// Trace from a fixed source port to a fixed destination port (i.e. 5000 -> 80). /// /// When both ports are fixed another element of the IP header is required to vary per probe - /// such that probes can be identified. Typically this is only used for UDP, whereby the + /// such that probes can be identified. Typically, this is only used for UDP, whereby the /// checksum is manipulated by adjusting the payload and therefore used as the identifier. /// /// Note that this case is not currently implemented. @@ -247,119 +245,32 @@ impl PortDirection { } } -/// Build a `ChannelConfig`. -#[derive(Debug)] -pub struct ChannelConfigBuilder { - config: ChannelConfig, +/// Tracer state configuration. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct StateConfig { + /// The maximum number of samples to record per hop. + /// + /// Once the maximum number of samples has been reached the oldest sample + /// is discarded (FIFO). + pub max_samples: usize, + /// The maximum number of flows to record. + /// + /// Once the maximum number of flows has been reached no new flows will be + /// created, existing flows are updated and are never removed. + pub max_flows: usize, } -impl ChannelConfigBuilder { - /// Create a new `ChannelConfigBuilder` between a source and target. - #[must_use] - pub fn new(source_addr: IpAddr, target_addr: IpAddr) -> Self { - Self { - config: ChannelConfig { - source_addr, - target_addr, - ..ChannelConfig::default() - }, - } - } - - /// Set the channel protocol. - #[must_use] - pub fn protocol(self, protocol: Protocol) -> Self { - Self { - config: ChannelConfig { - protocol, - ..self.config - }, - } - } - - /// Set the channel privilege mode. - #[must_use] - pub fn privilege_mode(self, privilege_mode: PrivilegeMode) -> Self { - Self { - config: ChannelConfig { - privilege_mode, - ..self.config - }, - } - } - - /// Set the channel packet size. - #[must_use] - pub fn packet_size(self, packet_size: PacketSize) -> Self { - Self { - config: ChannelConfig { - packet_size, - ..self.config - }, - } - } - - /// Set the channel payload pattern. - #[must_use] - pub fn payload_pattern(self, payload_pattern: PayloadPattern) -> Self { - Self { - config: ChannelConfig { - payload_pattern, - ..self.config - }, - } - } - - /// Set the channel type of service. - #[must_use] - pub fn tos(self, tos: TypeOfService) -> Self { - Self { - config: ChannelConfig { tos, ..self.config }, - } - } - - /// Set the channel ICMP extensions mode. - #[must_use] - pub fn icmp_extension_mode(self, icmp_extension_mode: IcmpExtensionParseMode) -> Self { - Self { - config: ChannelConfig { - icmp_extension_mode, - ..self.config - }, - } - } - - /// Set the channel read timeout. - #[must_use] - pub fn read_timeout(self, read_timeout: Duration) -> Self { - Self { - config: ChannelConfig { - read_timeout, - ..self.config - }, - } - } - - /// Set the channel TCP connect timeout. - #[must_use] - pub fn tcp_connect_timeout(self, tcp_connect_timeout: Duration) -> Self { +impl Default for StateConfig { + fn default() -> Self { Self { - config: ChannelConfig { - tcp_connect_timeout, - ..self.config - }, + max_samples: defaults::DEFAULT_MAX_SAMPLES, + max_flows: defaults::DEFAULT_MAX_FLOWS, } } - - /// Build the `ChannelConfig` from this `ChannelConfigBuilder`. - #[must_use] - pub fn build(self) -> ChannelConfig { - self.config - } } /// Tracer network channel configuration. -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct ChannelConfig { pub privilege_mode: PrivilegeMode, pub protocol: Protocol, @@ -369,43 +280,11 @@ pub struct ChannelConfig { pub payload_pattern: PayloadPattern, pub initial_sequence: Sequence, pub tos: TypeOfService, - pub icmp_extension_mode: IcmpExtensionParseMode, + pub icmp_extension_parse_mode: IcmpExtensionParseMode, pub read_timeout: Duration, pub tcp_connect_timeout: Duration, } -impl ChannelConfig { - #[allow(clippy::too_many_arguments)] - #[must_use] - pub fn new( - privilege_mode: PrivilegeMode, - protocol: Protocol, - source_addr: IpAddr, - target_addr: IpAddr, - packet_size: u16, - payload_pattern: u8, - initial_sequence: u16, - tos: u8, - icmp_extension_mode: IcmpExtensionParseMode, - read_timeout: Duration, - tcp_connect_timeout: Duration, - ) -> Self { - Self { - privilege_mode, - protocol, - source_addr, - target_addr, - packet_size: PacketSize(packet_size), - payload_pattern: PayloadPattern(payload_pattern), - initial_sequence: Sequence(initial_sequence), - tos: TypeOfService(tos), - icmp_extension_mode, - read_timeout, - tcp_connect_timeout, - } - } -} - impl Default for ChannelConfig { fn default() -> Self { Self { @@ -417,163 +296,16 @@ impl Default for ChannelConfig { payload_pattern: PayloadPattern(defaults::DEFAULT_STRATEGY_PAYLOAD_PATTERN), initial_sequence: Sequence(defaults::DEFAULT_STRATEGY_INITIAL_SEQUENCE), tos: TypeOfService(defaults::DEFAULT_STRATEGY_TOS), - icmp_extension_mode: defaults::DEFAULT_ICMP_EXTENSION_PARSE_MODE, + icmp_extension_parse_mode: defaults::DEFAULT_ICMP_EXTENSION_PARSE_MODE, read_timeout: defaults::DEFAULT_STRATEGY_READ_TIMEOUT, tcp_connect_timeout: defaults::DEFAULT_STRATEGY_TCP_CONNECT_TIMEOUT, } } } -/// Build a `Config`. -#[derive(Debug)] -pub struct ConfigBuilder { - config: Config, -} - -impl ConfigBuilder { - /// Create a new `ConfigBuilder`. - #[must_use] - pub fn new(trace_identifier: TraceId, target_addr: IpAddr) -> Self { - Self { - config: Config { - target_addr, - trace_identifier, - ..Config::default() - }, - } - } - - /// Set the tracer protocol. - #[must_use] - pub fn protocol(self, protocol: Protocol) -> Self { - Self { - config: Config { - protocol, - ..self.config - }, - } - } - - /// Set the tracer maximum rounds. - #[must_use] - pub fn max_rounds(self, max_rounds: MaxRounds) -> Self { - Self { - config: Config { - max_rounds: Some(max_rounds), - ..self.config - }, - } - } - - /// Set the tracer first ttl. - #[must_use] - pub fn first_ttl(self, first_ttl: TimeToLive) -> Self { - Self { - config: Config { - first_ttl, - ..self.config - }, - } - } - - /// Set the tracer max ttl. - #[must_use] - pub fn max_ttl(self, max_ttl: TimeToLive) -> Self { - Self { - config: Config { - max_ttl, - ..self.config - }, - } - } - - /// Set the tracer grace duration. - #[must_use] - pub fn grace_duration(self, grace_duration: Duration) -> Self { - Self { - config: Config { - grace_duration, - ..self.config - }, - } - } - - /// Set the tracer max inflight. - #[must_use] - pub fn max_inflight(self, max_inflight: MaxInflight) -> Self { - Self { - config: Config { - max_inflight, - ..self.config - }, - } - } - - /// Set the tracer initial sequence. - #[must_use] - pub fn initial_sequence(self, initial_sequence: Sequence) -> Self { - Self { - config: Config { - initial_sequence, - ..self.config - }, - } - } - - /// Set the tracer multipath strategy. - #[must_use] - pub fn multipath_strategy(self, multipath_strategy: MultipathStrategy) -> Self { - Self { - config: Config { - multipath_strategy, - ..self.config - }, - } - } - - /// Set the tracer port direction. - #[must_use] - pub fn port_direction(self, port_direction: PortDirection) -> Self { - Self { - config: Config { - port_direction, - ..self.config - }, - } - } - - /// Set the tracer minimum round duration. - #[must_use] - pub fn min_round_duration(self, min_round_duration: Duration) -> Self { - Self { - config: Config { - min_round_duration, - ..self.config - }, - } - } - - /// Set the tracer maximum round duration. - #[must_use] - pub fn max_round_duration(self, max_round_duration: Duration) -> Self { - Self { - config: Config { - max_round_duration, - ..self.config - }, - } - } - - /// Build the `Config` from this `ConfigBuilder`. - #[must_use] - pub fn build(self) -> Config { - self.config - } -} - -/// Tracing algorithm configuration. +/// Tracing strategy configuration. #[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct Config { +pub struct StrategyConfig { pub target_addr: IpAddr, pub protocol: Protocol, pub trace_identifier: TraceId, @@ -589,66 +321,7 @@ pub struct Config { pub max_round_duration: Duration, } -impl Config { - #[allow(clippy::too_many_arguments)] - pub fn new( - target_addr: IpAddr, - protocol: Protocol, - max_rounds: Option, - trace_identifier: u16, - first_ttl: u8, - max_ttl: u8, - grace_duration: Duration, - max_inflight: u8, - initial_sequence: u16, - multipath_strategy: MultipathStrategy, - port_direction: PortDirection, - min_round_duration: Duration, - max_round_duration: Duration, - ) -> TraceResult { - if first_ttl > MAX_TTL { - return Err(TracerError::BadConfig(format!( - "first_ttl ({first_ttl}) > {MAX_TTL}" - ))); - } - if max_ttl > MAX_TTL { - return Err(TracerError::BadConfig(format!( - "max_ttl ({first_ttl}) > {MAX_TTL}" - ))); - } - if initial_sequence > MAX_INITIAL_SEQUENCE { - return Err(TracerError::BadConfig(format!( - "initial_sequence ({initial_sequence}) > {MAX_INITIAL_SEQUENCE}" - ))); - } - let max_rounds = match max_rounds { - Some(max_rounds) if max_rounds > 0 => NonZeroUsize::new(max_rounds).map(MaxRounds), - Some(_) => { - return Err(TracerError::BadConfig(String::from( - "max_rounds must be greater than zero", - ))); - } - None => None, - }; - Ok(Self { - target_addr, - protocol, - trace_identifier: TraceId(trace_identifier), - max_rounds, - first_ttl: TimeToLive(first_ttl), - max_ttl: TimeToLive(max_ttl), - grace_duration, - max_inflight: MaxInflight(max_inflight), - initial_sequence: Sequence(initial_sequence), - multipath_strategy, - port_direction, - min_round_duration, - max_round_duration, - }) - } -} - -impl Default for Config { +impl Default for StrategyConfig { fn default() -> Self { Self { target_addr: IpAddr::V4(Ipv4Addr::UNSPECIFIED), @@ -667,99 +340,3 @@ impl Default for Config { } } } - -#[cfg(test)] -mod tests { - use super::*; - use std::num::NonZeroUsize; - - const SOURCE_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)); - const TARGET_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::new(2, 2, 2, 2)); - - #[test] - fn test_channel_config_builder_minimal() { - let cfg = ChannelConfigBuilder::new(SOURCE_ADDR, TARGET_ADDR).build(); - assert_eq!(SOURCE_ADDR, cfg.source_addr); - assert_eq!(TARGET_ADDR, cfg.target_addr); - assert_eq!( - ChannelConfig { - source_addr: SOURCE_ADDR, - target_addr: TARGET_ADDR, - ..Default::default() - }, - cfg - ); - } - - #[test] - fn test_channel_config_builder_full() { - let cfg = ChannelConfigBuilder::new(SOURCE_ADDR, TARGET_ADDR) - .protocol(Protocol::Tcp) - .privilege_mode(PrivilegeMode::Unprivileged) - .packet_size(PacketSize(128)) - .payload_pattern(PayloadPattern(0xff)) - .tos(TypeOfService(0x1a)) - .icmp_extension_mode(IcmpExtensionParseMode::Enabled) - .read_timeout(Duration::from_millis(50)) - .tcp_connect_timeout(Duration::from_millis(100)) - .build(); - assert_eq!(SOURCE_ADDR, cfg.source_addr); - assert_eq!(TARGET_ADDR, cfg.target_addr); - assert_eq!(Protocol::Tcp, cfg.protocol); - assert_eq!(PrivilegeMode::Unprivileged, cfg.privilege_mode); - assert_eq!(PacketSize(128), cfg.packet_size); - assert_eq!(PayloadPattern(0xff), cfg.payload_pattern); - assert_eq!(TypeOfService(0x1a), cfg.tos); - assert_eq!(IcmpExtensionParseMode::Enabled, cfg.icmp_extension_mode); - assert_eq!(Duration::from_millis(50), cfg.read_timeout); - assert_eq!(Duration::from_millis(100), cfg.tcp_connect_timeout); - } - - #[test] - fn test_config_builder_minimal() { - let cfg = ConfigBuilder::new(TraceId(0), TARGET_ADDR).build(); - assert_eq!(TraceId(0), cfg.trace_identifier); - assert_eq!(TARGET_ADDR, cfg.target_addr); - assert_eq!( - Config { - trace_identifier: TraceId(0), - target_addr: TARGET_ADDR, - ..Default::default() - }, - cfg - ); - } - - #[test] - fn test_config_builder_full() { - let cfg = ConfigBuilder::new(TraceId(0), TARGET_ADDR) - .protocol(Protocol::Udp) - .max_rounds(MaxRounds(NonZeroUsize::new(10).unwrap())) - .first_ttl(TimeToLive(2)) - .max_ttl(TimeToLive(16)) - .grace_duration(Duration::from_millis(100)) - .max_inflight(MaxInflight(22)) - .initial_sequence(Sequence(35000)) - .multipath_strategy(MultipathStrategy::Paris) - .port_direction(PortDirection::FixedSrc(Port(33000))) - .min_round_duration(Duration::from_millis(500)) - .max_round_duration(Duration::from_millis(1500)) - .build(); - assert_eq!(TraceId(0), cfg.trace_identifier); - assert_eq!(TARGET_ADDR, cfg.target_addr); - assert_eq!(Protocol::Udp, cfg.protocol); - assert_eq!( - Some(MaxRounds(NonZeroUsize::new(10).unwrap())), - cfg.max_rounds - ); - assert_eq!(TimeToLive(2), cfg.first_ttl); - assert_eq!(TimeToLive(16), cfg.max_ttl); - assert_eq!(Duration::from_millis(100), cfg.grace_duration); - assert_eq!(MaxInflight(22), cfg.max_inflight); - assert_eq!(Sequence(35000), cfg.initial_sequence); - assert_eq!(MultipathStrategy::Paris, cfg.multipath_strategy); - assert_eq!(PortDirection::FixedSrc(Port(33000)), cfg.port_direction); - assert_eq!(Duration::from_millis(500), cfg.min_round_duration); - assert_eq!(Duration::from_millis(1500), cfg.max_round_duration); - } -} diff --git a/crates/trippy-core/src/constants.rs b/crates/trippy-core/src/constants.rs index 150dcd049..3f41cdf8a 100644 --- a/crates/trippy-core/src/constants.rs +++ b/crates/trippy-core/src/constants.rs @@ -1,4 +1,7 @@ /// The maximum time-to-live value allowed. +/// +/// The IP `ttl` is an u8 (0..255) but since a `ttl` of zero isn't useful we only allow 254 distinct +/// hops (1..255). pub const MAX_TTL: u8 = 254; /// The maximum number of sequence numbers allowed per round. diff --git a/crates/trippy-core/src/error.rs b/crates/trippy-core/src/error.rs index d187feb1c..dabe012dd 100644 --- a/crates/trippy-core/src/error.rs +++ b/crates/trippy-core/src/error.rs @@ -29,6 +29,10 @@ pub enum TracerError { InvalidSourceAddr(IpAddr), #[error("missing address from socket call")] MissingAddr, + #[error("connect callback error: {0}")] + PrivilegeError(#[from] trippy_privilege::Error), + #[error("tracer error: {0}")] + Other(String), } /// Custom IO error result. diff --git a/crates/trippy/src/backend/flows.rs b/crates/trippy-core/src/flows.rs similarity index 100% rename from crates/trippy/src/backend/flows.rs rename to crates/trippy-core/src/flows.rs diff --git a/crates/trippy-core/src/lib.rs b/crates/trippy-core/src/lib.rs index e1b200e42..df115d6a8 100644 --- a/crates/trippy-core/src/lib.rs +++ b/crates/trippy-core/src/lib.rs @@ -18,7 +18,9 @@ //! use trippy_core::Builder; //! //! let addr = IpAddr::from_str("1.1.1.1")?; -//! Builder::new(addr, |round| println!("{:?}", round)).start()?; +//! Builder::new(addr) +//! .build()? +//! .run_with(|round| println!("{:?}", round))?; //! # Ok(()) //! # } //! ``` @@ -34,15 +36,24 @@ //! use trippy_core::{Builder, MultipathStrategy, Port, PortDirection, PrivilegeMode, Protocol}; //! //! let addr = IpAddr::from_str("1.1.1.1")?; -//! Builder::new(addr, |round| println!("{:?}", round)) +//! Builder::new(addr) //! .privilege_mode(PrivilegeMode::Unprivileged) //! .protocol(Protocol::Udp) //! .multipath_strategy(MultipathStrategy::Dublin) //! .port_direction(PortDirection::FixedBoth(Port(33000), Port(3500))) -//! .start()?; +//! .build()? +//! .run_with(|round| println!("{:?}", round))?; //! # Ok(()) //! # } //! ``` +//! +//! # See Also +//! +//! - [`Builder`] - Build a [`Tracer`]. +//! - [`Tracer::run`] - Run the tracer on the current thread. +//! - [`Tracer::run_with`] - Run the tracer with a custom round handler. +//! - [`Tracer::spawn`] - Run the tracer on a new thread. +//! - [`Tracer::spawn_with`] - Run the tracer on a new thread with a custom round handler. #![warn(clippy::all, clippy::pedantic, clippy::nursery, rust_2018_idioms)] #![allow( clippy::module_name_repetitions, @@ -51,7 +62,8 @@ clippy::option_if_let_else, clippy::missing_const_for_fn, clippy::cast_possible_truncation, - clippy::missing_errors_doc + clippy::missing_errors_doc, + clippy::cast_precision_loss )] #![deny(unsafe_code)] @@ -59,24 +71,31 @@ mod builder; mod config; mod constants; mod error; +mod flows; mod net; mod probe; +mod state; +mod strategy; mod tracer; mod types; +use net::channel::TracerChannel; +use net::source::SourceAddr; + pub use builder::Builder; pub use config::{ - defaults, ChannelConfig, ChannelConfigBuilder, Config, ConfigBuilder, IcmpExtensionParseMode, - MultipathStrategy, PortDirection, PrivilegeMode, Protocol, + defaults, IcmpExtensionParseMode, MultipathStrategy, PortDirection, PrivilegeMode, Protocol, }; -pub use net::channel::TracerChannel; -pub use net::source::SourceAddr; -pub use net::{PlatformImpl, SocketImpl}; +pub use constants::MAX_TTL; +pub use error::TracerError; +pub use flows::{FlowEntry, FlowId}; pub use probe::{ Extension, Extensions, IcmpPacketType, MplsLabelStack, MplsLabelStackMember, Probe, ProbeComplete, ProbeState, UnknownExtension, }; -pub use tracer::{CompletionReason, Tracer, TracerRound}; +pub use state::{Hop, TraceState}; +pub use strategy::{CompletionReason, TracerRound, TracerStrategy}; +pub use tracer::Tracer; pub use types::{ Flags, MaxInflight, MaxRounds, PacketSize, PayloadPattern, Port, Round, Sequence, TimeToLive, TraceId, TypeOfService, diff --git a/crates/trippy-core/src/net/channel.rs b/crates/trippy-core/src/net/channel.rs index f6675f713..abc8f2a36 100644 --- a/crates/trippy-core/src/net/channel.rs +++ b/crates/trippy-core/src/net/channel.rs @@ -1,10 +1,10 @@ -use crate::config::IcmpExtensionParseMode; +use crate::config::{ChannelConfig, IcmpExtensionParseMode}; use crate::error::{TraceResult, TracerError}; use crate::net::socket::Socket; use crate::net::{ipv4, ipv6, platform, Network}; use crate::probe::{Probe, ProbeResponse}; use crate::types::{PacketSize, PayloadPattern, TypeOfService}; -use crate::{ChannelConfig, Port, PrivilegeMode, Protocol, Sequence}; +use crate::{Port, PrivilegeMode, Protocol, Sequence}; use arrayvec::ArrayVec; use std::net::IpAddr; use std::time::{Duration, SystemTime}; @@ -66,7 +66,7 @@ impl TracerChannel { payload_pattern: config.payload_pattern, initial_sequence: config.initial_sequence, tos: config.tos, - icmp_extension_mode: config.icmp_extension_mode, + icmp_extension_mode: config.icmp_extension_parse_mode, read_timeout: config.read_timeout, tcp_connect_timeout: config.tcp_connect_timeout, send_socket, diff --git a/crates/trippy-core/src/net/platform/unix.rs b/crates/trippy-core/src/net/platform/unix.rs index db832d558..f0ebee178 100644 --- a/crates/trippy-core/src/net/platform/unix.rs +++ b/crates/trippy-core/src/net/platform/unix.rs @@ -20,7 +20,7 @@ mod address { use crate::error::{TraceResult, TracerError}; use crate::net::platform::Ipv4ByteOrder; use crate::net::socket::Socket; - use crate::SocketImpl; + use crate::net::SocketImpl; use nix::sys::socket::{AddressFamily, SockaddrLike}; use std::net::{IpAddr, SocketAddr}; use tracing::instrument; diff --git a/crates/trippy-core/src/net/socket.rs b/crates/trippy-core/src/net/socket.rs index b3d381d40..3db2f7cf4 100644 --- a/crates/trippy-core/src/net/socket.rs +++ b/crates/trippy-core/src/net/socket.rs @@ -52,8 +52,9 @@ where #[derive(Debug)] pub enum SocketError { ConnectionRefused, + #[allow(dead_code)] HostUnreachable, - Other(std::io::Error), + Other(#[allow(dead_code)] std::io::Error), } #[cfg(test)] diff --git a/crates/trippy/src/backend/trace.rs b/crates/trippy-core/src/state.rs similarity index 90% rename from crates/trippy/src/backend/trace.rs rename to crates/trippy-core/src/state.rs index feea8c142..be3768843 100644 --- a/crates/trippy/src/backend/trace.rs +++ b/crates/trippy-core/src/state.rs @@ -1,99 +1,104 @@ -use crate::backend::flows::{Flow, FlowId, FlowRegistry}; -use crate::config::MAX_HOPS; +use crate::config::StateConfig; +use crate::constants::MAX_TTL; +use crate::flows::{Flow, FlowId, FlowRegistry}; +use crate::{Extensions, IcmpPacketType, ProbeState, Round, TimeToLive, TracerRound}; use indexmap::IndexMap; use std::collections::HashMap; use std::iter::once; use std::net::IpAddr; use std::time::Duration; -use trippy_core::{Extensions, IcmpPacketType, ProbeState, Round, TimeToLive, TracerRound}; -/// The state of all hops in a trace. -#[derive(Debug, Clone)] -pub struct Trace { - /// The maximum number of samples to record per hop. - /// - /// Once the maximum number of samples has been reached the oldest sample - /// is discarded (FIFO). - max_samples: usize, - /// The maximum number of flows to record. - /// - /// Once the maximum number of flows has been reached no new flows will be - /// created, existing flows are updated and are never removed. - max_flows: usize, +/// The state of a trace. +#[derive(Debug, Clone, Default)] +pub struct TraceState { + state_config: StateConfig, /// The flow id for the current round. round_flow_id: FlowId, /// Tracing data per registered flow id. - trace_data: HashMap, + state: HashMap, /// Flow registry. registry: FlowRegistry, /// Tracing error message. error: Option, } -impl Trace { +impl TraceState { /// Create a new `Trace`. - pub fn new(max_samples: usize, max_flows: usize) -> Self { + #[must_use] + pub fn new(state_config: StateConfig) -> Self { Self { - trace_data: once((Self::default_flow_id(), TraceData::new(max_samples))) - .collect::>(), + state: once(( + Self::default_flow_id(), + FlowState::new(state_config.max_samples), + )) + .collect::>(), round_flow_id: Self::default_flow_id(), - max_samples, - max_flows, + state_config, registry: FlowRegistry::new(), error: None, } } /// Return the id of the default flow. + #[must_use] pub fn default_flow_id() -> FlowId { FlowId(0) } /// Information about each hop for a given flow. + #[must_use] pub fn hops(&self, flow_id: FlowId) -> &[Hop] { - self.trace_data[&flow_id].hops() + self.state[&flow_id].hops() } /// Is a given `Hop` the target hop for a given flow? /// /// A `Hop` is considered to be the target if it has the highest `ttl` value observed. /// - /// Note that if the target host does not respond to probes then the the highest `ttl` observed + /// Note that if the target host does not respond to probes then the highest `ttl` observed /// will be one greater than the `ttl` of the last host which did respond. + #[must_use] pub fn is_target(&self, hop: &Hop, flow_id: FlowId) -> bool { - self.trace_data[&flow_id].is_target(hop) + self.state[&flow_id].is_target(hop) } /// Is a given `Hop` in the current round for a given flow? + #[must_use] pub fn is_in_round(&self, hop: &Hop, flow_id: FlowId) -> bool { - self.trace_data[&flow_id].is_in_round(hop) + self.state[&flow_id].is_in_round(hop) } /// Return the target `Hop` for a given flow. + #[must_use] pub fn target_hop(&self, flow_id: FlowId) -> &Hop { - self.trace_data[&flow_id].target_hop() + self.state[&flow_id].target_hop() } /// The current round of tracing for a given flow. + #[must_use] pub fn round(&self, flow_id: FlowId) -> Option { - self.trace_data[&flow_id].round() + self.state[&flow_id].round() } /// The total rounds of tracing for a given flow. + #[must_use] pub fn round_count(&self, flow_id: FlowId) -> usize { - self.trace_data[&flow_id].round_count() + self.state[&flow_id].round_count() } /// The `FlowId` for the current round. + #[must_use] pub fn round_flow_id(&self) -> FlowId { self.round_flow_id } /// The registry of flows in the trace. + #[must_use] pub fn flows(&self) -> &[(Flow, FlowId)] { self.registry.flows() } + #[must_use] pub fn error(&self) -> Option<&str> { self.error.as_deref() } @@ -103,17 +108,19 @@ impl Trace { } /// The maximum number of samples to record per hop. + #[must_use] pub fn max_samples(&self) -> usize { - self.max_samples + self.state_config.max_samples } /// The maximum number of flows to record. + #[must_use] pub fn max_flows(&self) -> usize { - self.max_flows + self.state_config.max_flows } /// Update the tracing state from a `TracerRound`. - pub(super) fn update_from_round(&mut self, round: &TracerRound<'_>) { + pub fn update_from_round(&mut self, round: &TracerRound<'_>) { let flow = Flow::from_hops( round .probes @@ -126,7 +133,7 @@ impl Trace { .take(usize::from(round.largest_ttl.0)), ); self.update_trace_flow(Self::default_flow_id(), round); - if self.registry.flows().len() < self.max_flows { + if self.registry.flows().len() < self.state_config.max_flows { let flow_id = self.registry.register(flow); self.round_flow_id = flow_id; self.update_trace_flow(flow_id, round); @@ -135,9 +142,9 @@ impl Trace { fn update_trace_flow(&mut self, flow_id: FlowId, round: &TracerRound<'_>) { let flow_trace = self - .trace_data + .state .entry(flow_id) - .or_insert_with(|| TraceData::new(self.max_samples)); + .or_insert_with(|| FlowState::new(self.state_config.max_samples)); flow_trace.update_from_round(round); } } @@ -187,6 +194,7 @@ pub struct Hop { impl Hop { /// The time-to-live of this hop. + #[must_use] pub fn ttl(&self) -> u8 { self.ttl } @@ -201,21 +209,25 @@ impl Hop { } /// The number of unique address observed for this time-to-live. + #[must_use] pub fn addr_count(&self) -> usize { self.addrs.len() } /// The total number of probes sent. + #[must_use] pub fn total_sent(&self) -> usize { self.total_sent } /// The total number of probes responses received. + #[must_use] pub fn total_recv(&self) -> usize { self.total_recv } /// The % of packets that are lost. + #[must_use] pub fn loss_pct(&self) -> f64 { if self.total_sent > 0 { let lost = self.total_sent - self.total_recv; @@ -226,21 +238,25 @@ impl Hop { } /// The duration of the last probe. + #[must_use] pub fn last_ms(&self) -> Option { self.last.map(|last| last.as_secs_f64() * 1000_f64) } /// The duration of the best probe observed. + #[must_use] pub fn best_ms(&self) -> Option { self.best.map(|last| last.as_secs_f64() * 1000_f64) } /// The duration of the worst probe observed. + #[must_use] pub fn worst_ms(&self) -> Option { self.worst.map(|last| last.as_secs_f64() * 1000_f64) } /// The average duration of all probes. + #[must_use] pub fn avg_ms(&self) -> f64 { if self.total_recv() > 0 { (self.total_time.as_secs_f64() * 1000_f64) / self.total_recv as f64 @@ -250,6 +266,7 @@ impl Hop { } /// The standard deviation of all probes. + #[must_use] pub fn stddev_ms(&self) -> f64 { if self.total_recv > 1 { (self.m2 / (self.total_recv - 1) as f64).sqrt() @@ -259,50 +276,60 @@ impl Hop { } /// The duration of the jitter probe observed. + #[must_use] pub fn jitter_ms(&self) -> Option { self.jitter.map(|j| j.as_secs_f64() * 1000_f64) } - /// The duration of the jworst probe observed. + /// The duration of the worst probe observed. + #[must_use] pub fn jmax_ms(&self) -> Option { self.jmax.map(|x| x.as_secs_f64() * 1000_f64) } /// The jitter average duration of all probes. + #[must_use] pub fn javg_ms(&self) -> f64 { self.javg } /// The jitter interval of all probes. + #[must_use] pub fn jinta(&self) -> f64 { self.jinta } /// The source port for last probe for this hop. + #[must_use] pub fn last_src_port(&self) -> u16 { self.last_src_port } /// The destination port for last probe for this hop. + #[must_use] pub fn last_dest_port(&self) -> u16 { self.last_dest_port } /// The sequence number for the last probe for this hop. + #[must_use] pub fn last_sequence(&self) -> u16 { self.last_sequence } /// The icmp packet type for the last probe for this hop. + #[must_use] pub fn last_icmp_packet_type(&self) -> Option { self.last_icmp_packet_type } /// The last N samples. + #[must_use] pub fn samples(&self) -> &[Duration] { &self.samples } + #[must_use] pub fn extensions(&self) -> Option<&Extensions> { self.extensions.as_ref() } @@ -335,9 +362,9 @@ impl Default for Hop { } } -/// Data for a trace. +/// Data for a single trace flow. #[derive(Debug, Clone)] -struct TraceData { +struct FlowState { /// The maximum number of samples to record. max_samples: usize, /// The lowest ttl observed across all rounds. @@ -354,7 +381,7 @@ struct TraceData { hops: Vec, } -impl TraceData { +impl FlowState { fn new(max_samples: usize) -> Self { Self { max_samples, @@ -363,7 +390,7 @@ impl TraceData { highest_ttl_for_round: 0, round: None, round_count: 0, - hops: (0..MAX_HOPS).map(|_| Hop::default()).collect(), + hops: (0..MAX_TTL).map(|_| Hop::default()).collect(), } } @@ -491,6 +518,10 @@ impl TraceData { #[cfg(test)] mod tests { use super::*; + use crate::{ + CompletionReason, Flags, IcmpPacketType, Port, Probe, ProbeComplete, ProbeState, Sequence, + TimeToLive, TraceId, + }; use anyhow::anyhow; use serde::Deserialize; use std::collections::HashSet; @@ -498,10 +529,6 @@ mod tests { use std::str::FromStr; use std::time::SystemTime; use test_case::test_case; - use trippy_core::{ - CompletionReason, Flags, IcmpPacketType, Port, Probe, ProbeComplete, ProbeState, Sequence, - TimeToLive, TraceId, - }; /// A test scenario. #[derive(Deserialize, Debug)] @@ -639,7 +666,7 @@ mod tests { macro_rules! file { ($path:expr) => {{ - let yaml = include_str!(concat!("../../tests/resources/backend/", $path)); + let yaml = include_str!(concat!("../tests/resources/backend/", $path)); serde_yaml::from_str(yaml).unwrap() }}; } @@ -649,7 +676,10 @@ mod tests { #[test_case(file!("ipv4_4probes_all_status.yaml"))] #[test_case(file!("ipv4_4probes_0latency.yaml"))] fn test_scenario(scenario: Scenario) { - let mut trace = Trace::new(100, 1); + let mut trace = TraceState::new(StateConfig { + max_flows: 1, + ..StateConfig::default() + }); for (i, round) in scenario.rounds.into_iter().enumerate() { let probes = round .probes @@ -662,7 +692,7 @@ mod tests { TracerRound::new(&probes, largest_ttl, CompletionReason::TargetFound); trace.update_from_round(&tracer_round); } - let actual_hops = trace.hops(Trace::default_flow_id()); + let actual_hops = trace.hops(TraceState::default_flow_id()); let expected_hops = scenario.expected.hops; for (actual, expected) in actual_hops.iter().zip(expected_hops) { assert_eq!(actual.ttl(), expected.ttl); diff --git a/crates/trippy-core/src/strategy.rs b/crates/trippy-core/src/strategy.rs new file mode 100644 index 000000000..997e62fd5 --- /dev/null +++ b/crates/trippy-core/src/strategy.rs @@ -0,0 +1,1231 @@ +use self::state::TracerState; +use crate::config::StrategyConfig; +use crate::error::{TraceResult, TracerError}; +use crate::net::Network; +use crate::probe::{ + ProbeResponse, ProbeResponseData, ProbeResponseSeq, ProbeResponseSeqIcmp, ProbeResponseSeqTcp, + ProbeResponseSeqUdp, ProbeState, +}; +use crate::types::{Sequence, TimeToLive, TraceId}; +use crate::{MultipathStrategy, PortDirection, Protocol}; +use std::net::IpAddr; +use std::time::{Duration, SystemTime}; +use tracing::instrument; + +/// The output from a round of tracing. +#[derive(Debug, Clone)] +pub struct TracerRound<'a> { + /// The state of all `ProbeState` that were sent in the round. + pub probes: &'a [ProbeState], + /// The largest time-to-live (ttl) for which we received a reply in the round. + pub largest_ttl: TimeToLive, + /// Indicates what triggered the completion of the tracing round. + pub reason: CompletionReason, +} + +impl<'a> TracerRound<'a> { + #[must_use] + pub fn new( + probes: &'a [ProbeState], + largest_ttl: TimeToLive, + reason: CompletionReason, + ) -> Self { + Self { + probes, + largest_ttl, + reason, + } + } +} + +/// Indicates what triggered the completion of the tracing round. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum CompletionReason { + /// The round ended because the target was found. + TargetFound, + /// The round ended because the time exceeded the configured maximum round time. + RoundTimeLimitExceeded, +} + +/// Trace a path to a target. +#[derive(Debug, Clone)] +pub struct TracerStrategy { + config: StrategyConfig, + publish: F, +} + +impl)> TracerStrategy { + #[instrument(skip_all)] + pub fn new(config: &StrategyConfig, publish: F) -> Self { + tracing::debug!(?config); + Self { + config: *config, + publish, + } + } + + /// Run a continuous trace and publish results. + #[instrument(skip(self, network))] + pub fn run(self, mut network: N) -> TraceResult<()> { + let mut state = TracerState::new(self.config); + while !state.finished(self.config.max_rounds) { + self.send_request(&mut network, &mut state)?; + self.recv_response(&mut network, &mut state)?; + self.update_round(&mut state); + } + Ok(()) + } + + /// Send the next probe if required. + /// + /// Send a `ProbeState` for the next time-to-live (ttl) if all the following are true: + /// + /// 1 - the target host has not been found + /// 2 - the next ttl is not greater than the maximum allowed ttl + /// 3 - if the target ttl of the target is known: + /// - the next ttl is not greater than the ttl of the target host observed from the prior + /// round + /// otherwise: + /// - the number of unknown-in-flight probes is lower than the maximum allowed + #[instrument(skip(self, network, st))] + fn send_request(&self, network: &mut N, st: &mut TracerState) -> TraceResult<()> { + let can_send_ttl = if let Some(target_ttl) = st.target_ttl() { + st.ttl() <= target_ttl + } else { + st.ttl() - st.max_received_ttl().unwrap_or_default() + < TimeToLive(self.config.max_inflight.0) + }; + if !st.target_found() && st.ttl() <= self.config.max_ttl && can_send_ttl { + let sent = SystemTime::now(); + match self.config.protocol { + Protocol::Icmp => { + network.send_probe(st.next_probe(sent))?; + } + Protocol::Udp => network.send_probe(st.next_probe(sent))?, + Protocol::Tcp => { + let mut probe = if st.round_has_capacity() { + st.next_probe(sent) + } else { + return Err(TracerError::InsufficientCapacity); + }; + while let Err(err) = network.send_probe(probe) { + match err { + TracerError::AddressNotAvailable(_) => { + if st.round_has_capacity() { + probe = st.reissue_probe(SystemTime::now()); + } else { + return Err(TracerError::InsufficientCapacity); + } + } + other => return Err(other), + } + } + } + }; + } + Ok(()) + } + + /// Read and process the next incoming `ICMP` packet. + /// + /// We allow multiple probes to be in-flight at any time, and we cannot guarantee that responses + /// will be received in-order. We therefore maintain a buffer which holds details of each + /// `ProbeState` which is indexed by the offset of the sequence number from the sequence number + /// at the beginning of the round. The sequence number is set in the outgoing `ICMP` + /// `EchoRequest` (or `UDP` / `TCP`) packet and returned in both the `TimeExceeded` and + /// `EchoReply` responses. + /// + /// Each incoming `ICMP` packet contains the original `ICMP` `EchoRequest` packet from which we + /// can read the `identifier` that we set which we can now validate to ensure we only + /// process responses which correspond to packets sent from this process. For The `UDP` and + /// `TCP` protocols, only packets destined for our src port will be delivered to us by the + /// OS and so no other `identifier` is needed, and so we allow the special case value of 0. + /// + /// When we process an `EchoReply` from the target host we extract the time-to-live from the + /// corresponding original `EchoRequest`. Note that this may not be the greatest + /// time-to-live that was sent in the round as the algorithm will send `EchoRequest` with + /// larger time-to-live values before the `EchoReply` is received. + #[instrument(skip(self, network, st))] + fn recv_response(&self, network: &mut N, st: &mut TracerState) -> TraceResult<()> { + let next = network.recv_probe()?; + match next { + Some(ProbeResponse::TimeExceeded(data, icmp_code, extensions)) => { + let (trace_id, sequence, received, host) = self.extract(&data); + let is_target = host == self.config.target_addr; + if self.check_trace_id(trace_id) && st.in_round(sequence) && self.validate(&data) { + st.complete_probe_time_exceeded( + sequence, host, received, is_target, icmp_code, extensions, + ); + } + } + Some(ProbeResponse::DestinationUnreachable(data, icmp_code, extensions)) => { + let (trace_id, sequence, received, host) = self.extract(&data); + if self.check_trace_id(trace_id) && st.in_round(sequence) && self.validate(&data) { + st.complete_probe_unreachable(sequence, host, received, icmp_code, extensions); + } + } + Some(ProbeResponse::EchoReply(data, icmp_code)) => { + let (trace_id, sequence, received, host) = self.extract(&data); + if self.check_trace_id(trace_id) && st.in_round(sequence) && self.validate(&data) { + st.complete_probe_echo_reply(sequence, host, received, icmp_code); + } + } + Some(ProbeResponse::TcpReply(data) | ProbeResponse::TcpRefused(data)) => { + let (trace_id, sequence, received, host) = self.extract(&data); + if self.check_trace_id(trace_id) && st.in_round(sequence) && self.validate(&data) { + st.complete_probe_other(sequence, host, received); + } + } + None => {} + } + Ok(()) + } + + /// Check if the round is complete and publish the results. + /// + /// A round is considered to be complete when: + /// + /// 1 - the round has exceeded the minimum round duration AND + /// 2 - the duration since the last packet was received exceeds the grace period AND + /// 3 - either: + /// A - the target has been found OR + /// B - the target has not been found and the round has exceeded the maximum round duration + #[instrument(skip(self, st))] + fn update_round(&self, st: &mut TracerState) { + let now = SystemTime::now(); + let round_duration = now.duration_since(st.round_start()).unwrap_or_default(); + let round_min = round_duration > self.config.min_round_duration; + let grace_exceeded = exceeds(st.received_time(), now, self.config.grace_duration); + let round_max = round_duration > self.config.max_round_duration; + let target_found = st.target_found(); + if round_min && grace_exceeded && target_found || round_max { + self.publish_trace(st); + st.advance_round(self.config.first_ttl); + } + } + + /// Publish details of all `ProbeState` in the completed round. + /// + /// If the round completed without receiving an `EchoReply` from the target host then we also + /// publish the next `ProbeState` which is assumed to represent the TTL of the target host. + #[instrument(skip(self, state))] + fn publish_trace(&self, state: &TracerState) { + let max_received_ttl = if let Some(target_ttl) = state.target_ttl() { + target_ttl + } else { + state + .max_received_ttl() + .map_or(TimeToLive(0), |max_received_ttl| { + let max_sent_ttl = state.ttl() - TimeToLive(1); + max_sent_ttl.min(max_received_ttl + TimeToLive(1)) + }) + }; + let probes = state.probes(); + let largest_ttl = max_received_ttl; + let reason = if state.target_found() { + CompletionReason::TargetFound + } else { + CompletionReason::RoundTimeLimitExceeded + }; + (self.publish)(&TracerRound::new(probes, largest_ttl, reason)); + } + + /// Check if the `TraceId` matches the expected value for this tracer. + /// + /// A special value of `0` is accepted for `udp` and `tcp` which do not have an identifier. + #[instrument(skip(self))] + fn check_trace_id(&self, trace_id: TraceId) -> bool { + self.config.trace_identifier == trace_id || trace_id == TraceId(0) + } + + /// Validate the probe response data. + /// + /// Carries out specific check for UDP/TCP probe responses. This is + /// required as the network layer may receive incoming ICMP + /// `DestinationUnreachable` (and other types) packets with a UDP/TCP + /// original datagram which does not correspond to a probe sent by the + /// tracer and must therefore be ignored. + /// + /// For UDP and TCP probe responses, check that the src/dest ports and + /// dest address match the expected values. + /// + /// For ICMP probe responses no additional checks are required. + fn validate(&self, resp: &ProbeResponseData) -> bool { + fn validate_ports(port_direction: PortDirection, src_port: u16, dest_port: u16) -> bool { + match port_direction { + PortDirection::FixedSrc(src) if src.0 == src_port => true, + PortDirection::FixedDest(dest) if dest.0 == dest_port => true, + PortDirection::FixedBoth(src, dest) if src.0 == src_port && dest.0 == dest_port => { + true + } + _ => false, + } + } + match resp.resp_seq { + ProbeResponseSeq::Icmp(_) => true, + ProbeResponseSeq::Udp(ProbeResponseSeqUdp { + dest_addr, + src_port, + dest_port, + has_magic, + .. + }) => { + let check_ports = validate_ports(self.config.port_direction, src_port, dest_port); + let check_dest_addr = self.config.target_addr == dest_addr; + let check_magic = match (self.config.multipath_strategy, self.config.target_addr) { + (MultipathStrategy::Dublin, IpAddr::V6(_)) => has_magic, + _ => true, + }; + check_dest_addr && check_ports && check_magic + } + ProbeResponseSeq::Tcp(ProbeResponseSeqTcp { + dest_addr, + src_port, + dest_port, + }) => { + let check_ports = validate_ports(self.config.port_direction, src_port, dest_port); + let check_dest_addr = self.config.target_addr == dest_addr; + check_dest_addr && check_ports + } + } + } + + /// Extract the `TraceId`, `Sequence`, `SystemTime` and `IpAddr` from the `ProbeResponseData` in + /// a protocol specific way. + #[instrument(skip(self))] + fn extract(&self, resp: &ProbeResponseData) -> (TraceId, Sequence, SystemTime, IpAddr) { + match resp.resp_seq { + ProbeResponseSeq::Icmp(ProbeResponseSeqIcmp { + identifier, + sequence, + }) => ( + TraceId(identifier), + Sequence(sequence), + resp.recv, + resp.addr, + ), + ProbeResponseSeq::Udp(ProbeResponseSeqUdp { + identifier, + src_port, + dest_port, + checksum, + payload_len, + .. + }) => { + let sequence = match ( + self.config.multipath_strategy, + self.config.port_direction, + self.config.target_addr, + ) { + (MultipathStrategy::Classic, PortDirection::FixedDest(_), _) => src_port, + (MultipathStrategy::Classic, _, _) => dest_port, + (MultipathStrategy::Paris, _, _) => checksum, + (MultipathStrategy::Dublin, _, IpAddr::V4(_)) => identifier, + (MultipathStrategy::Dublin, _, IpAddr::V6(_)) => { + self.config.initial_sequence.0 + payload_len + } + }; + (TraceId(0), Sequence(sequence), resp.recv, resp.addr) + } + ProbeResponseSeq::Tcp(ProbeResponseSeqTcp { + src_port, + dest_port, + .. + }) => { + let sequence = match self.config.port_direction { + PortDirection::FixedSrc(_) => dest_port, + _ => src_port, + }; + (TraceId(0), Sequence(sequence), resp.recv, resp.addr) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::net::MockNetwork; + use crate::probe::IcmpPacketCode; + use crate::{MaxRounds, Port}; + use std::net::Ipv4Addr; + use std::num::NonZeroUsize; + + // The network can return both `DestinationUnreachable` and `TcpRefused` + // for the same sequence number. This can occur for the target hop for + // TCP protocol as the network layer check for ICMP responses such as + // `DestinationUnreachable` and also synthesizes a `TcpRefused` response. + // + // This test simulates sending 1 TCP probe (seq=33000) and receiving two + // responses for that probe, a `DestinationUnreachable` followed by a + // `TcpRefused`. + #[test] + fn test_tcp_dest_unreachable_and_refused() -> anyhow::Result<()> { + let sequence = 33000; + let target_addr = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)); + + let mut network = MockNetwork::new(); + let mut seq = mockall::Sequence::new(); + network.expect_send_probe().times(1).returning(|_| Ok(())); + network + .expect_recv_probe() + .times(1) + .in_sequence(&mut seq) + .returning(move || { + Ok(Some(ProbeResponse::DestinationUnreachable( + ProbeResponseData::new( + SystemTime::now(), + target_addr, + ProbeResponseSeq::Tcp(ProbeResponseSeqTcp::new(target_addr, sequence, 80)), + ), + IcmpPacketCode(1), + None, + ))) + }); + network + .expect_recv_probe() + .times(1) + .in_sequence(&mut seq) + .returning(move || { + Ok(Some(ProbeResponse::TcpRefused(ProbeResponseData::new( + SystemTime::now(), + target_addr, + ProbeResponseSeq::Tcp(ProbeResponseSeqTcp::new(target_addr, sequence, 80)), + )))) + }); + + let config = StrategyConfig { + target_addr, + max_rounds: Some(MaxRounds(NonZeroUsize::MIN)), + initial_sequence: Sequence(sequence), + port_direction: PortDirection::FixedDest(Port(80)), + protocol: Protocol::Tcp, + ..Default::default() + }; + let tracer = TracerStrategy::new(&config, |_| {}); + let mut state = TracerState::new(config); + tracer.send_request(&mut network, &mut state)?; + tracer.recv_response(&mut network, &mut state)?; + tracer.recv_response(&mut network, &mut state)?; + Ok(()) + } +} + +/// Mutable state needed for the tracing algorithm. +/// +/// This is contained within a submodule to ensure that mutations are only performed via methods on +/// the `TracerState` struct. +mod state { + use crate::constants::MAX_SEQUENCE_PER_ROUND; + use crate::probe::{Extensions, IcmpPacketCode, IcmpPacketType, Probe, ProbeState}; + use crate::strategy::StrategyConfig; + use crate::types::{MaxRounds, Port, Round, Sequence, TimeToLive, TraceId}; + use crate::{Flags, MultipathStrategy, PortDirection, Protocol}; + use std::array::from_fn; + use std::net::IpAddr; + use std::time::SystemTime; + use tracing::instrument; + + /// The maximum number of `ProbeState` entries in the buffer. + /// + /// This is larger than maximum number of time-to-live (TTL) we can support to allow for skipped + /// sequences. + const BUFFER_SIZE: u16 = MAX_SEQUENCE_PER_ROUND; + + /// The maximum sequence number. + /// + /// The sequence number is only ever wrapped between rounds, and so we need to ensure that there + /// are enough sequence numbers for a complete round. + /// + /// A sequence number can be skipped if, for example, the port for that sequence number cannot + /// be bound as it is already in use. + /// + /// To ensure each `ProbeState` is in the correct place in the buffer (i.e. the index into the buffer + /// is always `Probe.sequence - round_sequence`), when we skip a sequence we leave the + /// skipped `ProbeState` in-place and use the next slot for the next sequence. + /// + /// We cap the number of sequences that can potentially be skipped in a round to ensure that + /// sequence number does not even need to wrap around during a round. + /// + /// We only ever send `ttl` in the range 1..255, and so we may use all buffer capacity, except + /// the minimum needed to send up to a max `ttl` of 255 (a `ttl` of 0 is never sent). + const MAX_SEQUENCE: Sequence = Sequence(u16::MAX - BUFFER_SIZE); + + /// Mutable state needed for the tracing algorithm. + #[derive(Debug)] + pub struct TracerState { + /// Tracer configuration. + config: StrategyConfig, + /// The state of all `ProbeState` requests and responses. + buffer: [ProbeState; BUFFER_SIZE as usize], + /// An increasing sequence number for every `EchoRequest`. + sequence: Sequence, + /// The starting sequence number of the current round. + round_sequence: Sequence, + /// The time-to-live for the _next_ `EchoRequest` packet to be sent. + ttl: TimeToLive, + /// The current round. + round: Round, + /// The timestamp of when the current round started. + round_start: SystemTime, + /// Did we receive an `EchoReply` from the target host in this round? + target_found: bool, + /// The maximum time-to-live echo response packet we have received. + max_received_ttl: Option, + /// The observed time-to-live of the `EchoReply` from the target host. + /// + /// Note that this is _not_ reset each round and that it can also _change_ over time, + /// including going _down_ as responses can be received out-of-order. + target_ttl: Option, + /// The timestamp of the echo response packet. + received_time: Option, + } + + impl TracerState { + pub fn new(config: StrategyConfig) -> Self { + Self { + config, + buffer: from_fn(|_| ProbeState::default()), + sequence: config.initial_sequence, + round_sequence: config.initial_sequence, + ttl: config.first_ttl, + round: Round(0), + round_start: SystemTime::now(), + target_found: false, + max_received_ttl: None, + target_ttl: None, + received_time: None, + } + } + + /// Get a slice of `ProbeState` for the current round. + pub fn probes(&self) -> &[ProbeState] { + let round_size = self.sequence - self.round_sequence; + &self.buffer[..round_size.0 as usize] + } + + /// Get the `ProbeState` for `sequence` + pub fn probe_at(&self, sequence: Sequence) -> ProbeState { + self.buffer[usize::from(sequence - self.round_sequence)].clone() + } + + pub const fn ttl(&self) -> TimeToLive { + self.ttl + } + + pub const fn round_start(&self) -> SystemTime { + self.round_start + } + + pub const fn target_found(&self) -> bool { + self.target_found + } + + pub const fn max_received_ttl(&self) -> Option { + self.max_received_ttl + } + + pub const fn target_ttl(&self) -> Option { + self.target_ttl + } + + pub const fn received_time(&self) -> Option { + self.received_time + } + + /// Is `sequence` in the current round? + pub fn in_round(&self, sequence: Sequence) -> bool { + sequence >= self.round_sequence && sequence.0 - self.round_sequence.0 < BUFFER_SIZE + } + + /// Do we have capacity in the current round for another sequence? + pub fn round_has_capacity(&self) -> bool { + let round_size = self.sequence - self.round_sequence; + round_size.0 < BUFFER_SIZE + } + + /// Are all rounds complete? + pub fn finished(&self, max_rounds: Option) -> bool { + match max_rounds { + None => false, + Some(max_rounds) => self.round.0 > max_rounds.0.get() - 1, + } + } + + /// Create and return the next `Probe` at the current `sequence` and `ttl`. + /// + /// We post-increment `ttl` here and so in practice we only allow `ttl` values in the range + /// `1..254` to allow us to use a `u8`. + #[instrument(skip(self))] + pub fn next_probe(&mut self, sent: SystemTime) -> Probe { + let (src_port, dest_port, identifier, flags) = self.probe_data(); + let probe = Probe::new( + self.sequence, + identifier, + src_port, + dest_port, + self.ttl, + self.round, + sent, + flags, + ); + let probe_index = usize::from(self.sequence - self.round_sequence); + self.buffer[probe_index] = ProbeState::Awaited(probe.clone()); + debug_assert!(self.ttl < TimeToLive(u8::MAX)); + self.ttl += TimeToLive(1); + debug_assert!(self.sequence < Sequence(u16::MAX)); + self.sequence += Sequence(1); + probe + } + + /// Re-issue the `Probe` with the next sequence number. + /// + /// This will mark the `ProbeState` at the previous `sequence` as skipped and re-create it with + /// the previous `ttl` and the current `sequence`. + /// + /// For example, if the sequence is `4` and the `ttl` is `5` prior to calling this method + /// then afterward: + /// - The `ProbeState` at sequence `3` will be set to `Skipped` state + /// - A new `ProbeState` will be created at sequence `4` with a `ttl` of `5` + #[instrument(skip(self))] + pub fn reissue_probe(&mut self, sent: SystemTime) -> Probe { + let probe_index = usize::from(self.sequence - self.round_sequence); + self.buffer[probe_index - 1] = ProbeState::Skipped; + let (src_port, dest_port, identifier, flags) = self.probe_data(); + let probe = Probe::new( + self.sequence, + identifier, + src_port, + dest_port, + self.ttl - TimeToLive(1), + self.round, + sent, + flags, + ); + self.buffer[probe_index] = ProbeState::Awaited(probe.clone()); + debug_assert!(self.sequence < Sequence(u16::MAX)); + self.sequence += Sequence(1); + probe + } + + /// Determine the `src_port`, `dest_port` and `identifier` for the current probe. + /// + /// This will differ depending on the `TracerProtocol`, `MultipathStrategy` & + /// `PortDirection`. + fn probe_data(&self) -> (Port, Port, TraceId, Flags) { + match self.config.protocol { + Protocol::Icmp => self.probe_icmp_data(), + Protocol::Udp => self.probe_udp_data(), + Protocol::Tcp => self.probe_tcp_data(), + } + } + + /// Determine the `src_port`, `dest_port` and `identifier` for the current ICMP probe. + fn probe_icmp_data(&self) -> (Port, Port, TraceId, Flags) { + ( + Port(0), + Port(0), + self.config.trace_identifier, + Flags::empty(), + ) + } + + /// Determine the `src_port`, `dest_port` and `identifier` for the current UDP probe. + fn probe_udp_data(&self) -> (Port, Port, TraceId, Flags) { + match self.config.multipath_strategy { + MultipathStrategy::Classic => match self.config.port_direction { + PortDirection::FixedSrc(src_port) => ( + Port(src_port.0), + Port(self.sequence.0), + TraceId(0), + Flags::empty(), + ), + PortDirection::FixedDest(dest_port) => ( + Port(self.sequence.0), + Port(dest_port.0), + TraceId(0), + Flags::empty(), + ), + PortDirection::FixedBoth(_, _) | PortDirection::None => { + unimplemented!() + } + }, + MultipathStrategy::Paris => { + let round_port = ((self.config.initial_sequence.0 as usize + self.round.0) + % usize::from(u16::MAX)) as u16; + match self.config.port_direction { + PortDirection::FixedSrc(src_port) => ( + Port(src_port.0), + Port(round_port), + TraceId(0), + Flags::PARIS_CHECKSUM, + ), + PortDirection::FixedDest(dest_port) => ( + Port(round_port), + Port(dest_port.0), + TraceId(0), + Flags::PARIS_CHECKSUM, + ), + PortDirection::FixedBoth(src_port, dest_port) => ( + Port(src_port.0), + Port(dest_port.0), + TraceId(0), + Flags::PARIS_CHECKSUM, + ), + PortDirection::None => unimplemented!(), + } + } + MultipathStrategy::Dublin => { + let round_port = ((self.config.initial_sequence.0 as usize + self.round.0) + % usize::from(u16::MAX)) as u16; + match self.config.port_direction { + PortDirection::FixedSrc(src_port) => ( + Port(src_port.0), + Port(round_port), + TraceId(self.sequence.0), + Flags::DUBLIN_IPV6_PAYLOAD_LENGTH, + ), + PortDirection::FixedDest(dest_port) => ( + Port(round_port), + Port(dest_port.0), + TraceId(self.sequence.0), + Flags::DUBLIN_IPV6_PAYLOAD_LENGTH, + ), + PortDirection::FixedBoth(src_port, dest_port) => ( + Port(src_port.0), + Port(dest_port.0), + TraceId(self.sequence.0), + Flags::DUBLIN_IPV6_PAYLOAD_LENGTH, + ), + PortDirection::None => unimplemented!(), + } + } + } + } + + /// Determine the `src_port`, `dest_port` and `identifier` for the current TCP probe. + fn probe_tcp_data(&self) -> (Port, Port, TraceId, Flags) { + let (src_port, dest_port) = match self.config.port_direction { + PortDirection::FixedSrc(src_port) => (src_port.0, self.sequence.0), + PortDirection::FixedDest(dest_port) => (self.sequence.0, dest_port.0), + PortDirection::FixedBoth(_, _) | PortDirection::None => unimplemented!(), + }; + (Port(src_port), Port(dest_port), TraceId(0), Flags::empty()) + } + + /// Mark the `ProbeState` at `sequence` completed as `TimeExceeded` and update the round state. + #[instrument(skip(self))] + pub fn complete_probe_time_exceeded( + &mut self, + sequence: Sequence, + host: IpAddr, + received: SystemTime, + is_target: bool, + icmp_code: IcmpPacketCode, + extensions: Option, + ) { + self.complete_probe( + sequence, + IcmpPacketType::TimeExceeded(icmp_code), + host, + received, + is_target, + extensions, + ); + } + + /// Mark the `ProbeState` at `sequence` completed as `Unreachable` and update the round state. + #[instrument(skip(self))] + pub fn complete_probe_unreachable( + &mut self, + sequence: Sequence, + host: IpAddr, + received: SystemTime, + icmp_code: IcmpPacketCode, + extensions: Option, + ) { + self.complete_probe( + sequence, + IcmpPacketType::Unreachable(icmp_code), + host, + received, + true, + extensions, + ); + } + + /// Mark the `ProbeState` at `sequence` completed as `EchoReply` and update the round state. + #[instrument(skip(self))] + pub fn complete_probe_echo_reply( + &mut self, + sequence: Sequence, + host: IpAddr, + received: SystemTime, + icmp_code: IcmpPacketCode, + ) { + self.complete_probe( + sequence, + IcmpPacketType::EchoReply(icmp_code), + host, + received, + true, + None, + ); + } + + /// Mark the `ProbeState` at `sequence` completed as `NotApplicable` and update the round state. + #[instrument(skip(self))] + pub fn complete_probe_other( + &mut self, + sequence: Sequence, + host: IpAddr, + received: SystemTime, + ) { + self.complete_probe( + sequence, + IcmpPacketType::NotApplicable, + host, + received, + true, + None, + ); + } + + /// Update the state of a `ProbeState` and the trace. + /// + /// We want to update: + /// + /// - the `target_ttl` to be the time-to-live of the `ProbeState` request from the target + /// - the `max_received_ttl` we have observed this round + /// - the latest packet `received_time` in this round + /// - whether the target has been found in this round + /// + /// The ICMP replies may arrive out-of-order, and so we must be careful here to avoid + /// overwriting the state with stale values. We may also receive multiple replies + /// from the target host with differing time-to-live values and so must ensure we + /// use the time-to-live with the lowest sequence number. + #[instrument(skip(self))] + fn complete_probe( + &mut self, + sequence: Sequence, + icmp_packet_type: IcmpPacketType, + host: IpAddr, + received: SystemTime, + is_target: bool, + extensions: Option, + ) { + // Retrieve and update the `ProbeState` at `sequence`. + let probe = self.probe_at(sequence); + let awaited = match probe { + ProbeState::Awaited(awaited) => awaited, + // there is a valid scenario for TCP where a probe is already + // `Complete`, see `test_tcp_dest_unreachable_and_refused`. + ProbeState::Complete(_) => { + return; + } + _ => { + debug_assert!( + false, + "completed probe was not in Awaited state (probe={probe:#?})" + ); + return; + } + }; + let completed = awaited.complete(host, received, icmp_packet_type, extensions); + let ttl = completed.ttl; + self.buffer[usize::from(sequence - self.round_sequence)] = + ProbeState::Complete(completed); + + // If this `ProbeState` found the target then we set the `target_tll` if not already set, + // being careful to account for `Probes` being received out-of-order. + // + // If this `ProbeState` did not find the target but has a ttl that is greater or equal to the + // target ttl (if known) then we reset the target ttl to None. This is to + // support Equal Cost Multi-path Routing (ECMP) cases where the number of + // hops to the target will vary over the lifetime of the trace. + self.target_ttl = if is_target { + match self.target_ttl { + None => Some(ttl), + Some(target_ttl) if ttl < target_ttl => Some(ttl), + Some(target_ttl) => Some(target_ttl), + } + } else { + match self.target_ttl { + Some(target_ttl) if ttl >= target_ttl => None, + Some(target_ttl) => Some(target_ttl), + None => None, + } + }; + + self.max_received_ttl = match self.max_received_ttl { + None => Some(ttl), + Some(max_received_ttl) => Some(max_received_ttl.max(ttl)), + }; + + self.received_time = Some(received); + self.target_found |= is_target; + } + + /// Advance to the next round. + /// + /// If, during the rond which just completed, we went above the max sequence number then we + /// reset it here. We do this here to avoid having to deal with the sequence number + /// wrapping during a round, which is more problematic. + #[instrument(skip(self))] + pub fn advance_round(&mut self, first_ttl: TimeToLive) { + if self.sequence >= self.max_sequence() { + self.sequence = self.config.initial_sequence; + } + self.target_found = false; + self.round_sequence = self.sequence; + self.received_time = None; + self.round_start = SystemTime::now(); + self.max_received_ttl = None; + self.round += Round(1); + self.ttl = first_ttl; + } + + /// The maximum sequence number allowed. + /// + /// The Dublin multipath strategy for IPv6/udp encodes the sequence + /// number as the payload length and consequently the maximum sequence + /// number must be no larger than the maximum IPv6/udp payload size. + /// + /// It is also required that the range of possible sequence numbers is + /// _at least_ `BUFFER_SIZE` to ensure delayed responses from a prior + /// round are not incorrectly associated with later rounds (see + /// `in_round` function). + fn max_sequence(&self) -> Sequence { + match (self.config.multipath_strategy, self.config.target_addr) { + (MultipathStrategy::Dublin, IpAddr::V6(_)) => { + self.config.initial_sequence + Sequence(BUFFER_SIZE) + } + _ => MAX_SEQUENCE, + } + } + } + + #[cfg(test)] + mod tests { + use super::*; + use crate::probe::IcmpPacketType; + use crate::types::MaxInflight; + use rand::Rng; + use std::net::{IpAddr, Ipv4Addr}; + use std::time::Duration; + + #[allow( + clippy::cognitive_complexity, + clippy::too_many_lines, + clippy::bool_assert_comparison + )] + #[test] + fn test_state() { + let mut state = TracerState::new(cfg(Sequence(33000))); + + // Validate the initial TracerState + assert_eq!(state.round, Round(0)); + assert_eq!(state.sequence, Sequence(33000)); + assert_eq!(state.round_sequence, Sequence(33000)); + assert_eq!(state.ttl, TimeToLive(1)); + assert_eq!(state.max_received_ttl, None); + assert_eq!(state.received_time, None); + assert_eq!(state.target_ttl, None); + assert_eq!(state.target_found, false); + + // The initial state of the probe before sending + let prob_init = state.probe_at(Sequence(33000)); + assert_eq!(ProbeState::NotSent, prob_init); + + // Prepare probe 1 (round 0, sequence 33000, ttl 1) for sending + let sent_1 = SystemTime::now(); + let probe_1 = state.next_probe(sent_1); + assert_eq!(probe_1.sequence, Sequence(33000)); + assert_eq!(probe_1.ttl, TimeToLive(1)); + assert_eq!(probe_1.round, Round(0)); + assert_eq!(probe_1.sent, sent_1); + + // Update the state of the probe 1 after receiving a TimeExceeded + let received_1 = SystemTime::now(); + let host = IpAddr::V4(Ipv4Addr::LOCALHOST); + state.complete_probe_time_exceeded( + Sequence(33000), + host, + received_1, + false, + IcmpPacketCode(1), + None, + ); + + // Validate the state of the probe 1 after the update + let probe_1_fetch = state.probe_at(Sequence(33000)).try_into_complete().unwrap(); + assert_eq!(probe_1_fetch.sequence, Sequence(33000)); + assert_eq!(probe_1_fetch.ttl, TimeToLive(1)); + assert_eq!(probe_1_fetch.round, Round(0)); + assert_eq!(probe_1_fetch.received, received_1); + assert_eq!(probe_1_fetch.host, host); + assert_eq!(probe_1_fetch.sent, sent_1); + assert_eq!( + probe_1_fetch.icmp_packet_type, + IcmpPacketType::TimeExceeded(IcmpPacketCode(1)) + ); + + // Validate the TracerState after the update + assert_eq!(state.round, Round(0)); + assert_eq!(state.sequence, Sequence(33001)); + assert_eq!(state.round_sequence, Sequence(33000)); + assert_eq!(state.ttl, TimeToLive(2)); + assert_eq!(state.max_received_ttl, Some(TimeToLive(1))); + assert_eq!(state.received_time, Some(received_1)); + assert_eq!(state.target_ttl, None); + assert_eq!(state.target_found, false); + + // Validate the probes() iterator returns only a single probe + { + let mut probe_iter = state.probes().iter(); + let probe_next1 = probe_iter.next().unwrap(); + assert_eq!(ProbeState::Complete(probe_1_fetch), probe_next1.clone()); + assert_eq!(None, probe_iter.next()); + } + + // Advance to the next round + state.advance_round(TimeToLive(1)); + + // Validate the TracerState after the round update + assert_eq!(state.round, Round(1)); + assert_eq!(state.sequence, Sequence(33001)); + assert_eq!(state.round_sequence, Sequence(33001)); + assert_eq!(state.ttl, TimeToLive(1)); + assert_eq!(state.max_received_ttl, None); + assert_eq!(state.received_time, None); + assert_eq!(state.target_ttl, None); + assert_eq!(state.target_found, false); + + // Prepare probe 2 (round 1, sequence 33001, ttl 1) for sending + let sent_2 = SystemTime::now(); + let probe_2 = state.next_probe(sent_2); + assert_eq!(probe_2.sequence, Sequence(33001)); + assert_eq!(probe_2.ttl, TimeToLive(1)); + assert_eq!(probe_2.round, Round(1)); + assert_eq!(probe_2.sent, sent_2); + + // Prepare probe 3 (round 1, sequence 33002, ttl 2) for sending + let sent_3 = SystemTime::now(); + let probe_3 = state.next_probe(sent_3); + assert_eq!(probe_3.sequence, Sequence(33002)); + assert_eq!(probe_3.ttl, TimeToLive(2)); + assert_eq!(probe_3.round, Round(1)); + assert_eq!(probe_3.sent, sent_3); + + // Update the state of probe 2 after receiving a TimeExceeded + let received_2 = SystemTime::now(); + let host = IpAddr::V4(Ipv4Addr::LOCALHOST); + state.complete_probe_time_exceeded( + Sequence(33001), + host, + received_2, + false, + IcmpPacketCode(1), + None, + ); + let probe_2_recv = state.probe_at(Sequence(33001)); + + // Validate the TracerState after the update to probe 2 + assert_eq!(state.round, Round(1)); + assert_eq!(state.sequence, Sequence(33003)); + assert_eq!(state.round_sequence, Sequence(33001)); + assert_eq!(state.ttl, TimeToLive(3)); + assert_eq!(state.max_received_ttl, Some(TimeToLive(1))); + assert_eq!(state.received_time, Some(received_2)); + assert_eq!(state.target_ttl, None); + assert_eq!(state.target_found, false); + + // Validate the probes() iterator returns the two probes in the states we expect + { + let mut probe_iter = state.probes().iter(); + let probe_next1 = probe_iter.next().unwrap(); + assert_eq!(&probe_2_recv, probe_next1); + let probe_next2 = probe_iter.next().unwrap(); + assert_eq!(ProbeState::Awaited(probe_3), probe_next2.clone()); + } + + // Update the state of probe 3 after receiving a EchoReply + let received_3 = SystemTime::now(); + let host = IpAddr::V4(Ipv4Addr::LOCALHOST); + state.complete_probe_echo_reply(Sequence(33002), host, received_3, IcmpPacketCode(0)); + let probe_3_recv = state.probe_at(Sequence(33002)); + + // Validate the TracerState after the update to probe 3 + assert_eq!(state.round, Round(1)); + assert_eq!(state.sequence, Sequence(33003)); + assert_eq!(state.round_sequence, Sequence(33001)); + assert_eq!(state.ttl, TimeToLive(3)); + assert_eq!(state.max_received_ttl, Some(TimeToLive(2))); + assert_eq!(state.received_time, Some(received_3)); + assert_eq!(state.target_ttl, Some(TimeToLive(2))); + assert_eq!(state.target_found, true); + + // Validate the probes() iterator returns the two probes in the states we expect + { + let mut probe_iter = state.probes().iter(); + let probe_next1 = probe_iter.next().unwrap(); + assert_eq!(&probe_2_recv, probe_next1); + let probe_next2 = probe_iter.next().unwrap(); + assert_eq!(&probe_3_recv, probe_next2); + } + } + + #[test] + fn test_sequence_wrap1() { + // Start from MAX_SEQUENCE - 1 which is (65279 - 1) == 65278 + let initial_sequence = Sequence(65278); + let mut state = TracerState::new(cfg(initial_sequence)); + assert_eq!(state.round, Round(0)); + assert_eq!(state.sequence, initial_sequence); + assert_eq!(state.round_sequence, initial_sequence); + + // Create a probe at seq 65278 + assert_eq!( + state.next_probe(SystemTime::now()).sequence, + Sequence(65278) + ); + assert_eq!(state.sequence, Sequence(65279)); + + // Validate the probes() + { + let mut iter = state.probes().iter(); + assert_eq!( + iter.next() + .unwrap() + .clone() + .try_into_awaited() + .unwrap() + .sequence, + Sequence(65278) + ); + iter.take(BUFFER_SIZE as usize - 1) + .for_each(|p| assert!(matches!(p, ProbeState::NotSent))); + } + + // Advance the round, which will wrap the sequence back to initial_sequence + state.advance_round(TimeToLive(1)); + assert_eq!(state.round, Round(1)); + assert_eq!(state.sequence, initial_sequence); + assert_eq!(state.round_sequence, initial_sequence); + + // Create a probe at seq 65278 + assert_eq!( + state.next_probe(SystemTime::now()).sequence, + Sequence(65278) + ); + assert_eq!(state.sequence, Sequence(65279)); + + // Validate the probes() again + { + let mut iter = state.probes().iter(); + assert_eq!( + iter.next() + .unwrap() + .clone() + .try_into_awaited() + .unwrap() + .sequence, + Sequence(65278) + ); + iter.take(BUFFER_SIZE as usize - 1) + .for_each(|p| assert!(matches!(p, ProbeState::NotSent))); + } + } + + #[test] + fn test_sequence_wrap2() { + let total_rounds = 2000; + let max_probe_per_round = 254; + let mut state = TracerState::new(cfg(Sequence(33000))); + for _ in 0..total_rounds { + for _ in 0..max_probe_per_round { + let _probe = state.next_probe(SystemTime::now()); + } + state.advance_round(TimeToLive(1)); + } + assert_eq!(state.round, Round(2000)); + assert_eq!(state.round_sequence, Sequence(57130)); + assert_eq!(state.sequence, Sequence(57130)); + } + + #[test] + fn test_sequence_wrap3() { + let total_rounds = 2000; + let max_probe_per_round = 20; + let mut state = TracerState::new(cfg(Sequence(33000))); + let mut rng = rand::thread_rng(); + for _ in 0..total_rounds { + for _ in 0..rng.gen_range(0..max_probe_per_round) { + state.next_probe(SystemTime::now()); + } + state.advance_round(TimeToLive(1)); + } + } + + #[test] + fn test_sequence_wrap_with_skip() { + let total_rounds = 2000; + let max_probe_per_round = 254; + let mut state = TracerState::new(cfg(Sequence(33000))); + for _ in 0..total_rounds { + for _ in 0..max_probe_per_round { + _ = state.next_probe(SystemTime::now()); + _ = state.reissue_probe(SystemTime::now()); + } + state.advance_round(TimeToLive(1)); + } + assert_eq!(state.round, Round(2000)); + assert_eq!(state.round_sequence, Sequence(41128)); + assert_eq!(state.sequence, Sequence(41128)); + } + + #[test] + fn test_in_round() { + let state = TracerState::new(cfg(Sequence(33000))); + assert!(state.in_round(Sequence(33000))); + assert!(state.in_round(Sequence(33511))); + assert!(!state.in_round(Sequence(33512))); + } + + #[test] + #[should_panic(expected = "assertion failed: !state.in_round(Sequence(64491))")] + fn test_in_delayed_probe_not_in_round() { + let mut state = TracerState::new(cfg(Sequence(64000))); + for _ in 0..55 { + _ = state.next_probe(SystemTime::now()); + } + state.advance_round(TimeToLive(1)); + assert!(!state.in_round(Sequence(64491))); + } + + fn cfg(initial_sequence: Sequence) -> StrategyConfig { + StrategyConfig { + target_addr: IpAddr::V4(Ipv4Addr::UNSPECIFIED), + protocol: Protocol::Icmp, + trace_identifier: TraceId::default(), + max_rounds: None, + first_ttl: TimeToLive(1), + max_ttl: TimeToLive(24), + grace_duration: Duration::default(), + max_inflight: MaxInflight::default(), + initial_sequence, + multipath_strategy: MultipathStrategy::Classic, + port_direction: PortDirection::None, + min_round_duration: Duration::default(), + max_round_duration: Duration::default(), + } + } + } +} + +/// Returns true if the duration between start and end is grater than a duration, false otherwise. +fn exceeds(start: Option, end: SystemTime, dur: Duration) -> bool { + start.map_or(false, |start| { + end.duration_since(start).unwrap_or_default() > dur + }) +} diff --git a/crates/trippy-core/src/tracer.rs b/crates/trippy-core/src/tracer.rs index 2d6643920..b92199aab 100644 --- a/crates/trippy-core/src/tracer.rs +++ b/crates/trippy-core/src/tracer.rs @@ -1,1230 +1,709 @@ -use self::state::TracerState; -use crate::error::{TraceResult, TracerError}; -use crate::net::Network; -use crate::probe::{ - ProbeResponse, ProbeResponseData, ProbeResponseSeq, ProbeResponseSeqIcmp, ProbeResponseSeqTcp, - ProbeResponseSeqUdp, ProbeState, +use crate::error::TraceResult; +use crate::{ + IcmpExtensionParseMode, MaxInflight, MaxRounds, MultipathStrategy, PacketSize, PayloadPattern, + PortDirection, PrivilegeMode, Protocol, Sequence, TimeToLive, TraceId, TraceState, TracerError, + TracerRound, TypeOfService, }; -use crate::types::{Sequence, TimeToLive, TraceId}; -use crate::Config; -use crate::{MultipathStrategy, PortDirection, Protocol}; +use std::fmt::Debug; use std::net::IpAddr; -use std::time::{Duration, SystemTime}; -use tracing::instrument; +use std::sync::Arc; +use std::thread; +use std::thread::JoinHandle; +use std::time::Duration; -/// The output from a round of tracing. +/// A traceroute implementation. +/// +/// See the [`crate`] documentation for more information. +/// +/// Note that this is type cheaply cloneable. #[derive(Debug, Clone)] -pub struct TracerRound<'a> { - /// The state of all `ProbeState` that were sent in the round. - pub probes: &'a [ProbeState], - /// The largest time-to-live (ttl) for which we received a reply in the round. - pub largest_ttl: TimeToLive, - /// Indicates what triggered the completion of the tracing round. - pub reason: CompletionReason, +pub struct Tracer { + inner: Arc, } -impl<'a> TracerRound<'a> { +impl Tracer { + /// Create a `Tracer`. + /// + /// Use the [`crate::Builder`] type to create a [`Tracer`]. + #[allow(clippy::too_many_arguments)] #[must_use] - pub fn new( - probes: &'a [ProbeState], - largest_ttl: TimeToLive, - reason: CompletionReason, + pub(crate) fn new( + interface: Option, + source_addr: Option, + target_addr: IpAddr, + privilege_mode: PrivilegeMode, + protocol: Protocol, + packet_size: PacketSize, + payload_pattern: PayloadPattern, + tos: TypeOfService, + icmp_extension_parse_mode: IcmpExtensionParseMode, + read_timeout: Duration, + tcp_connect_timeout: Duration, + trace_identifier: TraceId, + max_rounds: Option, + first_ttl: TimeToLive, + max_ttl: TimeToLive, + grace_duration: Duration, + max_inflight: MaxInflight, + initial_sequence: Sequence, + multipath_strategy: MultipathStrategy, + port_direction: PortDirection, + min_round_duration: Duration, + max_round_duration: Duration, + max_samples: usize, + max_flows: usize, + drop_privileges: bool, ) -> Self { Self { - probes, - largest_ttl, - reason, - } - } -} - -/// Indicates what triggered the completion of the tracing round. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum CompletionReason { - /// The round ended because the target was found. - TargetFound, - /// The round ended because the time exceeded the configured maximum round time. - RoundTimeLimitExceeded, -} - -/// Trace a path to a target. -#[derive(Debug, Clone)] -pub struct Tracer { - config: Config, - publish: F, -} - -impl)> Tracer { - #[instrument(skip_all)] - pub fn new(config: &Config, publish: F) -> Self { - tracing::debug!(?config); - Self { - config: *config, - publish, + inner: Arc::new(inner::TracerInner::new( + interface, + source_addr, + target_addr, + privilege_mode, + protocol, + packet_size, + payload_pattern, + tos, + icmp_extension_parse_mode, + read_timeout, + tcp_connect_timeout, + trace_identifier, + max_rounds, + first_ttl, + max_ttl, + grace_duration, + max_inflight, + initial_sequence, + multipath_strategy, + port_direction, + min_round_duration, + max_round_duration, + max_samples, + max_flows, + drop_privileges, + )), } } - /// Run a continuous trace and publish results. - #[instrument(skip(self, network))] - pub fn trace(self, mut network: N) -> TraceResult<()> { - let mut state = TracerState::new(self.config); - while !state.finished(self.config.max_rounds) { - self.send_request(&mut network, &mut state)?; - self.recv_response(&mut network, &mut state)?; - self.update_round(&mut state); - } - Ok(()) + /// Run the [`Tracer`]. + /// + /// This method will block until either the trace completes all rounds (if + /// [`crate::Builder::max_rounds`] has been called to set to a non-zero + /// value) or until the trace fails. + /// + /// At the completion of the trace, the state of the tracer can be + /// retrieved using the [`Tracer::snapshot`] method. + /// + /// If you want to run the tracer indefinitely (by not setting + /// [`crate::Builder::max_rounds`]), you can either clone and run the + /// tracer on a separate thread by using the [`Tracer::spawn`] method or + /// by use the [`Tracer::run_with`] method in the current thread to gather + /// pee round state manually. + /// + /// # Example + /// + /// The following will run the tracer for a fixed number (3) of rounds and + /// then retrieve the final state snapshot: + /// + /// ```no_run + /// # fn main() -> anyhow::Result<()> { + /// # use std::net::IpAddr; + /// # use std::str::FromStr; + /// use trippy_core::Builder; + /// + /// let addr = IpAddr::from_str("1.1.1.1")?; + /// let tracer = Builder::new(addr).max_rounds(Some(3)).build()?; + /// tracer.run()?; + /// let _state = tracer.snapshot(); + /// # Ok(()) + /// # } + /// ``` + /// + /// # See Also + /// + /// - [`Tracer::run_with`] - Run the tracer with a custom round handler. + /// - [`Tracer::spawn`] - Spawn the tracer on a new thread without a + /// custom round handler. + pub fn run(&self) -> TraceResult<()> { + self.inner.run() } - /// Send the next probe if required. + /// Run the [`Tracer`] with a custom round handler. /// - /// Send a `ProbeState` for the next time-to-live (ttl) if all the following are true: + /// This method will block until either the trace completes all rounds (if + /// [`crate::Builder::max_rounds`] has been called to set to a non-zero + /// value) or until the trace fails. /// - /// 1 - the target host has not been found - /// 2 - the next ttl is not greater than the maximum allowed ttl - /// 3 - if the target ttl of the target is known: - /// - the next ttl is not greater than the ttl of the target host observed from the prior - /// round - /// otherwise: - /// - the number of unknown-in-flight probes is lower than the maximum allowed - #[instrument(skip(self, network, st))] - fn send_request(&self, network: &mut N, st: &mut TracerState) -> TraceResult<()> { - let can_send_ttl = if let Some(target_ttl) = st.target_ttl() { - st.ttl() <= target_ttl - } else { - st.ttl() - st.max_received_ttl().unwrap_or_default() - < TimeToLive(self.config.max_inflight.0) - }; - if !st.target_found() && st.ttl() <= self.config.max_ttl && can_send_ttl { - let sent = SystemTime::now(); - match self.config.protocol { - Protocol::Icmp => { - network.send_probe(st.next_probe(sent))?; - } - Protocol::Udp => network.send_probe(st.next_probe(sent))?, - Protocol::Tcp => { - let mut probe = if st.round_has_capacity() { - st.next_probe(sent) - } else { - return Err(TracerError::InsufficientCapacity); - }; - while let Err(err) = network.send_probe(probe) { - match err { - TracerError::AddressNotAvailable(_) => { - if st.round_has_capacity() { - probe = st.reissue_probe(SystemTime::now()); - } else { - return Err(TracerError::InsufficientCapacity); - } - } - other => return Err(other), - } - } - } - }; - } - Ok(()) + /// At the completion of the trace, the state of the tracer can be + /// retrieved using the [`Tracer::snapshot`] method. + /// + /// This method will additionally call the provided function for each round + /// that is completed. This can be useful if you want to gather round state + /// manually if the tracer is run indefinitely (by not setting + /// [`crate::Builder::max_rounds`]) + /// + /// # Example + /// + /// The following will run the tracer indefinitely and print the data from + /// each round of tracing: + /// + /// ```no_run + /// # fn main() -> anyhow::Result<()> { + /// # use std::net::IpAddr; + /// # use std::str::FromStr; + /// use trippy_core::Builder; + /// + /// let addr = IpAddr::from_str("1.1.1.1")?; + /// let tracer = Builder::new(addr).build()?; + /// tracer.run_with(|round| println!("{:?}", round))?; + /// # Ok(()) + /// # } + /// ``` + /// + /// # See Also + /// + /// - [`Tracer::run`] - Run the tracer without a custom round handler. + pub fn run_with)>(&self, func: F) -> TraceResult<()> { + self.inner.run_with(func) } - /// Read and process the next incoming `ICMP` packet. + /// Spawn the tracer on a new thread. /// - /// We allow multiple probes to be in-flight at any time, and we cannot guarantee that responses - /// will be received in-order. We therefore maintain a buffer which holds details of each - /// `ProbeState` which is indexed by the offset of the sequence number from the sequence number - /// at the beginning of the round. The sequence number is set in the outgoing `ICMP` - /// `EchoRequest` (or `UDP` / `TCP`) packet and returned in both the `TimeExceeded` and - /// `EchoReply` responses. + /// This method will spawn a new thread to run the tracer and immediately + /// return the [`Tracer`] and a handle to the thread, so it may be joined + /// with [`JoinHandle::join`]. /// - /// Each incoming `ICMP` packet contains the original `ICMP` `EchoRequest` packet from which we - /// can read the `identifier` that we set which we can now validate to ensure we only - /// process responses which correspond to packets sent from this process. For The `UDP` and - /// `TCP` protocols, only packets destined for our src port will be delivered to us by the - /// OS and so no other `identifier` is needed, and so we allow the special case value of 0. + /// If you want to run the tracer indefinitely (by not setting + /// [`crate::Builder::max_rounds`]) you can use this method to spawn the + /// tracer on a new thread and return the [`Tracer`] such that a + /// [`Tracer::snapshot`] of the state can be taken at any time. /// - /// When we process an `EchoReply` from the target host we extract the time-to-live from the - /// corresponding original `EchoRequest`. Note that this may not be the greatest - /// time-to-live that was sent in the round as the algorithm will send `EchoRequest` with - /// larger time-to-live values before the `EchoReply` is received. - #[instrument(skip(self, network, st))] - fn recv_response(&self, network: &mut N, st: &mut TracerState) -> TraceResult<()> { - let next = network.recv_probe()?; - match next { - Some(ProbeResponse::TimeExceeded(data, icmp_code, extensions)) => { - let (trace_id, sequence, received, host) = self.extract(&data); - let is_target = host == self.config.target_addr; - if self.check_trace_id(trace_id) && st.in_round(sequence) && self.validate(&data) { - st.complete_probe_time_exceeded( - sequence, host, received, is_target, icmp_code, extensions, - ); - } - } - Some(ProbeResponse::DestinationUnreachable(data, icmp_code, extensions)) => { - let (trace_id, sequence, received, host) = self.extract(&data); - if self.check_trace_id(trace_id) && st.in_round(sequence) && self.validate(&data) { - st.complete_probe_unreachable(sequence, host, received, icmp_code, extensions); - } - } - Some(ProbeResponse::EchoReply(data, icmp_code)) => { - let (trace_id, sequence, received, host) = self.extract(&data); - if self.check_trace_id(trace_id) && st.in_round(sequence) && self.validate(&data) { - st.complete_probe_echo_reply(sequence, host, received, icmp_code); - } - } - Some(ProbeResponse::TcpReply(data) | ProbeResponse::TcpRefused(data)) => { - let (trace_id, sequence, received, host) = self.extract(&data); - if self.check_trace_id(trace_id) && st.in_round(sequence) && self.validate(&data) { - st.complete_probe_other(sequence, host, received); - } - } - None => {} - } - Ok(()) + /// # Example + /// + /// The following will spawn a tracer on a new thread and take a snapshot + /// of the state every 5 seconds: + /// + /// ```no_run + /// # fn main() -> anyhow::Result<()> { + /// # use std::net::IpAddr; + /// # use std::str::FromStr; + /// # use std::thread; + /// # use std::time::Duration; + /// use trippy_core::Builder; + /// + /// let addr = IpAddr::from_str("1.1.1.1")?; + /// let (tracer, _) = Builder::new(addr).build()?.spawn()?; + /// loop { + /// thread::sleep(Duration::from_secs(5)); + /// // get the latest state. + /// let _state = tracer.snapshot(); + /// } + /// # Ok(()) + /// # } + /// ``` + /// + /// # See Also + /// + /// - [`Tracer::run`] - Run the tracer on the current thread. + pub fn spawn(self) -> TraceResult<(Self, JoinHandle>)> { + let tracer = self.clone(); + let handle = thread::Builder::new() + .name(format!("tracer-{}", self.trace_identifier().0)) + .spawn(move || tracer.run()) + .map_err(|err| TracerError::Other(err.to_string()))?; + Ok((self, handle)) } - /// Check if the round is complete and publish the results. + /// Spawn the tracer with a custom round handler on a new thread. /// - /// A round is considered to be complete when: + /// This method will spawn a new thread to run the tracer with a custom + /// round handler and immediately return the [`Tracer`] and a handle to the + /// thread, so it may be joined with [`JoinHandle::join`]. /// - /// 1 - the round has exceeded the minimum round duration AND - /// 2 - the duration since the last packet was received exceeds the grace period AND - /// 3 - either: - /// A - the target has been found OR - /// B - the target has not been found and the round has exceeded the maximum round duration - #[instrument(skip(self, st))] - fn update_round(&self, st: &mut TracerState) { - let now = SystemTime::now(); - let round_duration = now.duration_since(st.round_start()).unwrap_or_default(); - let round_min = round_duration > self.config.min_round_duration; - let grace_exceeded = exceeds(st.received_time(), now, self.config.grace_duration); - let round_max = round_duration > self.config.max_round_duration; - let target_found = st.target_found(); - if round_min && grace_exceeded && target_found || round_max { - self.publish_trace(st); - st.advance_round(self.config.first_ttl); - } + /// # Example + /// + /// The following will spawn a tracer on a new thread with a custom round + /// handler to print the data from each round of tracing and also take a + /// snapshot of the state every 5 seconds until the tracer completes all + /// rounds: + /// + /// ```no_run + /// # fn main() -> anyhow::Result<()> { + /// # use std::net::IpAddr; + /// # use std::str::FromStr; + /// # use std::thread; + /// # use std::time::Duration; + /// use trippy_core::Builder; + /// + /// let addr = IpAddr::from_str("1.1.1.1")?; + /// let (tracer, handle) = Builder::new(addr) + /// .max_rounds(Some(3)) + /// .build()? + /// .spawn_with(|round| println!("{:?}", round))?; + /// for i in 0..3 { + /// thread::sleep(Duration::from_secs(5)); + /// // get the latest state. + /// let _state = tracer.snapshot(); + /// } + /// handle.join().unwrap()?; + /// # Ok(()) + /// # } + /// ``` + /// + /// # See Also + /// + /// - [`Tracer::spawn`] - Spawn the tracer on a new thread without a + /// custom round handler. + pub fn spawn_with) + Send + 'static>( + self, + func: F, + ) -> TraceResult<(Self, JoinHandle>)> { + let tracer = self.clone(); + let handle = thread::Builder::new() + .name(format!("tracer-{}", self.trace_identifier().0)) + .spawn(move || tracer.run_with(func)) + .map_err(|err| TracerError::Other(err.to_string()))?; + Ok((self, handle)) } - /// Publish details of all `ProbeState` in the completed round. - /// - /// If the round completed without receiving an `EchoReply` from the target host then we also - /// publish the next `ProbeState` which is assumed to represent the TTL of the target host. - #[instrument(skip(self, state))] - fn publish_trace(&self, state: &TracerState) { - let max_received_ttl = if let Some(target_ttl) = state.target_ttl() { - target_ttl - } else { - state - .max_received_ttl() - .map_or(TimeToLive(0), |max_received_ttl| { - let max_sent_ttl = state.ttl() - TimeToLive(1); - max_sent_ttl.min(max_received_ttl + TimeToLive(1)) - }) - }; - let probes = state.probes(); - let largest_ttl = max_received_ttl; - let reason = if state.target_found() { - CompletionReason::TargetFound - } else { - CompletionReason::RoundTimeLimitExceeded - }; - (self.publish)(&TracerRound::new(probes, largest_ttl, reason)); + /// Take a snapshot of the tracer state. + #[must_use] + pub fn snapshot(&self) -> TraceState { + self.inner.snapshot() } - /// Check if the `TraceId` matches the expected value for this tracer. - /// - /// A special value of `0` is accepted for `udp` and `tcp` which do not have an identifier. - #[instrument(skip(self))] - fn check_trace_id(&self, trace_id: TraceId) -> bool { - self.config.trace_identifier == trace_id || trace_id == TraceId(0) + /// Clear the tracer state. + pub fn clear(&self) { + self.inner.clear(); } - /// Validate the probe response data. - /// - /// Carries out specific check for UDP/TCP probe responses. This is - /// required as the network layer may receive incoming ICMP - /// `DestinationUnreachable` (and other types) packets with a UDP/TCP - /// original datagram which does not correspond to a probe sent by the - /// tracer and must therefore be ignored. - /// - /// For UDP and TCP probe responses, check that the src/dest ports and - /// dest address match the expected values. - /// - /// For ICMP probe responses no additional checks are required. - fn validate(&self, resp: &ProbeResponseData) -> bool { - fn validate_ports(port_direction: PortDirection, src_port: u16, dest_port: u16) -> bool { - match port_direction { - PortDirection::FixedSrc(src) if src.0 == src_port => true, - PortDirection::FixedDest(dest) if dest.0 == dest_port => true, - PortDirection::FixedBoth(src, dest) if src.0 == src_port && dest.0 == dest_port => { - true - } - _ => false, - } - } - match resp.resp_seq { - ProbeResponseSeq::Icmp(_) => true, - ProbeResponseSeq::Udp(ProbeResponseSeqUdp { - dest_addr, - src_port, - dest_port, - has_magic, - .. - }) => { - let check_ports = validate_ports(self.config.port_direction, src_port, dest_port); - let check_dest_addr = self.config.target_addr == dest_addr; - let check_magic = match (self.config.multipath_strategy, self.config.target_addr) { - (MultipathStrategy::Dublin, IpAddr::V6(_)) => has_magic, - _ => true, - }; - check_dest_addr && check_ports && check_magic - } - ProbeResponseSeq::Tcp(ProbeResponseSeqTcp { - dest_addr, - src_port, - dest_port, - }) => { - let check_ports = validate_ports(self.config.port_direction, src_port, dest_port); - let check_dest_addr = self.config.target_addr == dest_addr; - check_dest_addr && check_ports - } - } + #[must_use] + pub fn max_flows(&self) -> usize { + self.inner.max_flows() } - /// Extract the `TraceId`, `Sequence`, `SystemTime` and `IpAddr` from the `ProbeResponseData` in - /// a protocol specific way. - #[instrument(skip(self))] - fn extract(&self, resp: &ProbeResponseData) -> (TraceId, Sequence, SystemTime, IpAddr) { - match resp.resp_seq { - ProbeResponseSeq::Icmp(ProbeResponseSeqIcmp { - identifier, - sequence, - }) => ( - TraceId(identifier), - Sequence(sequence), - resp.recv, - resp.addr, - ), - ProbeResponseSeq::Udp(ProbeResponseSeqUdp { - identifier, - src_port, - dest_port, - checksum, - payload_len, - .. - }) => { - let sequence = match ( - self.config.multipath_strategy, - self.config.port_direction, - self.config.target_addr, - ) { - (MultipathStrategy::Classic, PortDirection::FixedDest(_), _) => src_port, - (MultipathStrategy::Classic, _, _) => dest_port, - (MultipathStrategy::Paris, _, _) => checksum, - (MultipathStrategy::Dublin, _, IpAddr::V4(_)) => identifier, - (MultipathStrategy::Dublin, _, IpAddr::V6(_)) => { - self.config.initial_sequence.0 + payload_len - } - }; - (TraceId(0), Sequence(sequence), resp.recv, resp.addr) - } - ProbeResponseSeq::Tcp(ProbeResponseSeqTcp { - src_port, - dest_port, - .. - }) => { - let sequence = match self.config.port_direction { - PortDirection::FixedSrc(_) => dest_port, - _ => src_port, - }; - (TraceId(0), Sequence(sequence), resp.recv, resp.addr) - } - } + #[must_use] + pub fn max_samples(&self) -> usize { + self.inner.max_samples() } -} -#[cfg(test)] -mod tests { - use super::*; - use crate::net::MockNetwork; - use crate::probe::IcmpPacketCode; - use crate::{MaxRounds, Port}; - use std::net::Ipv4Addr; - use std::num::NonZeroUsize; - - // The network can return both `DestinationUnreachable` and `TcpRefused` - // for the same sequence number. This can occur for the target hop for - // TCP protocol as the network layer check for ICMP responses such as - // `DestinationUnreachable` and also synthesizes a `TcpRefused` response. - // - // This test simulates sending 1 TCP probe (seq=33000) and receiving two - // responses for that probe, a `DestinationUnreachable` followed by a - // `TcpRefused`. - #[test] - fn test_tcp_dest_unreachable_and_refused() -> anyhow::Result<()> { - let sequence = 33000; - let target_addr = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)); - - let mut network = MockNetwork::new(); - let mut seq = mockall::Sequence::new(); - network.expect_send_probe().times(1).returning(|_| Ok(())); - network - .expect_recv_probe() - .times(1) - .in_sequence(&mut seq) - .returning(move || { - Ok(Some(ProbeResponse::DestinationUnreachable( - ProbeResponseData::new( - SystemTime::now(), - target_addr, - ProbeResponseSeq::Tcp(ProbeResponseSeqTcp::new(target_addr, sequence, 80)), - ), - IcmpPacketCode(1), - None, - ))) - }); - network - .expect_recv_probe() - .times(1) - .in_sequence(&mut seq) - .returning(move || { - Ok(Some(ProbeResponse::TcpRefused(ProbeResponseData::new( - SystemTime::now(), - target_addr, - ProbeResponseSeq::Tcp(ProbeResponseSeqTcp::new(target_addr, sequence, 80)), - )))) - }); + #[must_use] + pub fn privilege_mode(&self) -> PrivilegeMode { + self.inner.privilege_mode() + } - let config = Config { - target_addr, - max_rounds: Some(MaxRounds(NonZeroUsize::MIN)), - initial_sequence: Sequence(sequence), - port_direction: PortDirection::FixedDest(Port(80)), - protocol: Protocol::Tcp, - ..Default::default() - }; - let tracer = Tracer::new(&config, |_| {}); - let mut state = TracerState::new(config); - tracer.send_request(&mut network, &mut state)?; - tracer.recv_response(&mut network, &mut state)?; - tracer.recv_response(&mut network, &mut state)?; - Ok(()) + #[must_use] + pub fn protocol(&self) -> Protocol { + self.inner.protocol() + } + + #[must_use] + pub fn interface(&self) -> Option<&str> { + self.inner.interface() + } + + #[must_use] + pub fn source_addr(&self) -> Option { + self.inner.source_addr() + } + + #[must_use] + pub fn target_addr(&self) -> IpAddr { + self.inner.target_addr() + } + + #[must_use] + pub fn packet_size(&self) -> PacketSize { + self.inner.packet_size() + } + + #[must_use] + pub fn payload_pattern(&self) -> PayloadPattern { + self.inner.payload_pattern() + } + + #[must_use] + pub fn initial_sequence(&self) -> Sequence { + self.inner.initial_sequence() + } + + #[must_use] + pub fn tos(&self) -> TypeOfService { + self.inner.tos() + } + + #[must_use] + pub fn icmp_extension_parse_mode(&self) -> IcmpExtensionParseMode { + self.inner.icmp_extension_parse_mode() + } + + #[must_use] + pub fn read_timeout(&self) -> Duration { + self.inner.read_timeout() + } + + #[must_use] + pub fn tcp_connect_timeout(&self) -> Duration { + self.inner.tcp_connect_timeout() + } + + #[must_use] + pub fn trace_identifier(&self) -> TraceId { + self.inner.trace_identifier() + } + + #[must_use] + pub fn max_rounds(&self) -> Option { + self.inner.max_rounds() + } + + #[must_use] + pub fn first_ttl(&self) -> TimeToLive { + self.inner.first_ttl() + } + + #[must_use] + pub fn max_ttl(&self) -> TimeToLive { + self.inner.max_ttl() + } + + #[must_use] + pub fn grace_duration(&self) -> Duration { + self.inner.grace_duration() + } + + #[must_use] + pub fn max_inflight(&self) -> MaxInflight { + self.inner.max_inflight() + } + + #[must_use] + pub fn multipath_strategy(&self) -> MultipathStrategy { + self.inner.multipath_strategy() + } + + #[must_use] + pub fn port_direction(&self) -> PortDirection { + self.inner.port_direction() + } + + #[must_use] + pub fn min_round_duration(&self) -> Duration { + self.inner.min_round_duration() + } + + #[must_use] + pub fn max_round_duration(&self) -> Duration { + self.inner.max_round_duration() } } -/// Mutable state needed for the tracing algorithm. -/// -/// This is contained within a submodule to ensure that mutations are only performed via methods on -/// the `TracerState` struct. -mod state { - use crate::constants::MAX_SEQUENCE_PER_ROUND; - use crate::probe::{Extensions, IcmpPacketCode, IcmpPacketType, Probe, ProbeState}; - use crate::types::{MaxRounds, Port, Round, Sequence, TimeToLive, TraceId}; - use crate::{Config, Flags, MultipathStrategy, PortDirection, Protocol}; - use std::array::from_fn; +mod inner { + use crate::config::{ChannelConfig, StateConfig, StrategyConfig}; + use crate::error::TraceResult; + use crate::net::{PlatformImpl, SocketImpl}; + use crate::{ + IcmpExtensionParseMode, MaxInflight, MaxRounds, MultipathStrategy, PacketSize, + PayloadPattern, PortDirection, PrivilegeMode, Protocol, Sequence, SourceAddr, TimeToLive, + TraceId, TraceState, TracerChannel, TracerError, TracerRound, TracerStrategy, + TypeOfService, + }; + use parking_lot::RwLock; + use std::fmt::Debug; use std::net::IpAddr; - use std::time::SystemTime; + use std::sync::OnceLock; + use std::time::Duration; use tracing::instrument; + use trippy_privilege::Privilege; - /// The maximum number of `ProbeState` entries in the buffer. - /// - /// This is larger than maximum number of time-to-live (TTL) we can support to allow for skipped - /// sequences. - const BUFFER_SIZE: u16 = MAX_SEQUENCE_PER_ROUND; - - /// The maximum sequence number. - /// - /// The sequence number is only ever wrapped between rounds, and so we need to ensure that there - /// are enough sequence numbers for a complete round. - /// - /// A sequence number can be skipped if, for example, the port for that sequence number cannot - /// be bound as it is already in use. - /// - /// To ensure each `ProbeState` is in the correct place in the buffer (i.e. the index into the buffer - /// is always `Probe.sequence - round_sequence`), when we skip a sequence we leave the - /// skipped `ProbeState` in-place and use the next slot for the next sequence. - /// - /// We cap the number of sequences that can potentially be skipped in a round to ensure that - /// sequence number does not even need to wrap around during a round. - /// - /// We only ever send `ttl` in the range 1..255, and so we may use all buffer capacity, except - /// the minimum needed to send up to a max `ttl` of 255 (a `ttl` of 0 is never sent). - const MAX_SEQUENCE: Sequence = Sequence(u16::MAX - BUFFER_SIZE); - - /// Mutable state needed for the tracing algorithm. #[derive(Debug)] - pub struct TracerState { - /// Tracer configuration. - config: Config, - /// The state of all `ProbeState` requests and responses. - buffer: [ProbeState; BUFFER_SIZE as usize], - /// An increasing sequence number for every `EchoRequest`. - sequence: Sequence, - /// The starting sequence number of the current round. - round_sequence: Sequence, - /// The time-to-live for the _next_ `EchoRequest` packet to be sent. - ttl: TimeToLive, - /// The current round. - round: Round, - /// The timestamp of when the current round started. - round_start: SystemTime, - /// Did we receive an `EchoReply` from the target host in this round? - target_found: bool, - /// The maximum time-to-live echo response packet we have received. - max_received_ttl: Option, - /// The observed time-to-live of the `EchoReply` from the target host. - /// - /// Note that this is _not_ reset each round and that it can also _change_ over time, - /// including going _down_ as responses can be received out-of-order. - target_ttl: Option, - /// The timestamp of the echo response packet. - received_time: Option, + pub(super) struct TracerInner { + source_addr: Option, + interface: Option, + target_addr: IpAddr, + privilege_mode: PrivilegeMode, + protocol: Protocol, + packet_size: PacketSize, + payload_pattern: PayloadPattern, + tos: TypeOfService, + icmp_extension_parse_mode: IcmpExtensionParseMode, + read_timeout: Duration, + tcp_connect_timeout: Duration, + trace_identifier: TraceId, + max_rounds: Option, + first_ttl: TimeToLive, + max_ttl: TimeToLive, + grace_duration: Duration, + max_inflight: MaxInflight, + initial_sequence: Sequence, + multipath_strategy: MultipathStrategy, + port_direction: PortDirection, + min_round_duration: Duration, + max_round_duration: Duration, + max_samples: usize, + max_flows: usize, + drop_privileges: bool, + state: RwLock, + src: OnceLock, } - impl TracerState { - pub fn new(config: Config) -> Self { + impl TracerInner { + #[allow(clippy::too_many_arguments)] + pub(super) fn new( + interface: Option, + source_addr: Option, + target_addr: IpAddr, + privilege_mode: PrivilegeMode, + protocol: Protocol, + packet_size: PacketSize, + payload_pattern: PayloadPattern, + tos: TypeOfService, + icmp_extension_parse_mode: IcmpExtensionParseMode, + read_timeout: Duration, + tcp_connect_timeout: Duration, + trace_identifier: TraceId, + max_rounds: Option, + first_ttl: TimeToLive, + max_ttl: TimeToLive, + grace_duration: Duration, + max_inflight: MaxInflight, + initial_sequence: Sequence, + multipath_strategy: MultipathStrategy, + port_direction: PortDirection, + min_round_duration: Duration, + max_round_duration: Duration, + max_samples: usize, + max_flows: usize, + drop_privileges: bool, + ) -> Self { Self { - config, - buffer: from_fn(|_| ProbeState::default()), - sequence: config.initial_sequence, - round_sequence: config.initial_sequence, - ttl: config.first_ttl, - round: Round(0), - round_start: SystemTime::now(), - target_found: false, - max_received_ttl: None, - target_ttl: None, - received_time: None, + source_addr, + interface, + target_addr, + privilege_mode, + protocol, + packet_size, + payload_pattern, + tos, + icmp_extension_parse_mode, + read_timeout, + tcp_connect_timeout, + trace_identifier, + max_rounds, + first_ttl, + max_ttl, + grace_duration, + max_inflight, + initial_sequence, + multipath_strategy, + port_direction, + min_round_duration, + max_round_duration, + max_samples, + max_flows, + drop_privileges, + state: RwLock::new(TraceState::new(Self::make_state_config( + max_flows, + max_samples, + ))), + src: OnceLock::new(), } } - /// Get a slice of `ProbeState` for the current round. - pub fn probes(&self) -> &[ProbeState] { - let round_size = self.sequence - self.round_sequence; - &self.buffer[..round_size.0 as usize] + #[instrument(skip_all)] + pub(super) fn run(&self) -> TraceResult<()> { + self.run_internal(|_| ()) + .map_err(|err| self.handle_error(err)) } - /// Get the `ProbeState` for `sequence` - pub fn probe_at(&self, sequence: Sequence) -> ProbeState { - self.buffer[usize::from(sequence - self.round_sequence)].clone() + #[instrument(skip_all)] + pub(super) fn run_with)>(&self, func: F) -> TraceResult<()> { + self.run_internal(func) + .map_err(|err| self.handle_error(err)) } - pub const fn ttl(&self) -> TimeToLive { - self.ttl + pub(super) fn snapshot(&self) -> TraceState { + self.state.read().clone() } - pub const fn round_start(&self) -> SystemTime { - self.round_start + pub(super) fn clear(&self) { + *self.state.write() = + TraceState::new(Self::make_state_config(self.max_flows, self.max_samples)); } - pub const fn target_found(&self) -> bool { - self.target_found + pub(super) fn max_flows(&self) -> usize { + self.max_flows } - pub const fn max_received_ttl(&self) -> Option { - self.max_received_ttl + pub(super) fn max_samples(&self) -> usize { + self.max_samples } - pub const fn target_ttl(&self) -> Option { - self.target_ttl + pub(super) fn privilege_mode(&self) -> PrivilegeMode { + self.privilege_mode } - pub const fn received_time(&self) -> Option { - self.received_time + pub(super) fn protocol(&self) -> Protocol { + self.protocol } - /// Is `sequence` in the current round? - pub fn in_round(&self, sequence: Sequence) -> bool { - sequence >= self.round_sequence && sequence.0 - self.round_sequence.0 < BUFFER_SIZE + pub(super) fn interface(&self) -> Option<&str> { + self.interface.as_deref() } - /// Do we have capacity in the current round for another sequence? - pub fn round_has_capacity(&self) -> bool { - let round_size = self.sequence - self.round_sequence; - round_size.0 < BUFFER_SIZE + pub(super) fn source_addr(&self) -> Option { + self.src.get().copied() } - /// Are all rounds complete? - pub fn finished(&self, max_rounds: Option) -> bool { - match max_rounds { - None => false, - Some(max_rounds) => self.round.0 > max_rounds.0.get() - 1, - } + pub(super) fn target_addr(&self) -> IpAddr { + self.target_addr } - /// Create and return the next `Probe` at the current `sequence` and `ttl`. - /// - /// We post-increment `ttl` here and so in practice we only allow `ttl` values in the range - /// `1..254` to allow us to use a `u8`. - #[instrument(skip(self))] - pub fn next_probe(&mut self, sent: SystemTime) -> Probe { - let (src_port, dest_port, identifier, flags) = self.probe_data(); - let probe = Probe::new( - self.sequence, - identifier, - src_port, - dest_port, - self.ttl, - self.round, - sent, - flags, - ); - let probe_index = usize::from(self.sequence - self.round_sequence); - self.buffer[probe_index] = ProbeState::Awaited(probe.clone()); - debug_assert!(self.ttl < TimeToLive(u8::MAX)); - self.ttl += TimeToLive(1); - debug_assert!(self.sequence < Sequence(u16::MAX)); - self.sequence += Sequence(1); - probe + pub(super) fn packet_size(&self) -> PacketSize { + self.packet_size } - /// Re-issue the `Probe` with the next sequence number. - /// - /// This will mark the `ProbeState` at the previous `sequence` as skipped and re-create it with - /// the previous `ttl` and the current `sequence`. - /// - /// For example, if the sequence is `4` and the `ttl` is `5` prior to calling this method - /// then afterward: - /// - The `ProbeState` at sequence `3` will be set to `Skipped` state - /// - A new `ProbeState` will be created at sequence `4` with a `ttl` of `5` - #[instrument(skip(self))] - pub fn reissue_probe(&mut self, sent: SystemTime) -> Probe { - let probe_index = usize::from(self.sequence - self.round_sequence); - self.buffer[probe_index - 1] = ProbeState::Skipped; - let (src_port, dest_port, identifier, flags) = self.probe_data(); - let probe = Probe::new( - self.sequence, - identifier, - src_port, - dest_port, - self.ttl - TimeToLive(1), - self.round, - sent, - flags, - ); - self.buffer[probe_index] = ProbeState::Awaited(probe.clone()); - debug_assert!(self.sequence < Sequence(u16::MAX)); - self.sequence += Sequence(1); - probe + pub(super) fn payload_pattern(&self) -> PayloadPattern { + self.payload_pattern } - /// Determine the `src_port`, `dest_port` and `identifier` for the current probe. - /// - /// This will differ depending on the `TracerProtocol`, `MultipathStrategy` & - /// `PortDirection`. - fn probe_data(&self) -> (Port, Port, TraceId, Flags) { - match self.config.protocol { - Protocol::Icmp => self.probe_icmp_data(), - Protocol::Udp => self.probe_udp_data(), - Protocol::Tcp => self.probe_tcp_data(), - } + pub(super) fn initial_sequence(&self) -> Sequence { + self.initial_sequence } - /// Determine the `src_port`, `dest_port` and `identifier` for the current ICMP probe. - fn probe_icmp_data(&self) -> (Port, Port, TraceId, Flags) { - ( - Port(0), - Port(0), - self.config.trace_identifier, - Flags::empty(), - ) + pub(super) fn tos(&self) -> TypeOfService { + self.tos } - /// Determine the `src_port`, `dest_port` and `identifier` for the current UDP probe. - fn probe_udp_data(&self) -> (Port, Port, TraceId, Flags) { - match self.config.multipath_strategy { - MultipathStrategy::Classic => match self.config.port_direction { - PortDirection::FixedSrc(src_port) => ( - Port(src_port.0), - Port(self.sequence.0), - TraceId(0), - Flags::empty(), - ), - PortDirection::FixedDest(dest_port) => ( - Port(self.sequence.0), - Port(dest_port.0), - TraceId(0), - Flags::empty(), - ), - PortDirection::FixedBoth(_, _) | PortDirection::None => { - unimplemented!() - } - }, - MultipathStrategy::Paris => { - let round_port = ((self.config.initial_sequence.0 as usize + self.round.0) - % usize::from(u16::MAX)) as u16; - match self.config.port_direction { - PortDirection::FixedSrc(src_port) => ( - Port(src_port.0), - Port(round_port), - TraceId(0), - Flags::PARIS_CHECKSUM, - ), - PortDirection::FixedDest(dest_port) => ( - Port(round_port), - Port(dest_port.0), - TraceId(0), - Flags::PARIS_CHECKSUM, - ), - PortDirection::FixedBoth(src_port, dest_port) => ( - Port(src_port.0), - Port(dest_port.0), - TraceId(0), - Flags::PARIS_CHECKSUM, - ), - PortDirection::None => unimplemented!(), - } - } - MultipathStrategy::Dublin => { - let round_port = ((self.config.initial_sequence.0 as usize + self.round.0) - % usize::from(u16::MAX)) as u16; - match self.config.port_direction { - PortDirection::FixedSrc(src_port) => ( - Port(src_port.0), - Port(round_port), - TraceId(self.sequence.0), - Flags::DUBLIN_IPV6_PAYLOAD_LENGTH, - ), - PortDirection::FixedDest(dest_port) => ( - Port(round_port), - Port(dest_port.0), - TraceId(self.sequence.0), - Flags::DUBLIN_IPV6_PAYLOAD_LENGTH, - ), - PortDirection::FixedBoth(src_port, dest_port) => ( - Port(src_port.0), - Port(dest_port.0), - TraceId(self.sequence.0), - Flags::DUBLIN_IPV6_PAYLOAD_LENGTH, - ), - PortDirection::None => unimplemented!(), - } - } - } + pub(super) fn icmp_extension_parse_mode(&self) -> IcmpExtensionParseMode { + self.icmp_extension_parse_mode } - /// Determine the `src_port`, `dest_port` and `identifier` for the current TCP probe. - fn probe_tcp_data(&self) -> (Port, Port, TraceId, Flags) { - let (src_port, dest_port) = match self.config.port_direction { - PortDirection::FixedSrc(src_port) => (src_port.0, self.sequence.0), - PortDirection::FixedDest(dest_port) => (self.sequence.0, dest_port.0), - PortDirection::FixedBoth(_, _) | PortDirection::None => unimplemented!(), - }; - (Port(src_port), Port(dest_port), TraceId(0), Flags::empty()) + pub(super) fn read_timeout(&self) -> Duration { + self.read_timeout } - /// Mark the `ProbeState` at `sequence` completed as `TimeExceeded` and update the round state. - #[instrument(skip(self))] - pub fn complete_probe_time_exceeded( - &mut self, - sequence: Sequence, - host: IpAddr, - received: SystemTime, - is_target: bool, - icmp_code: IcmpPacketCode, - extensions: Option, - ) { - self.complete_probe( - sequence, - IcmpPacketType::TimeExceeded(icmp_code), - host, - received, - is_target, - extensions, - ); + pub(super) fn tcp_connect_timeout(&self) -> Duration { + self.tcp_connect_timeout } - /// Mark the `ProbeState` at `sequence` completed as `Unreachable` and update the round state. - #[instrument(skip(self))] - pub fn complete_probe_unreachable( - &mut self, - sequence: Sequence, - host: IpAddr, - received: SystemTime, - icmp_code: IcmpPacketCode, - extensions: Option, - ) { - self.complete_probe( - sequence, - IcmpPacketType::Unreachable(icmp_code), - host, - received, - true, - extensions, - ); + pub(super) fn trace_identifier(&self) -> TraceId { + self.trace_identifier } - /// Mark the `ProbeState` at `sequence` completed as `EchoReply` and update the round state. - #[instrument(skip(self))] - pub fn complete_probe_echo_reply( - &mut self, - sequence: Sequence, - host: IpAddr, - received: SystemTime, - icmp_code: IcmpPacketCode, - ) { - self.complete_probe( - sequence, - IcmpPacketType::EchoReply(icmp_code), - host, - received, - true, - None, - ); + pub(super) fn max_rounds(&self) -> Option { + self.max_rounds } - /// Mark the `ProbeState` at `sequence` completed as `NotApplicable` and update the round state. - #[instrument(skip(self))] - pub fn complete_probe_other( - &mut self, - sequence: Sequence, - host: IpAddr, - received: SystemTime, - ) { - self.complete_probe( - sequence, - IcmpPacketType::NotApplicable, - host, - received, - true, - None, - ); + pub(super) fn first_ttl(&self) -> TimeToLive { + self.first_ttl } - /// Update the state of a `ProbeState` and the trace. - /// - /// We want to update: - /// - /// - the `target_ttl` to be the time-to-live of the `ProbeState` request from the target - /// - the `max_received_ttl` we have observed this round - /// - the latest packet `received_time` in this round - /// - whether the target has been found in this round - /// - /// The ICMP replies may arrive out-of-order, and so we must be careful here to avoid - /// overwriting the state with stale values. We may also receive multiple replies - /// from the target host with differing time-to-live values and so must ensure we - /// use the time-to-live with the lowest sequence number. - #[instrument(skip(self))] - fn complete_probe( - &mut self, - sequence: Sequence, - icmp_packet_type: IcmpPacketType, - host: IpAddr, - received: SystemTime, - is_target: bool, - extensions: Option, - ) { - // Retrieve and update the `ProbeState` at `sequence`. - let probe = self.probe_at(sequence); - let awaited = match probe { - ProbeState::Awaited(awaited) => awaited, - // there is a valid scenario for TCP where a probe is already - // `Complete`, see `test_tcp_dest_unreachable_and_refused`. - ProbeState::Complete(_) => { - return; - } - _ => { - debug_assert!( - false, - "completed probe was not in Awaited state (probe={probe:#?})" - ); - return; - } - }; - let completed = awaited.complete(host, received, icmp_packet_type, extensions); - let ttl = completed.ttl; - self.buffer[usize::from(sequence - self.round_sequence)] = - ProbeState::Complete(completed); - - // If this `ProbeState` found the target then we set the `target_tll` if not already set, - // being careful to account for `Probes` being received out-of-order. - // - // If this `ProbeState` did not find the target but has a ttl that is greater or equal to the - // target ttl (if known) then we reset the target ttl to None. This is to - // support Equal Cost Multi-path Routing (ECMP) cases where the number of - // hops to the target will vary over the lifetime of the trace. - self.target_ttl = if is_target { - match self.target_ttl { - None => Some(ttl), - Some(target_ttl) if ttl < target_ttl => Some(ttl), - Some(target_ttl) => Some(target_ttl), - } - } else { - match self.target_ttl { - Some(target_ttl) if ttl >= target_ttl => None, - Some(target_ttl) => Some(target_ttl), - None => None, - } - }; - - self.max_received_ttl = match self.max_received_ttl { - None => Some(ttl), - Some(max_received_ttl) => Some(max_received_ttl.max(ttl)), - }; - - self.received_time = Some(received); - self.target_found |= is_target; + pub(super) fn max_ttl(&self) -> TimeToLive { + self.max_ttl } - /// Advance to the next round. - /// - /// If, during the rond which just completed, we went above the max sequence number then we - /// reset it here. We do this here to avoid having to deal with the sequence number - /// wrapping during a round, which is more problematic. - #[instrument(skip(self))] - pub fn advance_round(&mut self, first_ttl: TimeToLive) { - if self.sequence >= self.max_sequence() { - self.sequence = self.config.initial_sequence; - } - self.target_found = false; - self.round_sequence = self.sequence; - self.received_time = None; - self.round_start = SystemTime::now(); - self.max_received_ttl = None; - self.round += Round(1); - self.ttl = first_ttl; + pub(super) fn grace_duration(&self) -> Duration { + self.grace_duration } - /// The maximum sequence number allowed. - /// - /// The Dublin multipath strategy for IPv6/udp encodes the sequence - /// number as the payload length and consequently the maximum sequence - /// number must be no larger than the maximum IPv6/udp payload size. - /// - /// It is also required that the range of possible sequence numbers is - /// _at least_ `BUFFER_SIZE` to ensure delayed responses from a prior - /// round are not incorrectly associated with later rounds (see - /// `in_round` function). - fn max_sequence(&self) -> Sequence { - match (self.config.multipath_strategy, self.config.target_addr) { - (MultipathStrategy::Dublin, IpAddr::V6(_)) => { - self.config.initial_sequence + Sequence(BUFFER_SIZE) - } - _ => MAX_SEQUENCE, - } + pub(super) fn max_inflight(&self) -> MaxInflight { + self.max_inflight } - } - #[cfg(test)] - mod tests { - use super::*; - use crate::probe::IcmpPacketType; - use crate::types::MaxInflight; - use rand::Rng; - use std::net::{IpAddr, Ipv4Addr}; - use std::time::Duration; - - #[allow( - clippy::cognitive_complexity, - clippy::too_many_lines, - clippy::bool_assert_comparison - )] - #[test] - fn test_state() { - let mut state = TracerState::new(cfg(Sequence(33000))); - - // Validate the initial TracerState - assert_eq!(state.round, Round(0)); - assert_eq!(state.sequence, Sequence(33000)); - assert_eq!(state.round_sequence, Sequence(33000)); - assert_eq!(state.ttl, TimeToLive(1)); - assert_eq!(state.max_received_ttl, None); - assert_eq!(state.received_time, None); - assert_eq!(state.target_ttl, None); - assert_eq!(state.target_found, false); - - // The initial state of the probe before sending - let prob_init = state.probe_at(Sequence(33000)); - assert_eq!(ProbeState::NotSent, prob_init); - - // Prepare probe 1 (round 0, sequence 33000, ttl 1) for sending - let sent_1 = SystemTime::now(); - let probe_1 = state.next_probe(sent_1); - assert_eq!(probe_1.sequence, Sequence(33000)); - assert_eq!(probe_1.ttl, TimeToLive(1)); - assert_eq!(probe_1.round, Round(0)); - assert_eq!(probe_1.sent, sent_1); - - // Update the state of the probe 1 after receiving a TimeExceeded - let received_1 = SystemTime::now(); - let host = IpAddr::V4(Ipv4Addr::LOCALHOST); - state.complete_probe_time_exceeded( - Sequence(33000), - host, - received_1, - false, - IcmpPacketCode(1), - None, - ); - - // Validate the state of the probe 1 after the update - let probe_1_fetch = state.probe_at(Sequence(33000)).try_into_complete().unwrap(); - assert_eq!(probe_1_fetch.sequence, Sequence(33000)); - assert_eq!(probe_1_fetch.ttl, TimeToLive(1)); - assert_eq!(probe_1_fetch.round, Round(0)); - assert_eq!(probe_1_fetch.received, received_1); - assert_eq!(probe_1_fetch.host, host); - assert_eq!(probe_1_fetch.sent, sent_1); - assert_eq!( - probe_1_fetch.icmp_packet_type, - IcmpPacketType::TimeExceeded(IcmpPacketCode(1)) - ); - - // Validate the TracerState after the update - assert_eq!(state.round, Round(0)); - assert_eq!(state.sequence, Sequence(33001)); - assert_eq!(state.round_sequence, Sequence(33000)); - assert_eq!(state.ttl, TimeToLive(2)); - assert_eq!(state.max_received_ttl, Some(TimeToLive(1))); - assert_eq!(state.received_time, Some(received_1)); - assert_eq!(state.target_ttl, None); - assert_eq!(state.target_found, false); - - // Validate the probes() iterator returns only a single probe - { - let mut probe_iter = state.probes().iter(); - let probe_next1 = probe_iter.next().unwrap(); - assert_eq!(ProbeState::Complete(probe_1_fetch), probe_next1.clone()); - assert_eq!(None, probe_iter.next()); - } - - // Advance to the next round - state.advance_round(TimeToLive(1)); - - // Validate the TracerState after the round update - assert_eq!(state.round, Round(1)); - assert_eq!(state.sequence, Sequence(33001)); - assert_eq!(state.round_sequence, Sequence(33001)); - assert_eq!(state.ttl, TimeToLive(1)); - assert_eq!(state.max_received_ttl, None); - assert_eq!(state.received_time, None); - assert_eq!(state.target_ttl, None); - assert_eq!(state.target_found, false); - - // Prepare probe 2 (round 1, sequence 33001, ttl 1) for sending - let sent_2 = SystemTime::now(); - let probe_2 = state.next_probe(sent_2); - assert_eq!(probe_2.sequence, Sequence(33001)); - assert_eq!(probe_2.ttl, TimeToLive(1)); - assert_eq!(probe_2.round, Round(1)); - assert_eq!(probe_2.sent, sent_2); - - // Prepare probe 3 (round 1, sequence 33002, ttl 2) for sending - let sent_3 = SystemTime::now(); - let probe_3 = state.next_probe(sent_3); - assert_eq!(probe_3.sequence, Sequence(33002)); - assert_eq!(probe_3.ttl, TimeToLive(2)); - assert_eq!(probe_3.round, Round(1)); - assert_eq!(probe_3.sent, sent_3); - - // Update the state of probe 2 after receiving a TimeExceeded - let received_2 = SystemTime::now(); - let host = IpAddr::V4(Ipv4Addr::LOCALHOST); - state.complete_probe_time_exceeded( - Sequence(33001), - host, - received_2, - false, - IcmpPacketCode(1), - None, - ); - let probe_2_recv = state.probe_at(Sequence(33001)); - - // Validate the TracerState after the update to probe 2 - assert_eq!(state.round, Round(1)); - assert_eq!(state.sequence, Sequence(33003)); - assert_eq!(state.round_sequence, Sequence(33001)); - assert_eq!(state.ttl, TimeToLive(3)); - assert_eq!(state.max_received_ttl, Some(TimeToLive(1))); - assert_eq!(state.received_time, Some(received_2)); - assert_eq!(state.target_ttl, None); - assert_eq!(state.target_found, false); - - // Validate the probes() iterator returns the two probes in the states we expect - { - let mut probe_iter = state.probes().iter(); - let probe_next1 = probe_iter.next().unwrap(); - assert_eq!(&probe_2_recv, probe_next1); - let probe_next2 = probe_iter.next().unwrap(); - assert_eq!(ProbeState::Awaited(probe_3), probe_next2.clone()); - } + pub(super) fn multipath_strategy(&self) -> MultipathStrategy { + self.multipath_strategy + } - // Update the state of probe 3 after receiving a EchoReply - let received_3 = SystemTime::now(); - let host = IpAddr::V4(Ipv4Addr::LOCALHOST); - state.complete_probe_echo_reply(Sequence(33002), host, received_3, IcmpPacketCode(0)); - let probe_3_recv = state.probe_at(Sequence(33002)); - - // Validate the TracerState after the update to probe 3 - assert_eq!(state.round, Round(1)); - assert_eq!(state.sequence, Sequence(33003)); - assert_eq!(state.round_sequence, Sequence(33001)); - assert_eq!(state.ttl, TimeToLive(3)); - assert_eq!(state.max_received_ttl, Some(TimeToLive(2))); - assert_eq!(state.received_time, Some(received_3)); - assert_eq!(state.target_ttl, Some(TimeToLive(2))); - assert_eq!(state.target_found, true); - - // Validate the probes() iterator returns the two probes in the states we expect - { - let mut probe_iter = state.probes().iter(); - let probe_next1 = probe_iter.next().unwrap(); - assert_eq!(&probe_2_recv, probe_next1); - let probe_next2 = probe_iter.next().unwrap(); - assert_eq!(&probe_3_recv, probe_next2); - } + pub(super) fn port_direction(&self) -> PortDirection { + self.port_direction } - #[test] - fn test_sequence_wrap1() { - // Start from MAX_SEQUENCE - 1 which is (65279 - 1) == 65278 - let initial_sequence = Sequence(65278); - let mut state = TracerState::new(cfg(initial_sequence)); - assert_eq!(state.round, Round(0)); - assert_eq!(state.sequence, initial_sequence); - assert_eq!(state.round_sequence, initial_sequence); - - // Create a probe at seq 65278 - assert_eq!( - state.next_probe(SystemTime::now()).sequence, - Sequence(65278) - ); - assert_eq!(state.sequence, Sequence(65279)); - - // Validate the probes() - { - let mut iter = state.probes().iter(); - assert_eq!( - iter.next() - .unwrap() - .clone() - .try_into_awaited() - .unwrap() - .sequence, - Sequence(65278) - ); - iter.take(BUFFER_SIZE as usize - 1) - .for_each(|p| assert!(matches!(p, ProbeState::NotSent))); - } + pub(super) fn min_round_duration(&self) -> Duration { + self.min_round_duration + } - // Advance the round, which will wrap the sequence back to initial_sequence - state.advance_round(TimeToLive(1)); - assert_eq!(state.round, Round(1)); - assert_eq!(state.sequence, initial_sequence); - assert_eq!(state.round_sequence, initial_sequence); - - // Create a probe at seq 65278 - assert_eq!( - state.next_probe(SystemTime::now()).sequence, - Sequence(65278) - ); - assert_eq!(state.sequence, Sequence(65279)); - - // Validate the probes() again - { - let mut iter = state.probes().iter(); - assert_eq!( - iter.next() - .unwrap() - .clone() - .try_into_awaited() - .unwrap() - .sequence, - Sequence(65278) - ); - iter.take(BUFFER_SIZE as usize - 1) - .for_each(|p| assert!(matches!(p, ProbeState::NotSent))); - } + pub(super) fn max_round_duration(&self) -> Duration { + self.max_round_duration } - #[test] - fn test_sequence_wrap2() { - let total_rounds = 2000; - let max_probe_per_round = 254; - let mut state = TracerState::new(cfg(Sequence(33000))); - for _ in 0..total_rounds { - for _ in 0..max_probe_per_round { - let _probe = state.next_probe(SystemTime::now()); - } - state.advance_round(TimeToLive(1)); + #[instrument(skip_all)] + fn run_internal)>(&self, func: F) -> TraceResult<()> { + // if we are given a source address, validate it otherwise + // discover it based on the target address and interface. + let source_addr = match self.source_addr { + None => SourceAddr::discover::( + self.target_addr, + self.port_direction, + self.interface.as_deref(), + )?, + Some(addr) => SourceAddr::validate::(addr)?, + }; + self.src + .set(source_addr) + .map_err(|_| TracerError::Other(String::from("failed to set source_addr")))?; + let channel_config = self.make_channel_config(source_addr); + let channel = TracerChannel::::connect(&channel_config)?; + if self.drop_privileges { + Privilege::drop_privileges()?; } - assert_eq!(state.round, Round(2000)); - assert_eq!(state.round_sequence, Sequence(57130)); - assert_eq!(state.sequence, Sequence(57130)); + let strategy_config = self.make_strategy_config(); + let strategy = TracerStrategy::new(&strategy_config, |round| { + self.handler(round); + func(round); + }); + strategy.run(channel)?; + Ok(()) } - #[test] - fn test_sequence_wrap3() { - let total_rounds = 2000; - let max_probe_per_round = 20; - let mut state = TracerState::new(cfg(Sequence(33000))); - let mut rng = rand::thread_rng(); - for _ in 0..total_rounds { - for _ in 0..rng.gen_range(0..max_probe_per_round) { - state.next_probe(SystemTime::now()); - } - state.advance_round(TimeToLive(1)); - } + fn handler(&self, round: &TracerRound<'_>) { + self.state.write().update_from_round(round); } - #[test] - fn test_sequence_wrap_with_skip() { - let total_rounds = 2000; - let max_probe_per_round = 254; - let mut state = TracerState::new(cfg(Sequence(33000))); - for _ in 0..total_rounds { - for _ in 0..max_probe_per_round { - _ = state.next_probe(SystemTime::now()); - _ = state.reissue_probe(SystemTime::now()); - } - state.advance_round(TimeToLive(1)); - } - assert_eq!(state.round, Round(2000)); - assert_eq!(state.round_sequence, Sequence(41128)); - assert_eq!(state.sequence, Sequence(41128)); + fn handle_error(&self, err: TracerError) -> TracerError { + self.state.write().set_error(Some(err.to_string())); + err } - #[test] - fn test_in_round() { - let state = TracerState::new(cfg(Sequence(33000))); - assert!(state.in_round(Sequence(33000))); - assert!(state.in_round(Sequence(33511))); - assert!(!state.in_round(Sequence(33512))); + fn make_state_config(max_flows: usize, max_samples: usize) -> StateConfig { + StateConfig { + max_samples, + max_flows, + } } - #[test] - #[should_panic(expected = "assertion failed: !state.in_round(Sequence(64491))")] - fn test_in_delayed_probe_not_in_round() { - let mut state = TracerState::new(cfg(Sequence(64000))); - for _ in 0..55 { - _ = state.next_probe(SystemTime::now()); + fn make_channel_config(&self, source_addr: IpAddr) -> ChannelConfig { + ChannelConfig { + privilege_mode: self.privilege_mode, + protocol: self.protocol, + source_addr, + target_addr: self.target_addr, + packet_size: self.packet_size, + payload_pattern: self.payload_pattern, + initial_sequence: self.initial_sequence, + tos: self.tos, + icmp_extension_parse_mode: self.icmp_extension_parse_mode, + read_timeout: self.read_timeout, + tcp_connect_timeout: self.tcp_connect_timeout, } - state.advance_round(TimeToLive(1)); - assert!(!state.in_round(Sequence(64491))); } - fn cfg(initial_sequence: Sequence) -> Config { - Config { - target_addr: IpAddr::V4(Ipv4Addr::UNSPECIFIED), - protocol: Protocol::Icmp, - trace_identifier: TraceId::default(), - max_rounds: None, - first_ttl: TimeToLive(1), - max_ttl: TimeToLive(24), - grace_duration: Duration::default(), - max_inflight: MaxInflight::default(), - initial_sequence, - multipath_strategy: MultipathStrategy::Classic, - port_direction: PortDirection::None, - min_round_duration: Duration::default(), - max_round_duration: Duration::default(), + fn make_strategy_config(&self) -> StrategyConfig { + StrategyConfig { + target_addr: self.target_addr, + protocol: self.protocol, + trace_identifier: self.trace_identifier, + max_rounds: self.max_rounds, + first_ttl: self.first_ttl, + max_ttl: self.max_ttl, + grace_duration: self.grace_duration, + max_inflight: self.max_inflight, + initial_sequence: self.initial_sequence, + multipath_strategy: self.multipath_strategy, + port_direction: self.port_direction, + min_round_duration: self.min_round_duration, + max_round_duration: self.max_round_duration, } } } } - -/// Returns true if the duration between start and end is grater than a duration, false otherwise. -fn exceeds(start: Option, end: SystemTime, dur: Duration) -> bool { - start.map_or(false, |start| { - end.duration_since(start).unwrap_or_default() > dur - }) -} diff --git a/crates/trippy/tests/resources/backend/ipv4_3probes_3hops_completed.yaml b/crates/trippy-core/tests/resources/backend/ipv4_3probes_3hops_completed.yaml similarity index 100% rename from crates/trippy/tests/resources/backend/ipv4_3probes_3hops_completed.yaml rename to crates/trippy-core/tests/resources/backend/ipv4_3probes_3hops_completed.yaml diff --git a/crates/trippy/tests/resources/backend/ipv4_3probes_3hops_mixed_multi.yaml b/crates/trippy-core/tests/resources/backend/ipv4_3probes_3hops_mixed_multi.yaml similarity index 100% rename from crates/trippy/tests/resources/backend/ipv4_3probes_3hops_mixed_multi.yaml rename to crates/trippy-core/tests/resources/backend/ipv4_3probes_3hops_mixed_multi.yaml diff --git a/crates/trippy/tests/resources/backend/ipv4_4probes_0latency.yaml b/crates/trippy-core/tests/resources/backend/ipv4_4probes_0latency.yaml similarity index 100% rename from crates/trippy/tests/resources/backend/ipv4_4probes_0latency.yaml rename to crates/trippy-core/tests/resources/backend/ipv4_4probes_0latency.yaml diff --git a/crates/trippy/tests/resources/backend/ipv4_4probes_all_status.yaml b/crates/trippy-core/tests/resources/backend/ipv4_4probes_all_status.yaml similarity index 100% rename from crates/trippy/tests/resources/backend/ipv4_4probes_all_status.yaml rename to crates/trippy-core/tests/resources/backend/ipv4_4probes_all_status.yaml diff --git a/crates/trippy-core/tests/resources/simulation/ipv4_icmp_wrap.yaml b/crates/trippy-core/tests/resources/simulation/ipv4_icmp_wrap.yaml index 515ef0b24..ff7174902 100644 --- a/crates/trippy-core/tests/resources/simulation/ipv4_icmp_wrap.yaml +++ b/crates/trippy-core/tests/resources/simulation/ipv4_icmp_wrap.yaml @@ -1,9 +1,9 @@ name: IPv4/ICMP wrap sequence -target: 10.0.0.107 -rounds: 10 +target: 10.0.0.130 +rounds: 20 protocol: Icmp icmp_identifier: 8 -initial_sequence: 65000 +initial_sequence: 64511 hops: - ttl: 1 resp: !SingleHost @@ -32,4 +32,96 @@ hops: - ttl: 7 resp: !SingleHost addr: 10.0.0.107 + rtt_ms: 0 + - ttl: 8 + resp: !SingleHost + addr: 10.0.0.108 + rtt_ms: 0 + - ttl: 9 + resp: !SingleHost + addr: 10.0.0.109 + rtt_ms: 0 + - ttl: 10 + resp: !SingleHost + addr: 10.0.0.110 + rtt_ms: 0 + - ttl: 11 + resp: !SingleHost + addr: 10.0.0.111 + rtt_ms: 0 + - ttl: 12 + resp: !SingleHost + addr: 10.0.0.112 + rtt_ms: 0 + - ttl: 13 + resp: !SingleHost + addr: 10.0.0.113 + rtt_ms: 0 + - ttl: 14 + resp: !SingleHost + addr: 10.0.0.114 + rtt_ms: 0 + - ttl: 15 + resp: !SingleHost + addr: 10.0.0.115 + rtt_ms: 0 + - ttl: 16 + resp: !SingleHost + addr: 10.0.0.116 + rtt_ms: 0 + - ttl: 17 + resp: !SingleHost + addr: 10.0.0.117 + rtt_ms: 0 + - ttl: 18 + resp: !SingleHost + addr: 10.0.0.118 + rtt_ms: 0 + - ttl: 19 + resp: !SingleHost + addr: 10.0.0.119 + rtt_ms: 0 + - ttl: 20 + resp: !SingleHost + addr: 10.0.0.120 + rtt_ms: 0 + - ttl: 21 + resp: !SingleHost + addr: 10.0.0.121 + rtt_ms: 0 + - ttl: 22 + resp: !SingleHost + addr: 10.0.0.122 + rtt_ms: 0 + - ttl: 23 + resp: !SingleHost + addr: 10.0.0.123 + rtt_ms: 0 + - ttl: 24 + resp: !SingleHost + addr: 10.0.0.124 + rtt_ms: 0 + - ttl: 25 + resp: !SingleHost + addr: 10.0.0.125 + rtt_ms: 0 + - ttl: 26 + resp: !SingleHost + addr: 10.0.0.126 + rtt_ms: 0 + - ttl: 27 + resp: !SingleHost + addr: 10.0.0.127 + rtt_ms: 0 + - ttl: 28 + resp: !SingleHost + addr: 10.0.0.128 + rtt_ms: 0 + - ttl: 29 + resp: !SingleHost + addr: 10.0.0.129 + rtt_ms: 0 + - ttl: 30 + resp: !SingleHost + addr: 10.0.0.130 rtt_ms: 0 \ No newline at end of file diff --git a/crates/trippy-core/tests/sim/tracer.rs b/crates/trippy-core/tests/sim/tracer.rs index b3c520fc8..9f6577d1a 100644 --- a/crates/trippy-core/tests/sim/tracer.rs +++ b/crates/trippy-core/tests/sim/tracer.rs @@ -1,14 +1,13 @@ use crate::simulation::{Response, Simulation, SingleHost}; use std::cell::RefCell; -use std::num::NonZeroUsize; use std::sync::Arc; use std::thread; use std::time::Duration; use tokio_util::sync::CancellationToken; use tracing::info; use trippy_core::{ - defaults, Builder, CompletionReason, MaxRounds, MultipathStrategy, PacketSize, PayloadPattern, - PortDirection, PrivilegeMode, ProbeState, Protocol, Sequence, TimeToLive, TraceId, TracerRound, + defaults, Builder, CompletionReason, MultipathStrategy, PortDirection, PrivilegeMode, + ProbeState, Protocol, TimeToLive, TracerRound, }; // The length of time to wait after the completion of the tracing before @@ -50,27 +49,27 @@ impl Tracer { pub fn trace(&self) -> anyhow::Result<()> { let result = RefCell::new(Ok(())); - let tracer_res = Builder::new(self.sim.target, |round| self.validate_round(round, &result)) + let tracer = Builder::new(self.sim.target) .privilege_mode(PrivilegeMode::from(self.sim.privilege_mode)) - .trace_identifier(TraceId(self.sim.icmp_identifier)) - .initial_sequence(Sequence( + .trace_identifier(self.sim.icmp_identifier) + .initial_sequence( self.sim .initial_sequence .unwrap_or(defaults::DEFAULT_STRATEGY_INITIAL_SEQUENCE), - )) + ) .protocol(Protocol::from(self.sim.protocol)) .port_direction(PortDirection::from(self.sim.port_direction)) .multipath_strategy(MultipathStrategy::from(self.sim.multipath_strategy)) - .packet_size(PacketSize( + .packet_size( self.sim .packet_size .unwrap_or(defaults::DEFAULT_STRATEGY_PACKET_SIZE), - )) - .payload_pattern(PayloadPattern( + ) + .payload_pattern( self.sim .payload_pattern .unwrap_or(defaults::DEFAULT_STRATEGY_PAYLOAD_PATTERN), - )) + ) .min_round_duration(self.sim.min_round_duration.map_or( defaults::DEFAULT_STRATEGY_MIN_ROUND_DURATION, Duration::from_millis, @@ -83,13 +82,10 @@ impl Tracer { defaults::DEFAULT_STRATEGY_GRACE_DURATION, Duration::from_millis, )) - .max_rounds(MaxRounds( - self.sim - .rounds - .and_then(NonZeroUsize::new) - .unwrap_or(NonZeroUsize::MIN), - )) - .start() + .max_rounds(self.sim.rounds.or(Some(1))) + .build()?; + let tracer_res = tracer + .run_with(|round| self.validate_round(round, &result)) .map_err(anyhow::Error::from); thread::sleep(CLEANUP_DELAY); self.token.cancel(); diff --git a/crates/trippy/Cargo.toml b/crates/trippy/Cargo.toml index effd98ba9..ee5f2df11 100644 --- a/crates/trippy/Cargo.toml +++ b/crates/trippy/Cargo.toml @@ -19,16 +19,15 @@ path = "src/main.rs" name = "trip" [dependencies] -trippy-core = { version = "0.11.0-dev", path = "../trippy-core" } -trippy-privilege = { version = "0.11.0-dev", path = "../trippy-privilege" } -trippy-dns = { version = "0.11.0-dev", path = "../trippy-dns" } +trippy-core.workspace = true +trippy-privilege.workspace = true +trippy-dns.workspace = true thiserror.workspace = true anyhow.workspace = true itertools.workspace = true tracing.workspace = true serde.workspace = true derive_more.workspace = true -parking_lot.workspace = true clap = { version = "4.4.0", default-features = false, features = [ "cargo", "derive", "wrap_help", "usage", "unstable-styles", "color", "suggestions", "error-context" ] } clap_complete = "4.4.9" humantime = "2.1.0" @@ -40,7 +39,6 @@ comfy-table = { version = "7.1.0", default-features = false } strum = { version = "0.26.2", default-features = false, features = [ "std", "derive" ] } etcetera = "0.8.0" toml = { version = "0.8.14", default-features = false, features = [ "parse" ] } -indexmap = { version = "2.2.6", default-features = false } maxminddb = "0.24.0" tracing-subscriber = { version = "0.3.18", default-features = false, features = [ "json", "env-filter" ] } tracing-chrome = "0.7.2" diff --git a/crates/trippy/src/app.rs b/crates/trippy/src/app.rs index c8fca0b8f..75f609181 100644 --- a/crates/trippy/src/app.rs +++ b/crates/trippy/src/app.rs @@ -1,26 +1,18 @@ -use crate::backend::trace::Trace; -use crate::backend::Backend; use crate::config::{LogFormat, LogSpanEvents, Mode, TrippyConfig}; use crate::frontend::TuiConfig; use crate::geoip::GeoIpLookup; use crate::{frontend, report}; use anyhow::{anyhow, Error}; -use parking_lot::RwLock; use std::net::IpAddr; -use std::sync::Arc; -use std::thread; -use std::time::Duration; use tracing_chrome::{ChromeLayerBuilder, FlushGuard}; use tracing_subscriber::fmt::format::FmtSpan; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; -use trippy_core::{ - ChannelConfig, Config, IcmpExtensionParseMode, MultipathStrategy, PlatformImpl, PortDirection, - PrivilegeMode, Protocol, SocketImpl, SourceAddr, -}; +use trippy_core::{Builder, Tracer}; use trippy_dns::{DnsResolver, Resolver}; use trippy_privilege::Privilege; +/// Run the trippy application. pub fn run_trippy(cfg: &TrippyConfig, pid: u16) -> anyhow::Result<()> { let _guard = configure_logging(cfg); let resolver = start_dns_resolver(cfg)?; @@ -60,33 +52,34 @@ fn start_tracer( target_addr: IpAddr, trace_identifier: u16, ) -> Result { - let source_addr = match cfg.source_addr { - None => SourceAddr::discover::( - target_addr, - cfg.port_direction, - cfg.interface.as_deref(), - )?, - Some(addr) => SourceAddr::validate::(addr)?, - }; - let channel_config = make_channel_config(cfg, source_addr, target_addr); - let tracer_config = make_tracer_config(cfg, target_addr, trace_identifier)?; - let backend = Backend::new( - tracer_config, - channel_config, - cfg.max_samples, - cfg.max_flows(), - ); - let trace_data = backend.trace(); - thread::Builder::new() - .name(format!("tracer-{}", tracer_config.trace_identifier.0)) - .spawn(move || backend.start().expect("failed to run tracer backend"))?; - Ok(make_trace_info( - cfg, - trace_data, - source_addr, - target_host.to_string(), - target_addr, - )) + let (tracer, _) = Builder::new(target_addr) + .interface(cfg.interface.clone()) + .source_addr(cfg.source_addr) + .privilege_mode(cfg.privilege_mode) + .protocol(cfg.protocol) + .packet_size(cfg.packet_size) + .payload_pattern(cfg.payload_pattern) + .tos(cfg.tos) + .icmp_extension_parse_mode(cfg.icmp_extension_parse_mode) + .read_timeout(cfg.read_timeout) + .tcp_connect_timeout(cfg.min_round_duration) + .trace_identifier(trace_identifier) + .max_rounds(cfg.max_rounds) + .first_ttl(cfg.first_ttl) + .max_ttl(cfg.max_ttl) + .grace_duration(cfg.grace_duration) + .max_inflight(cfg.max_inflight) + .initial_sequence(cfg.initial_sequence) + .multipath_strategy(cfg.multipath_strategy) + .port_direction(cfg.port_direction) + .min_round_duration(cfg.min_round_duration) + .max_round_duration(cfg.max_round_duration) + .max_flows(cfg.max_flows()) + .max_samples(cfg.max_samples) + .drop_privileges(true) + .build()? + .spawn()?; + Ok(make_trace_info(tracer, target_host.to_string())) } /// Run the TUI, stream or report. @@ -195,84 +188,6 @@ fn configure_logging(cfg: &TrippyConfig) -> Option { None } -/// Make the tracer configuration. -fn make_tracer_config( - args: &TrippyConfig, - target_addr: IpAddr, - trace_identifier: u16, -) -> anyhow::Result { - Ok(Config::new( - target_addr, - args.protocol, - args.max_rounds, - trace_identifier, - args.first_ttl, - args.max_ttl, - args.grace_duration, - args.max_inflight, - args.initial_sequence, - args.multipath_strategy, - args.port_direction, - args.min_round_duration, - args.max_round_duration, - )?) -} - -/// Make the tracer configuration. -fn make_channel_config( - args: &TrippyConfig, - source_addr: IpAddr, - target_addr: IpAddr, -) -> ChannelConfig { - ChannelConfig::new( - args.privilege_mode, - args.protocol, - source_addr, - target_addr, - args.packet_size, - args.payload_pattern, - args.initial_sequence, - args.tos, - args.icmp_extension_parse_mode, - args.read_timeout, - args.min_round_duration, - ) -} - -/// Make the per-trace information. -fn make_trace_info( - args: &TrippyConfig, - trace_data: Arc>, - source_addr: IpAddr, - target: String, - target_addr: IpAddr, -) -> TraceInfo { - TraceInfo::new( - trace_data, - source_addr, - target, - target_addr, - args.privilege_mode, - args.multipath_strategy, - args.port_direction, - args.protocol, - args.first_ttl, - args.max_ttl, - args.grace_duration, - args.min_round_duration, - args.max_round_duration, - args.max_inflight, - args.initial_sequence, - args.icmp_extension_parse_mode, - args.read_timeout, - args.packet_size, - args.payload_pattern, - args.interface.clone(), - args.geoip_mmdb_file.clone(), - args.dns_resolve_all, - ) -} - /// Make the TUI configuration. fn make_tui_config(args: &TrippyConfig) -> TuiConfig { TuiConfig::new( @@ -288,86 +203,29 @@ fn make_tui_config(args: &TrippyConfig) -> TuiConfig { args.tui_theme, &args.tui_bindings, &args.tui_custom_columns, + args.geoip_mmdb_file.clone(), + args.dns_resolve_all, ) } +/// Make the per-trace information. +fn make_trace_info(tracer: Tracer, target: String) -> TraceInfo { + TraceInfo::new(tracer, target) +} + /// Information about a `Trace` needed for the Tui, stream and reports. #[derive(Debug, Clone)] pub struct TraceInfo { - pub data: Arc>, - pub source_addr: IpAddr, + pub data: Tracer, pub target_hostname: String, - pub target_addr: IpAddr, - pub privilege_mode: PrivilegeMode, - pub multipath_strategy: MultipathStrategy, - pub port_direction: PortDirection, - pub protocol: Protocol, - pub first_ttl: u8, - pub max_ttl: u8, - pub grace_duration: Duration, - pub min_round_duration: Duration, - pub max_round_duration: Duration, - pub max_inflight: u8, - pub initial_sequence: u16, - pub icmp_extensions: IcmpExtensionParseMode, - pub read_timeout: Duration, - pub packet_size: u16, - pub payload_pattern: u8, - pub interface: Option, - pub geoip_mmdb_file: Option, - pub dns_resolve_all: bool, } impl TraceInfo { - #[allow(clippy::too_many_arguments)] #[must_use] - pub fn new( - data: Arc>, - source_addr: IpAddr, - target_hostname: String, - target_addr: IpAddr, - privilege_mode: PrivilegeMode, - multipath_strategy: MultipathStrategy, - port_direction: PortDirection, - protocol: Protocol, - first_ttl: u8, - max_ttl: u8, - grace_duration: Duration, - min_round_duration: Duration, - max_round_duration: Duration, - max_inflight: u8, - initial_sequence: u16, - icmp_extensions: IcmpExtensionParseMode, - read_timeout: Duration, - packet_size: u16, - payload_pattern: u8, - interface: Option, - geoip_mmdb_file: Option, - dns_resolve_all: bool, - ) -> Self { + pub fn new(data: Tracer, target_hostname: String) -> Self { Self { data, - source_addr, target_hostname, - target_addr, - privilege_mode, - multipath_strategy, - port_direction, - protocol, - first_ttl, - max_ttl, - grace_duration, - min_round_duration, - max_round_duration, - max_inflight, - initial_sequence, - icmp_extensions, - read_timeout, - packet_size, - payload_pattern, - interface, - geoip_mmdb_file, - dns_resolve_all, } } } diff --git a/crates/trippy/src/backend.rs b/crates/trippy/src/backend.rs deleted file mode 100644 index 4647eb25f..000000000 --- a/crates/trippy/src/backend.rs +++ /dev/null @@ -1,63 +0,0 @@ -use parking_lot::RwLock; -use std::fmt::Debug; -use std::sync::Arc; -use trace::Trace; -use tracing::instrument; -use trippy_core::{ChannelConfig, Config, SocketImpl, Tracer, TracerChannel}; -use trippy_privilege::Privilege; - -pub mod flows; -pub mod trace; - -/// A tracing backend. -#[derive(Debug)] -pub struct Backend { - tracer_config: Config, - channel_config: ChannelConfig, - trace: Arc>, -} - -impl Backend { - /// Create a tracing `Backend`. - pub fn new( - tracer_config: Config, - channel_config: ChannelConfig, - max_samples: usize, - max_flows: usize, - ) -> Self { - Self { - tracer_config, - channel_config, - trace: Arc::new(RwLock::new(Trace::new(max_samples, max_flows))), - } - } - - pub fn trace(&self) -> Arc> { - self.trace.clone() - } - - /// Run the tracing backend. - /// - /// Note that this implementation blocks the tracer on the `RwLock` and so any delays in the the TUI - /// will delay the next round of the trace. - #[instrument(skip_all)] - pub fn start(&self) -> anyhow::Result<()> { - let td = self.trace.clone(); - let channel = - TracerChannel::::connect(&self.channel_config).map_err(|err| { - td.write().set_error(Some(err.to_string())); - err - })?; - Privilege::drop_privileges()?; - let tracer = Tracer::new(&self.tracer_config, move |round| { - self.trace.write().update_from_round(round); - }); - match tracer.trace(channel) { - Ok(()) => {} - Err(err) => { - td.write().set_error(Some(err.to_string())); - } - }; - Ok(()) - } -} diff --git a/crates/trippy/src/config.rs b/crates/trippy/src/config.rs index 14a252d01..46cebfe61 100644 --- a/crates/trippy/src/config.rs +++ b/crates/trippy/src/config.rs @@ -9,6 +9,7 @@ use std::net::IpAddr; use std::time::Duration; use trippy_core::{ defaults, IcmpExtensionParseMode, MultipathStrategy, PortDirection, PrivilegeMode, Protocol, + MAX_TTL, }; use trippy_dns::{IpAddrFamily, ResolveMethod}; @@ -23,7 +24,6 @@ use crate::config::file::ConfigTui; pub use binding::{TuiBindings, TuiCommandItem, TuiKeyBinding}; pub use cmd::Args; pub use columns::{TuiColumn, TuiColumns}; -pub use constants::MAX_HOPS; pub use theme::{TuiColor, TuiTheme, TuiThemeItem}; use trippy_privilege::Privilege; @@ -931,13 +931,13 @@ fn validate_flows(mode: Mode, strategy: MultipathStrategy) -> anyhow::Result<()> /// Validate `first_ttl` and `max_ttl`. fn validate_ttl(first_ttl: u8, max_ttl: u8) -> anyhow::Result<()> { - if (first_ttl as usize) < 1 || (first_ttl as usize) > MAX_HOPS { + if !(1..=MAX_TTL).contains(&first_ttl) { Err(anyhow!( - "first-ttl ({first_ttl}) must be in the range 1..{MAX_HOPS}" + "first-ttl ({first_ttl}) must be in the range 1..{MAX_TTL}" )) - } else if (max_ttl as usize) < 1 || (max_ttl as usize) > MAX_HOPS { + } else if !(1..=MAX_TTL).contains(&max_ttl) { Err(anyhow!( - "max-ttl ({max_ttl}) must be in the range 1..{MAX_HOPS}" + "max-ttl ({max_ttl}) must be in the range 1..{MAX_TTL}" )) } else if first_ttl > max_ttl { Err(anyhow!( @@ -1261,12 +1261,12 @@ mod tests { #[test_case("trip example.com", Ok(cfg().first_ttl(1).build()); "default first ttl")] #[test_case("trip example.com --first-ttl 5", Ok(cfg().first_ttl(5).build()); "custom first ttl")] #[test_case("trip example.com -f 5", Ok(cfg().first_ttl(5).build()); "custom first ttl short")] - #[test_case("trip example.com --first-ttl 0", Err(anyhow!("first-ttl (0) must be in the range 1..255")); "invalid low first ttl")] + #[test_case("trip example.com --first-ttl 0", Err(anyhow!("first-ttl (0) must be in the range 1..254")); "invalid low first ttl")] #[test_case("trip example.com --first-ttl 500", Err(anyhow!("error: invalid value '500' for '--first-ttl ': 500 is not in 0..=255 For more information, try '--help'.")); "invalid high first ttl")] #[test_case("trip example.com", Ok(cfg().first_ttl(1).build()); "default max ttl")] #[test_case("trip example.com --max-ttl 5", Ok(cfg().max_ttl(5).build()); "custom max ttl")] #[test_case("trip example.com -t 5", Ok(cfg().max_ttl(5).build()); "custom max ttl short")] - #[test_case("trip example.com --max-ttl 0", Err(anyhow!("max-ttl (0) must be in the range 1..255")); "invalid low max ttl")] + #[test_case("trip example.com --max-ttl 0", Err(anyhow!("max-ttl (0) must be in the range 1..254")); "invalid low max ttl")] #[test_case("trip example.com --max-ttl 500", Err(anyhow!("error: invalid value '500' for '--max-ttl ': 500 is not in 0..=255 For more information, try '--help'.")); "invalid high max ttl")] #[test_case("trip example.com --first-ttl 3 --max-ttl 2", Err(anyhow!("first-ttl (3) must be less than or equal to max-ttl (2)")); "first ttl higher than max ttl")] #[test_case("trip example.com --first-ttl 5 --max-ttl 5", Ok(cfg().first_ttl(5).max_ttl(5).build()); "custom first and max ttl")] diff --git a/crates/trippy/src/config/constants.rs b/crates/trippy/src/config/constants.rs index 78b91eb34..148035446 100644 --- a/crates/trippy/src/config/constants.rs +++ b/crates/trippy/src/config/constants.rs @@ -4,12 +4,6 @@ use crate::config::{ }; use std::time::Duration; -/// The maximum number of hops we allow. -/// -/// The IP `ttl` is a u8 (0..255) but since a `ttl` of zero isn't useful we only allow 255 distinct -/// hops. -pub const MAX_HOPS: usize = u8::MAX as usize; - /// The default value for `mode`. pub const DEFAULT_MODE: Mode = Mode::Tui; diff --git a/crates/trippy/src/frontend/config.rs b/crates/trippy/src/frontend/config.rs index 353420c74..8b8249710 100644 --- a/crates/trippy/src/frontend/config.rs +++ b/crates/trippy/src/frontend/config.rs @@ -32,6 +32,8 @@ pub struct TuiConfig { pub bindings: Bindings, /// The columns to display in the hops table. pub tui_columns: Columns, + pub geoip_mmdb_file: Option, + pub dns_resolve_all: bool, } impl TuiConfig { @@ -49,6 +51,9 @@ impl TuiConfig { tui_theme: TuiTheme, tui_bindings: &TuiBindings, tui_columns: &TuiColumns, + + geoip_mmdb_file: Option, + dns_resolve_all: bool, ) -> Self { Self { refresh_rate, @@ -63,6 +68,8 @@ impl TuiConfig { theme: Theme::from(tui_theme), bindings: Bindings::from(*tui_bindings), tui_columns: Columns::from(tui_columns.clone()), + geoip_mmdb_file, + dns_resolve_all, } } } diff --git a/crates/trippy/src/frontend/render/body.rs b/crates/trippy/src/frontend/render/body.rs index e61295e78..6f9a98fdd 100644 --- a/crates/trippy/src/frontend/render/body.rs +++ b/crates/trippy/src/frontend/render/body.rs @@ -1,8 +1,8 @@ -use crate::backend::trace::Trace; use crate::frontend::render::{bsod, chart, splash, table, world}; use crate::frontend::tui_app::TuiApp; use ratatui::layout::Rect; use ratatui::Frame; +use trippy_core::TraceState; /// Render the body. /// @@ -11,7 +11,11 @@ use ratatui::Frame; pub fn render(f: &mut Frame<'_>, rec: Rect, app: &mut TuiApp) { if let Some(err) = app.selected_tracer_data.error() { bsod::render(f, rec, err); - } else if app.tracer_data().hops(Trace::default_flow_id()).is_empty() { + } else if app + .tracer_data() + .hops(TraceState::default_flow_id()) + .is_empty() + { splash::render(f, app, rec); } else if app.show_chart { chart::render(f, app, rec); diff --git a/crates/trippy/src/frontend/render/header.rs b/crates/trippy/src/frontend/render/header.rs index 9390f336e..043fa0746 100644 --- a/crates/trippy/src/frontend/render/header.rs +++ b/crates/trippy/src/frontend/render/header.rs @@ -40,22 +40,22 @@ pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { .style(Style::default()) .block(header_block.clone()) .alignment(Alignment::Right); - let protocol = match app.tracer_config().protocol { + let protocol = match app.tracer_config().data.protocol() { Protocol::Icmp => format!( "icmp({}, {})", - render_target_family(app.tracer_config().target_addr), - app.tracer_config().privilege_mode + render_target_family(app.tracer_config().data.target_addr()), + app.tracer_config().data.privilege_mode() ), Protocol::Udp => format!( "udp({}, {}, {})", - render_target_family(app.tracer_config().target_addr), - app.tracer_config().multipath_strategy, - app.tracer_config().privilege_mode + render_target_family(app.tracer_config().data.target_addr()), + app.tracer_config().data.multipath_strategy(), + app.tracer_config().data.privilege_mode() ), Protocol::Tcp => format!( "tcp({}, {})", - render_target_family(app.tracer_config().target_addr), - app.tracer_config().privilege_mode + render_target_family(app.tracer_config().data.target_addr()), + app.tracer_config().data.privilege_mode() ), }; let details = if app.show_hop_details { @@ -138,28 +138,29 @@ fn render_target_family(target: IpAddr) -> &'static str { /// Render the source address of the trace. fn render_source(app: &TuiApp) -> String { - let src_hostname = app - .resolver - .lazy_reverse_lookup(app.tracer_config().source_addr); - let src_addr = app.tracer_config().source_addr; - match app.tracer_config().port_direction { - PortDirection::None => { - format!("{src_hostname} ({src_addr})") - } - PortDirection::FixedDest(_) => { - format!("{src_hostname}:* ({src_addr}:*)") - } - PortDirection::FixedSrc(src) | PortDirection::FixedBoth(src, _) => { - format!("{src_hostname}:{} ({src_addr}:{})", src.0, src.0) + if let Some(src_addr) = app.tracer_config().data.source_addr() { + let src_hostname = app.resolver.lazy_reverse_lookup(src_addr); + match app.tracer_config().data.port_direction() { + PortDirection::None => { + format!("{src_hostname} ({src_addr})") + } + PortDirection::FixedDest(_) => { + format!("{src_hostname}:* ({src_addr}:*)") + } + PortDirection::FixedSrc(src) | PortDirection::FixedBoth(src, _) => { + format!("{src_hostname}:{} ({src_addr}:{})", src.0, src.0) + } } + } else { + String::from("unknown") } } /// Render the destination address. fn render_destination(app: &TuiApp) -> String { let dest_hostname = &app.tracer_config().target_hostname; - let dest_addr = app.tracer_config().target_addr; - match app.tracer_config().port_direction { + let dest_addr = app.tracer_config().data.target_addr(); + match app.tracer_config().data.port_direction() { PortDirection::None => { format!("{dest_hostname} ({dest_addr})") } diff --git a/crates/trippy/src/frontend/render/settings.rs b/crates/trippy/src/frontend/render/settings.rs index 552f7f532..8677491ec 100644 --- a/crates/trippy/src/frontend/render/settings.rs +++ b/crates/trippy/src/frontend/render/settings.rs @@ -199,43 +199,55 @@ fn format_tui_settings(app: &TuiApp) -> Vec { /// Format trace settings. fn format_trace_settings(app: &TuiApp) -> Vec { let cfg = app.tracer_config(); - let interface = if let Some(iface) = cfg.interface.as_deref() { + let interface = if let Some(iface) = cfg.data.interface() { iface.to_string() } else { "auto".to_string() }; - let (src_port, dst_port) = match cfg.port_direction { + let (src_port, dst_port) = match cfg.data.port_direction() { PortDirection::None => ("n/a".to_string(), "n/a".to_string()), PortDirection::FixedDest(dst) => ("auto".to_string(), format!("{}", dst.0)), PortDirection::FixedSrc(src) => (format!("{}", src.0), "auto".to_string()), PortDirection::FixedBoth(src, dst) => (format!("{}", src.0), format!("{}", dst.0)), }; vec![ - SettingsItem::new("first-ttl", format!("{}", cfg.first_ttl)), - SettingsItem::new("max-ttl", format!("{}", cfg.max_ttl)), + SettingsItem::new("first-ttl", format!("{}", cfg.data.first_ttl().0)), + SettingsItem::new("max-ttl", format!("{}", cfg.data.max_ttl().0)), SettingsItem::new( "min-round-duration", - format!("{}", format_duration(cfg.min_round_duration)), + format!("{}", format_duration(cfg.data.min_round_duration())), ), SettingsItem::new( "max-round-duration", - format!("{}", format_duration(cfg.max_round_duration)), + format!("{}", format_duration(cfg.data.max_round_duration())), ), SettingsItem::new( "grace-duration", - format!("{}", format_duration(cfg.grace_duration)), + format!("{}", format_duration(cfg.data.grace_duration())), + ), + SettingsItem::new("max-inflight", format!("{}", cfg.data.max_inflight().0)), + SettingsItem::new( + "initial-sequence", + format!("{}", cfg.data.initial_sequence().0), ), - SettingsItem::new("max-inflight", format!("{}", cfg.max_inflight)), - SettingsItem::new("initial-sequence", format!("{}", cfg.initial_sequence)), SettingsItem::new( "read-timeout", - format!("{}", format_duration(cfg.read_timeout)), + format!("{}", format_duration(cfg.data.read_timeout())), + ), + SettingsItem::new("packet-size", format!("{}", cfg.data.packet_size().0)), + SettingsItem::new( + "payload-pattern", + format!("{}", cfg.data.payload_pattern().0), + ), + SettingsItem::new( + "icmp-extensions", + format!("{}", cfg.data.icmp_extension_parse_mode()), ), - SettingsItem::new("packet-size", format!("{}", cfg.packet_size)), - SettingsItem::new("payload-pattern", format!("{}", cfg.payload_pattern)), - SettingsItem::new("icmp-extensions", format!("{}", cfg.icmp_extensions)), SettingsItem::new("interface", interface), - SettingsItem::new("multipath-strategy", cfg.multipath_strategy.to_string()), + SettingsItem::new( + "multipath-strategy", + cfg.data.multipath_strategy().to_string(), + ), SettingsItem::new("target-port", dst_port), SettingsItem::new("source-port", src_port), SettingsItem::new( @@ -262,7 +274,7 @@ fn format_dns_settings(app: &TuiApp) -> Vec { ), SettingsItem::new( "dns-resolve-all", - format!("{}", app.tracer_config().dns_resolve_all), + format!("{}", app.tui_config.dns_resolve_all), ), SettingsItem::new( "dns-lookup-as-info", @@ -275,7 +287,7 @@ fn format_dns_settings(app: &TuiApp) -> Vec { fn format_geoip_settings(app: &TuiApp) -> Vec { vec![SettingsItem::new( "geoip-mmdb-file", - app.tracer_config() + app.tui_config .geoip_mmdb_file .as_deref() .unwrap_or("none") diff --git a/crates/trippy/src/frontend/render/table.rs b/crates/trippy/src/frontend/render/table.rs index 8976d60f7..aa85b2f9a 100644 --- a/crates/trippy/src/frontend/render/table.rs +++ b/crates/trippy/src/frontend/render/table.rs @@ -1,4 +1,3 @@ -use crate::backend::trace::Hop; use crate::config::{AddressMode, AsMode, GeoIpMode, IcmpExtensionMode}; use crate::frontend::columns::{ColumnType, Columns}; use crate::frontend::config::TuiConfig; @@ -12,6 +11,7 @@ use ratatui::widgets::{Block, BorderType, Borders, Cell, Row, Table}; use ratatui::Frame; use std::net::IpAddr; use std::rc::Rc; +use trippy_core::Hop; use trippy_core::{Extension, Extensions, IcmpPacketType, MplsLabelStackMember, UnknownExtension}; use trippy_dns::{AsInfo, DnsEntry, DnsResolver, Resolved, Resolver, Unresolved}; diff --git a/crates/trippy/src/frontend/render/world.rs b/crates/trippy/src/frontend/render/world.rs index 0239f0664..efe090b59 100644 --- a/crates/trippy/src/frontend/render/world.rs +++ b/crates/trippy/src/frontend/render/world.rs @@ -1,4 +1,3 @@ -use crate::backend::trace::Hop; use crate::frontend::tui_app::TuiApp; use itertools::Itertools; use ratatui::layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}; @@ -9,6 +8,7 @@ use ratatui::widgets::canvas::{Canvas, Circle, Context, Map, MapResolution, Rect use ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph}; use ratatui::Frame; use std::collections::HashMap; +use trippy_core::Hop; /// Render the `GeoIp` map. pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { @@ -148,7 +148,7 @@ fn render_map_info_panel(f: &mut Frame<'_>, app: &TuiApp, rect: Rect, entries: & "**Hidden**".to_string() } else { match locations.as_slice() { - _ if app.tracer_config().geoip_mmdb_file.is_none() => "GeoIp not enabled".to_string(), + _ if app.tui_config.geoip_mmdb_file.is_none() => "GeoIp not enabled".to_string(), [] if selected_hop.addr_count() > 0 => format!( "No GeoIp data for hop {} ({})", selected_hop.ttl(), diff --git a/crates/trippy/src/frontend/tui_app.rs b/crates/trippy/src/frontend/tui_app.rs index a39cdaeee..dba819d4c 100644 --- a/crates/trippy/src/frontend/tui_app.rs +++ b/crates/trippy/src/frontend/tui_app.rs @@ -1,17 +1,17 @@ use crate::app::TraceInfo; -use crate::backend::flows::FlowId; -use crate::backend::trace::Hop; -use crate::backend::trace::Trace; use crate::frontend::config::TuiConfig; use crate::frontend::render::settings::{SETTINGS_TABS, SETTINGS_TAB_COLUMNS}; use crate::geoip::GeoIpLookup; use itertools::Itertools; use ratatui::widgets::TableState; use std::time::SystemTime; +use trippy_core::FlowId; +use trippy_core::Hop; +use trippy_core::TraceState; use trippy_dns::{DnsResolver, ResolveMethod}; pub struct TuiApp { - pub selected_tracer_data: Trace, + pub selected_tracer_data: TraceState, pub trace_info: Vec, pub tui_config: TuiConfig, /// The state of the hop table. @@ -54,7 +54,7 @@ impl TuiApp { trace_info: Vec, ) -> Self { Self { - selected_tracer_data: Trace::new(0, 0), + selected_tracer_data: TraceState::default(), trace_info, tui_config, table_state: TableState::default(), @@ -62,7 +62,7 @@ impl TuiApp { trace_selected: 0, settings_tab_selected: 0, selected_hop_address: 0, - selected_flow: Trace::default_flow_id(), + selected_flow: TraceState::default_flow_id(), flow_counts: vec![], resolver, geoip_lookup, @@ -78,19 +78,16 @@ impl TuiApp { } } - pub fn tracer_data(&self) -> &Trace { + pub fn tracer_data(&self) -> &TraceState { &self.selected_tracer_data } pub fn snapshot_trace_data(&mut self) { - self.selected_tracer_data = self.trace_info[self.trace_selected].data.read().clone(); + self.selected_tracer_data = self.trace_info[self.trace_selected].data.snapshot(); } pub fn clear_trace_data(&mut self) { - *self.trace_info[self.trace_selected].data.write() = Trace::new( - self.selected_tracer_data.max_samples(), - self.selected_tracer_data.max_flows(), - ); + self.trace_info[self.trace_selected].data.clear(); } pub fn selected_hop_or_target(&self) -> &Hop { diff --git a/crates/trippy/src/lib.rs b/crates/trippy/src/lib.rs index b454fd0e7..457f97570 100644 --- a/crates/trippy/src/lib.rs +++ b/crates/trippy/src/lib.rs @@ -1,5 +1,19 @@ #![allow(rustdoc::broken_intra_doc_links, rustdoc::bare_urls)] #![doc = include_str!("../../../README.md")] -// Re-export the core library, so it may be used from trippy crate. -pub use trippy_core::*; +// Re-export the user facing libraries, so they may be used from trippy crate directly. + +/// A network tracer. +pub mod core { + pub use trippy_core::*; +} + +/// A lazy DNS resolver. +pub mod dns { + pub use trippy_dns::*; +} + +/// Discover platform privileges. +pub mod privilege { + pub use trippy_privilege::*; +} diff --git a/crates/trippy/src/main.rs b/crates/trippy/src/main.rs index 5ebf6301e..43452b1c2 100644 --- a/crates/trippy/src/main.rs +++ b/crates/trippy/src/main.rs @@ -22,7 +22,6 @@ use std::process; use trippy_privilege::Privilege; mod app; -mod backend; mod config; mod frontend; mod geoip; diff --git a/crates/trippy/src/report.rs b/crates/trippy/src/report.rs index 1e37f9991..1f0976f4d 100644 --- a/crates/trippy/src/report.rs +++ b/crates/trippy/src/report.rs @@ -1,7 +1,6 @@ -use crate::backend::trace::Trace; use anyhow::anyhow; -use parking_lot::RwLock; -use std::sync::Arc; +use trippy_core::TraceState; +use trippy_core::Tracer; pub mod csv; pub mod dot; @@ -13,12 +12,12 @@ pub mod table; mod types; /// Block until trace data for round `round` is available. -fn wait_for_round(trace_data: &Arc>, report_cycles: usize) -> anyhow::Result { - let mut trace = trace_data.read().clone(); - while trace.round(Trace::default_flow_id()).is_none() - || trace.round(Trace::default_flow_id()) < Some(report_cycles - 1) +fn wait_for_round(trace_data: &Tracer, report_cycles: usize) -> anyhow::Result { + let mut trace = trace_data.snapshot(); + while trace.round(TraceState::default_flow_id()).is_none() + || trace.round(TraceState::default_flow_id()) < Some(report_cycles - 1) { - trace = trace_data.read().clone(); + trace = trace_data.snapshot(); if let Some(err) = trace.error() { return Err(anyhow!("error: {}", err)); } diff --git a/crates/trippy/src/report/csv.rs b/crates/trippy/src/report/csv.rs index 9b2a83642..0eb8a8679 100644 --- a/crates/trippy/src/report/csv.rs +++ b/crates/trippy/src/report/csv.rs @@ -1,10 +1,9 @@ use crate::app::TraceInfo; -use crate::backend; -use crate::backend::trace::Trace; use crate::report::types::fixed_width; use itertools::Itertools; use serde::Serialize; use std::net::IpAddr; +use trippy_core::TraceState; use trippy_dns::Resolver; /// Generate a CSV report of trace data. @@ -15,8 +14,13 @@ pub fn report( ) -> anyhow::Result<()> { let trace = super::wait_for_round(&info.data, report_cycles)?; let mut writer = csv::Writer::from_writer(std::io::stdout()); - for hop in trace.hops(Trace::default_flow_id()) { - let row = CsvRow::new(&info.target_hostname, info.target_addr, hop, resolver); + for hop in trace.hops(TraceState::default_flow_id()) { + let row = CsvRow::new( + &info.target_hostname, + info.data.target_addr(), + hop, + resolver, + ); writer.serialize(row)?; } Ok(()) @@ -59,7 +63,7 @@ impl CsvRow { fn new( target: &str, target_addr: IpAddr, - hop: &backend::trace::Hop, + hop: &trippy_core::Hop, resolver: &R, ) -> Self { let ttl = hop.ttl(); diff --git a/crates/trippy/src/report/dot.rs b/crates/trippy/src/report/dot.rs index a36cf5d7e..92559ecde 100644 --- a/crates/trippy/src/report/dot.rs +++ b/crates/trippy/src/report/dot.rs @@ -1,9 +1,9 @@ use crate::app::TraceInfo; -use crate::backend::flows::FlowEntry; use petgraph::dot::{Config, Dot}; use petgraph::graphmap::DiGraphMap; use std::fmt::{Debug, Formatter}; use std::net::{IpAddr, Ipv4Addr}; +use trippy_core::FlowEntry; /// Run a trace and generate a dot file. pub fn report(info: &TraceInfo, report_cycles: usize) -> anyhow::Result<()> { @@ -14,7 +14,7 @@ pub fn report(info: &TraceInfo, report_cycles: usize) -> anyhow::Result<()> { } } super::wait_for_round(&info.data, report_cycles)?; - let trace = info.data.read().clone(); + let trace = info.data.snapshot(); let mut graph: DiGraphMap = DiGraphMap::new(); for (flow, _id) in trace.flows() { for (fst, snd) in flow.entries.windows(2).map(|pair| (pair[0], pair[1])) { diff --git a/crates/trippy/src/report/flows.rs b/crates/trippy/src/report/flows.rs index f1c2aa759..0d49014ec 100644 --- a/crates/trippy/src/report/flows.rs +++ b/crates/trippy/src/report/flows.rs @@ -3,7 +3,7 @@ use crate::app::TraceInfo; /// Run a trace and report all flows observed. pub fn report(info: &TraceInfo, report_cycles: usize) -> anyhow::Result<()> { super::wait_for_round(&info.data, report_cycles)?; - let trace = info.data.read().clone(); + let trace = info.data.snapshot(); for (flow, flow_id) in trace.flows() { println!("flow {flow_id}: {flow}"); } diff --git a/crates/trippy/src/report/json.rs b/crates/trippy/src/report/json.rs index ffd4f20ef..f3fde9916 100644 --- a/crates/trippy/src/report/json.rs +++ b/crates/trippy/src/report/json.rs @@ -1,6 +1,6 @@ use crate::app::TraceInfo; -use crate::backend::trace::Trace; use crate::report::types::{Hop, Host, Info, Report}; +use trippy_core::TraceState; use trippy_dns::Resolver; /// Generate a json report of trace data. @@ -11,14 +11,14 @@ pub fn report( ) -> anyhow::Result<()> { let trace = super::wait_for_round(&info.data, report_cycles)?; let hops: Vec = trace - .hops(Trace::default_flow_id()) + .hops(TraceState::default_flow_id()) .iter() .map(|hop| Hop::from((hop, resolver))) .collect(); let report = Report { info: Info { target: Host { - ip: info.target_addr, + ip: info.data.target_addr(), hostname: info.target_hostname.to_string(), }, }, diff --git a/crates/trippy/src/report/stream.rs b/crates/trippy/src/report/stream.rs index 99c9cbbce..1770d3639 100644 --- a/crates/trippy/src/report/stream.rs +++ b/crates/trippy/src/report/stream.rs @@ -1,19 +1,23 @@ use crate::app::TraceInfo; -use crate::backend::trace::Trace; use crate::report::types::Hop; use anyhow::anyhow; use std::thread::sleep; +use trippy_core::TraceState; use trippy_dns::Resolver; /// Display a continuous stream of trace data. pub fn report(info: &TraceInfo, resolver: &R) -> anyhow::Result<()> { - println!("Tracing to {} ({})", info.target_hostname, info.target_addr); + println!( + "Tracing to {} ({})", + info.target_hostname, + info.data.target_addr() + ); loop { - let trace_data = &info.data.read().clone(); + let trace_data = &info.data.snapshot(); if let Some(err) = trace_data.error() { return Err(anyhow!("error: {}", err)); } - for hop in trace_data.hops(Trace::default_flow_id()) { + for hop in trace_data.hops(TraceState::default_flow_id()) { let hop = Hop::from((hop, resolver)); let ttl = hop.ttl; let addrs = hop.hosts.to_string(); @@ -30,6 +34,6 @@ pub fn report(info: &TraceInfo, resolver: &R) -> anyhow::Result<()> "ttl={ttl} addrs={addrs} exts={exts} loss_pct={loss_pct:.1} sent={sent} recv={recv} last={last:.1} best={best:.1} worst={worst:.1} avg={avg:.1} stddev={stddev:.1}" ); } - sleep(info.min_round_duration); + sleep(info.data.min_round_duration()); } } diff --git a/crates/trippy/src/report/table.rs b/crates/trippy/src/report/table.rs index e3639619c..f5fb5064d 100644 --- a/crates/trippy/src/report/table.rs +++ b/crates/trippy/src/report/table.rs @@ -1,8 +1,8 @@ use crate::app::TraceInfo; -use crate::backend::trace::Trace; use comfy_table::presets::{ASCII_MARKDOWN, UTF8_FULL}; use comfy_table::{ContentArrangement, Table}; use itertools::Itertools; +use trippy_core::TraceState; use trippy_dns::Resolver; /// Generate a markdown table report of trace data. @@ -38,7 +38,7 @@ fn run_report_table( .load_preset(preset) .set_content_arrangement(ContentArrangement::Dynamic) .set_header(columns); - for hop in trace.hops(Trace::default_flow_id()) { + for hop in trace.hops(TraceState::default_flow_id()) { let ttl = hop.ttl().to_string(); let ips = hop.addrs().join("\n"); let ip = if ips.is_empty() { diff --git a/crates/trippy/src/report/types.rs b/crates/trippy/src/report/types.rs index 33f349cab..49e805e9e 100644 --- a/crates/trippy/src/report/types.rs +++ b/crates/trippy/src/report/types.rs @@ -1,4 +1,3 @@ -use crate::backend; use itertools::Itertools; use serde::{Serialize, Serializer}; use std::fmt::{Display, Formatter}; @@ -45,8 +44,8 @@ pub struct Hop { pub jinta: f64, } -impl From<(&backend::trace::Hop, &R)> for Hop { - fn from((value, resolver): (&backend::trace::Hop, &R)) -> Self { +impl From<(&trippy_core::Hop, &R)> for Hop { + fn from((value, resolver): (&trippy_core::Hop, &R)) -> Self { let hosts = Hosts::from((value.addrs(), resolver)); let extensions = value.extensions().map(Extensions::from).unwrap_or_default(); Self { diff --git a/examples/hello-world/Cargo.toml b/examples/hello-world/Cargo.toml new file mode 100644 index 000000000..9f5223b3e --- /dev/null +++ b/examples/hello-world/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "hello-world" +version = "0.1.0" +license = "Apache-2.0" +edition = "2021" +rust-version = "1.75" + +[dependencies] +trippy = { version = "0.11.0-dev", path = "../../crates/trippy" } +anyhow = "1.0.86" \ No newline at end of file diff --git a/examples/hello-world/src/main.rs b/examples/hello-world/src/main.rs new file mode 100644 index 000000000..8cdba8bb1 --- /dev/null +++ b/examples/hello-world/src/main.rs @@ -0,0 +1,10 @@ +use std::str::FromStr; +use trippy::core::Builder; + +fn main() -> anyhow::Result<()> { + let addr = std::net::IpAddr::from_str("1.1.1.1")?; + Builder::new(addr) + .build()? + .run_with(|round| println!("{:?}", round))?; + Ok(()) +} diff --git a/examples/traceroute/Cargo.toml b/examples/traceroute/Cargo.toml new file mode 100644 index 000000000..7b7fc76af --- /dev/null +++ b/examples/traceroute/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "traceroute" +version = "0.1.0" +license = "Apache-2.0" +edition = "2021" +rust-version = "1.75" + +[dependencies] +trippy = { version = "0.11.0-dev", path = "../../crates/trippy" } +anyhow = "1.0.86" +itertools = "0.13.0" +clap = "4.5.7" \ No newline at end of file diff --git a/examples/traceroute/src/main.rs b/examples/traceroute/src/main.rs new file mode 100644 index 000000000..bbe1888b3 --- /dev/null +++ b/examples/traceroute/src/main.rs @@ -0,0 +1,119 @@ +use anyhow::anyhow; +use clap::Parser; +use itertools::Itertools; +use std::net::IpAddr; +use std::str::FromStr; +use std::time::Duration; +use trippy::core::{Builder, Port, PortDirection, Protocol, TraceState}; +use trippy::dns::{Config, DnsResolver, Resolver}; + +/// A toy clone of BSD4.3 (macOS) traceroute. +/// +/// *** This is for demonstration purposes only. *** +#[derive(Parser, Debug)] +#[command(version, about, long_about = None, arg_required_else_help(true))] +struct Args { + host: String, + #[arg(short = 'f')] + first_ttl: Option, + #[arg(short = 'm')] + max_ttl: Option, + #[arg(short = 'i')] + interface: Option, + #[arg(short = 'p')] + port: Option, + #[arg(short = 'q')] + nqueries: Option, + #[arg(short = 's')] + src_addr: Option, + #[arg(short = 't')] + tos: Option, + #[arg(short = 'z')] + pausemsecs: Option, + #[arg(short = 'e')] + evasion: bool, +} + +fn main() -> anyhow::Result<()> { + let args = Args::parse(); + let hostname = args.host; + let interface = args.interface; + let src_addr = args + .src_addr + .as_ref() + .map(|addr| IpAddr::from_str(addr)) + .transpose()?; + let port = args.port.unwrap_or(33434); + let first_ttl = args.first_ttl.unwrap_or(1); + let max_ttl = args.max_ttl.unwrap_or(64); + let nqueries = args.nqueries.unwrap_or(3); + let tos = args.tos.unwrap_or(0); + let pausemecs = args.pausemsecs.unwrap_or(100); + let port_direction = if args.evasion { + PortDirection::FixedDest(Port(port)) + } else { + PortDirection::FixedSrc(Port(port)) + }; + let resolver = DnsResolver::start(Config::default())?; + let addrs: Vec<_> = resolver + .lookup(&hostname) + .map_err(|_| anyhow!(format!("traceroute: unknown host {}", hostname)))? + .into_iter() + .collect(); + let addr = match addrs.as_slice() { + [] => return Err(anyhow!("traceroute: unknown host {}", hostname)), + [addr] => *addr, + [addr, ..] => { + println!("traceroute: Warning: bbc.co.uk has multiple addresses; using {addr}"); + *addr + } + }; + let tracer = Builder::new(addr) + .interface(interface) + .source_addr(src_addr) + .protocol(Protocol::Udp) + .port_direction(port_direction) + .packet_size(52) + .first_ttl(first_ttl) + .max_ttl(max_ttl) + .tos(tos) + .max_flows(1) + .max_rounds(Some(nqueries)) + .min_round_duration(Duration::from_millis(pausemecs)) + .max_round_duration(Duration::from_millis(pausemecs)) + .build()?; + println!( + "traceroute to {} ({}), {} hops max, {} byte packets", + &hostname, + tracer.target_addr(), + tracer.max_ttl().0, + tracer.packet_size().0 + ); + tracer.run()?; + let snapshot = &tracer.snapshot(); + if let Some(err) = snapshot.error() { + return Err(anyhow!("error: {}", err)); + } + for hop in snapshot.hops(TraceState::default_flow_id()) { + let ttl = hop.ttl(); + let samples: String = hop + .samples() + .iter() + .map(|s| format!("{:.3} ms", s.as_secs_f64() * 1000_f64)) + .join(" "); + if hop.addr_count() > 0 { + for (i, addr) in hop.addrs().enumerate() { + let host = resolver.reverse_lookup(*addr).to_string(); + let address = format!("{host} ({addr})"); + if i != 0 { + println!(" {address} {samples}"); + } else { + println!(" {ttl} {address} {samples}"); + } + } + } else { + println!(" {ttl} * * * {samples}"); + } + } + Ok(()) +}