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 {}