use clap::Parser;
use ntpsec_rs_core::control_client::*;
#[derive(Parser, Debug)]
#[command(
name = "ntptrace-rs",
about = "Trace NTP synchronization chain to the reference clock source",
version
)]
struct Cli {
#[arg(default_value = "127.0.0.1")]
host: String,
#[arg(short = 'd', long, default_value = "10")]
max_depth: u32,
#[arg(short = 't', long, default_value = "5")]
timeout: u32,
}
fn print_header() {
println!(
" {:>15} {:>15} {:>2} {:>10} {:>10}",
"host", "refid", "st", "offset", "delay"
);
println!("{}", "-".repeat(60));
}
fn print_hop(host: &str, refid: &str, stratum: u8, offset: f64, delay: f64, depth: u32) {
let indent = " ".repeat(depth as usize);
println!(
"{}{:>15} {:>15} {:>2} {:>10.6} {:>10.6}",
indent, host, refid, stratum, offset, delay
);
}
fn fmt_ntp_value(val: &str) -> String {
if val.contains('.') {
val.to_string()
} else {
format!("{}.0", val)
}
}
fn main() {
let cli = Cli::parse();
println!(
"ntptrace-rs v{} — NTP trace tool (Rust)",
env!("CARGO_PKG_VERSION")
);
println!("Tracing from: {} (max depth: {})", cli.host, cli.max_depth);
println!();
print_header();
trace_host(&cli.host, &cli.host, 0, cli.max_depth, cli.timeout);
}
fn trace_host(current_host: &str, original_host: &str, depth: u32, max_depth: u32, timeout: u32) {
if depth > max_depth {
eprintln!("ntptrace-rs: maximum depth ({max_depth}) reached at {current_host}");
return;
}
let mut client = ControlClient::new(timeout, 1);
let sys = match client.read_system_vars(current_host, 123) {
Ok(sys) => sys,
Err(QueryError::Timeout) => {
eprintln!("ntptrace-rs: timeout querying {current_host}");
print_hop(current_host, "TIMEOUT", 16, 0.0, 0.0, depth);
return;
}
Err(QueryError::Network(e)) => {
eprintln!("ntptrace-rs: connection failed to {current_host}: {e}");
print_hop(current_host, "UNREACH", 16, 0.0, 0.0, depth);
return;
}
Err(e) => {
eprintln!("ntptrace-rs: error querying {current_host}: {e}");
print_hop(current_host, "ERROR", 16, 0.0, 0.0, depth);
return;
}
};
let stratum = sys.stratum();
let refid = sys.get("refid").unwrap_or("").to_string();
let offset = sys
.get("offset")
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0);
let delay = sys
.get("delay")
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0);
print_hop(current_host, &refid, stratum, offset, delay, depth);
if stratum >= 15 {
return;
}
let syspeer = match sys.get("syspeer") {
Some(peer_id) => peer_id.parse::<u16>().ok(),
None => sys.get("associd").and_then(|v| v.parse::<u16>().ok()),
};
let peer_host = match syspeer {
Some(associd) => {
match client.read_peer_vars(current_host, 123, associd) {
Ok(pv) => pv.get("srcadr").map(|s| s.to_string()),
Err(_) => None,
}
}
None => None,
};
if let Some(next_host) = peer_host {
let next_host = next_host
.trim_start_matches('[')
.trim_end_matches(']')
.split(':')
.next()
.unwrap_or(&next_host)
.to_string();
if next_host == current_host || next_host == original_host {
return;
}
trace_host(&next_host, original_host, depth + 1, max_depth, timeout);
}
}