use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::time::{Duration, Instant};
use std::mem::MaybeUninit;
use socket2::{Domain, Protocol, Socket, Type};
use crate::dns;
use crate::error::Error;
#[derive(Debug, Clone)]
pub struct TracerouteHop {
pub ttl: u8,
pub addr: Option<IpAddr>,
pub hostname: Option<String>,
pub rtt: Duration,
pub responded: bool,
pub icmp_type: Option<u8>,
}
impl TracerouteHop {
fn timeout(ttl: u8, probe_timeout: Duration) -> Self {
Self {
ttl,
addr: None,
hostname: None,
rtt: probe_timeout,
responded: false,
icmp_type: None,
}
}
fn success(ttl: u8, addr: IpAddr, rtt: Duration, icmp_type: u8) -> Self {
Self {
ttl,
addr: Some(addr),
hostname: None,
rtt,
responded: true,
icmp_type: Some(icmp_type),
}
}
}
#[derive(Debug, Clone)]
pub struct TracerouteResult {
pub target: String,
pub target_ip: IpAddr,
pub hops: Vec<TracerouteHop>,
pub reached_destination: bool,
pub total_time: Duration,
}
impl TracerouteResult {
pub fn hop_count(&self) -> usize {
self.hops.len()
}
pub fn responsive_hops(&self) -> Vec<&TracerouteHop> {
self.hops.iter().filter(|h| h.responded).collect()
}
pub fn path(&self) -> Vec<IpAddr> {
self.hops.iter().filter_map(|h| h.addr).collect()
}
pub fn path_contains(&self, ip: IpAddr) -> bool {
self.hops.iter().any(|h| h.addr == Some(ip))
}
}
#[derive(Debug, Clone)]
pub struct TracerouteOptions {
pub max_hops: u8,
pub timeout_per_hop: Duration,
pub probes_per_hop: u8,
pub start_port: u16,
}
impl Default for TracerouteOptions {
fn default() -> Self {
Self {
max_hops: 30,
timeout_per_hop: Duration::from_secs(2),
probes_per_hop: 1,
start_port: 33434,
}
}
}
pub async fn traceroute(target: &str, options: &TracerouteOptions) -> crate::Result<TracerouteResult> {
let start_time = Instant::now();
let dns_result = dns::resolve_ipv4(target).await?;
let target_ip = dns_result.ip;
let target_ipv4 = match target_ip {
IpAddr::V4(ipv4) => ipv4,
IpAddr::V6(_) => return Err(Error::InvalidTarget("IPv6 traceroute not yet supported".to_string())),
};
let mut hops = Vec::new();
let mut reached_destination = false;
for ttl in 1..=options.max_hops {
let hop = probe_hop(target_ipv4, ttl, options).await;
if let Some(addr) = hop.addr {
if addr == target_ip {
reached_destination = true;
}
}
if hop.icmp_type == Some(0) {
reached_destination = true;
}
hops.push(hop);
if reached_destination {
break;
}
}
Ok(TracerouteResult {
target: target.to_string(),
target_ip,
hops,
reached_destination,
total_time: start_time.elapsed(),
})
}
async fn probe_hop(target: Ipv4Addr, ttl: u8, options: &TracerouteOptions) -> TracerouteHop {
let timeout_duration = options.timeout_per_hop;
let result = tokio::task::spawn_blocking(move || {
probe_hop_sync(target, ttl, timeout_duration)
}).await;
match result {
Ok(Ok(hop)) => hop,
Ok(Err(_)) => TracerouteHop::timeout(ttl, timeout_duration),
Err(_) => TracerouteHop::timeout(ttl, timeout_duration),
}
}
fn probe_hop_sync(target: Ipv4Addr, ttl: u8, probe_timeout: Duration) -> Result<TracerouteHop, Error> {
let socket = Socket::new(Domain::IPV4, Type::RAW, Some(Protocol::ICMPV4))?;
socket.set_ttl(ttl as u32)?;
socket.set_read_timeout(Some(probe_timeout))?;
let identifier = std::process::id() as u16;
let sequence = ttl as u16;
let packet = build_icmp_packet(identifier, sequence);
let dest = SocketAddr::new(IpAddr::V4(target), 0);
let start = Instant::now();
socket.send_to(&packet, &dest.into())?;
let mut recv_buf: [MaybeUninit<u8>; 1024] = unsafe { MaybeUninit::uninit().assume_init() };
match socket.recv_from(&mut recv_buf) {
Ok((len, from_addr)) => {
let rtt = start.elapsed();
let buf: &[u8] = unsafe {
std::slice::from_raw_parts(recv_buf.as_ptr() as *const u8, len)
};
if len >= 28 {
let ip_header_len = ((buf[0] & 0x0F) * 4) as usize;
if len > ip_header_len {
let icmp_type = buf[ip_header_len];
let from_ip = match from_addr.as_socket_ipv4() {
Some(addr) => IpAddr::V4(*addr.ip()),
None => return Err(Error::Icmp("Invalid response address".to_string())),
};
return Ok(TracerouteHop::success(ttl, from_ip, rtt, icmp_type));
}
}
Err(Error::Icmp("Invalid response".to_string()))
}
Err(_) => Ok(TracerouteHop::timeout(ttl, probe_timeout)),
}
}
fn build_icmp_packet(identifier: u16, sequence: u16) -> Vec<u8> {
let mut packet = vec![0u8; 64];
packet[0] = 8;
packet[1] = 0;
packet[2] = 0;
packet[3] = 0;
packet[4] = (identifier >> 8) as u8;
packet[5] = identifier as u8;
packet[6] = (sequence >> 8) as u8;
packet[7] = sequence as u8;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
for (i, byte) in now.to_be_bytes().iter().enumerate() {
if i + 8 < packet.len() {
packet[i + 8] = *byte;
}
}
let checksum = compute_checksum(&packet);
packet[2] = (checksum >> 8) as u8;
packet[3] = checksum as u8;
packet
}
fn compute_checksum(data: &[u8]) -> u16 {
let mut sum: u32 = 0;
let mut i = 0;
while i < data.len() {
let word = if i + 1 < data.len() {
((data[i] as u32) << 8) | (data[i + 1] as u32)
} else {
(data[i] as u32) << 8
};
sum = sum.wrapping_add(word);
i += 2;
}
while sum >> 16 != 0 {
sum = (sum & 0xFFFF) + (sum >> 16);
}
!sum as u16
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_checksum() {
let data = [0x08, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01];
let checksum = compute_checksum(&data);
assert!(checksum > 0);
}
#[tokio::test]
async fn test_traceroute_localhost() {
let options = TracerouteOptions {
max_hops: 5,
timeout_per_hop: Duration::from_secs(1),
..Default::default()
};
let result = traceroute("127.0.0.1", &options).await;
match result {
Ok(r) => {
println!("Traceroute to {}: {} hops, reached: {}",
r.target, r.hop_count(), r.reached_destination);
for hop in &r.hops {
println!(" TTL {}: {:?} ({:.2}ms)",
hop.ttl, hop.addr, hop.rtt.as_secs_f64() * 1000.0);
}
}
Err(e) => {
println!("Traceroute failed (may need root): {e}");
}
}
}
}