use std::net::Ipv4Addr;
#[derive(Debug, Clone, PartialEq)]
pub enum HopResult {
Reply { rtt_ms: f64 },
Timeout,
}
#[derive(Debug, Clone)]
pub struct TraceHop {
pub ttl: u8,
pub ip: String,
pub hostname: Option<String>,
pub result: HopResult,
}
pub struct TracerouteState {
pub target: String,
pub editing: bool, pub hops: Vec<TraceHop>,
pub running: bool,
pub complete: bool,
pub selected: usize,
pub error: Option<String>,
pub pending_ticks: u8,
pub next_hop_ttl: u8,
pub target_ip: Option<Ipv4Addr>,
#[cfg(feature = "real-capture")]
real_lines: Vec<String>,
#[cfg(feature = "real-capture")]
child: Option<std::process::Child>,
#[cfg(feature = "real-capture")]
reader: Option<std::io::BufReader<std::process::ChildStdout>>,
}
impl Default for TracerouteState {
fn default() -> Self {
Self {
target: String::new(),
editing: false,
hops: Vec::new(),
running: false,
complete: false,
selected: 0,
error: None,
pending_ticks: 0,
next_hop_ttl: 1,
target_ip: None,
#[cfg(feature = "real-capture")]
real_lines: Vec::new(),
#[cfg(feature = "real-capture")]
child: None,
#[cfg(feature = "real-capture")]
reader: None,
}
}
}
impl TracerouteState {
pub fn start(&mut self) {
let tgt = self.target.trim().to_string();
if tgt.is_empty() { return; }
self.hops.clear();
self.complete = false;
self.error = None;
self.selected = 0;
self.next_hop_ttl = 1;
self.pending_ticks = 0;
self.target_ip = tgt.parse::<Ipv4Addr>().ok().or_else(|| {
let b: Vec<u8> = tgt.bytes().collect();
Some(Ipv4Addr::new(
8,
b.first().copied().unwrap_or(8).wrapping_add(1).max(1),
b.get(1).copied().unwrap_or(8).wrapping_add(1).max(1),
b.get(2).copied().unwrap_or(8).wrapping_add(1).max(1),
))
});
#[cfg(feature = "real-capture")]
self.start_real(&tgt);
#[cfg(not(feature = "real-capture"))]
{ self.running = true; }
}
#[cfg(feature = "real-capture")]
fn start_real(&mut self, target: &str) {
use std::process::{Command, Stdio};
use std::io::BufReader;
let (cmd, args): (&str, Vec<&str>) = if cfg!(target_os = "windows") {
("tracert", vec!["-d", target]) } else if cfg!(target_os = "macos") {
("traceroute", vec!["-n", "-q", "1", "-w", "1", target])
} else {
("traceroute", vec!["-n", "-q", "1", "-w", "1", target])
};
match Command::new(cmd)
.args(&args)
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
{
Ok(mut child) => {
let stdout = child.stdout.take().unwrap();
self.reader = Some(BufReader::new(stdout));
self.child = Some(child);
self.running = true;
self.real_lines.clear();
}
Err(e) => {
self.error = Some(format!(
"traceroute unavailable ({}), showing simulation", e
));
self.running = true; }
}
}
pub fn tick(&mut self) -> bool {
if !self.running { return false; }
#[cfg(feature = "real-capture")]
if self.reader.is_some() {
return self.tick_real();
}
self.tick_simulated()
}
#[cfg(feature = "real-capture")]
fn tick_real(&mut self) -> bool {
use std::io::BufRead;
let reader = match self.reader.as_mut() {
Some(r) => r,
None => return self.tick_simulated(),
};
let mut line = String::new();
match reader.read_line(&mut line) {
Ok(0) => {
self.running = false;
self.complete = true;
self.reader = None;
if let Some(mut child) = self.child.take() { let _ = child.wait(); }
false
}
Ok(_) => {
let line = line.trim().to_string();
if let Some(hop) = parse_traceroute_line(&line) {
let ttl = hop.ttl;
let is_target = self.target_ip
.map(|ip| hop.ip == ip.to_string())
.unwrap_or(false);
self.hops.push(hop);
if is_target || ttl >= 30 {
self.running = false;
self.complete = true;
self.reader = None;
if let Some(mut child) = self.child.take() { let _ = child.wait(); }
}
true
} else {
false
}
}
Err(_) => {
self.running = false;
self.complete = true;
false
}
}
}
fn tick_simulated(&mut self) -> bool {
self.pending_ticks += 1;
if self.pending_ticks < 6 { return false; }
self.pending_ticks = 0;
let ttl = self.next_hop_ttl;
let target_ip = match self.target_ip {
Some(ip) => ip,
None => { self.running = false; return false; }
};
let hop = simulate_hop(ttl, target_ip);
let is_dest = hop.ip == target_ip.to_string();
self.hops.push(hop);
self.next_hop_ttl += 1;
if is_dest || ttl >= 30 {
self.running = false;
self.complete = true;
}
true
}
pub fn clear(&mut self) {
#[cfg(feature = "real-capture")]
{
if let Some(mut child) = self.child.take() { let _ = child.kill(); }
self.reader = None;
self.real_lines.clear();
}
*self = Self::default();
}
pub fn scroll_down(&mut self) {
if self.selected + 1 < self.hops.len() { self.selected += 1; }
}
pub fn scroll_up(&mut self) {
if self.selected > 0 { self.selected -= 1; }
}
}
#[cfg(feature = "real-capture")]
fn parse_traceroute_line(line: &str) -> Option<TraceHop> {
let line = line.trim();
if line.is_empty() || line.starts_with("traceroute") || line.starts_with("Tracing") {
return None;
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 2 { return None; }
let ttl: u8 = parts[0].parse().ok()?;
if parts.get(1) == Some(&"*") {
return Some(TraceHop {
ttl,
ip: "*".into(),
hostname: None,
result: HopResult::Timeout,
});
}
let ip_str = parts.iter().find(|&&p| p.parse::<Ipv4Addr>().is_ok())?;
let ip = ip_str.to_string();
let rtt_ms = parts.windows(2)
.find(|w| w[1] == "ms")
.and_then(|w| w[0].trim_start_matches('<').parse::<f64>().ok())
.unwrap_or(0.0);
let hostname = parts.iter()
.find(|&&p| p != *ip_str && p.contains('.') && !p.ends_with("ms"))
.map(|&s| s.trim_end_matches(')').trim_start_matches('(').to_string());
Some(TraceHop {
ttl,
ip,
hostname,
result: HopResult::Reply { rtt_ms },
})
}
fn simulate_hop(ttl: u8, target: Ipv4Addr) -> TraceHop {
let seed = (ttl as u64)
.wrapping_mul(2654435761)
.wrapping_add(u32::from(target) as u64);
let tgt_octets = target.octets();
let max_ttl = 8u8 + (seed % 8) as u8;
if ttl >= max_ttl {
let rtt = 10.0 + (seed % 200) as f64 / 10.0;
return TraceHop {
ttl,
ip: target.to_string(),
hostname: Some(synthesize_hostname(tgt_octets)),
result: HopResult::Reply { rtt_ms: rtt },
};
}
let timeout = (seed >> 4) % 5 == 0;
let hop_ip = intermediate_ip(ttl, seed, tgt_octets);
let rtt = 1.0 * ttl as f64 + (seed % 50) as f64 / 10.0;
TraceHop {
ttl,
ip: if timeout { "*".into() } else { hop_ip.to_string() },
hostname: if timeout { None } else { Some(synthesize_hostname(hop_ip.octets())) },
result: if timeout { HopResult::Timeout } else { HopResult::Reply { rtt_ms: rtt } },
}
}
fn intermediate_ip(ttl: u8, seed: u64, tgt: [u8; 4]) -> Ipv4Addr {
let prefixes: [[u8; 3]; 7] = [
[10, 0, 0], [172, 16, 0], [100, 64, 0],
[195, 66, 36], [23, 0, 0], [34, 0, 0], [74, 125, 0],
];
let a = prefixes[(seed as usize) % prefixes.len()];
Ipv4Addr::new(a[0], a[1].wrapping_add(ttl), a[2].wrapping_add((seed >> 8) as u8), tgt[3].wrapping_add(ttl))
}
fn synthesize_hostname(ip: [u8; 4]) -> String {
let suffixes = [
"core.net", "be.net", "r.isp.net", "backbone.com",
"akamai.net", "goog.com", "cloudflare.com", "level3.net",
];
let idx = (ip[2] as usize + ip[3] as usize) % suffixes.len();
format!("{}-{}-{}-{}.{}", ip[0], ip[1], ip[2], ip[3], suffixes[idx])
}