Skip to main content

manycastr/worker/inbound/
trace.rs

1use crate::custom_module::manycastr::reply::ReplyData;
2use crate::custom_module::manycastr::{Address, Reply, TraceReply};
3use crate::net::{ICMPPacket, IPv4Packet};
4use crate::worker::inbound::ReplyMeta;
5use crate::worker::inbound::ping::parse_icmp;
6use crate::worker::trace_codec::TraceTag;
7
8/// Parse ICMP Time Exceeded (and Destination Unreachable) packets into a trace Reply.
9///
10/// Supports traceroute probes sent via ICMP, UDP, or TCP. The original protocol is
11/// detected from the IP header embedded in the Time Exceeded payload, and probe
12/// identification is decoded accordingly:
13///
14/// - **ICMP**: identifier (worker_hi + timestamp) + sequence (TTL + worker_lo)
15/// - **UDP (Paris)**: IP identification / IPv6 flow label (worker_hi + timestamp) + UDP checksum (TTL + worker_lo)
16/// - **TCP**: seq number (worker_id + TTL + timestamp)
17///
18/// Falls back to `parse_icmp` for Echo Reply packets (destination reached in ICMP traceroute).
19///
20/// # Arguments
21/// * `packet_bytes` - the bytes of the packet to parse (excluding the Ethernet header)
22/// * `m_id` - measurement ID encoded in ICMP payload.
23/// * `meta` - received packet metadata (hop address, TTL, kernel receive time)
24///
25/// # Returns
26/// * `Option<Reply>` - the received trace reply (None if not a valid trace response)
27pub fn parse_trace(packet_bytes: &[u8], m_id: u32, meta: ReplyMeta) -> Option<Reply> {
28    let ReplyMeta { src, ttl, rx_time } = meta;
29    let is_v6 = src.is_v6();
30
31    // ICMP error type numbers, and where the ICMP header starts
32    let (time_exceeded, dest_unreachable, icmp_start) = if is_v6 {
33        (3u8, 1u8, 0usize)
34    } else {
35        (11u8, 3u8, 20usize)
36    };
37
38    // Only Time Exceeded / Destination Unreachable quote the original probe
39    match packet_bytes.get(icmp_start) {
40        Some(&t) if t == time_exceeded || t == dest_unreachable => {}
41        _ => return parse_icmp(packet_bytes, m_id, true, meta),
42    }
43
44    // Hop address + TTL of the outer error packet.
45    let (hop_addr, hop_ttl) = if is_v6 {
46        (src, ttl)
47    } else {
48        let ip = IPv4Packet::from(packet_bytes);
49        (Address::from(ip.src), ip.ttl as u32)
50    };
51
52    // Need the full 8-byte ICMP header before parsing it (the parser unwraps those bytes).
53    if packet_bytes.len() < icmp_start + 8 {
54        return parse_icmp(packet_bytes, m_id, true, meta);
55    }
56
57    // The ICMP payload is the quoted original probe (its IP header + first 8 transport bytes).
58    let icmp = ICMPPacket::from(&packet_bytes[icmp_start..]);
59    let quoted = parse_quoted_probe(&icmp.payload, is_v6)?;
60
61    // Recover the probe identity from the protocol-specific carrier fields (layouts in trace_codec).
62    let t = quoted.transport;
63    let tag = match quoted.protocol {
64        // ICMP (1) / ICMPv6 (58): identifier + sequence number
65        1 | 58 => TraceTag::decode_split(
66            u16::from_be_bytes([t[4], t[5]]),
67            u16::from_be_bytes([t[6], t[7]]),
68        ),
69        // UDP (17): id field (IPv4 identification / IPv6 flow label) + UDP checksum
70        17 => TraceTag::decode_split(quoted.id_field, u16::from_be_bytes([t[6], t[7]])),
71        // TCP (6): whole identity in the 32-bit sequence number
72        6 => TraceTag::decode_tcp_seq(u32::from_be_bytes([t[4], t[5], t[6], t[7]])),
73        _ => return None,
74    };
75
76    Some(make_trace_reply(
77        hop_addr,
78        hop_ttl,
79        rx_time,
80        tag.ts14 as u64,
81        tag.worker_id,
82        quoted.dst,
83        tag.ttl as u32,
84    ))
85}
86
87/// Fields recovered from the original (quoted) probe inside an ICMP error message.
88struct QuotedProbe<'a> {
89    /// IP protocol number of the quoted probe (1/58 ICMP, 17 UDP, 6 TCP).
90    protocol: u8,
91    /// Destination address of the quoted probe (the trace target).
92    dst: Address,
93    /// Codec id field: IPv4 Identification, or the low 16 bits of the IPv6 flow label.
94    id_field: u16,
95    /// The quoted transport header — guaranteed to be at least 8 bytes.
96    transport: &'a [u8],
97}
98
99/// Parse the quoted IP datagram carried in an ICMP error message.
100fn parse_quoted_probe(payload: &[u8], is_v6: bool) -> Option<QuotedProbe<'_>> {
101    if is_v6 {
102        // IPv6 header is a fixed 40 bytes; need 8 transport bytes after it.
103        if payload.len() < 48 {
104            return None;
105        }
106        let id_field = (u32::from_be_bytes(payload[0..4].try_into().ok()?) & 0x000F_FFFF) as u16;
107        Some(QuotedProbe {
108            protocol: payload[6], // Next Header
109            dst: Address::from(u128::from_be_bytes(payload[24..40].try_into().ok()?)),
110            id_field,
111            transport: &payload[40..],
112        })
113    } else {
114        let ihl = ((*payload.first()? & 0x0F) as usize) * 4;
115        let transport = payload.get(ihl..)?;
116        if transport.len() < 8 {
117            return None;
118        }
119        Some(QuotedProbe {
120            protocol: *payload.get(9)?, // Protocol
121            dst: Address::from(u32::from_be_bytes(payload.get(16..20)?.try_into().ok()?)),
122            id_field: u16::from_be_bytes([*payload.get(4)?, *payload.get(5)?]), // IP Identification
123            transport,
124        })
125    }
126}
127
128/// Helper to construct a trace Reply from decoded fields
129fn make_trace_reply(
130    hop_addr: Address,
131    ttl: u32,
132    rx_time: u64,
133    tx_time: u64,
134    tx_id: u32,
135    trace_dst: Address,
136    hop_count: u32,
137) -> Reply {
138    Reply {
139        reply_data: Some(ReplyData::Trace(TraceReply {
140            hop_addr: Some(hop_addr),
141            ttl,
142            rtt: super::rtt_ms(rx_time, tx_time, super::TxEncoding::Trace14),
143            tx_id,
144            trace_dst: Some(trace_dst),
145            hop_count,
146        })),
147    }
148}