Skip to main content

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}