Skip to main content

netcore/
path.rs

1//! `Path` — the answer to "can I reach X, and if not, where does it die?"
2//!
3//! A path is the composition of three trait calls: resolve the target,
4//! select egress, run probes. [`Diagnostician::trace_path`] assembles one.
5
6use std::net::{IpAddr, SocketAddr};
7use std::time::Duration;
8
9use serde::{Deserialize, Serialize};
10
11use crate::connection::{ConnectionId, Family};
12use crate::diag::Finding;
13use crate::dns::{DnsError, DnsResolution};
14
15/// End-to-end path probe result: target resolution, egress selection, probe
16/// outcomes, and the final verdict.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct Path {
19    /// The target that was probed.
20    pub target: Target,
21    /// `None` when the target was given as an IP literal.
22    pub resolution: Option<DnsResolution>,
23    /// Which interface and source address carry traffic to the target.
24    pub egress: Egress,
25    /// All probe results collected for this path.
26    pub probes: ProbeResults,
27    /// The overall reachability verdict.
28    pub verdict: Verdict,
29    /// Diagnostic findings produced while tracing the path.
30    pub findings: Vec<Finding>,
31}
32
33/// What the user asked us to reach.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(tag = "type", rename_all = "snake_case")]
36pub enum Target {
37    Ip {
38        ip: IpAddr,
39        port: Option<u16>,
40    },
41    Host {
42        name: String,
43        port: Option<u16>,
44    },
45    /// Full URL — triggers TLS + HTTP probes.
46    Url {
47        url: String,
48    },
49}
50
51/// Which probes to run for a given target shape. The strategy is a function
52/// of the target: LAN IP gets ARP+ping; bare hostname gets DNS+TCP:443; a
53/// URL adds TLS+HTTP on top.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "snake_case")]
56pub enum ProbeStrategy {
57    LanIp,
58    IcmpOnly,
59    UnspecifiedTcp,
60    SpecificPort,
61    HttpUrl,
62}
63
64/// Which connection carries traffic to the target, as reported by
65/// `ip route get`.
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct Egress {
68    /// The connection that owns the egress interface.
69    pub connection_id: ConnectionId,
70    /// Kernel interface name (e.g. `"eth0"`).
71    pub iface: String,
72    /// Preferred source address selected by the kernel.
73    pub src: IpAddr,
74    /// Next-hop gateway, when the destination is not directly connected.
75    pub gateway: Option<IpAddr>,
76    /// Address family used for the route lookup.
77    pub family_used: Family,
78    /// Families for which the kernel said "Network is unreachable" — e.g. V6
79    /// on an IPv4-only upstream.
80    pub family_unreachable: Vec<Family>,
81    /// True when `ip rule` matched on uid; egress may differ for other users
82    /// (split-tunnel VPNs commonly do this).
83    pub uid_scoped: bool,
84}
85
86/// All probe results collected for a single path trace.
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct ProbeResults {
89    /// Which probe strategy was selected for this target.
90    pub strategy: ProbeStrategy,
91    /// ICMP to the gateway — cheap, almost always run.
92    pub gateway_ping: Option<PingResult>,
93    /// ICMP to the target — may be filtered, so not authoritative.
94    pub target_ping: Option<PingResult>,
95    /// TCP connect probe result.
96    pub tcp_connect: Option<TcpProbeResult>,
97    /// TLS handshake probe result.
98    pub tls_handshake: Option<TlsProbeResult>,
99    /// HTTP HEAD probe result.
100    pub http_head: Option<HttpProbeResult>,
101    /// Traceroute. Lazy: only populated on failure or with `--trace`.
102    pub trace: Option<Vec<Hop>>,
103}
104
105/// Result of an ICMP echo probe.
106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
107pub struct PingResult {
108    /// Number of echo requests sent.
109    pub sent: u32,
110    /// Number of echo replies received.
111    pub received: u32,
112    pub rtt_min: Option<Duration>,
113    pub rtt_avg: Option<Duration>,
114    pub rtt_max: Option<Duration>,
115}
116
117impl PingResult {
118    /// Compute packet loss as a percentage (0.0–100.0).
119    pub fn loss_pct(&self) -> f32 {
120        if self.sent == 0 {
121            return 0.0;
122        }
123        let lost = self.sent.saturating_sub(self.received);
124        (lost as f32 / self.sent as f32) * 100.0
125    }
126}
127
128/// Result of a TCP connect probe.
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130pub struct TcpProbeResult {
131    /// The target socket address.
132    pub addr: SocketAddr,
133    /// Whether the three-way handshake completed.
134    pub connected: bool,
135    /// Time from start to connection or failure.
136    pub took: Duration,
137    /// Human-readable error string when `connected` is false.
138    pub error: Option<String>,
139}
140
141/// Result of a TLS handshake probe.
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143pub struct TlsProbeResult {
144    /// The target socket address.
145    pub peer: SocketAddr,
146    /// The SNI hostname presented.
147    pub sni: String,
148    /// Whether the handshake completed with a valid certificate chain.
149    pub negotiated: bool,
150    /// Time from start to handshake completion or failure.
151    pub took: Duration,
152    /// Human-readable error string when `negotiated` is false.
153    pub error: Option<String>,
154}
155
156/// Result of an HTTP HEAD probe.
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
158pub struct HttpProbeResult {
159    /// The URL that was probed.
160    pub url: String,
161    /// HTTP response status code, when a response was received.
162    pub status: Option<u16>,
163    /// Time from request start to response or failure.
164    pub took: Duration,
165    /// Human-readable error string on failure.
166    pub error: Option<String>,
167}
168
169/// A single traceroute hop.
170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
171pub struct Hop {
172    /// TTL value for this hop.
173    pub ttl: u8,
174    /// IP address of the responding router, `None` on timeout.
175    pub ip: Option<IpAddr>,
176    /// Round-trip time to this hop, `None` on timeout.
177    pub rtt: Option<Duration>,
178    /// Reverse-DNS hostname, when available.
179    pub hostname: Option<String>,
180}
181
182impl Eq for Hop {}
183
184/// Outcome of a `reach` call. The CLI renders a one-line summary from this,
185/// then the `findings` for detail.
186#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187#[serde(tag = "type", rename_all = "snake_case")]
188pub enum Verdict {
189    Reachable {
190        latency_ms: u64,
191        family_used: Family,
192    },
193    /// One family worked, the other didn't. Common on dual-stack hosts with
194    /// broken IPv6.
195    PartiallyReachable {
196        working: Family,
197        broken: Family,
198    },
199    DnsFailed {
200        error: DnsError,
201    },
202    /// Kernel has no route to the target.
203    NoEgress {
204        reason: String,
205    },
206    GatewayDown {
207        gateway: IpAddr,
208    },
209    PacketLoss {
210        loss_pct: f32,
211    },
212    /// TCP RST — service up, port closed.
213    TcpRefused {
214        addr: SocketAddr,
215    },
216    /// TCP silence — firewall dropping, or the host is off.
217    TcpTimeout {
218        addr: SocketAddr,
219    },
220    TlsFailed {
221        err: String,
222    },
223    HttpFailed {
224        status: u16,
225    },
226}
227
228impl Eq for Verdict {}