use crate::dns::resolver;
use crate::services::Services;
use crate::socket::factory::create_probe_socket_with_options_and_verbose;
use crate::traceroute::config::PreferredFamily;
use crate::traceroute::engine::TracerouteEngine;
use crate::traceroute::{TracerouteConfig, TracerouteError, TracerouteResult};
use std::net::IpAddr;
#[derive(Debug)]
pub struct Traceroute {
config: TracerouteConfig,
target_ip: IpAddr,
services: Option<Services>,
}
impl Traceroute {
pub async fn new_with_services(
mut config: TracerouteConfig,
services: Services,
) -> Result<Self, TracerouteError> {
let target_ip = Self::resolve_target(&mut config).await?;
Ok(Self {
config,
target_ip,
services: Some(services),
})
}
async fn resolve_target(config: &mut TracerouteConfig) -> Result<IpAddr, TracerouteError> {
let target_is_literal =
config.target.parse::<IpAddr>().is_ok() || config.target == "localhost";
if !target_is_literal {
if let Some(ip) = config.target_ip {
check_family(ip, config.preferred_family, &config.target)?;
return Ok(ip);
}
}
let ip = resolve_target_with_family(&config.target, config.preferred_family).await?;
config.target_ip = Some(ip);
Ok(ip)
}
pub async fn new(mut config: TracerouteConfig) -> Result<Self, TracerouteError> {
let target_ip = Self::resolve_target(&mut config).await?;
Ok(Self {
config,
target_ip,
services: None,
})
}
pub async fn run(self) -> Result<TracerouteResult, TracerouteError> {
let mut timing_config = self.config.timing.clone();
timing_config.socket_read_timeout = self.config.probe_timeout;
let socket = create_probe_socket_with_options_and_verbose(
self.target_ip,
timing_config,
self.config.protocol,
self.config.socket_mode,
self.config.verbose,
)
.await?;
let engine = if let Some(services) = self.services {
TracerouteEngine::new_with_services(
socket,
self.config.clone(),
self.target_ip,
std::sync::Arc::new(services),
)
.await?
} else {
TracerouteEngine::new(socket, self.config.clone(), self.target_ip).await?
};
let result = engine.run().await?;
Ok(result)
}
}
fn check_family(ip: IpAddr, family: PreferredFamily, target: &str) -> Result<(), TracerouteError> {
match (family, ip) {
(PreferredFamily::V4, IpAddr::V6(_)) => Err(TracerouteError::ResolutionError(format!(
"{target} is an IPv6 address but IPv4 was requested (-4)"
))),
(PreferredFamily::V6, IpAddr::V4(_)) => Err(TracerouteError::ResolutionError(format!(
"{target} is an IPv4 address but IPv6 was requested (-6)"
))),
_ => Ok(()),
}
}
pub async fn resolve_target_with_family(
target: &str,
family: PreferredFamily,
) -> Result<IpAddr, TracerouteError> {
if let Some((addr_part, zone)) = target.split_once('%') {
if addr_part.parse::<std::net::Ipv6Addr>().is_ok() {
return Err(TracerouteError::ResolutionError(format!(
"zone-scoped IPv6 targets ({addr_part}%{zone}) are not yet supported"
)));
}
}
if let Ok(ip) = target.parse::<IpAddr>() {
check_family(ip, family, target)?;
return Ok(ip);
}
if target == "localhost" {
return Ok(match family {
PreferredFamily::V6 => IpAddr::V6(std::net::Ipv6Addr::LOCALHOST),
_ => IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
});
}
match family {
PreferredFamily::V4 => resolve_first_a(target).await,
PreferredFamily::V6 => resolve_first_aaaa(target).await,
_ => {
match resolve_first_a(target).await {
Ok(ip) => Ok(ip),
Err(v4_err) => match resolve_first_aaaa(target).await {
Ok(ip) => Ok(ip),
Err(_) => Err(v4_err),
},
}
}
}
}
async fn resolve_first_a(target: &str) -> Result<IpAddr, TracerouteError> {
let addrs = resolver::resolve_a(target)
.await
.map_err(|e| TracerouteError::ResolutionError(format!("{target}: {e}")))?;
addrs
.first()
.map(|a| IpAddr::V4(*a))
.ok_or_else(|| TracerouteError::ResolutionError(format!("{target}: no IPv4 addresses")))
}
async fn resolve_first_aaaa(target: &str) -> Result<IpAddr, TracerouteError> {
let addrs = tokio::net::lookup_host((target, 0))
.await
.map_err(|e| TracerouteError::ResolutionError(format!("{target}: {e}")))?;
addrs
.into_iter()
.find(std::net::SocketAddr::is_ipv6)
.map(|a| a.ip())
.ok_or_else(|| {
TracerouteError::ResolutionError(format!("{target}: no IPv6 (AAAA) addresses"))
})
}
pub async fn trace_async(target: &str) -> Result<TracerouteResult, TracerouteError> {
let config = TracerouteConfig::builder().target(target).build()?;
trace_with_config_async(config).await
}
pub async fn trace_with_config_async(
config: TracerouteConfig,
) -> Result<TracerouteResult, TracerouteError> {
let traceroute = Traceroute::new(config).await?;
traceroute.run().await
}
pub(crate) async fn trace_with_services(
config: TracerouteConfig,
services: &Services,
) -> Result<TracerouteResult, TracerouteError> {
let traceroute = Traceroute::new_with_services(config, services.clone()).await?;
traceroute.run().await
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[tokio::test]
async fn test_async_traceroute_creation() {
let config = TracerouteConfig::builder()
.target("127.0.0.1")
.build()
.expect("failed to build traceroute config");
let result = Traceroute::new(config).await;
assert!(result.is_ok());
let traceroute = result.expect("Traceroute creation should succeed");
assert_eq!(
traceroute.target_ip,
IpAddr::V4("127.0.0.1".parse().expect("valid IPv4 address"))
);
}
#[tokio::test]
async fn test_async_traceroute_ipv6_literal_resolves() {
let config = TracerouteConfig::builder()
.target("::1")
.target_ip(IpAddr::V6("::1".parse().expect("valid IPv6 address")))
.build()
.expect("failed to build traceroute config");
let traceroute = Traceroute::new(config)
.await
.expect("IPv6 literal must resolve");
assert_eq!(
traceroute.target_ip,
IpAddr::V6("::1".parse().expect("valid IPv6 address"))
);
}
#[tokio::test]
async fn test_resolve_ip_literals_by_family() {
use crate::traceroute::config::PreferredFamily;
for family in [PreferredFamily::Auto, PreferredFamily::V4] {
let ip = resolve_target_with_family("8.8.8.8", family)
.await
.expect("v4 literal resolves");
assert_eq!(ip, IpAddr::V4("8.8.8.8".parse().expect("valid IPv4")));
}
for family in [PreferredFamily::Auto, PreferredFamily::V6] {
let ip = resolve_target_with_family("2001:4860:4860::8888", family)
.await
.expect("v6 literal resolves");
assert_eq!(
ip,
IpAddr::V6("2001:4860:4860::8888".parse().expect("valid IPv6"))
);
}
let err = resolve_target_with_family("8.8.8.8", PreferredFamily::V6)
.await
.expect_err("v4 literal under -6 must fail");
assert!(matches!(&err, TracerouteError::ResolutionError(m) if m.contains("IPv6")));
let err = resolve_target_with_family("2001:4860:4860::8888", PreferredFamily::V4)
.await
.expect_err("v6 literal under -4 must fail");
assert!(matches!(&err, TracerouteError::ResolutionError(m) if m.contains("IPv4")));
}
#[tokio::test]
async fn test_resolve_localhost_by_family() {
use crate::traceroute::config::PreferredFamily;
assert_eq!(
resolve_target_with_family("localhost", PreferredFamily::Auto)
.await
.expect("localhost resolves"),
IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
);
assert_eq!(
resolve_target_with_family("localhost", PreferredFamily::V6)
.await
.expect("localhost -6 resolves"),
IpAddr::V6(std::net::Ipv6Addr::LOCALHOST)
);
}
#[tokio::test]
async fn test_resolve_rejects_zone_scoped_literal() {
use crate::traceroute::config::PreferredFamily;
let err = resolve_target_with_family("fe80::1%en0", PreferredFamily::Auto)
.await
.expect_err("zone-scoped literal must be rejected");
assert!(
matches!(&err, TracerouteError::ResolutionError(m) if m.contains("zone-scoped")),
"unexpected error: {err:?}"
);
}
#[tokio::test]
async fn test_async_traceroute_with_ip() {
let config = TracerouteConfig::builder()
.target("8.8.8.8")
.target_ip(IpAddr::V4("8.8.8.8".parse().expect("valid IPv4 address")))
.build()
.expect("failed to build traceroute config");
let result = Traceroute::new(config).await;
assert!(result.is_ok());
let traceroute = result.expect("Traceroute creation should succeed");
assert_eq!(
traceroute.target_ip,
IpAddr::V4("8.8.8.8".parse().expect("valid IPv4 address"))
);
}
#[tokio::test]
async fn test_trace_async_localhost() {
let result = trace_async("127.0.0.1").await;
assert!(
matches!(
&result,
Ok(_)
| Err(TracerouteError::InsufficientPermissions { .. })
| Err(TracerouteError::SocketError(_))
),
"Unexpected error: {:?}",
result
);
if let Ok(trace_result) = result {
assert_eq!(trace_result.target, "127.0.0.1");
}
}
#[tokio::test]
async fn test_trace_with_config_async() {
let config = TracerouteConfig::builder()
.target("127.0.0.1")
.max_hops(3)
.probe_timeout(Duration::from_millis(100))
.build()
.expect("failed to build traceroute config");
let result = trace_with_config_async(config).await;
assert!(
matches!(
&result,
Ok(_)
| Err(TracerouteError::InsufficientPermissions { .. })
| Err(TracerouteError::SocketError(_))
),
"Unexpected error: {:?}",
result
);
if let Ok(trace_result) = result {
assert_eq!(trace_result.target, "127.0.0.1");
assert!(trace_result.hops.len() <= 3);
}
}
#[tokio::test]
async fn test_async_traceroute_hostname_resolution() {
let config = TracerouteConfig::builder()
.target("localhost")
.build()
.expect("failed to build traceroute config");
let result = Traceroute::new(config).await;
assert!(result.is_ok());
let traceroute = result.expect("Traceroute creation should succeed");
assert_eq!(
traceroute.target_ip,
IpAddr::V4("127.0.0.1".parse().expect("valid IPv4 address"))
);
}
#[tokio::test]
async fn test_async_traceroute_invalid_hostname() {
let config = TracerouteConfig::builder()
.target("this.hostname.definitely.does.not.exist.invalid")
.build()
.expect("failed to build traceroute config");
let result = Traceroute::new(config).await;
assert!(
matches!(&result, Err(TracerouteError::ResolutionError(_))),
"Expected resolution error, got: {:?}",
result
);
}
#[tokio::test]
async fn test_verbose_config_does_not_touch_environment() {
let config = TracerouteConfig::builder()
.target("127.0.0.1")
.verbose(2)
.max_hops(1) .probe_timeout(Duration::from_millis(100))
.build()
.expect("failed to build traceroute config");
assert_eq!(config.verbose, 2);
let traceroute = Traceroute::new(config)
.await
.expect("Traceroute creation for localhost should succeed");
let before = std::env::var("FTR_VERBOSE").ok();
let _ = tokio::time::timeout(Duration::from_secs(5), traceroute.run()).await;
assert_eq!(std::env::var("FTR_VERBOSE").ok(), before);
}
}