Skip to main content

mtr_ng/
session.rs

1use crate::{Args, HopStats, Result};
2use anyhow::anyhow;
3use hickory_resolver::{config::*, TokioAsyncResolver};
4use pnet::packet::{
5    icmp::{IcmpPacket, IcmpType, IcmpTypes},
6    ip::IpNextHeaderProtocols,
7    ipv4::Ipv4Packet,
8    util, MutablePacket, Packet,
9};
10use rand;
11use socket2::{Domain, Protocol, Socket, Type};
12use std::{
13    collections::HashMap,
14    mem::MaybeUninit,
15    net::{IpAddr, Ipv4Addr, SocketAddr},
16    sync::Arc,
17    time::{Duration, Instant},
18};
19use tokio::time;
20use tracing::{debug, info, warn};
21
22const MIN_SEQUENCE: u16 = 33000;
23const MAX_SEQUENCE: u16 = 65535;
24
25// Add callback type for real-time updates
26pub type UpdateCallback = Arc<dyn Fn() + Send + Sync>;
27
28#[derive(Debug, Clone)]
29pub struct RTTUpdate {
30    pub hop: usize,        // Hop number (0-based index)
31    pub rtt: Duration,     // Round trip time
32    pub addr: IpAddr,      // IP address that responded
33    pub sent_count: usize, // Number of packets sent to this hop so far
34}
35
36#[derive(Debug, Clone)]
37pub enum NetworkEvent {
38    RTTUpdate(RTTUpdate),
39    HopTimeout { hop: usize, sent_count: usize },
40    TargetReached { hop: usize },
41    RoundComplete { round: usize },
42}
43
44#[derive(Debug, Clone)]
45pub struct SequenceEntry {
46    pub index: usize,       // hop index (like original mtr)
47    pub transit: bool,      // is this sequence in transit?
48    pub saved_seq: u32,     // saved sequence for this host
49    pub send_time: Instant, // when packet was sent
50}
51
52#[derive(Clone)]
53pub struct MtrSession {
54    pub target: String,
55    pub target_addr: IpAddr,
56    pub hops: Vec<HopStats>,
57    pub args: Args,
58    pub resolver: TokioAsyncResolver,
59    pub packet_id: u16,
60    pub next_sequence: u16,
61    pub sequence_table: HashMap<u16, SequenceEntry>, // sequence -> entry (like original mtr)
62    pub batch_at: usize,  // current hop index being sent (like original mtr)
63    pub num_hosts: usize, // number of active hops
64    pub update_callback: Option<UpdateCallback>, // callback for real-time updates
65}
66
67impl MtrSession {
68    pub async fn new(args: Args) -> Result<Self> {
69        let resolver =
70            TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default());
71
72        // Resolve target hostname to IP
73        let target_addr = if let Ok(ip) = args.target.parse::<IpAddr>() {
74            ip
75        } else {
76            let response = resolver.lookup_ip(&args.target).await?;
77            response
78                .iter()
79                .next()
80                .ok_or_else(|| anyhow!("Failed to resolve hostname"))?
81        };
82
83        let mut hops: Vec<HopStats> = (1..=args.max_hops).map(HopStats::new).collect();
84
85        // Configure EMA alpha for all hops from command line args
86        for hop in &mut hops {
87            hop.set_ema_alpha(args.ema_alpha);
88        }
89        let packet_id = std::process::id() as u16;
90
91        Ok(Self {
92            target: args.target.clone(),
93            target_addr,
94            hops,
95            args,
96            resolver,
97            packet_id,
98            next_sequence: MIN_SEQUENCE,
99            sequence_table: HashMap::new(),
100            batch_at: 0,   // Start at hop 1 (index 0)
101            num_hosts: 10, // Initial estimate
102            update_callback: None,
103        })
104    }
105
106    pub async fn run_trace(&mut self) -> Result<()> {
107        info!("Starting trace to {} ({})", self.target, self.target_addr);
108
109        match self.target_addr {
110            IpAddr::V4(ipv4) => self.run_ipv4_trace(ipv4).await,
111            IpAddr::V6(_) => {
112                warn!("IPv6 not yet implemented, falling back to simulation");
113                self.run_simulated_trace().await
114            }
115        }
116    }
117
118    async fn run_ipv4_trace(&mut self, target: Ipv4Addr) -> Result<()> {
119        // Try to create raw socket for ICMP
120        match self.create_raw_socket() {
121            Ok((send_socket, recv_socket)) => {
122                info!("Using raw ICMP sockets for real traceroute");
123                self.run_mtr_algorithm(target, send_socket, recv_socket)
124                    .await
125            }
126            Err(e) => {
127                warn!("Failed to create raw socket ({}), falling back to simulation. Try running with sudo for real traceroute.", e);
128                self.run_simulated_trace().await
129            }
130        }
131    }
132
133    fn create_raw_socket(&self) -> Result<(Socket, Socket)> {
134        // Create raw ICMP socket for sending
135        let send_socket = Socket::new(Domain::IPV4, Type::RAW, Some(Protocol::ICMPV4))?;
136
137        // Create raw socket for receiving ICMP responses
138        let recv_socket = Socket::new(Domain::IPV4, Type::RAW, Some(Protocol::ICMPV4))?;
139
140        // Set socket options
141        send_socket.set_nonblocking(true)?;
142        recv_socket.set_nonblocking(true)?;
143
144        // Set receive timeout to be very short for non-blocking operation
145        recv_socket.set_read_timeout(Some(Duration::from_millis(1)))?;
146
147        Ok((send_socket, recv_socket))
148    }
149
150    // Implementation of the exact MTR algorithm from the C code
151    async fn run_mtr_algorithm(
152        &mut self,
153        target: Ipv4Addr,
154        send_socket: Socket,
155        recv_socket: Socket,
156    ) -> Result<()> {
157        let mut round = 0;
158
159        loop {
160            if let Some(count) = self.args.count {
161                if round >= count {
162                    break;
163                }
164            }
165            // Send batch (one packet per active hop, like original mtr)
166            let restart = self.net_send_batch(target, &send_socket).await?;
167
168            // Collect responses for this interval
169            let collect_duration = Duration::from_millis(self.args.interval);
170            let start_collect = Instant::now();
171
172            while start_collect.elapsed() < collect_duration {
173                self.net_process_return(&recv_socket, target).await;
174                tokio::time::sleep(Duration::from_millis(1)).await;
175            }
176
177            if restart {
178                round += 1;
179                if let Some(count) = self.args.count {
180                    debug!("Completed round {}/{}, restarting batch", round, count);
181                } else {
182                    debug!("Completed round {} (continuous), restarting batch", round);
183                }
184            }
185        }
186
187        Ok(())
188    }
189
190    // Equivalent to net_send_batch in original mtr
191    async fn net_send_batch(&mut self, target: Ipv4Addr, send_socket: &Socket) -> Result<bool> {
192        let mut n_unknown = 0;
193        let mut restart = false;
194
195        // Send query for current hop (like original mtr's net_send_query)
196        self.net_send_query(target, send_socket, self.batch_at)
197            .await?;
198
199        // Check all previous hops to see if we should restart
200        for i in 0..self.batch_at {
201            if self.hops[i].addr.is_none() {
202                n_unknown += 1;
203            }
204
205            // Check if we've reached the target at this hop
206            if let Some(IpAddr::V4(addr)) = self.hops[i].addr {
207                if addr == target {
208                    restart = true;
209                    self.num_hosts = i + 1;
210                    break;
211                }
212            }
213        }
214
215        // Restart conditions (same as original mtr)
216        if self.batch_at >= (self.args.max_hops as usize) - 1
217            || n_unknown > 5 // maxUnknown equivalent
218            || (self.hops.get(self.batch_at).and_then(|h| h.addr).is_some_and(|addr| {
219                matches!(addr, IpAddr::V4(a) if a == target)
220            }))
221        {
222            restart = true;
223            self.num_hosts = self.batch_at + 1;
224        }
225
226        if restart {
227            self.batch_at = 0; // Reset to hop 1
228        } else {
229            self.batch_at += 1;
230        }
231
232        Ok(restart)
233    }
234
235    // Equivalent to net_send_query in original mtr
236    async fn net_send_query(
237        &mut self,
238        target: Ipv4Addr,
239        send_socket: &Socket,
240        index: usize,
241    ) -> Result<()> {
242        let seq = self.prepare_sequence(index);
243        let time_to_live = (index + 1) as u32;
244
245        debug!(
246            "Sending probe: hop={}, TTL={}, seq={}",
247            index + 1,
248            time_to_live,
249            seq
250        );
251
252        Self::send_icmp_packet_static(send_socket, target, time_to_live, self.packet_id, seq)?;
253
254        // Record actual send time after packet transmission
255        self.save_sequence_with_send_time(index, seq, Instant::now());
256
257        Ok(())
258    }
259
260    // Prepare sequence without recording send time yet
261    fn prepare_sequence(&mut self, index: usize) -> u16 {
262        let seq = self.next_sequence;
263
264        // Advance sequence (with wraparound like original)
265        self.next_sequence += 1;
266        if self.next_sequence == MAX_SEQUENCE {
267            self.next_sequence = MIN_SEQUENCE;
268        }
269
270        // Only increment sent counter, don't record send time yet
271        self.hops[index].increment_sent();
272
273        seq
274    }
275
276    fn save_sequence_with_send_time(&mut self, index: usize, seq: u16, send_time: Instant) {
277        // Clean up old sequence entries to prevent memory leaks and collisions
278        self.cleanup_old_sequences();
279
280        // Record sequence entry with actual send time
281        let entry = SequenceEntry {
282            index,
283            transit: true,
284            saved_seq: self.hops[index].sent as u32,
285            send_time,
286        };
287
288        self.sequence_table.insert(seq, entry);
289
290        debug!(
291            "Saved sequence: seq={}, hop={}, sent_count={}",
292            seq,
293            index + 1,
294            self.hops[index].sent
295        );
296    }
297
298    fn cleanup_old_sequences(&mut self) {
299        // Remove entries older than 5 seconds to prevent sequence number collisions
300        let cutoff_time = Instant::now() - Duration::from_secs(5);
301        let mut timed_out_entries = Vec::new();
302
303        self.sequence_table.retain(|seq, entry| {
304            if entry.send_time < cutoff_time {
305                debug!("Packet timed out: seq={}, hop={}", seq, entry.index + 1);
306                timed_out_entries.push(entry.index);
307                false
308            } else {
309                true
310            }
311        });
312
313        // Add timeouts to the packet history for timed out entries
314        for hop_index in timed_out_entries {
315            if hop_index < self.hops.len() {
316                self.hops[hop_index].add_timeout();
317            }
318        }
319    }
320
321    // Equivalent to mark_sequence_complete in original mtr
322    fn mark_sequence_complete(&mut self, seq: u16) -> Option<(usize, Instant)> {
323        if let Some(entry) = self.sequence_table.remove(&seq) {
324            if entry.transit {
325                // Validate that response isn't too old (prevents sequence number collision issues)
326                let age = entry.send_time.elapsed();
327                if age.as_secs() > 5 {
328                    debug!(
329                        "Discarding very old response for seq {} (age: {:.1}s)",
330                        seq,
331                        age.as_secs_f64()
332                    );
333                    return None;
334                }
335                return Some((entry.index, entry.send_time));
336            }
337        }
338        None
339    }
340
341    // Equivalent to net_process_return and net_process_ping in original mtr
342    async fn net_process_return(&mut self, recv_socket: &Socket, target: Ipv4Addr) {
343        // Try to read multiple responses
344        for _ in 0..10 {
345            match Self::receive_icmp_response(recv_socket) {
346                Ok((source_ip, icmp_type, seq, receive_time)) => {
347                    self.net_process_ping(seq, source_ip, icmp_type, receive_time, target)
348                        .await;
349                }
350                Err(_) => break, // No more responses
351            }
352        }
353    }
354
355    // Equivalent to net_process_ping in original mtr
356    async fn net_process_ping(
357        &mut self,
358        seq: u16,
359        addr: Ipv4Addr,
360        icmp_type: IcmpType,
361        receive_time: Instant,
362        target: Ipv4Addr,
363    ) {
364        let (index, send_time) = match self.mark_sequence_complete(seq) {
365            Some((idx, send_time)) => (idx, send_time),
366            None => {
367                debug!("Received response for unknown sequence: {}", seq);
368                return;
369            }
370        };
371
372        // Calculate RTT properly using send time from sequence table
373        let rtt = receive_time.duration_since(send_time);
374
375        debug!(
376            "Hop {}: Got {} from {} in {:.1}ms",
377            index + 1,
378            icmp_type_name(icmp_type),
379            addr,
380            rtt.as_secs_f64() * 1000.0
381        );
382
383        // Update hop statistics with multi-path tracking
384        self.hops[index].add_rtt_from_addr(IpAddr::V4(addr), rtt);
385
386        // Perform DNS lookup for this specific address if needed
387        if !self.args.numeric {
388            if let Ok(names) = self.resolver.reverse_lookup(IpAddr::V4(addr)).await {
389                if let Some(name) = names.iter().next() {
390                    self.hops[index].set_hostname_for_addr(
391                        IpAddr::V4(addr),
392                        name.to_string().trim_end_matches('.').to_string(),
393                    );
394                } else {
395                    self.hops[index].set_hostname_for_addr(IpAddr::V4(addr), addr.to_string());
396                }
397            } else {
398                self.hops[index].set_hostname_for_addr(IpAddr::V4(addr), addr.to_string());
399            }
400        }
401
402        // Check if we reached the target
403        if addr == target && matches!(icmp_type, IcmpTypes::EchoReply) {
404            info!("Reached target {} at hop {}", target, index + 1);
405        }
406
407        // Trigger real-time UI update when a response arrives
408        if let Some(callback) = &self.update_callback {
409            callback();
410        }
411    }
412
413    fn receive_icmp_response(socket: &Socket) -> Result<(Ipv4Addr, IcmpType, u16, Instant)> {
414        let mut buffer = [MaybeUninit::uninit(); 1500];
415        let receive_time = Instant::now();
416
417        match socket.recv_from(&mut buffer) {
418            Ok((size, _addr)) => {
419                // Convert MaybeUninit to initialized data
420                let initialized_buffer: Vec<u8> = buffer[..size]
421                    .iter()
422                    .map(|b| unsafe { b.assume_init() })
423                    .collect();
424
425                // Parse IP packet
426                if let Some(ip_packet) = Ipv4Packet::new(&initialized_buffer) {
427                    let source_ip = ip_packet.get_source();
428
429                    // Check if it's an ICMP packet
430                    if ip_packet.get_next_level_protocol() == IpNextHeaderProtocols::Icmp {
431                        let icmp_start = (ip_packet.get_header_length() * 4) as usize;
432                        if let Some(icmp_packet) =
433                            IcmpPacket::new(&initialized_buffer[icmp_start..])
434                        {
435                            let icmp_type = icmp_packet.get_icmp_type();
436
437                            match icmp_type {
438                                IcmpTypes::TimeExceeded => {
439                                    // Extract original packet info
440                                    if let Some((_orig_id, orig_seq)) =
441                                        Self::extract_original_packet_info(icmp_packet.payload())
442                                    {
443                                        return Ok((source_ip, icmp_type, orig_seq, receive_time));
444                                    }
445                                }
446                                IcmpTypes::EchoReply => {
447                                    // Parse echo reply
448                                    if let Some(echo_reply) =
449                                        pnet::packet::icmp::echo_reply::EchoReplyPacket::new(
450                                            icmp_packet.payload(),
451                                        )
452                                    {
453                                        return Ok((
454                                            source_ip,
455                                            icmp_type,
456                                            echo_reply.get_sequence_number(),
457                                            receive_time,
458                                        ));
459                                    }
460                                }
461                                IcmpTypes::DestinationUnreachable => {
462                                    // Extract original packet info
463                                    if let Some((_orig_id, orig_seq)) =
464                                        Self::extract_original_packet_info(icmp_packet.payload())
465                                    {
466                                        return Ok((source_ip, icmp_type, orig_seq, receive_time));
467                                    }
468                                }
469                                _ => {
470                                    debug!(
471                                        "Received unhandled ICMP type: {:?} from {}",
472                                        icmp_type, source_ip
473                                    );
474                                }
475                            }
476                        }
477                    }
478                }
479            }
480            Err(e) => {
481                if e.kind() != std::io::ErrorKind::WouldBlock
482                    && e.kind() != std::io::ErrorKind::TimedOut
483                {
484                    debug!("Socket recv error: {}", e);
485                }
486            }
487        }
488
489        Err(anyhow!("No valid response received"))
490    }
491
492    fn send_icmp_packet_static(
493        socket: &Socket,
494        target: Ipv4Addr,
495        ttl: u32,
496        id: u16,
497        sequence: u16,
498    ) -> Result<()> {
499        // Create ICMP echo request packet
500        let mut icmp_buffer = [0u8; 64];
501        let mut icmp_packet =
502            pnet::packet::icmp::echo_request::MutableEchoRequestPacket::new(&mut icmp_buffer)
503                .ok_or_else(|| anyhow!("Failed to create ICMP packet"))?;
504
505        icmp_packet.set_icmp_type(IcmpTypes::EchoRequest);
506        icmp_packet.set_icmp_code(pnet::packet::icmp::IcmpCode::new(0));
507        icmp_packet.set_identifier(id);
508        icmp_packet.set_sequence_number(sequence);
509
510        // Add some payload data to make packet more identifiable
511        let payload = format!("mtr-{}-{}", id, sequence);
512        let payload_bytes = payload.as_bytes();
513        if payload_bytes.len() <= icmp_packet.payload().len() {
514            icmp_packet.payload_mut()[..payload_bytes.len()].copy_from_slice(payload_bytes);
515        }
516
517        // Calculate checksum
518        let checksum = util::checksum(icmp_packet.packet(), 1);
519        icmp_packet.set_checksum(checksum);
520
521        // Set TTL on socket
522        socket.set_ttl(ttl)?;
523
524        // Send packet
525        let target_addr = SocketAddr::new(IpAddr::V4(target), 0);
526        socket.send_to(icmp_packet.packet(), &target_addr.into())?;
527
528        Ok(())
529    }
530
531    fn extract_original_packet_info(payload: &[u8]) -> Option<(u16, u16)> {
532        // For TimeExceeded and DestinationUnreachable, payload contains original IP packet
533        if payload.len() >= 28 {
534            // IP header (20) + ICMP header (8) minimum
535            // Skip 4 bytes of ICMP error header
536            if let Some(orig_ip_packet) = Ipv4Packet::new(&payload[4..]) {
537                if orig_ip_packet.get_next_level_protocol() == IpNextHeaderProtocols::Icmp {
538                    let orig_icmp_start = 4 + (orig_ip_packet.get_header_length() * 4) as usize;
539                    if orig_icmp_start < payload.len() && orig_icmp_start + 8 <= payload.len() {
540                        if let Some(orig_icmp) =
541                            pnet::packet::icmp::echo_request::EchoRequestPacket::new(
542                                &payload[orig_icmp_start..],
543                            )
544                        {
545                            return Some((
546                                orig_icmp.get_identifier(),
547                                orig_icmp.get_sequence_number(),
548                            ));
549                        }
550                    }
551                }
552            }
553        }
554        None
555    }
556
557    async fn run_simulated_trace(&mut self) -> Result<()> {
558        info!("Running simulated traceroute (use sudo for real network tracing)");
559
560        for round in 0..self.args.count.unwrap_or(10) {
561            debug!("Simulation Round {}", round + 1);
562
563            for hop in &mut self.hops {
564                hop.increment_sent();
565
566                // Simulate realistic network behavior
567                let base_latency = hop.hop as u64 * 10 + 20; // Base latency increases with hops
568                let jitter = rand::random::<u64>() % 50; // Random jitter
569                let packet_loss_chance = (hop.hop as f64 * 0.05).min(0.25); // Higher loss chance for testing
570
571                if rand::random::<f64>() > packet_loss_chance {
572                    let rtt = Duration::from_millis(base_latency + jitter);
573                    hop.add_rtt(rtt);
574
575                    // Simulate realistic IP addresses and hostnames
576                    if hop.addr.is_none() {
577                        // Generate realistic-looking IP addresses
578                        match hop.hop {
579                            1 => {
580                                hop.addr = Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)));
581                                hop.hostname = if !self.args.numeric {
582                                    Some("gateway.local".to_string())
583                                } else {
584                                    None
585                                };
586                            }
587                            2..=3 => {
588                                hop.addr = Some(IpAddr::V4(Ipv4Addr::new(10, 0, hop.hop, 1)));
589                                hop.hostname = if !self.args.numeric {
590                                    Some(format!("core-{}.isp.net", hop.hop))
591                                } else {
592                                    None
593                                };
594                            }
595                            _ => {
596                                let final_octet = if hop.hop >= 8 { 8 } else { hop.hop };
597                                hop.addr = Some(IpAddr::V4(Ipv4Addr::new(8, 8, 8, final_octet)));
598                                hop.hostname = if !self.args.numeric {
599                                    Some("dns.google".to_string())
600                                } else {
601                                    None
602                                };
603                            }
604                        }
605                    }
606
607                    // Stop at target (simulate reaching destination)
608                    if hop.hop >= 8 {
609                        break;
610                    }
611                } else {
612                    hop.add_timeout();
613                }
614            }
615
616            time::sleep(Duration::from_millis(self.args.interval)).await;
617        }
618
619        Ok(())
620    }
621
622    pub fn set_update_callback(&mut self, callback: UpdateCallback) {
623        self.update_callback = Some(callback);
624    }
625
626    // TODO: Channel-based real-time trace (to be implemented)
627    // This will replace the shared mutex approach with lock-free channels
628
629    // Run MTR algorithm directly on shared session for real-time UI updates
630    pub async fn run_trace_with_realtime_updates(
631        session_arc: std::sync::Arc<std::sync::Mutex<Self>>,
632    ) -> Result<()> {
633        // Extract basic configuration
634        let (target_addr, args) = {
635            let session = session_arc.lock().unwrap();
636            (session.target_addr, session.args.clone())
637        };
638
639        info!("Starting real-time trace to {}", target_addr);
640
641        match target_addr {
642            IpAddr::V4(ipv4) => Self::run_ipv4_trace_realtime(session_arc, ipv4, args).await,
643            IpAddr::V6(_) => {
644                warn!("IPv6 not yet implemented, falling back to simulation");
645                Self::run_simulated_trace_realtime(session_arc, args).await
646            }
647        }
648    }
649
650    async fn run_ipv4_trace_realtime(
651        session_arc: std::sync::Arc<std::sync::Mutex<Self>>,
652        target: Ipv4Addr,
653        args: Args,
654    ) -> Result<()> {
655        // Try to create raw socket for ICMP
656        let socket_result = {
657            let session = session_arc.lock().unwrap();
658            session.create_raw_socket()
659        };
660
661        match socket_result {
662            Ok((send_socket, recv_socket)) => {
663                info!("Using raw ICMP sockets for real traceroute");
664                Self::run_mtr_algorithm_realtime(
665                    session_arc,
666                    target,
667                    send_socket,
668                    recv_socket,
669                    args,
670                )
671                .await
672            }
673            Err(e) => {
674                warn!("Failed to create raw socket ({}), falling back to simulation. Try running with sudo for real traceroute.", e);
675                Self::run_simulated_trace_realtime(session_arc, args).await
676            }
677        }
678    }
679
680    async fn run_mtr_algorithm_realtime(
681        session_arc: std::sync::Arc<std::sync::Mutex<Self>>,
682        target: Ipv4Addr,
683        send_socket: Socket,
684        recv_socket: Socket,
685        args: Args,
686    ) -> Result<()> {
687        let mut round = 0;
688        loop {
689            if let Some(count) = args.count {
690                if round >= count {
691                    break;
692                }
693                debug!("MTR Round {}/{}", round + 1, count);
694            } else {
695                debug!("MTR Round {} (continuous)", round + 1);
696            }
697
698            let round_start = Instant::now();
699            let round_duration = Duration::from_millis(args.interval);
700
701            // Use select! to run sending and receiving concurrently
702            tokio::select! {
703                _ = Self::net_send_batch_realtime(&session_arc, target, &send_socket) => {
704                    debug!("Batch sending completed");
705                }
706                _ = async {
707                    while round_start.elapsed() < round_duration {
708                        Self::net_process_return_realtime(&session_arc, &recv_socket, target).await;
709                        tokio::time::sleep(Duration::from_millis(1)).await;
710                    }
711                } => {
712                    debug!("Round duration completed");
713                }
714            }
715
716            // Continue receiving until round ends
717            while round_start.elapsed() < round_duration {
718                Self::net_process_return_realtime(&session_arc, &recv_socket, target).await;
719                tokio::time::sleep(Duration::from_millis(1)).await;
720            }
721
722            if let Some(count) = args.count {
723                debug!("Completed round {}/{}", round + 1, count);
724            } else {
725                debug!("Completed round {} (continuous)", round + 1);
726            }
727            round += 1;
728        }
729
730        Ok(())
731    }
732
733    async fn net_send_batch_realtime(
734        session_arc: &std::sync::Arc<std::sync::Mutex<Self>>,
735        target: Ipv4Addr,
736        send_socket: &Socket,
737    ) -> Result<bool> {
738        let (max_hops, packet_id, num_hosts);
739
740        // Extract configuration
741        {
742            let session = session_arc.lock().unwrap();
743            max_hops = session.args.max_hops;
744            packet_id = session.packet_id;
745            num_hosts = session.num_hosts;
746        }
747
748        // Send one packet to each hop in quick succession (proper MTR batch)
749        let max_hop_to_send = if num_hosts > 0 {
750            (num_hosts + 2).min(max_hops as usize) // Send a bit beyond discovered hops
751        } else {
752            8.min(max_hops as usize) // Initial discovery
753        };
754
755        for hop_index in 0..max_hop_to_send {
756            // Send query for this hop (extract packet info without holding lock)
757            let mut next_sequence;
758
759            // Extract needed values first
760            {
761                let session = session_arc.lock().unwrap();
762                next_sequence = session.next_sequence;
763            }
764
765            // Create sequence and prepare for sending
766            let seq = next_sequence;
767            next_sequence += 1;
768            if next_sequence == MAX_SEQUENCE {
769                next_sequence = MIN_SEQUENCE;
770            }
771
772            // Update session with new sequence and increment sent count (but no send_time yet)
773            {
774                let mut session = session_arc.lock().unwrap();
775                session.next_sequence = next_sequence;
776                session.hops[hop_index].increment_sent();
777            }
778
779            // Send packet without holding any locks
780            let time_to_live = (hop_index + 1) as u32;
781            debug!(
782                "Sending batch probe: hop={}, TTL={}, seq={}",
783                hop_index + 1,
784                time_to_live,
785                seq
786            );
787            Self::send_icmp_packet_static(send_socket, target, time_to_live, packet_id, seq)?;
788
789            // Record ACTUAL send time immediately after each packet is sent
790            let actual_send_time = Instant::now();
791            {
792                let mut session = session_arc.lock().unwrap();
793
794                // Clean up old sequences before adding new ones
795                session.cleanup_old_sequences();
796
797                let entry = SequenceEntry {
798                    index: hop_index,
799                    transit: true,
800                    saved_seq: session.hops[hop_index].sent as u32,
801                    send_time: actual_send_time, // Individual send time for each packet
802                };
803                session.sequence_table.insert(seq, entry);
804            }
805
806            // Small delay between packets to spread out send times
807            tokio::time::sleep(Duration::from_millis(50)).await;
808        }
809
810        // Check restart conditions and update hop discovery
811        let mut _restart = false;
812        let mut _n_unknown = 0;
813        {
814            let mut session = session_arc.lock().unwrap();
815
816            // Count unknown hops and check for target reached
817            for i in 0..max_hop_to_send {
818                if session.hops[i].addr.is_none() {
819                    _n_unknown += 1;
820                } else if let Some(IpAddr::V4(addr)) = session.hops[i].addr {
821                    if addr == target {
822                        _restart = true;
823                        session.num_hosts = i + 1;
824                        debug!("Target reached at hop {}", i + 1);
825                        break;
826                    }
827                }
828            }
829
830            // Update num_hosts based on responses
831            if session.num_hosts == 0 && max_hop_to_send >= 8 {
832                session.num_hosts = max_hop_to_send;
833            }
834        }
835
836        // Always restart after each complete batch (that's how MTR works)
837        Ok(true)
838    }
839
840    async fn net_process_return_realtime(
841        session_arc: &std::sync::Arc<std::sync::Mutex<Self>>,
842        recv_socket: &Socket,
843        target: Ipv4Addr,
844    ) {
845        // Try to read multiple responses
846        for _ in 0..10 {
847            match Self::receive_icmp_response(recv_socket) {
848                Ok((source_ip, icmp_type, seq, receive_time)) => {
849                    Self::net_process_ping_realtime(
850                        session_arc,
851                        seq,
852                        source_ip,
853                        icmp_type,
854                        receive_time,
855                        target,
856                    )
857                    .await;
858                }
859                Err(_) => break, // No more responses
860            }
861        }
862    }
863
864    async fn net_process_ping_realtime(
865        session_arc: &std::sync::Arc<std::sync::Mutex<Self>>,
866        seq: u16,
867        addr: Ipv4Addr,
868        icmp_type: IcmpType,
869        receive_time: Instant,
870        target: Ipv4Addr,
871    ) {
872        let (callback, hop_index) = {
873            let mut session = session_arc.lock().unwrap();
874
875            let (index, send_time) = match session.mark_sequence_complete(seq) {
876                Some((idx, send_time)) => (idx, send_time),
877                None => {
878                    debug!("Received response for unknown sequence: {}", seq);
879                    return;
880                }
881            };
882
883            // Calculate RTT properly using send time from sequence table
884            let rtt = receive_time.duration_since(send_time);
885
886            debug!(
887                "Hop {}: Got {} from {} in {:.1}ms",
888                index + 1,
889                icmp_type_name(icmp_type),
890                addr,
891                rtt.as_secs_f64() * 1000.0
892            );
893
894            // Update hop statistics (like original mtr)
895            session.hops[index].add_rtt(rtt);
896
897            // Set hop address if not already set
898            if session.hops[index].addr.is_none() {
899                session.hops[index].addr = Some(IpAddr::V4(addr));
900
901                if !session.args.numeric {
902                    // Set temporary IP address as hostname, will be resolved later
903                    session.hops[index].hostname = Some(addr.to_string());
904                }
905            }
906
907            // Check if we reached the target
908            if addr == target && matches!(icmp_type, IcmpTypes::EchoReply) {
909                info!("Reached target {} at hop {}", target, index + 1);
910            }
911
912            (session.update_callback.clone(), index)
913        };
914
915        // Spawn reverse DNS lookup task to avoid blocking
916        let session_arc_clone = Arc::clone(session_arc);
917        let addr_for_dns = addr;
918        tokio::spawn(async move {
919            let (do_resolve, resolver_clone) = {
920                let session = session_arc_clone.lock().unwrap();
921                if !session.args.numeric {
922                    if let Some(hostname) = &session.hops[hop_index].hostname {
923                        // Check if hostname is just the IP address (needs resolution)
924                        if hostname == &addr_for_dns.to_string() {
925                            (true, session.resolver.clone())
926                        } else {
927                            (false, session.resolver.clone())
928                        }
929                    } else {
930                        (false, session.resolver.clone())
931                    }
932                } else {
933                    (false, session.resolver.clone())
934                }
935            };
936
937            if do_resolve {
938                if let Ok(names) = resolver_clone
939                    .reverse_lookup(IpAddr::V4(addr_for_dns))
940                    .await
941                {
942                    if let Some(name) = names.iter().next() {
943                        let mut session = session_arc_clone.lock().unwrap();
944                        session.hops[hop_index].hostname =
945                            Some(name.to_string().trim_end_matches('.').to_string());
946
947                        // Trigger UI update after hostname resolution
948                        if let Some(callback) = &session.update_callback {
949                            callback();
950                        }
951                    }
952                }
953            }
954        });
955
956        // Trigger real-time UI update when a response arrives (outside lock)
957        if let Some(callback) = callback {
958            callback();
959        }
960    }
961
962    async fn run_simulated_trace_realtime(
963        session_arc: std::sync::Arc<std::sync::Mutex<Self>>,
964        args: Args,
965    ) -> Result<()> {
966        info!("Running simulated traceroute (use sudo for real network tracing)");
967
968        for round in 0..args.count.unwrap_or(10) {
969            debug!("Simulation Round {}", round + 1);
970
971            // Get callback and config
972            let (callback, numeric) = {
973                let session = session_arc.lock().unwrap();
974                (session.update_callback.clone(), session.args.numeric)
975            };
976
977            // Send batch: simulate each hop quickly (proper MTR batch behavior)
978            for hop_num in 1..=8 {
979                {
980                    let mut session = session_arc.lock().unwrap();
981                    if let Some(hop) = session.hops.get_mut(hop_num - 1) {
982                        hop.increment_sent();
983
984                        // Simulate realistic network behavior
985                        let base_latency = hop.hop as u64 * 10 + 20; // Base latency increases with hops
986                        let jitter = rand::random::<u64>() % 50; // Random jitter
987                        let packet_loss_chance = (hop.hop as f64 * 0.05).min(0.25); // Higher loss chance for testing
988
989                        if rand::random::<f64>() > packet_loss_chance {
990                            let rtt = Duration::from_millis(base_latency + jitter);
991
992                            // Simulate multi-path for certain hops
993                            let addr = match hop_num {
994                                3 => {
995                                    // Hop 3: Load balancing - randomly use different IPs
996                                    let rand_choice = rand::random::<f32>();
997                                    if rand_choice < 0.6 {
998                                        Ipv4Addr::new(10, 0, 3, 1) // Primary 60%
999                                    } else if rand_choice < 0.8 {
1000                                        Ipv4Addr::new(10, 0, 3, 2) // Alt 1: 20%
1001                                    } else {
1002                                        Ipv4Addr::new(10, 0, 3, 3) // Alt 2: 20%
1003                                    }
1004                                }
1005                                5 => {
1006                                    // Hop 5: ECMP routing - two paths
1007                                    if rand::random::<bool>() {
1008                                        Ipv4Addr::new(8, 8, 8, 8) // Primary
1009                                    } else {
1010                                        Ipv4Addr::new(8, 8, 4, 4) // Alt
1011                                    }
1012                                }
1013                                _ => {
1014                                    // Use deterministic IP for other hops
1015                                    match hop_num {
1016                                        1 => Ipv4Addr::new(192, 168, 1, 1),
1017                                        2 => Ipv4Addr::new(10, 0, 2, 1),
1018                                        4 => Ipv4Addr::new(172, 16, 4, 1),
1019                                        6 => Ipv4Addr::new(8, 8, 8, 8),
1020                                        7 => Ipv4Addr::new(8, 8, 8, 8),
1021                                        8 => Ipv4Addr::new(8, 8, 8, 8),
1022                                        _ => Ipv4Addr::new(10, 0, hop_num as u8, 1),
1023                                    }
1024                                }
1025                            };
1026
1027                            hop.add_rtt_from_addr(IpAddr::V4(addr), rtt);
1028
1029                            // Set hostnames for multi-path demo
1030                            if !numeric {
1031                                let hostname = match addr {
1032                                    a if a == Ipv4Addr::new(192, 168, 1, 1) => {
1033                                        "gateway.local".to_string()
1034                                    }
1035                                    a if a == Ipv4Addr::new(10, 0, 2, 1) => {
1036                                        "core-2.isp.net".to_string()
1037                                    }
1038                                    a if a == Ipv4Addr::new(10, 0, 3, 1) => {
1039                                        "core-3.isp.net".to_string()
1040                                    }
1041                                    a if a == Ipv4Addr::new(10, 0, 3, 2) => {
1042                                        "alt-router-3-2.isp.net".to_string()
1043                                    }
1044                                    a if a == Ipv4Addr::new(10, 0, 3, 3) => {
1045                                        "backup-router-3-3.isp.net".to_string()
1046                                    }
1047                                    a if a == Ipv4Addr::new(172, 16, 4, 1) => {
1048                                        "border-4.isp.net".to_string()
1049                                    }
1050                                    a if a == Ipv4Addr::new(8, 8, 8, 8) => "dns.google".to_string(),
1051                                    a if a == Ipv4Addr::new(8, 8, 4, 4) => {
1052                                        "dns-alt.google".to_string()
1053                                    }
1054                                    _ => addr.to_string(),
1055                                };
1056                                hop.set_hostname_for_addr(IpAddr::V4(addr), hostname);
1057                            }
1058
1059                            // Simulate realistic IP addresses and hostnames
1060                            if hop.addr.is_none() {
1061                                let hop_number = hop.hop;
1062
1063                                // Generate realistic-looking IP addresses
1064                                match hop_number {
1065                                    1 => {
1066                                        hop.addr = Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)));
1067                                        hop.hostname = if !numeric {
1068                                            Some("gateway.local".to_string())
1069                                        } else {
1070                                            None
1071                                        };
1072                                    }
1073                                    2..=3 => {
1074                                        hop.addr =
1075                                            Some(IpAddr::V4(Ipv4Addr::new(10, 0, hop_number, 1)));
1076                                        hop.hostname = if !numeric {
1077                                            Some(format!("core-{}.isp.net", hop_number))
1078                                        } else {
1079                                            None
1080                                        };
1081                                    }
1082                                    _ => {
1083                                        let final_octet =
1084                                            if hop_number >= 8 { 8 } else { hop_number };
1085                                        hop.addr =
1086                                            Some(IpAddr::V4(Ipv4Addr::new(8, 8, 8, final_octet)));
1087                                        hop.hostname = if !numeric {
1088                                            Some("dns.google".to_string())
1089                                        } else {
1090                                            None
1091                                        };
1092                                    }
1093                                }
1094                            }
1095                        } else {
1096                            hop.add_timeout();
1097                        }
1098                    }
1099                }
1100
1101                // Trigger UI update for each hop response
1102                if let Some(callback) = &callback {
1103                    callback();
1104                }
1105
1106                // Very small delay between hop responses (batch sending simulation)
1107                tokio::time::sleep(Duration::from_millis(10)).await;
1108            }
1109
1110            // Wait for the interval before next round
1111            time::sleep(Duration::from_millis(args.interval)).await;
1112        }
1113
1114        Ok(())
1115    }
1116}
1117
1118fn icmp_type_name(icmp_type: IcmpType) -> &'static str {
1119    match icmp_type {
1120        IcmpTypes::TimeExceeded => "TimeExceeded",
1121        IcmpTypes::EchoReply => "EchoReply",
1122        IcmpTypes::DestinationUnreachable => "DestUnreach",
1123        _ => "Other",
1124    }
1125}
1126
1127#[cfg(test)]
1128mod tests {
1129    use super::*;
1130
1131    #[tokio::test]
1132    async fn test_mtr_session_new_with_ip() {
1133        let args = Args {
1134            target: "192.168.1.1".to_string(),
1135            count: Some(5),
1136            interval: 500,
1137            max_hops: 20,
1138            report: false,
1139            numeric: true,
1140            sparkline_scale: crate::SparklineScale::Logarithmic,
1141            ema_alpha: 0.1,
1142            fields: None,
1143            sixel: false,
1144            show_all: false,
1145        };
1146
1147        let session = MtrSession::new(args).await;
1148        assert!(session.is_ok());
1149
1150        let session = session.unwrap();
1151        assert_eq!(session.target, "192.168.1.1");
1152        assert_eq!(session.target_addr.to_string(), "192.168.1.1");
1153        assert_eq!(session.hops.len(), 20);
1154        assert_eq!(session.args.count, Some(5));
1155        assert_eq!(session.args.interval, 500);
1156    }
1157
1158    #[tokio::test]
1159    async fn test_mtr_session_new_with_localhost() {
1160        let args = Args {
1161            target: "localhost".to_string(),
1162            count: Some(3),
1163            interval: 1000,
1164            max_hops: 15,
1165            report: true,
1166            numeric: false,
1167            sparkline_scale: crate::SparklineScale::Logarithmic,
1168            ema_alpha: 0.1,
1169            fields: None,
1170            sixel: false,
1171            show_all: false,
1172        };
1173
1174        let session = MtrSession::new(args).await;
1175        assert!(session.is_ok());
1176
1177        let session = session.unwrap();
1178        assert_eq!(session.target, "localhost");
1179        assert_eq!(session.hops.len(), 15);
1180        assert!(session.args.report);
1181        assert!(!session.args.numeric);
1182    }
1183
1184    #[test]
1185    fn test_mtr_session_clone() {
1186        let args = Args {
1187            target: "example.com".to_string(),
1188            count: Some(10),
1189            interval: 1000,
1190            max_hops: 30,
1191            report: false,
1192            numeric: false,
1193            sparkline_scale: crate::SparklineScale::Logarithmic,
1194            ema_alpha: 0.1,
1195            fields: None,
1196            sixel: false,
1197            show_all: false,
1198        };
1199
1200        // We can't easily test MtrSession::new in sync context due to async resolver,
1201        // but we can test that the struct supports Clone
1202        // This is mainly a compilation test
1203        let args_clone = args.clone();
1204        assert_eq!(args.target, args_clone.target);
1205        assert_eq!(args.count, args_clone.count);
1206    }
1207}