use std::collections::HashMap;
use std::net::IpAddr;
use chrono::NaiveTime;
use serde::{Serialize, Deserialize};
pub type DnsCache = HashMap<IpAddr, Option<String>>;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum ConnProto {
Tcp,
Udp,
}
impl ConnProto {
pub fn label(&self) -> &str {
match self {
Self::Tcp => "TCP",
Self::Udp => "UDP",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum TcpState {
Closed,
Listen,
SynSent,
SynReceived,
Established,
FinWait1,
FinWait2,
CloseWait,
Closing,
LastAck,
TimeWait,
DeleteTcb,
Unknown(u32),
}
impl TcpState {
pub fn from_raw(v: u32) -> Self {
match v {
1 => Self::Closed,
2 => Self::Listen,
3 => Self::SynSent,
4 => Self::SynReceived,
5 => Self::Established,
6 => Self::FinWait1,
7 => Self::FinWait2,
8 => Self::CloseWait,
9 => Self::Closing,
10 => Self::LastAck,
11 => Self::TimeWait,
12 => Self::DeleteTcb,
_ => Self::Unknown(v),
}
}
pub fn label(&self) -> &str {
match self {
Self::Closed => "CLOSED",
Self::Listen => "LISTEN",
Self::SynSent => "SYN_SENT",
Self::SynReceived => "SYN_RECV",
Self::Established => "ESTABLISHED",
Self::FinWait1 => "FIN_WAIT1",
Self::FinWait2 => "FIN_WAIT2",
Self::CloseWait => "CLOSE_WAIT",
Self::Closing => "CLOSING",
Self::LastAck => "LAST_ACK",
Self::TimeWait => "TIME_WAIT",
Self::DeleteTcb => "DELETE_TCB",
Self::Unknown(_) => "UNKNOWN",
}
}
pub fn color(&self) -> ratatui::style::Color {
use ratatui::style::Color;
match self {
Self::Established => Color::Green,
Self::Listen => Color::Cyan,
Self::SynSent | Self::SynReceived => Color::Yellow,
Self::TimeWait | Self::FinWait1 | Self::FinWait2 => Color::Magenta,
Self::CloseWait | Self::Closing | Self::LastAck => Color::LightRed,
Self::Closed | Self::DeleteTcb => Color::DarkGray,
Self::Unknown(_) => Color::Gray,
}
}
}
#[derive(Clone, Debug)]
pub struct Connection {
pub proto: ConnProto,
pub local_addr: IpAddr,
pub local_port: u16,
pub remote_addr: Option<IpAddr>,
pub remote_port: Option<u16>,
pub state: Option<TcpState>,
pub pid: u32,
pub process_name: String,
pub dns_hostname: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ConnKey {
pub proto: ConnProto,
pub local_addr: IpAddr,
pub local_port: u16,
pub remote_addr: Option<IpAddr>,
pub remote_port: Option<u16>,
}
impl Connection {
pub fn key(&self) -> ConnKey {
ConnKey {
proto: self.proto.clone(),
local_addr: self.local_addr,
local_port: self.local_port,
remote_addr: self.remote_addr,
remote_port: self.remote_port,
}
}
pub fn is_outbound(&self) -> bool {
if let Some(rp) = self.remote_port {
matches!(rp, 80 | 443 | 22 | 21 | 25 | 53 | 110 | 143 | 993 | 995
| 587 | 465 | 8080 | 8443 | 3306 | 5432 | 6379 | 27017)
|| (self.local_port > 1024 && rp <= 1024)
|| (self.local_port > 49152)
} else {
false
}
}
}
pub struct SpeedHistory {
pub download: std::collections::VecDeque<f64>,
pub upload: std::collections::VecDeque<f64>,
pub max_points: usize,
}
impl SpeedHistory {
pub fn new(max_points: usize) -> Self {
Self {
download: std::collections::VecDeque::with_capacity(max_points),
upload: std::collections::VecDeque::with_capacity(max_points),
max_points,
}
}
pub fn push(&mut self, down: f64, up: f64) {
self.download.push_back(down);
self.upload.push_back(up);
if self.download.len() > self.max_points {
self.download.pop_front();
}
if self.upload.len() > self.max_points {
self.upload.pop_front();
}
}
}
#[derive(Clone, Debug)]
pub enum TrafficEventKind {
NewConnection,
ConnectionClosed,
StateChange,
DataActivity,
}
#[derive(Clone, Debug)]
pub struct TrafficEntry {
pub event: TrafficEventKind,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BottomTab {
Dashboard,
Connections,
Servers,
Packets,
Topology,
Alerts,
Firewall,
Devices,
Networks,
}
impl BottomTab {
pub fn next(&self) -> Self {
match self {
Self::Dashboard => Self::Connections,
Self::Connections => Self::Servers,
Self::Servers => Self::Packets,
Self::Packets => Self::Topology,
Self::Topology => Self::Alerts,
Self::Alerts => Self::Firewall,
Self::Firewall => Self::Devices,
Self::Devices => Self::Networks,
Self::Networks => Self::Dashboard,
}
}
pub fn prev(&self) -> Self {
match self {
Self::Dashboard => Self::Networks,
Self::Connections => Self::Dashboard,
Self::Servers => Self::Connections,
Self::Packets => Self::Servers,
Self::Topology => Self::Packets,
Self::Alerts => Self::Topology,
Self::Firewall => Self::Alerts,
Self::Devices => Self::Firewall,
Self::Networks => Self::Devices,
}
}
pub fn label(&self) -> &str {
match self {
Self::Dashboard => "Dashboard",
Self::Connections => "Connections",
Self::Servers => "Servers",
Self::Packets => "Packets",
Self::Topology => "Topology",
Self::Alerts => "Alerts",
Self::Firewall => "Firewall",
Self::Devices => "Devices",
Self::Networks => "Networks",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DashboardTimeRange {
Minutes5,
Minutes15,
Hour1,
Hours24,
}
impl DashboardTimeRange {
pub fn label(&self) -> &str {
match self {
Self::Minutes5 => "5m",
Self::Minutes15 => "15m",
Self::Hour1 => "1h",
Self::Hours24 => "24h",
}
}
pub fn samples(&self) -> usize {
match self {
Self::Minutes5 => 300,
Self::Minutes15 => 900,
Self::Hour1 => 3600,
Self::Hours24 => 86400,
}
}
}
pub struct TrafficHistory {
pub samples: std::collections::VecDeque<(f64, f64)>,
pub max_samples: usize,
}
impl TrafficHistory {
pub fn new(max: usize) -> Self {
Self {
samples: std::collections::VecDeque::with_capacity(max.min(8192)),
max_samples: max,
}
}
pub fn push(&mut self, down_bps: f64, up_bps: f64) {
self.samples.push_back((down_bps, up_bps));
while self.samples.len() > self.max_samples {
self.samples.pop_front();
}
}
}
pub type PidCache = HashMap<u32, String>;
#[derive(Clone, Debug)]
#[allow(dead_code)]
pub struct PacketSnippet {
pub timestamp: NaiveTime,
pub direction: PacketDirection,
pub src_ip: IpAddr,
pub dst_ip: IpAddr,
pub src_port: u16,
pub dst_port: u16,
pub protocol: ConnProto,
pub snippet: String,
pub payload_size: usize,
pub ttl: u8,
pub ip_total_len: u16,
pub ip_id: u16,
pub tcp_flags: u8,
pub tcp_seq: u32,
pub tcp_ack_num: u32,
pub tcp_window: u16,
pub raw_payload: Vec<u8>,
}
impl PacketSnippet {
pub fn tcp_flags_str(&self) -> String {
if self.protocol != ConnProto::Tcp {
return String::new();
}
let mut flags = Vec::new();
if self.tcp_flags & 0x02 != 0 { flags.push("SYN"); }
if self.tcp_flags & 0x10 != 0 { flags.push("ACK"); }
if self.tcp_flags & 0x01 != 0 { flags.push("FIN"); }
if self.tcp_flags & 0x04 != 0 { flags.push("RST"); }
if self.tcp_flags & 0x08 != 0 { flags.push("PSH"); }
if self.tcp_flags & 0x20 != 0 { flags.push("URG"); }
if flags.is_empty() {
String::new()
} else {
format!("[{}]", flags.join(","))
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum PacketDirection {
Inbound,
Outbound,
}
#[derive(Clone, Debug)]
pub enum AlertSeverity {
Info,
Warning,
Critical,
}
impl AlertSeverity {
pub fn label(&self) -> &str {
match self {
Self::Info => "INFO",
Self::Warning => "WARN",
Self::Critical => "CRIT",
}
}
pub fn color(&self) -> ratatui::style::Color {
use ratatui::style::Color;
match self {
Self::Info => Color::Rgb(80, 180, 255),
Self::Warning => Color::Rgb(255, 200, 60),
Self::Critical => Color::Rgb(255, 80, 80),
}
}
}
#[derive(Clone, Debug)]
pub enum AlertKind {
NewAppFirstConnection { process_name: String, remote: String },
DnsServerChanged { old_servers: Vec<IpAddr>, new_servers: Vec<IpAddr> },
SuspiciousHost { process_name: String, ip: IpAddr, reason: String },
RdpConnection { remote_addr: IpAddr, inbound: bool },
BandwidthSpike { direction: String, speed_bps: f64, threshold_bps: f64 },
NewDevice { ip: IpAddr, mac: String, hostname: Option<String> },
DeviceLeft { ip: IpAddr, mac: String },
ArpAnomaly { ip: IpAddr, expected_mac: String, actual_mac: String },
BandwidthOverage { used_bytes: u64, limit_bytes: u64 },
AppChanged { process_name: String, detail: String },
TrafficAnomaly { process_name: String, current_bytes: u64, baseline_bytes: u64 },
HostsFileChanged { detail: String },
ProxyChanged { detail: String },
EvilTwinDetected { detail: String },
InternetLost { detail: String },
InternetRestored,
}
impl AlertKind {
pub fn label(&self) -> &str {
match self {
Self::NewAppFirstConnection { .. } => "New App",
Self::DnsServerChanged { .. } => "DNS Changed",
Self::SuspiciousHost { .. } => "Suspicious Host",
Self::RdpConnection { .. } => "RDP Detected",
Self::BandwidthSpike { .. } => "Bandwidth Spike",
Self::NewDevice { .. } => "New Device",
Self::DeviceLeft { .. } => "Device Left",
Self::ArpAnomaly { .. } => "ARP Anomaly",
Self::BandwidthOverage { .. } => "Data Overage",
Self::AppChanged { .. } => "App Changed",
Self::TrafficAnomaly { .. } => "Anomaly",
Self::HostsFileChanged { .. } => "Hosts Changed",
Self::ProxyChanged { .. } => "Proxy Changed",
Self::EvilTwinDetected { .. } => "Evil Twin",
Self::InternetLost { .. } => "No Internet",
Self::InternetRestored => "Internet OK",
}
}
pub fn severity(&self) -> AlertSeverity {
match self {
Self::NewAppFirstConnection { .. } => AlertSeverity::Info,
Self::DnsServerChanged { .. } => AlertSeverity::Warning,
Self::SuspiciousHost { .. } => AlertSeverity::Critical,
Self::RdpConnection { .. } => AlertSeverity::Warning,
Self::BandwidthSpike { .. } => AlertSeverity::Info,
Self::NewDevice { .. } => AlertSeverity::Info,
Self::DeviceLeft { .. } => AlertSeverity::Info,
Self::ArpAnomaly { .. } => AlertSeverity::Critical,
Self::BandwidthOverage { .. } => AlertSeverity::Warning,
Self::AppChanged { .. } => AlertSeverity::Warning,
Self::TrafficAnomaly { .. } => AlertSeverity::Warning,
Self::HostsFileChanged { .. } => AlertSeverity::Critical,
Self::ProxyChanged { .. } => AlertSeverity::Warning,
Self::EvilTwinDetected { .. } => AlertSeverity::Critical,
Self::InternetLost { .. } => AlertSeverity::Critical,
Self::InternetRestored => AlertSeverity::Info,
}
}
pub fn description(&self) -> String {
match self {
Self::NewAppFirstConnection { process_name, remote } => {
format!("{} connected to {} for the first time", process_name, remote)
}
Self::DnsServerChanged { old_servers, new_servers } => {
format!("DNS servers changed: {:?} → {:?}", old_servers, new_servers)
}
Self::SuspiciousHost { process_name, ip, reason } => {
format!("{} connected to suspicious host {} ({})", process_name, ip, reason)
}
Self::RdpConnection { remote_addr, inbound } => {
let dir = if *inbound { "Inbound" } else { "Outbound" };
format!("{} RDP connection from {}", dir, remote_addr)
}
Self::BandwidthSpike { direction, speed_bps, .. } => {
format!("{} spike: {}/s", direction, crate::utils::format_bytes(*speed_bps as u64))
}
Self::NewDevice { ip, mac, hostname } => {
let name = hostname.as_deref().unwrap_or("unknown");
format!("New device: {} ({}) - {}", ip, mac, name)
}
Self::DeviceLeft { ip, mac } => {
format!("Device left: {} ({})", ip, mac)
}
Self::ArpAnomaly { ip, expected_mac, actual_mac } => {
format!("ARP anomaly: {} changed MAC {} → {}", ip, expected_mac, actual_mac)
}
Self::BandwidthOverage { used_bytes, limit_bytes } => {
format!("Data plan exceeded: {} / {} used",
crate::utils::format_bytes(*used_bytes),
crate::utils::format_bytes(*limit_bytes))
}
Self::AppChanged { process_name, detail } => {
format!("{}: {}", process_name, detail)
}
Self::TrafficAnomaly { process_name, current_bytes, baseline_bytes } => {
format!("{}: traffic {}x above baseline ({} vs {})",
process_name,
if *baseline_bytes > 0 { current_bytes / baseline_bytes } else { 0 },
crate::utils::format_bytes(*current_bytes),
crate::utils::format_bytes(*baseline_bytes))
}
Self::HostsFileChanged { detail } => {
format!("Hosts file modified: {}", detail)
}
Self::ProxyChanged { detail } => {
format!("Proxy settings changed: {}", detail)
}
Self::EvilTwinDetected { detail } => {
format!("Evil twin WiFi detected: {}", detail)
}
Self::InternetLost { detail } => {
format!("Internet connectivity lost: {}", detail)
}
Self::InternetRestored => {
"Internet connectivity restored".to_string()
}
}
}
pub fn category(&self) -> AlertCategory {
match self {
Self::SuspiciousHost { .. }
| Self::ArpAnomaly { .. }
| Self::EvilTwinDetected { .. }
| Self::RdpConnection { .. } => AlertCategory::Security,
Self::NewAppFirstConnection { .. }
| Self::AppChanged { .. }
=> AlertCategory::NetworkAccess,
Self::DnsServerChanged { .. }
| Self::HostsFileChanged { .. }
| Self::ProxyChanged { .. } => AlertCategory::SystemChanges,
Self::NewDevice { .. }
| Self::DeviceLeft { .. } => AlertCategory::DeviceActivity,
Self::BandwidthSpike { .. }
| Self::BandwidthOverage { .. }
| Self::TrafficAnomaly { .. } => AlertCategory::Bandwidth,
Self::InternetLost { .. }
| Self::InternetRestored => AlertCategory::Connectivity,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum AlertCategory {
Security,
NetworkAccess,
SystemChanges,
DeviceActivity,
Bandwidth,
Connectivity,
}
impl AlertCategory {
pub fn label(&self) -> &'static str {
match self {
Self::Security => "\u{1f6e1} Security & Threats",
Self::NetworkAccess => "\u{1f310} Network Access",
Self::SystemChanges => "\u{2699} System Changes",
Self::DeviceActivity => "\u{1f4f1} Device Activity",
Self::Bandwidth => "\u{1f4ca} Bandwidth & Usage",
Self::Connectivity => "\u{1f50c} Connectivity",
}
}
pub fn color(&self) -> ratatui::style::Color {
use ratatui::style::Color;
match self {
Self::Security => Color::Rgb(255, 80, 80),
Self::NetworkAccess => Color::Rgb(100, 200, 255),
Self::SystemChanges => Color::Rgb(255, 200, 60),
Self::DeviceActivity => Color::Rgb(80, 220, 160),
Self::Bandwidth => Color::Rgb(180, 140, 255),
Self::Connectivity => Color::Rgb(255, 160, 60),
}
}
pub fn all() -> &'static [AlertCategory] {
&[
AlertCategory::Security,
AlertCategory::NetworkAccess,
AlertCategory::SystemChanges,
AlertCategory::DeviceActivity,
AlertCategory::Bandwidth,
AlertCategory::Connectivity,
]
}
}
#[derive(Clone, Debug)]
pub struct Alert {
pub timestamp: NaiveTime,
pub kind: AlertKind,
pub read: bool,
}
#[derive(Clone, Debug)]
pub struct AppBandwidth {
pub process_name: String,
pub download_bytes: u64,
pub upload_bytes: u64,
pub active_connections: usize,
pub last_seen: NaiveTime,
pub recent_down: std::collections::VecDeque<f64>,
pub recent_up: std::collections::VecDeque<f64>,
}
impl AppBandwidth {
pub fn new(name: String) -> Self {
Self {
process_name: name,
download_bytes: 0,
upload_bytes: 0,
active_connections: 0,
last_seen: chrono::Local::now().time(),
recent_down: std::collections::VecDeque::from(vec![0.0; 20]),
recent_up: std::collections::VecDeque::from(vec![0.0; 20]),
}
}
pub fn total_bytes(&self) -> u64 {
self.download_bytes + self.upload_bytes
}
pub fn smooth_down(&self) -> f64 {
let n = self.recent_down.len();
if n == 0 { return 0.0; }
let take = n.min(3);
let sum: f64 = self.recent_down.iter().rev().take(take).sum();
sum / take as f64
}
pub fn smooth_up(&self) -> f64 {
let n = self.recent_up.len();
if n == 0 { return 0.0; }
let take = n.min(3);
let sum: f64 = self.recent_up.iter().rev().take(take).sum();
sum / take as f64
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum NetworkCategory {
Vpn,
Docker,
Wsl,
HyperV,
Virtual,
Secondary,
Bluetooth,
MeshVpn,
Hotspot,
Tunnel,
}
#[derive(Clone, Debug)]
pub struct RemoteNetwork {
pub name: String,
pub category: NetworkCategory,
pub adapter_name: String,
pub local_ip: std::net::Ipv4Addr,
pub subnet_mask: std::net::Ipv4Addr,
pub subnet_cidr: String,
pub gateway: Option<std::net::Ipv4Addr>,
pub devices: Vec<LanDevice>,
}
#[derive(Clone, Debug)]
pub struct LanDevice {
pub ip: IpAddr,
pub mac: String,
pub hostname: Option<String>,
pub vendor: Option<String>,
pub first_seen: NaiveTime,
pub last_seen: NaiveTime,
pub is_online: bool,
pub custom_name: Option<String>,
pub discovery_info: String,
pub open_ports: String,
pub bytes_sent: u64,
pub bytes_received: u64,
pub tick_sent: u64,
pub tick_received: u64,
pub speed_sent: f64,
pub speed_received: f64,
}
#[derive(Clone, Debug)]
pub struct FirewallRule {
pub name: String,
pub enabled: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum FirewallMode {
Normal,
AskToConnect,
Lockdown,
}
impl FirewallMode {
pub fn label(&self) -> &str {
match self {
Self::Normal => "Normal",
Self::AskToConnect => "Ask to Connect",
Self::Lockdown => "Lockdown",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum FirewallAppAction {
Allow,
Deny,
Drop,
}
#[derive(Clone, Debug)]
pub struct FirewallAppDetail {
pub app_name: String,
pub app_path: Option<String>,
pub is_blocked: bool,
pub current_action: Option<FirewallAppAction>,
pub conn_count: usize,
pub download_bytes: u64,
pub upload_bytes: u64,
pub current_down_speed: f64,
pub current_up_speed: f64,
pub peak_down_speed: f64,
pub peak_up_speed: f64,
pub last_seen: String,
pub selected_action: usize, }
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DataPlan {
pub limit_bytes: u64,
pub reset_day: u8, pub alert_pct: u8, }
impl Default for DataPlan {
fn default() -> Self {
Self {
limit_bytes: 0, reset_day: 1,
alert_pct: 80,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UsageRecord {
pub date: String, pub download_bytes: u64,
pub upload_bytes: u64,
pub per_app: HashMap<String, (u64, u64)>, }
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UsageStore {
pub data_plan: DataPlan,
pub daily_records: Vec<UsageRecord>,
}
impl Default for UsageStore {
fn default() -> Self {
Self {
data_plan: DataPlan::default(),
daily_records: Vec::new(),
}
}
}
#[derive(Clone, Debug)]
pub struct ThreatInfo {
pub ip: IpAddr,
pub reason: String,
}
#[derive(Clone, Debug)]
pub enum DetailKind {
Connection(Connection),
Alert(Alert),
Device(LanDevice),
FirewallApp(FirewallAppDetail),
Server {
kind_label: String,
kind_icon: String,
category: String,
port: u16,
proto: String,
bind_addr: String,
pid: u32,
process_name: String,
exe_path: String,
cmdline: String,
product_name: String,
company_name: String,
version: String,
http_title: String,
banner: String,
response_headers: Vec<(String, String)>,
active_connections: u32,
first_seen: String,
is_responsive: bool,
tls_detected: bool,
category_color: (u8, u8, u8),
detected_techs: Vec<(String, String, String)>, },
}