Skip to main content

netcore/
diag.rs

1//! Layer 3 — judgments. A typed vocabulary for health and failure.
2//!
3//! Every symptom gets tagged with a [`Layer`], so the diagnostician can sort
4//! findings by layer and surface the lowest broken one first (fix L1 before
5//! worrying about L7).
6
7use std::time::Duration;
8
9use serde::{Deserialize, Serialize};
10
11use crate::connection::ConnectionId;
12use crate::dns::DnsResolution;
13use crate::link::{L4Proto, Link, Neighbor, Route};
14use crate::path::ProbeResults;
15
16/// Where in the stack a finding sits. Ordered roughly bottom-up so sorting by
17/// layer surfaces the lowest broken piece first.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum Layer {
21    Link,
22    Address,
23    Gateway,
24    Dns,
25    Internet,
26    Firewall,
27    Service,
28}
29
30/// How bad a finding is. Determines exit code and rendering color.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub enum Severity {
34    /// Informational: something to be aware of, but not blocking.
35    Info,
36    /// Degraded: something is wrong but connectivity may still partially work.
37    Warn,
38    /// Broken: connectivity is impaired at this layer.
39    Broken,
40}
41
42/// A single diagnostic observation.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Finding {
45    pub layer: Layer,
46    pub severity: Severity,
47    /// One-line plain English: "gateway 192.168.1.1 not responding to ARP".
48    pub summary: String,
49    /// Longer explanation with context, if useful.
50    pub detail: Option<String>,
51    pub remedy: Option<Remedy>,
52    pub evidence: Evidence,
53}
54
55/// What the user can do about this finding.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57#[serde(tag = "type", rename_all = "snake_case")]
58pub enum Remedy {
59    /// A specific command to try.
60    Run { cmd: String },
61    /// A physical/environmental thing to verify.
62    Check { what: String },
63    /// Ask the connection manager to reconnect this connection.
64    Reconnect { id: ConnectionId },
65    /// The operation requires more privilege than we have.
66    ElevatePrivileges,
67    /// No remedy suggested.
68    None,
69}
70
71/// The raw data that led to a finding. Keeps findings self-contained — a
72/// caller can always drill down from a finding to the evidence without
73/// re-running inventory.
74#[derive(Debug, Clone, Serialize, Deserialize)]
75#[serde(tag = "type", rename_all = "snake_case")]
76pub enum Evidence {
77    Link { link: Link },
78    Neighbor { neighbor: Neighbor },
79    Route { route: Route },
80    Dns { dns: DnsResolution },
81    Probe { probe: Box<ProbeResults> },
82    Text { text: String },
83}
84
85/// Overall health summary for a host.
86#[derive(Debug, Clone, Serialize, Deserialize)]
87#[serde(tag = "type", rename_all = "snake_case")]
88pub enum Health {
89    Ok,
90    Degraded { findings: Vec<Finding> },
91    Broken { findings: Vec<Finding> },
92}
93
94/// How deep `check()` should probe.
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(rename_all = "snake_case")]
97pub enum CheckScope {
98    /// Inventory + gateway L2 + DNS stub up + one ICMP to gateway.
99    Quick,
100    /// Adds AAAA reachability, TCP:443 to a known public target, and the
101    /// firewall posture check.
102    Full,
103}
104
105/// Firewall verdict for a given inbound (port, proto).
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
107#[serde(rename_all = "snake_case")]
108pub enum FirewallVerdict {
109    Allow,
110    Drop,
111    Reject,
112    /// No explicit rule matched — distinct from allow because a default policy
113    /// of DROP leaves this as the "unknown" case.
114    NoMatch,
115    Unknown,
116}
117
118/// Which firewall tool provided the inbound verdict.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
120#[serde(rename_all = "snake_case")]
121pub enum FirewallBackend {
122    /// nftables via `nft -j list ruleset`.
123    Nftables,
124    /// iptables via `iptables -L`.
125    Iptables,
126    /// Neither tool present or accessible.
127    Unknown,
128}
129
130/// What the reachability backend can actually do on this system.
131#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
132pub struct ProbeCapabilities {
133    pub has_ping: bool,
134    pub has_traceroute: bool,
135    pub has_mtr: bool,
136    pub has_tracepath: bool,
137    /// True when unprivileged ICMP sockets are usable (checked via
138    /// `net.ipv4.ping_group_range` or a test probe).
139    pub unprivileged_icmp: bool,
140}
141
142/// Options for an ICMP echo probe.
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
144pub struct PingOpts {
145    /// Number of echo requests to send.
146    pub count: u32,
147    /// Per-packet timeout.
148    pub timeout: Duration,
149}
150
151impl Default for PingOpts {
152    fn default() -> Self {
153        Self {
154            count: 2,
155            timeout: Duration::from_secs(1),
156        }
157    }
158}
159
160/// Options for a traceroute probe.
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
162pub struct TraceOpts {
163    /// Maximum TTL before giving up.
164    pub max_hops: u8,
165    /// How long to wait for each hop to respond.
166    pub timeout_per_hop: Duration,
167    /// Transport protocol for probes.
168    pub proto: L4Proto,
169}
170
171impl Default for TraceOpts {
172    fn default() -> Self {
173        Self {
174            max_hops: 20,
175            timeout_per_hop: Duration::from_secs(1),
176            proto: L4Proto::Udp,
177        }
178    }
179}