diff --git a/crates/trippy-core/src/probe.rs b/crates/trippy-core/src/probe.rs index dab53577..671861c8 100644 --- a/crates/trippy-core/src/probe.rs +++ b/crates/trippy-core/src/probe.rs @@ -1,4 +1,4 @@ -use crate::types::{Flags, Port, RoundId, Sequence, TimeToLive, TraceId}; +use crate::types::{Checksum, Flags, Port, RoundId, Sequence, TimeToLive, TraceId}; use std::net::IpAddr; use std::time::SystemTime; @@ -90,6 +90,7 @@ impl Probe { host: IpAddr, received: SystemTime, icmp_packet_type: IcmpPacketType, + checksum: Option, extensions: Option, ) -> ProbeComplete { ProbeComplete { @@ -103,6 +104,7 @@ impl Probe { host, received, icmp_packet_type, + checksum, extensions, } } @@ -141,6 +143,8 @@ pub struct ProbeComplete { pub received: SystemTime, /// The type of ICMP response packet received for the probe. pub icmp_packet_type: IcmpPacketType, + /// The UDP checksum of the original datagram. + pub checksum: Option, /// The ICMP response extensions. pub extensions: Option, } diff --git a/crates/trippy-core/src/state.rs b/crates/trippy-core/src/state.rs index c72f7806..51db22bd 100644 --- a/crates/trippy-core/src/state.rs +++ b/crates/trippy-core/src/state.rs @@ -186,6 +186,8 @@ pub struct Hop { last_sequence: u16, /// The icmp packet type for the last probe for this hop. last_icmp_packet_type: Option, + /// The NAT detection status for the last probe for this hop. + last_nat_status: bool, /// The history of round trip times across the last N rounds. samples: Vec, /// The ICMP extensions for this hop. @@ -325,6 +327,12 @@ impl Hop { self.last_icmp_packet_type } + /// The NAT detection status for the last probe for this hop. + #[must_use] + pub const fn last_nat_status(&self) -> bool { + self.last_nat_status + } + /// The last N samples. #[must_use] pub fn samples(&self) -> &[Duration] { @@ -360,6 +368,7 @@ impl Default for Hop { m2: 0f64, samples: Vec::default(), extensions: None, + last_nat_status: false, } } } @@ -434,12 +443,13 @@ impl FlowState { self.round_count += 1; self.highest_ttl = std::cmp::max(self.highest_ttl, round.largest_ttl.0); self.highest_ttl_for_round = round.largest_ttl.0; + let mut prev_hop_checksum = 0; for probe in round.probes { - self.update_from_probe(probe); + self.update_from_probe(probe, &mut prev_hop_checksum); } } - fn update_from_probe(&mut self, probe: &ProbeStatus) { + fn update_from_probe(&mut self, probe: &ProbeStatus, prev_hop_checksum: &mut u16) { match probe { ProbeStatus::Complete(complete) => { self.update_lowest_ttl(complete.ttl); @@ -482,6 +492,16 @@ impl FlowState { hop.last_dest_port = complete.dest_port.0; hop.last_sequence = complete.sequence.0; hop.last_icmp_packet_type = Some(complete.icmp_packet_type); + if let Some(checksum) = &complete.checksum { + if *prev_hop_checksum == 0 { + *prev_hop_checksum = checksum.0; + } else if *prev_hop_checksum != checksum.0 { + hop.last_nat_status = true; + *prev_hop_checksum = checksum.0; + } else { + hop.last_nat_status = false; + } + } } ProbeStatus::Awaited(awaited) => { self.update_lowest_ttl(awaited.ttl); @@ -607,6 +627,7 @@ mod tests { received, icmp_packet_type, None, + None, ), )) } diff --git a/crates/trippy-core/src/strategy.rs b/crates/trippy-core/src/strategy.rs index bd66d571..a9d71598 100644 --- a/crates/trippy-core/src/strategy.rs +++ b/crates/trippy-core/src/strategy.rs @@ -6,7 +6,7 @@ use crate::probe::{ ProbeStatus, Response, ResponseData, ResponseSeq, ResponseSeqIcmp, ResponseSeqTcp, ResponseSeqUdp, }; -use crate::types::{Sequence, TimeToLive, TraceId}; +use crate::types::{Checksum, Sequence, TimeToLive, TraceId}; use crate::{MultipathStrategy, PortDirection, Protocol}; use std::net::IpAddr; use std::time::{Duration, SystemTime}; @@ -150,30 +150,32 @@ impl)> Strategy { let next = network.recv_probe()?; match next { Some(Response::TimeExceeded(data, icmp_code, extensions)) => { - let (trace_id, sequence, received, host) = self.extract(&data); + let (trace_id, sequence, received, host, checksum) = self.extract(&data); let is_target = host == self.config.target_addr; if self.check_trace_id(trace_id) && st.in_round(sequence) && self.validate(&data) { st.complete_probe_time_exceeded( - sequence, host, received, is_target, icmp_code, extensions, + sequence, host, received, is_target, icmp_code, checksum, extensions, ); } } Some(Response::DestinationUnreachable(data, icmp_code, extensions)) => { - let (trace_id, sequence, received, host) = self.extract(&data); + let (trace_id, sequence, received, host, checksum) = self.extract(&data); if self.check_trace_id(trace_id) && st.in_round(sequence) && self.validate(&data) { - st.complete_probe_unreachable(sequence, host, received, icmp_code, extensions); + st.complete_probe_unreachable( + sequence, host, received, icmp_code, checksum, extensions, + ); } } Some(Response::EchoReply(data, icmp_code)) => { - let (trace_id, sequence, received, host) = self.extract(&data); + let (trace_id, sequence, received, host, checksum) = self.extract(&data); if self.check_trace_id(trace_id) && st.in_round(sequence) && self.validate(&data) { - st.complete_probe_echo_reply(sequence, host, received, icmp_code); + st.complete_probe_echo_reply(sequence, host, received, icmp_code, checksum); } } Some(Response::TcpReply(data) | Response::TcpRefused(data)) => { - let (trace_id, sequence, received, host) = self.extract(&data); + let (trace_id, sequence, received, host, checksum) = self.extract(&data); if self.check_trace_id(trace_id) && st.in_round(sequence) && self.validate(&data) { - st.complete_probe_other(sequence, host, received); + st.complete_probe_other(sequence, host, received, checksum); } } None => {} @@ -297,7 +299,10 @@ impl)> Strategy { /// Extract the `TraceId`, `Sequence`, `SystemTime` and `IpAddr` from the `ProbeResponseData` in /// a protocol specific way. #[instrument(skip(self))] - fn extract(&self, resp: &ResponseData) -> (TraceId, Sequence, SystemTime, IpAddr) { + fn extract( + &self, + resp: &ResponseData, + ) -> (TraceId, Sequence, SystemTime, IpAddr, Option) { match resp.resp_seq { ResponseSeq::Icmp(ResponseSeqIcmp { identifier, @@ -307,6 +312,7 @@ impl)> Strategy { Sequence(sequence), resp.recv, resp.addr, + None, ), ResponseSeq::Udp(ResponseSeqUdp { identifier, @@ -329,7 +335,18 @@ impl)> Strategy { self.config.initial_sequence.0 + payload_len } }; - (TraceId(0), Sequence(sequence), resp.recv, resp.addr) + // The UDP checksum is only usable for NAT detection for IPv4/dublin. + let checksum = match (self.config.multipath_strategy, self.config.target_addr) { + (MultipathStrategy::Dublin, IpAddr::V4(_)) => Some(Checksum(checksum)), + _ => None, + }; + ( + TraceId(0), + Sequence(sequence), + resp.recv, + resp.addr, + checksum, + ) } ResponseSeq::Tcp(ResponseSeqTcp { src_port, @@ -340,7 +357,7 @@ impl)> Strategy { PortDirection::FixedSrc(_) => dest_port, _ => src_port, }; - (TraceId(0), Sequence(sequence), resp.recv, resp.addr) + (TraceId(0), Sequence(sequence), resp.recv, resp.addr, None) } } } @@ -423,7 +440,7 @@ mod state { use crate::constants::MAX_SEQUENCE_PER_ROUND; use crate::probe::{Extensions, IcmpPacketCode, IcmpPacketType, Probe, ProbeStatus}; use crate::strategy::StrategyConfig; - use crate::types::{MaxRounds, Port, RoundId, Sequence, TimeToLive, TraceId}; + use crate::types::{Checksum, MaxRounds, Port, RoundId, Sequence, TimeToLive, TraceId}; use crate::{Flags, MultipathStrategy, PortDirection, Protocol}; use std::array::from_fn; use std::net::IpAddr; @@ -719,6 +736,7 @@ mod state { /// Mark the `ProbeState` at `sequence` completed as `TimeExceeded` and update the round /// state. + #[allow(clippy::too_many_arguments)] #[instrument(skip(self))] pub fn complete_probe_time_exceeded( &mut self, @@ -727,6 +745,7 @@ mod state { received: SystemTime, is_target: bool, icmp_code: IcmpPacketCode, + checksum: Option, extensions: Option, ) { self.complete_probe( @@ -735,6 +754,7 @@ mod state { host, received, is_target, + checksum, extensions, ); } @@ -748,6 +768,7 @@ mod state { host: IpAddr, received: SystemTime, icmp_code: IcmpPacketCode, + checksum: Option, extensions: Option, ) { self.complete_probe( @@ -756,6 +777,7 @@ mod state { host, received, true, + checksum, extensions, ); } @@ -768,6 +790,7 @@ mod state { host: IpAddr, received: SystemTime, icmp_code: IcmpPacketCode, + checksum: Option, ) { self.complete_probe( sequence, @@ -775,6 +798,7 @@ mod state { host, received, true, + checksum, None, ); } @@ -787,6 +811,7 @@ mod state { sequence: Sequence, host: IpAddr, received: SystemTime, + checksum: Option, ) { self.complete_probe( sequence, @@ -794,6 +819,7 @@ mod state { host, received, true, + checksum, None, ); } @@ -811,6 +837,7 @@ mod state { /// overwriting the state with stale values. We may also receive multiple replies /// from the target host with differing time-to-live values and so must ensure we /// use the time-to-live with the lowest sequence number. + #[allow(clippy::too_many_arguments)] #[instrument(skip(self))] fn complete_probe( &mut self, @@ -819,6 +846,7 @@ mod state { host: IpAddr, received: SystemTime, is_target: bool, + checksum: Option, extensions: Option, ) { // Retrieve and update the `ProbeState` at `sequence`. @@ -838,7 +866,8 @@ mod state { return; } }; - let completed = awaited.complete(host, received, icmp_packet_type, extensions); + let completed = + awaited.complete(host, received, icmp_packet_type, checksum, extensions); let ttl = completed.ttl; self.buffer[usize::from(sequence - self.round_sequence)] = ProbeStatus::Complete(completed); @@ -962,6 +991,7 @@ mod state { false, IcmpPacketCode(1), None, + None, ); // Validate the state of the probe 1 after the update @@ -1034,6 +1064,7 @@ mod state { false, IcmpPacketCode(1), None, + None, ); let probe_2_recv = state.probe_at(Sequence(33001)); @@ -1059,7 +1090,13 @@ mod state { // Update the state of probe 3 after receiving a EchoReply let received_3 = SystemTime::now(); let host = IpAddr::V4(Ipv4Addr::LOCALHOST); - state.complete_probe_echo_reply(Sequence(33002), host, received_3, IcmpPacketCode(0)); + state.complete_probe_echo_reply( + Sequence(33002), + host, + received_3, + IcmpPacketCode(0), + None, + ); let probe_3_recv = state.probe_at(Sequence(33002)); // Validate the TracerState after the update to probe 3 diff --git a/crates/trippy-core/src/types.rs b/crates/trippy-core/src/types.rs index 7d7cfa99..22e76b72 100644 --- a/crates/trippy-core/src/types.rs +++ b/crates/trippy-core/src/types.rs @@ -42,6 +42,10 @@ pub struct TypeOfService(pub u8); #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd)] pub struct Port(pub u16); +/// Checksum newtype. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd)] +pub struct Checksum(pub u16); + bitflags! { /// Probe flags. #[derive(Debug, Clone, PartialEq, Eq)]