Skip to main content

netpulse/
trace.rs

1// src/trace.rs — MTR-style Traceroute Orchestrator
2
3use crate::{
4    config::ProbeType,
5    error::NetPulseError,
6    probers::{icmp::IcmpProber, udp::UdpProber, Prober},
7    state::{new_app_state, TargetState},
8};
9use anyhow::Result;
10use std::sync::{
11    atomic::{AtomicBool, Ordering},
12    Arc,
13};
14use tokio::task::JoinHandle;
15use tokio::time::{self, Duration};
16
17pub async fn run_trace_mode(
18    target: String,
19    resolved_ip: String,
20    probe_type: ProbeType,
21    timeout_ms: u64,
22    interval_ms: u64,
23    window: usize,
24) -> Result<()> {
25    let prober: Arc<dyn Prober> = match probe_type {
26        ProbeType::Icmp => Arc::new(IcmpProber::new(timeout_ms)),
27        ProbeType::Udp => Arc::new(UdpProber::new(timeout_ms)),
28        ProbeType::Tcp => unreachable!("TCP tracing prevented by CLI"),
29    };
30
31    println!(
32        "Tracing route to {} ({}), {} probes...",
33        target,
34        resolved_ip,
35        prober.name()
36    );
37
38    let state = new_app_state();
39    let running = Arc::new(AtomicBool::new(true));
40
41    // Handle Ctrl-C gracefully
42    let running_clone = Arc::clone(&running);
43    tokio::spawn(async move {
44        let _ = tokio::signal::ctrl_c().await;
45        running_clone.store(false, Ordering::SeqCst);
46    });
47
48    // 1. Hop Discovery Phase
49    // We send probes from TTL 1 up to 30 to figure out how many hops are in the path.
50    let mut discovered_hops = Vec::new();
51    let mut max_ttl = 30;
52
53    for ttl in 1..=30 {
54        if !running.load(Ordering::Relaxed) {
55            return Ok(());
56        }
57
58        // Initialize state for this TTL so TUI can start showing something immediately
59        {
60            let mut guard = state.lock().unwrap();
61            let mut ts = TargetState::new_hop(window);
62            // We store the "name" as "Hop N: ???" initially
63            ts.last_ip = Some(String::from("???"));
64            guard.insert(ttl.to_string(), ts);
65        }
66
67        let seq = (ttl as u64) << 8;
68        let result = prober.probe(&resolved_ip, seq, Some(ttl as u32)).await;
69
70        match result {
71            Ok(res) => {
72                let ip = res
73                    .responder_ip
74                    .clone()
75                    .unwrap_or_else(|| String::from("???"));
76
77                {
78                    let mut guard = state.lock().unwrap();
79                    if let Some(ts) = guard.get_mut(&ttl.to_string()) {
80                        ts.buffer.push(res.clone());
81                        ts.last_rtt_us = res.rtt_us;
82                        ts.last_ip = Some(ip.clone());
83                        ts.seq += 1;
84                    }
85                }
86
87                discovered_hops.push((ttl, ip.clone()));
88
89                // If the responder is our actual final destination, we are done discovering!
90                // For UDP, Port Unreachable comes from the final dest. For ICMP, Echo Reply comes from the final dest.
91                // Our Prober abstraction might not explicitly flag "destination reached" versus "time exceeded" in the result struct,
92                // BUT if the responder IP matches our resolved target IP, it's definitely the destination.
93                if ip == resolved_ip {
94                    max_ttl = ttl;
95                    break;
96                }
97            }
98            Err(e) => {
99                if matches!(e, NetPulseError::InsufficientPrivileges) {
100                    eprintln!("Fatal error: {}", e);
101                    std::process::exit(1);
102                }
103                // Timeout or other error — might be a silent router.
104                // We just continue to the next TTL.
105                discovered_hops.push((ttl, String::from("???")));
106                {
107                    let mut guard = state.lock().unwrap();
108                    if let Some(ts) = guard.get_mut(&ttl.to_string()) {
109                        ts.seq += 1;
110                    }
111                }
112            }
113        }
114    }
115
116    // 2. Continuous Probing Phase
117    // Now we spawn one async task per TTL and ping it continuously.
118    let mut tasks: Vec<JoinHandle<()>> = Vec::new();
119    for ttl in 1..=max_ttl {
120        let prober_clone = Arc::clone(&prober);
121        let target_clone = resolved_ip.clone();
122        let state_clone = Arc::clone(&state);
123        let running_task = Arc::clone(&running);
124
125        let task = tokio::spawn(async move {
126            let mut ticker = time::interval(Duration::from_millis(interval_ms));
127            let mut local_seq = 1; // start from 1 since discovery used 0
128
129            while running_task.load(Ordering::Relaxed) {
130                ticker.tick().await;
131
132                let seq_val = ((ttl as u64) << 8) | (local_seq % 256);
133                let probe_res = match prober_clone
134                    .probe(&target_clone, seq_val, Some(ttl as u32))
135                    .await
136                {
137                    Ok(p) => p,
138                    Err(_) => crate::probers::ProbeResult::loss(&target_clone, seq_val),
139                };
140
141                {
142                    let mut guard = state_clone.lock().unwrap();
143                    if let Some(ts) = guard.get_mut(&ttl.to_string()) {
144                        ts.buffer.push(probe_res.clone());
145                        ts.last_rtt_us = probe_res.rtt_us;
146                        ts.seq += 1;
147                        if let Some(ip) = probe_res.responder_ip {
148                            ts.last_ip = Some(ip);
149                        }
150                    }
151                }
152                local_seq += 1;
153            }
154        });
155        tasks.push(task);
156    }
157
158    // 3. Render Trace TUI
159    crate::tui::trace_tui::run(
160        state.clone(),
161        max_ttl,
162        &target,
163        &resolved_ip,
164        prober.name(),
165        interval_ms,
166    )
167    .await?;
168
169    // On TUI exit, signal shutdown and wait for tasks
170    running.store(false, Ordering::SeqCst);
171    for task in tasks {
172        let _ = task.await;
173    }
174
175    Ok(())
176}