zond_engine/core/models/host/status.rs
1// Copyright (c) 2026 Erik Lening (hollowpointer) and Contributors
2//
3// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4// If a copy of the MPL was not distributed with this file, You can obtain one at
5// https://mozilla.org/MPL/2.0/.
6
7//! # Host Reachability Status
8//!
9//! This module defines the [`HostStatus`] model and supporting structures
10//! for recording how and why a host is considered reachable.
11//!
12//! The cornerstone of this module is the semantic ordering of status variants,
13//! allowing disparate scan results to be merged deterministically by prioritizing
14//! the most definitive evidence of a host's state.
15
16use std::sync::Arc;
17
18/// The high-level reachability state of a network host.
19///
20/// Variants are ordered by **semantic severity** and **reachability certainty**:
21/// `Unknown < Down < Filtered < Up`.
22///
23/// This ordering is critical for the [`Host::merge`](crate::core::models::Host::merge) logic:
24/// if one scan identifies a host as `Down` but a concurrent high-fidelity scan
25/// identifies it as `Up`, the `Up` status will prevail.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
27pub enum HostStatus {
28 /// Reachability has not yet been determined or the scan results were inconclusive.
29 Unknown,
30 /// The host is explicitly confirmed to be offline or unreachable (e.g., ICMP Host Unreachable).
31 Down,
32 /// The host exists on the network, but active probes are being dropped or rejected by a firewall.
33 Filtered,
34 /// The host is confirmed to be online and fully responding to probes.
35 Up,
36}
37
38/// Known protocols or events that provide evidence of host reachability.
39#[derive(Debug, Clone, PartialEq, Eq, Hash)]
40pub enum StatusProtocol {
41 /// Discovered via Address Resolution Protocol (Layer 2). Usually confirms local adjacency.
42 Arp,
43 /// Discovered via ICMP Echo Request/Reply.
44 IcmpEcho,
45 /// Discovered via a successful TCP 3-way handshake on an open port.
46 TcpSyn,
47 /// Discovered via a valid application-level response over UDP.
48 Udp,
49 /// A custom discovery method initiated by a specialized scanning script.
50 Custom(Arc<str>),
51}
52
53/// A structured rationale for a host's reachability state.
54///
55/// `StatusReason` pairs a protocol event with optional human-readable or machine-parsable
56/// details to provide a transparent "audit trail" for host discovery.
57#[derive(Debug, Clone, PartialEq, Eq, Hash)]
58pub struct StatusReason {
59 /// The specific protocol-level event that triggered this status.
60 pub protocol: StatusProtocol,
61
62 /// Extended details about the response (e.g., "Received TCP RST", "TTL Exceeded in transit").
63 ///
64 /// Stored as an `Arc<str>` to minimize heap churn when thousands of hosts report
65 /// identical rationales.
66 pub details: Option<Arc<str>>,
67}
68
69impl StatusReason {
70 /// Creates a new `StatusReason` with the specified protocol and details.
71 pub fn new(protocol: StatusProtocol, details: impl Into<Arc<str>>) -> Self {
72 Self {
73 protocol,
74 details: Some(details.into()),
75 }
76 }
77
78 /// Creates a new `StatusReason` containing only protocol-level evidence without extra details.
79 pub fn basic(protocol: StatusProtocol) -> Self {
80 Self {
81 protocol,
82 details: None,
83 }
84 }
85}
86
87impl HostStatus {
88 /// Returns `true` if the host is confirmed to be fully online and responding.
89 #[inline]
90 pub fn is_up(&self) -> bool {
91 matches!(self, HostStatus::Up)
92 }
93
94 /// Returns `true` if the host is explicitly confirmed to be offline.
95 #[inline]
96 pub fn is_down(&self) -> bool {
97 matches!(self, HostStatus::Down)
98 }
99
100 /// Returns `true` if there is evidence the host is present on the network,
101 /// even if communication is restricted by a firewall.
102 #[inline]
103 pub fn is_alive(&self) -> bool {
104 matches!(self, HostStatus::Up | HostStatus::Filtered)
105 }
106}
107
108impl std::fmt::Display for HostStatus {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 match self {
111 HostStatus::Unknown => write!(f, "Unknown"),
112 HostStatus::Down => write!(f, "Down"),
113 HostStatus::Filtered => write!(f, "Filtered"),
114 HostStatus::Up => write!(f, "Up"),
115 }
116 }
117}
118
119// ╔════════════════════════════════════════════╗
120// ║ ████████╗███████╗███████╗████████╗███████╗ ║
121// ║ ╚══██╔══╝██╔════╝██╔════╝╚══██╔══╝██╔════╝ ║
122// ║ ██║ █████╗ ███████╗ ██║ ███████╗ ║
123// ║ ██║ ██╔══╝ ╚════██║ ██║ ╚════██║ ║
124// ║ ██║ ███████╗███████║ ██║ ███████║ ║
125// ║ ╚═╝ ╚══════╝╚══════╝ ╚═╝ ╚══════╝ ║
126// ╚════════════════════════════════════════════╝
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn status_ordering_contract() {
134 // This test ensures that the derived Ord implementation matches the semantic severity.
135 assert!(HostStatus::Unknown < HostStatus::Down);
136 assert!(HostStatus::Down < HostStatus::Filtered);
137 assert!(HostStatus::Filtered < HostStatus::Up);
138 }
139
140 #[test]
141 fn status_alive_semantics() {
142 assert!(!HostStatus::Unknown.is_alive());
143 assert!(!HostStatus::Down.is_alive());
144 assert!(HostStatus::Filtered.is_alive());
145 assert!(HostStatus::Up.is_alive());
146 }
147
148 #[test]
149 fn status_display_consistency() {
150 assert_eq!(HostStatus::Unknown.to_string(), "Unknown");
151 assert_eq!(HostStatus::Up.to_string(), "Up");
152 }
153
154 #[test]
155 fn status_reason_ergonomics() {
156 let reason = StatusReason::new(
157 StatusProtocol::Custom(Arc::from("dns-probe")),
158 "Resolved A record successfully",
159 );
160
161 assert_eq!(
162 reason.protocol,
163 StatusProtocol::Custom(Arc::from("dns-probe"))
164 );
165 assert_eq!(
166 reason.details.as_deref(),
167 Some("Resolved A record successfully")
168 );
169 }
170}