use std::net::IpAddr;
use anyhow::Result;
use futures::stream::{self, StreamExt};
use tokio::process::Command;
const MAX_TRACEROUTE_PARALLEL: usize = 32;
async fn traceroute_one(host: IpAddr) -> (String, String) {
#[cfg(unix)]
{
let mut c = Command::new("traceroute");
c.arg("-n").arg("-q").arg("1");
#[cfg(target_os = "linux")]
c.arg("-w").arg("1");
c.arg(host.to_string());
match c.output().await {
Ok(out) => (
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
),
Err(e) => {
tracing::warn!(error = %e, %host, "traceroute failed");
(String::new(), String::new())
}
}
}
#[cfg(windows)]
{
let mut c = Command::new("tracert");
c.arg("-d").arg("-h").arg("15").arg(host.to_string());
match c.output().await {
Ok(out) => (
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
),
Err(e) => {
tracing::warn!(error = %e, %host, "tracert failed");
(String::new(), String::new())
}
}
}
#[cfg(not(any(unix, windows)))]
{
let _ = host;
(String::new(), String::new())
}
}
pub async fn run_traceroute(hosts: &[IpAddr], max_parallel: usize) -> Result<()> {
if hosts.is_empty() {
return Ok(());
}
let cap = max_parallel.clamp(1, MAX_TRACEROUTE_PARALLEL);
let chunks: Vec<(usize, IpAddr)> = hosts.iter().copied().enumerate().collect();
let mut out: Vec<(usize, String, String)> = stream::iter(chunks)
.map(|(idx, host)| async move {
let (stdout, stderr) = traceroute_one(host).await;
(idx, stdout, stderr)
})
.buffer_unordered(cap)
.collect()
.await;
out.sort_by_key(|(i, _, _)| *i);
for (_, stdout, stderr) in out {
print!("{stdout}");
if !stderr.is_empty() {
eprint!("{stderr}");
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn traceroute_empty_is_ok() {
run_traceroute(&[], 8).await.unwrap();
}
#[tokio::test]
async fn traceroute_localhost_completes() {
run_traceroute(&["127.0.0.1".parse().unwrap()], 1)
.await
.unwrap();
}
#[tokio::test]
async fn traceroute_multiple_hosts_preserves_order() {
let hosts = vec!["127.0.0.1".parse().unwrap(), "127.0.0.1".parse().unwrap()];
run_traceroute(&hosts, 2).await.unwrap();
}
#[tokio::test]
async fn traceroute_ipv6_localhost_completes() {
run_traceroute(&["::1".parse().unwrap()], 1).await.unwrap();
}
#[tokio::test]
async fn traceroute_max_parallel_clamped_to_one() {
run_traceroute(&["127.0.0.1".parse().unwrap()], 0)
.await
.unwrap();
}
#[tokio::test]
async fn traceroute_three_hosts_single_parallel() {
let hosts = vec![
"127.0.0.1".parse().unwrap(),
"127.0.0.1".parse().unwrap(),
"127.0.0.1".parse().unwrap(),
];
run_traceroute(&hosts, 1).await.unwrap();
}
#[tokio::test]
async fn traceroute_max_parallel_clamped_to_thirty_two() {
let hosts = vec!["127.0.0.1".parse().unwrap(); 3];
run_traceroute(&hosts, 999).await.unwrap();
}
#[tokio::test]
async fn traceroute_mixed_ipv4_ipv6_completes() {
let hosts = vec!["127.0.0.1".parse().unwrap(), "::1".parse().unwrap()];
run_traceroute(&hosts, 2).await.unwrap();
}
#[tokio::test]
async fn traceroute_four_hosts_parallel_two() {
let hosts = vec![
"127.0.0.1".parse().unwrap(),
"127.0.0.1".parse().unwrap(),
"127.0.0.1".parse().unwrap(),
"127.0.0.1".parse().unwrap(),
];
run_traceroute(&hosts, 2).await.unwrap();
}
#[tokio::test]
async fn traceroute_ipv6_only_single_host() {
run_traceroute(&["::1".parse().unwrap()], 1).await.unwrap();
}
#[tokio::test]
async fn traceroute_parallel_one_processes_sequentially() {
let hosts = vec!["127.0.0.1".parse().unwrap(), "127.0.0.1".parse().unwrap()];
run_traceroute(&hosts, 1).await.unwrap();
}
}