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 14, 2024
1 parent e4b4845 commit 4f7c961
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 47 deletions.
2 changes: 1 addition & 1 deletion crates/trippy-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ pub use probe::{
Extension, Extensions, IcmpPacketType, MplsLabelStack, MplsLabelStackMember, Probe,
ProbeComplete, ProbeStatus, UnknownExtension,
};
pub use state::{Hop, State};
pub use state::{Hop, NatStatus, State};
pub use strategy::{CompletionReason, Round, Strategy};
pub use tracer::Tracer;
pub use types::{
Expand Down
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
67 changes: 62 additions & 5 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: NatStatus,
/// 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) -> NatStatus {
self.last_nat_status
}

/// The last N samples.
#[must_use]
pub fn samples(&self) -> &[Duration] {
Expand Down Expand Up @@ -360,10 +368,22 @@ impl Default for Hop {
m2: 0f64,
samples: Vec::default(),
extensions: None,
last_nat_status: NatStatus::NotApplicable,
}
}
}

/// The state of a NAT detection for a `Hop`.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum NatStatus {
/// NAT detection was not applicable.
NotApplicable,
/// NAT was not detected at this hop.
NotDetected,
/// NAT was detected at this hop.
Detected,
}

/// Data for a single trace flow.
#[derive(Debug, Clone)]
struct FlowState {
Expand Down Expand Up @@ -434,12 +454,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 = None;
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 Option<u16>) {
match probe {
ProbeStatus::Complete(complete) => {
self.update_lowest_ttl(complete.ttl);
Expand Down Expand Up @@ -482,6 +503,18 @@ 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 let Some(prev_hop_checksum) = prev_hop_checksum {
if *prev_hop_checksum == checksum.0 {
hop.last_nat_status = NatStatus::NotDetected;
} else {
hop.last_nat_status = NatStatus::Detected;
*prev_hop_checksum = checksum.0;
}
} else {
*prev_hop_checksum = Some(checksum.0);
}
}
}
ProbeStatus::Awaited(awaited) => {
self.update_lowest_ttl(awaited.ttl);
Expand Down Expand Up @@ -520,6 +553,7 @@ impl FlowState {
#[cfg(test)]
mod tests {
use super::*;
use crate::types::Checksum;
use crate::{
CompletionReason, Flags, IcmpPacketType, Port, Probe, ProbeComplete, ProbeStatus, Sequence,
TimeToLive, TraceId,
Expand Down Expand Up @@ -562,9 +596,9 @@ mod tests {
type Error = anyhow::Error;

fn try_from(value: String) -> Result<Self, Self::Error> {
// format: {ttl} {status} {duration} {host} {sequence} {src_port} {dest_port}
// format: {ttl} {status} {duration} {host} {sequence} {src_port} {dest_port} {checksum}
let values = value.split_ascii_whitespace().collect::<Vec<_>>();
if values.len() == 7 {
if values.len() == 8 {
let ttl = TimeToLive(u8::from_str(values[0])?);
let state = values[1].to_ascii_lowercase();
let sequence = Sequence(u16::from_str(values[4])?);
Expand All @@ -590,6 +624,7 @@ mod tests {
let host = IpAddr::from_str(values[3])?;
let duration = Duration::from_millis(u64::from_str(values[2])?);
let received = sent.add(duration);
let checksum = Some(Checksum(u16::from_str(values[7])?));
let icmp_packet_type = IcmpPacketType::NotApplicable;
Ok(ProbeStatus::Complete(
Probe::new(
Expand All @@ -606,6 +641,7 @@ mod tests {
host,
received,
icmp_packet_type,
checksum,
None,
),
))
Expand All @@ -619,7 +655,7 @@ mod tests {
}
}

/// A helper struct so wwe may inject the round into the probes.
/// A helper struct so we may inject the round into the probes.
struct ProbeRound(ProbeData, RoundId);

impl From<ProbeRound> for ProbeStatus {
Expand Down Expand Up @@ -664,6 +700,25 @@ mod tests {
last_src: u16,
last_dest: u16,
last_sequence: u16,
last_nat_status: NatStatusWrapper,
}

/// A wrapper struct over `NatStatus` to allow deserialization.
#[derive(Deserialize, Debug, Clone)]
#[serde(try_from = "String")]
struct NatStatusWrapper(NatStatus);

impl TryFrom<String> for NatStatusWrapper {
type Error = anyhow::Error;

fn try_from(value: String) -> Result<Self, Self::Error> {
match value.to_ascii_lowercase().as_str() {
"none" => Ok(Self(NatStatus::NotApplicable)),
"nat" => Ok(Self(NatStatus::Detected)),
"no_nat" => Ok(Self(NatStatus::NotDetected)),
_ => Err(anyhow!("unknown nat status")),
}
}
}

macro_rules! file {
Expand All @@ -677,6 +732,7 @@ mod tests {
#[test_case(file!("ipv4_3probes_3hops_completed.yaml"))]
#[test_case(file!("ipv4_4probes_all_status.yaml"))]
#[test_case(file!("ipv4_4probes_0latency.yaml"))]
#[test_case(file!("ipv4_nat.yaml"))]
fn test_scenario(scenario: Scenario) {
let mut trace = State::new(StateConfig {
max_flows: 1,
Expand Down Expand Up @@ -716,6 +772,7 @@ mod tests {
assert_eq!(actual.last_src_port(), expected.last_src);
assert_eq!(actual.last_dest_port(), expected.last_dest);
assert_eq!(actual.last_sequence(), expected.last_sequence);
assert_eq!(actual.last_nat_status(), expected.last_nat_status.0);
assert_eq!(
Some(
actual
Expand Down
Loading

0 comments on commit 4f7c961

Please sign in to comment.