Skip to main content

ftr/
traceroute.rs

1//! Core traceroute functionality and utilities
2//!
3//! This module provides the main traceroute implementation including:
4//! - High-level API functions ([`trace`], [`trace_with_config`])
5//! - Configuration types ([`TracerouteConfig`], [`TracerouteConfigBuilder`])
6//! - Result types ([`TracerouteResult`], [`TracerouteProgress`])
7//! - Error handling ([`TracerouteError`])
8//!
9//! # Error Handling
10//!
11//! All traceroute operations return a `Result<T, TracerouteError>` where
12//! [`TracerouteError`] is an enum providing structured error information:
13//!
14//! - **`InsufficientPermissions`** - Includes what permissions are needed and suggestions
15//! - **`NotImplemented`** - Feature not yet implemented (e.g., TCP traceroute)
16//! - **`Ipv6NotSupported`** - IPv6 targets not yet supported
17//! - **`ResolutionError`** - DNS resolution failed
18//! - **`SocketError`** - Socket creation/operation failed
19//! - **`ConfigError`** - Invalid configuration
20//! - **`ProbeSendError`** - Failed to send probe packet
21//!
22//! This design allows library users to handle errors programmatically without
23//! parsing error strings.
24
25pub mod api;
26pub mod config;
27pub mod engine;
28pub mod error;
29pub mod result;
30pub mod types;
31
32#[cfg(test)]
33mod caching_test;
34
35use ip_network::Ipv4Network;
36use serde::{Deserialize, Serialize};
37use std::net::Ipv4Addr;
38
39// Re-export commonly used types
40pub use api::resolve_target_with_family;
41pub use api::{Traceroute, trace_async as trace, trace_with_config_async as trace_with_config};
42pub use config::{PreferredFamily, TimingConfig, TracerouteConfig, TracerouteConfigBuilder};
43pub use error::{ConfigError, TracerouteError};
44pub use result::{TracerouteProgress, TracerouteResult};
45pub use types::{ClassifiedHopInfo, IspInfo, RawHopInfo};
46
47/// Classification of a hop's network segment
48///
49/// Used to categorize network hops based on their location relative
50/// to the user's network topology.
51///
52/// # Examples
53///
54/// ```
55/// use ftr::SegmentType;
56///
57/// let segment = SegmentType::Isp;
58/// println!("Hop is in the {} segment", segment);
59/// ```
60///
61/// This enum is `#[non_exhaustive]`: new segment classifications may be
62/// added in minor releases, so downstream matches must include a wildcard arm.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
64#[non_exhaustive]
65pub enum SegmentType {
66    /// Local area network (private IP ranges like 192.168.x.x)
67    Lan,
68    /// Internet Service Provider network
69    Isp,
70    /// After ISP, across ASNs that differ from destination's ASN
71    Transit,
72    /// Within the destination's ASN
73    Destination,
74    /// Unknown or unclassified segment
75    Unknown,
76}
77
78impl std::fmt::Display for SegmentType {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        match self {
81            SegmentType::Lan => write!(f, "LAN   "),
82            SegmentType::Isp => write!(f, "ISP   "),
83            SegmentType::Transit => write!(f, "TRANSIT"),
84            SegmentType::Destination => write!(f, "DESTINATION"),
85            SegmentType::Unknown => write!(f, "UNKNOWN"),
86        }
87    }
88}
89
90/// Checks if an IP address is within private/internal ranges.
91pub fn is_internal_ip(ip: &Ipv4Addr) -> bool {
92    ip.is_private() || ip.is_loopback() || ip.is_link_local()
93}
94
95/// Checks if an IP is in the CGNAT range (100.64.0.0/10).
96pub fn is_cgnat(ip: &Ipv4Addr) -> bool {
97    let octets = ip.octets();
98    octets[0] == 100 && (64..=127).contains(&octets[1])
99}
100
101/// Checks if an IPv6 address is within private/internal ranges — the
102/// LAN-equivalent scopes for segment classification:
103///
104/// - loopback `::1` (RFC 4291 section 2.5.3)
105/// - link-local unicast `fe80::/10` (RFC 4291 section 2.5.6)
106/// - unique local addresses `fc00::/7` (RFC 4193 section 3.1)
107///
108/// Bit checks are written out explicitly (rather than via the
109/// `Ipv6Addr::is_*` helpers) so each range is auditable against its RFC.
110pub fn is_internal_ipv6(ip: &std::net::Ipv6Addr) -> bool {
111    let first_segment = ip.segments()[0];
112    ip.is_loopback()
113        // fe80::/10: top 10 bits are 1111 1110 10
114        || (first_segment & 0xffc0) == 0xfe80
115        // fc00::/7: top 7 bits are 1111 110
116        || (first_segment & 0xfe00) == 0xfc00
117}
118
119/// Parse an ASN string into components
120pub fn parse_asn(asn_str: &str) -> Option<(String, String, String)> {
121    // Format: "AS13335 | 104.16.0.0/12 | US | ARIN | CLOUDFLARENET"
122    let parts: Vec<&str> = asn_str.split(" | ").collect();
123    if parts.len() >= 5 {
124        Some((
125            parts[0].to_string(),
126            parts[1].to_string(),
127            parts[4].to_string(),
128        ))
129    } else {
130        None
131    }
132}
133
134/// Autonomous System Number (ASN) information for an IP address
135///
136/// Contains details about the network organization that owns a particular
137/// IP address range. This information is retrieved from IPtoASN.com.
138///
139/// # Examples
140///
141/// ```
142/// # use ftr::AsnInfo;
143/// let asn = AsnInfo {
144///     asn: 15169,
145///     prefix: "8.8.8.0/24".to_string(),
146///     country_code: "US".to_string(),
147///     registry: "ARIN".to_string(),
148///     name: "GOOGLE".to_string(),
149/// };
150///
151/// // Use the display_asn method for consistent formatting
152/// println!("{} - {} ({})", asn.display_asn(), asn.name, asn.country_code);
153/// ```
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
155pub struct AsnInfo {
156    /// Autonomous System Number (e.g., 13335)
157    ///
158    /// The numeric ASN without "AS" prefix. 0 indicates N/A (private/special IPs).
159    /// To display: if asn != 0 { format!("AS{}", asn) } else { "N/A" }
160    pub asn: u32,
161    /// IP prefix/CIDR block (e.g., "104.16.0.0/12")
162    pub prefix: String,
163    /// Two-letter country code (e.g., "US")
164    pub country_code: String,
165    /// Regional Internet Registry (e.g., "ARIN", "RIPE", "APNIC")
166    pub registry: String,
167    /// AS name/organization (e.g., "CLOUDFLARENET", "GOOGLE")
168    pub name: String,
169}
170
171impl AsnInfo {
172    /// Get the display string for the ASN
173    ///
174    /// Returns "AS12345" format for valid ASNs, or "N/A" for private/special IPs.
175    pub fn display_asn(&self) -> String {
176        if self.asn != 0 {
177            format!("AS{}", self.asn)
178        } else {
179            "N/A".to_string()
180        }
181    }
182}
183
184/// Parse CIDR notation into Ipv4Network
185pub fn parse_cidr(cidr: &str) -> Option<Ipv4Network> {
186    cidr.parse().ok()
187}
188
189#[cfg(test)]
190#[path = "traceroute/segment_classification_test.rs"]
191mod segment_classification_test;
192
193#[cfg(test)]
194#[allow(clippy::unwrap_used)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn test_segment_type_display() {
200        assert_eq!(SegmentType::Lan.to_string(), "LAN   ");
201        assert_eq!(SegmentType::Isp.to_string(), "ISP   ");
202        assert_eq!(SegmentType::Transit.to_string(), "TRANSIT");
203        assert_eq!(SegmentType::Destination.to_string(), "DESTINATION");
204        assert_eq!(SegmentType::Unknown.to_string(), "UNKNOWN");
205    }
206
207    #[test]
208    fn test_is_internal_ip() {
209        // Private ranges
210        assert!(is_internal_ip(&"192.168.1.1".parse().unwrap()));
211        assert!(is_internal_ip(&"10.0.0.1".parse().unwrap()));
212        assert!(is_internal_ip(&"172.16.0.1".parse().unwrap()));
213        assert!(is_internal_ip(&"172.31.255.255".parse().unwrap()));
214
215        // Loopback
216        assert!(is_internal_ip(&"127.0.0.1".parse().unwrap()));
217        assert!(is_internal_ip(&"127.255.255.255".parse().unwrap()));
218
219        // Link-local
220        assert!(is_internal_ip(&"169.254.1.1".parse().unwrap()));
221
222        // Public IPs
223        assert!(!is_internal_ip(&"8.8.8.8".parse().unwrap()));
224        assert!(!is_internal_ip(&"1.1.1.1".parse().unwrap()));
225        assert!(!is_internal_ip(&"172.32.0.1".parse().unwrap())); // Just outside private range
226    }
227
228    #[test]
229    fn test_is_cgnat() {
230        // CGNAT range: 100.64.0.0/10
231        assert!(is_cgnat(&"100.64.0.0".parse().unwrap()));
232        assert!(is_cgnat(&"100.64.0.1".parse().unwrap()));
233        assert!(is_cgnat(&"100.127.255.255".parse().unwrap()));
234        assert!(is_cgnat(&"100.100.100.100".parse().unwrap()));
235
236        // Just outside CGNAT range
237        assert!(!is_cgnat(&"100.63.255.255".parse().unwrap()));
238        assert!(!is_cgnat(&"100.128.0.0".parse().unwrap()));
239        assert!(!is_cgnat(&"99.64.0.0".parse().unwrap()));
240        assert!(!is_cgnat(&"101.64.0.0".parse().unwrap()));
241
242        // Other IPs
243        assert!(!is_cgnat(&"8.8.8.8".parse().unwrap()));
244        assert!(!is_cgnat(&"192.168.1.1".parse().unwrap()));
245    }
246
247    #[test]
248    fn test_is_internal_ipv6() {
249        use std::net::Ipv6Addr;
250        // Loopback
251        assert!(is_internal_ipv6(&Ipv6Addr::LOCALHOST));
252        // Link-local fe80::/10 spans fe80:: through febf:ffff:...
253        assert!(is_internal_ipv6(&"fe80::1".parse::<Ipv6Addr>().unwrap()));
254        assert!(is_internal_ipv6(
255            &"febf:ffff::1".parse::<Ipv6Addr>().unwrap()
256        ));
257        // Just outside link-local
258        assert!(!is_internal_ipv6(&"fec0::1".parse::<Ipv6Addr>().unwrap()));
259        assert!(!is_internal_ipv6(&"fe7f::1".parse::<Ipv6Addr>().unwrap()));
260        // ULA fc00::/7 spans fc00:: through fdff:ffff:...
261        assert!(is_internal_ipv6(&"fc00::1".parse::<Ipv6Addr>().unwrap()));
262        assert!(is_internal_ipv6(
263            &"fd12:3456:789a::1".parse::<Ipv6Addr>().unwrap()
264        ));
265        assert!(is_internal_ipv6(
266            &"fdff:ffff::ffff".parse::<Ipv6Addr>().unwrap()
267        ));
268        // Just outside ULA
269        assert!(!is_internal_ipv6(&"fbff::1".parse::<Ipv6Addr>().unwrap()));
270        // Global unicast and unspecified are not internal
271        assert!(!is_internal_ipv6(
272            &"2001:4860:4860::8888".parse::<Ipv6Addr>().unwrap()
273        ));
274        assert!(!is_internal_ipv6(
275            &"2606:4700::1".parse::<Ipv6Addr>().unwrap()
276        ));
277        assert!(!is_internal_ipv6(&Ipv6Addr::UNSPECIFIED));
278    }
279
280    #[test]
281    fn test_parse_asn() {
282        let asn_str = "AS13335 | 104.16.0.0/12 | US | ARIN | CLOUDFLARENET";
283        let result = parse_asn(asn_str);
284        assert_eq!(
285            result,
286            Some((
287                "AS13335".to_string(),
288                "104.16.0.0/12".to_string(),
289                "CLOUDFLARENET".to_string()
290            ))
291        );
292
293        // Invalid format
294        assert_eq!(parse_asn("invalid"), None);
295        assert_eq!(parse_asn("AS123 | incomplete"), None);
296    }
297
298    #[test]
299    fn test_parse_cidr() {
300        assert!(parse_cidr("192.168.0.0/16").is_some());
301        assert!(parse_cidr("10.0.0.0/8").is_some());
302        assert!(parse_cidr("172.16.0.0/12").is_some());
303
304        // Invalid CIDR
305        assert!(parse_cidr("invalid").is_none());
306        assert!(parse_cidr("192.168.0.0/33").is_none()); // Invalid prefix length
307        assert!(parse_cidr("256.0.0.0/8").is_none()); // Invalid IP
308    }
309
310    #[test]
311    fn test_asn_info() {
312        let asn_info = AsnInfo {
313            asn: 13335,
314            prefix: "104.16.0.0/12".to_string(),
315            country_code: "US".to_string(),
316            registry: "ARIN".to_string(),
317            name: "CLOUDFLARENET".to_string(),
318        };
319
320        assert_eq!(asn_info.asn, 13335);
321        assert_eq!(asn_info.country_code, "US");
322        assert_eq!(asn_info.display_asn(), "AS13335");
323
324        // Test Clone
325        let cloned = asn_info.clone();
326        assert_eq!(cloned, asn_info);
327    }
328
329    #[test]
330    fn test_asn_info_display() {
331        // Test normal ASN
332        let asn_info = AsnInfo {
333            asn: 12345,
334            prefix: "10.0.0.0/8".to_string(),
335            country_code: "US".to_string(),
336            registry: "ARIN".to_string(),
337            name: "EXAMPLE".to_string(),
338        };
339        assert_eq!(asn_info.display_asn(), "AS12345");
340
341        // Test N/A ASN (private/special IPs)
342        let private_asn = AsnInfo {
343            asn: 0,
344            prefix: "192.168.0.0/16".to_string(),
345            country_code: "".to_string(),
346            registry: "".to_string(),
347            name: "Private Use".to_string(),
348        };
349        assert_eq!(private_asn.display_asn(), "N/A");
350    }
351}