use crate::core::models::port::Port;
use std::{
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
net::IpAddr,
time::SystemTime,
};
pub mod hardware;
pub mod os;
pub mod status;
pub mod telemetry;
pub use hardware::HardwareInfo;
pub use os::OsFingerprint;
pub use status::{HostStatus, StatusProtocol, StatusReason};
pub use telemetry::HostTelemetry;
pub const MAX_PORTS_PER_HOST: usize = 1000;
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum NetworkRole {
Gateway,
DHCP,
DNS,
Tarpit,
}
#[derive(Debug, Clone)]
pub struct Host {
primary_ip: IpAddr,
ips: BTreeSet<IpAddr>,
hostname: Option<String>,
status: HostStatus,
reasons: HashSet<StatusReason>,
os: Option<Box<OsFingerprint>>,
hardware: Option<HardwareInfo>,
telemetry: HostTelemetry,
network_roles: HashSet<NetworkRole>,
scripts: Option<HashMap<String, String>>,
first_seen: SystemTime,
last_seen: SystemTime,
ports: BTreeMap<u16, Port>,
}
impl Host {
pub fn new(primary_ip: IpAddr) -> Self {
let mut ips = BTreeSet::new();
ips.insert(primary_ip);
let now = SystemTime::now();
Self {
primary_ip,
ips,
hostname: None,
status: HostStatus::Unknown,
reasons: HashSet::new(),
os: None,
hardware: None,
telemetry: HostTelemetry::default(),
network_roles: HashSet::new(),
scripts: None,
first_seen: now,
last_seen: now,
ports: BTreeMap::new(),
}
}
pub fn primary_ip(&self) -> IpAddr {
self.primary_ip
}
pub fn ips(&self) -> &BTreeSet<IpAddr> {
&self.ips
}
pub fn hostname(&self) -> Option<&str> {
self.hostname.as_deref()
}
pub fn status(&self) -> HostStatus {
self.status
}
pub fn reasons(&self) -> &HashSet<StatusReason> {
&self.reasons
}
pub fn os(&self) -> Option<&OsFingerprint> {
self.os.as_deref()
}
pub fn hardware(&self) -> Option<&HardwareInfo> {
self.hardware.as_ref()
}
pub fn telemetry(&self) -> &HostTelemetry {
&self.telemetry
}
pub fn network_roles(&self) -> &HashSet<NetworkRole> {
&self.network_roles
}
pub fn scripts(&self) -> Option<&HashMap<String, String>> {
self.scripts.as_ref()
}
pub fn first_seen(&self) -> SystemTime {
self.first_seen
}
pub fn last_seen(&self) -> SystemTime {
self.last_seen
}
pub fn set_primary_ip(&mut self, ip: IpAddr) {
self.primary_ip = ip;
self.ips.insert(ip);
self.last_seen = SystemTime::now();
}
pub fn add_ip(&mut self, ip: IpAddr) -> bool {
let is_new = self.ips.insert(ip);
self.last_seen = SystemTime::now();
is_new
}
pub fn extend_ips(&mut self, ips: impl IntoIterator<Item = IpAddr>) {
self.ips.extend(ips);
self.last_seen = SystemTime::now();
}
pub fn set_hostname(&mut self, hostname: Option<String>) {
self.hostname = hostname;
self.last_seen = SystemTime::now();
}
pub fn set_status(&mut self, status: HostStatus) {
self.status = status;
self.last_seen = SystemTime::now();
}
pub fn add_reason(&mut self, reason: StatusReason) {
self.reasons.insert(reason);
self.last_seen = SystemTime::now();
}
pub fn set_os(&mut self, os: OsFingerprint) {
self.os = Some(Box::new(os));
self.last_seen = SystemTime::now();
}
pub fn set_hardware(&mut self, hardware: HardwareInfo) {
self.hardware = Some(hardware);
self.last_seen = SystemTime::now();
}
pub fn with_mac(mut self, mac: crate::core::models::mac::MacAddr) -> Self {
self.set_hardware(HardwareInfo::new(mac));
self
}
pub fn add_rtt(&mut self, rtt: std::time::Duration) {
self.telemetry.add_rtt(rtt);
self.last_seen = SystemTime::now();
}
pub fn with_rtt(mut self, rtt: std::time::Duration) -> Self {
self.add_rtt(rtt);
self
}
pub fn set_rtts(&mut self, rtts: impl IntoIterator<Item = std::time::Duration>) {
for rtt in rtts {
self.telemetry.add_rtt(rtt);
}
self.last_seen = SystemTime::now();
}
pub fn add_network_role(&mut self, role: NetworkRole) {
self.network_roles.insert(role);
self.last_seen = SystemTime::now();
}
pub fn add_script_result(&mut self, key: String, value: String) {
self.scripts
.get_or_insert_with(HashMap::new)
.insert(key, value);
self.last_seen = SystemTime::now();
}
pub fn min_rtt(&self) -> Option<std::time::Duration> {
self.telemetry.min_rtt()
}
pub fn max_rtt(&self) -> Option<std::time::Duration> {
self.telemetry.max_rtt()
}
pub fn average_rtt(&self) -> Option<std::time::Duration> {
self.telemetry.average_rtt()
}
pub fn mac(&self) -> Option<crate::core::models::mac::MacAddr> {
self.hardware.as_ref().and_then(|h| h.most_recent_mac())
}
pub fn vendor(&self) -> Option<&str> {
self.hardware
.as_ref()
.and_then(|h| h.vendor.as_ref())
.map(|v| &**v)
}
pub fn is_alive(&self) -> bool {
self.status.is_alive()
}
pub fn ports(&self) -> impl Iterator<Item = &Port> {
self.ports.values()
}
pub fn port_count(&self) -> usize {
self.ports.len()
}
pub fn add_port(&mut self, new_port: Port) {
if self.ports.len() >= MAX_PORTS_PER_HOST && !self.ports.contains_key(&new_port.number()) {
self.network_roles.insert(NetworkRole::Tarpit);
return;
}
self.ports
.entry(new_port.number())
.and_modify(|p| p.merge(new_port.clone()))
.or_insert(new_port);
self.last_seen = SystemTime::now();
}
pub fn merge(&mut self, other: Host) {
self.ips.extend(other.ips);
if self.hostname.is_none() {
self.hostname = other.hostname;
}
if other.status > self.status {
self.status = other.status;
}
self.reasons.extend(other.reasons);
if let Some(other_os) = other.os {
if let Some(ref mut self_os) = self.os {
self_os.merge(*other_os);
} else {
self.os = Some(other_os);
}
}
if let Some(other_hw) = other.hardware {
if let Some(ref mut self_hw) = self.hardware {
self_hw.merge(other_hw);
} else {
self.hardware = Some(other_hw);
}
}
self.telemetry.merge(other.telemetry);
self.network_roles.extend(other.network_roles);
if let Some(other_scripts) = other.scripts {
let self_scripts = self.scripts.get_or_insert_with(HashMap::new);
self_scripts.extend(other_scripts);
}
for (_, port) in other.ports {
self.add_port(port);
}
if other.first_seen < self.first_seen {
self.first_seen = other.first_seen;
}
if other.last_seen > self.last_seen {
self.last_seen = other.last_seen;
}
}
}
impl std::fmt::Display for Host {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", self.primary_ip, self.status)?;
if let Some(ref os) = self.os {
write!(f, " - {}", os)?;
}
if self.network_roles.contains(&NetworkRole::Tarpit) {
write!(f, " [TARPIT]")?;
} else {
write!(f, " [{}]", self.telemetry)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::models::port::{Port, PortState, Protocol};
use std::net::Ipv4Addr;
static IP_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::new(192, 168, 0, 100));
#[test]
fn host_primary_ip_invariant() {
let mut host = Host::new(IP_ADDR);
let fresh_ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 5));
host.set_primary_ip(fresh_ip);
assert_eq!(host.primary_ip(), fresh_ip);
assert!(host.ips().contains(&fresh_ip));
}
#[test]
fn tarpit_boundary_test() {
let mut host = Host::new(IP_ADDR);
for i in 0..MAX_PORTS_PER_HOST {
host.add_port(Port::new(i as u16, Protocol::Tcp, PortState::Open));
}
assert!(!host.network_roles.contains(&NetworkRole::Tarpit));
host.add_port(Port::new(9999, Protocol::Tcp, PortState::Open));
assert!(host.network_roles.contains(&NetworkRole::Tarpit));
assert_eq!(host.port_count(), MAX_PORTS_PER_HOST);
}
#[test]
fn host_merge_promotes_status() {
let mut h1 = Host::new(IP_ADDR);
h1.set_status(HostStatus::Down);
let mut h2 = Host::new(IP_ADDR);
h2.set_status(HostStatus::Filtered);
h1.merge(h2);
assert_eq!(h1.status(), HostStatus::Filtered);
}
#[test]
fn merge_tarpit_collision_test() {
let mut h1 = Host::new(IP_ADDR);
for i in 0..600 {
h1.add_port(Port::new(i, Protocol::Tcp, PortState::Open));
}
let mut h2 = Host::new(IP_ADDR);
for i in 500..1100 {
h2.add_port(Port::new(i, Protocol::Tcp, PortState::Open));
}
h1.merge(h2);
assert_eq!(h1.port_count(), MAX_PORTS_PER_HOST);
assert!(h1.network_roles.contains(&NetworkRole::Tarpit));
}
}