Skip to content

Commit

Permalink
feat(core): NAT detection for IPv4/udp/dublin (#1104)
Browse files Browse the repository at this point in the history
  • Loading branch information
fujiapple852 committed Jul 13, 2024
1 parent 30b99ad commit 3623420
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 20 deletions.
6 changes: 5 additions & 1 deletion crates/trippy-core/src/probe.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -90,6 +90,7 @@ impl Probe {
host: IpAddr,
received: SystemTime,
icmp_packet_type: IcmpPacketType,
checksum: Option<Checksum>,
extensions: Option<Extensions>,
) -> ProbeComplete {
ProbeComplete {
Expand All @@ -103,6 +104,7 @@ impl Probe {
host,
received,
icmp_packet_type,
checksum,
extensions,
}
}
Expand Down Expand Up @@ -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<Checksum>,
/// The ICMP response extensions.
pub extensions: Option<Extensions>,
}
Expand Down
25 changes: 23 additions & 2 deletions crates/trippy-core/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IcmpPacketType>,
/// 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<Duration>,
/// The ICMP extensions for this hop.
Expand Down Expand Up @@ -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] {
Expand Down Expand Up @@ -360,6 +368,7 @@ impl Default for Hop {
m2: 0f64,
samples: Vec::default(),
extensions: None,
last_nat_status: false,
}
}
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -607,6 +627,7 @@ mod tests {
received,
icmp_packet_type,
None,
None,
),
))
}
Expand Down
70 changes: 53 additions & 17 deletions crates/trippy-core/src/strategy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -150,30 +150,32 @@ impl<F: Fn(&Round<'_>)> Strategy<F> {
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 => {}
Expand Down Expand Up @@ -294,10 +296,12 @@ impl<F: Fn(&Round<'_>)> Strategy<F> {
}
}

/// Extract the `TraceId`, `Sequence`, `SystemTime` and `IpAddr` from the `ProbeResponseData` in
/// a protocol specific way.
/// Extract the probe response data 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<Checksum>) {
match resp.resp_seq {
ResponseSeq::Icmp(ResponseSeqIcmp {
identifier,
Expand All @@ -307,6 +311,7 @@ impl<F: Fn(&Round<'_>)> Strategy<F> {
Sequence(sequence),
resp.recv,
resp.addr,
None,
),
ResponseSeq::Udp(ResponseSeqUdp {
identifier,
Expand All @@ -329,7 +334,18 @@ impl<F: Fn(&Round<'_>)> Strategy<F> {
self.config.initial_sequence.0 + payload_len
}
};
(TraceId(0), Sequence(sequence), resp.recv, resp.addr)
// The checksum is only usable for NAT detection for IPv4/udp/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,
Expand All @@ -340,7 +356,7 @@ impl<F: Fn(&Round<'_>)> Strategy<F> {
PortDirection::FixedSrc(_) => dest_port,
_ => src_port,
};
(TraceId(0), Sequence(sequence), resp.recv, resp.addr)
(TraceId(0), Sequence(sequence), resp.recv, resp.addr, None)
}
}
}
Expand Down Expand Up @@ -423,7 +439,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;
Expand Down Expand Up @@ -719,6 +735,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,
Expand All @@ -727,6 +744,7 @@ mod state {
received: SystemTime,
is_target: bool,
icmp_code: IcmpPacketCode,
checksum: Option<Checksum>,
extensions: Option<Extensions>,
) {
self.complete_probe(
Expand All @@ -735,6 +753,7 @@ mod state {
host,
received,
is_target,
checksum,
extensions,
);
}
Expand All @@ -748,6 +767,7 @@ mod state {
host: IpAddr,
received: SystemTime,
icmp_code: IcmpPacketCode,
checksum: Option<Checksum>,
extensions: Option<Extensions>,
) {
self.complete_probe(
Expand All @@ -756,6 +776,7 @@ mod state {
host,
received,
true,
checksum,
extensions,
);
}
Expand All @@ -768,13 +789,15 @@ mod state {
host: IpAddr,
received: SystemTime,
icmp_code: IcmpPacketCode,
checksum: Option<Checksum>,
) {
self.complete_probe(
sequence,
IcmpPacketType::EchoReply(icmp_code),
host,
received,
true,
checksum,
None,
);
}
Expand All @@ -787,13 +810,15 @@ mod state {
sequence: Sequence,
host: IpAddr,
received: SystemTime,
checksum: Option<Checksum>,
) {
self.complete_probe(
sequence,
IcmpPacketType::NotApplicable,
host,
received,
true,
checksum,
None,
);
}
Expand All @@ -811,6 +836,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,
Expand All @@ -819,6 +845,7 @@ mod state {
host: IpAddr,
received: SystemTime,
is_target: bool,
checksum: Option<Checksum>,
extensions: Option<Extensions>,
) {
// Retrieve and update the `ProbeState` at `sequence`.
Expand All @@ -838,7 +865,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);
Expand Down Expand Up @@ -962,6 +990,7 @@ mod state {
false,
IcmpPacketCode(1),
None,
None,
);

// Validate the state of the probe 1 after the update
Expand Down Expand Up @@ -1034,6 +1063,7 @@ mod state {
false,
IcmpPacketCode(1),
None,
None,
);
let probe_2_recv = state.probe_at(Sequence(33001));

Expand All @@ -1059,7 +1089,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
Expand Down
4 changes: 4 additions & 0 deletions crates/trippy-core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down

0 comments on commit 3623420

Please sign in to comment.