use serde::Serialize;
use std::fmt;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::time::{Duration, Instant};
pub const TICK_RATE: Duration = Duration::from_secs(2);
pub const GONE_RETENTION: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
pub enum Protocol {
Tcp,
Udp,
}
impl fmt::Display for Protocol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Protocol::Tcp => write!(f, "TCP"),
Protocol::Udp => write!(f, "UDP"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
pub enum ConnectionState {
Listen,
Established,
TimeWait,
CloseWait,
SynSent,
SynRecv,
FinWait1,
FinWait2,
Closing,
LastAck,
Closed,
Unknown,
}
impl fmt::Display for ConnectionState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
ConnectionState::Listen => "LISTEN",
ConnectionState::Established => "ESTABLISHED",
ConnectionState::TimeWait => "TIME_WAIT",
ConnectionState::CloseWait => "CLOSE_WAIT",
ConnectionState::SynSent => "SYN_SENT",
ConnectionState::SynRecv => "SYN_RECV",
ConnectionState::FinWait1 => "FIN_WAIT1",
ConnectionState::FinWait2 => "FIN_WAIT2",
ConnectionState::Closing => "CLOSING",
ConnectionState::LastAck => "LAST_ACK",
ConnectionState::Closed => "CLOSED",
ConnectionState::Unknown => "UNKNOWN",
};
write!(f, "{s}")
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ProcessInfo {
pub pid: u32,
pub name: String,
pub path: Option<PathBuf>,
pub cmdline: Option<String>,
pub user: Option<String>,
pub parent_pid: Option<u32>,
pub parent_name: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct PortEntry {
pub protocol: Protocol,
pub local_addr: SocketAddr,
pub remote_addr: Option<SocketAddr>,
pub state: ConnectionState,
pub process: ProcessInfo,
}
impl PortEntry {
pub fn local_port(&self) -> u16 {
self.local_addr.port()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntryStatus {
Unchanged,
New,
Gone,
}
#[derive(Debug, Clone)]
pub struct TrackedEntry {
pub entry: PortEntry,
pub status: EntryStatus,
pub seen_at: Instant,
pub first_seen: Option<Instant>,
pub suspicious: Vec<SuspiciousReason>,
pub container_name: Option<String>,
pub service_name: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SuspiciousReason {
NonRootPrivileged,
ScriptOnSensitive,
RootHighPortOutgoing,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SortColumn {
Port,
Service,
Protocol,
State,
Pid,
ProcessName,
User,
}
#[derive(Debug, Clone, Copy)]
pub struct SortState {
pub column: SortColumn,
pub ascending: bool,
}
impl Default for SortState {
fn default() -> Self {
Self {
column: SortColumn::Port,
ascending: true,
}
}
}
impl SortState {
pub fn toggle(&mut self, col: SortColumn) {
if self.column == col {
self.ascending = !self.ascending;
} else {
self.column = col;
self.ascending = true;
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DetailTab {
Tree,
Interface,
Connection,
}
impl DetailTab {
pub const ALL: &[DetailTab] = &[DetailTab::Tree, DetailTab::Interface, DetailTab::Connection];
pub fn index(self) -> usize {
Self::ALL
.iter()
.position(|&t| t == self)
.expect("all DetailTab variants must be listed in ALL")
}
pub fn next(self) -> Self {
Self::ALL[(self.index() + 1) % Self::ALL.len()]
}
pub fn prev(self) -> Self {
Self::ALL[(self.index() + Self::ALL.len() - 1) % Self::ALL.len()]
}
pub fn key_label(self) -> String {
(self.index() + 1).to_string()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ViewMode {
#[default]
Table,
Chart,
Topology,
ProcessDetail,
Namespaces,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExportFormat {
Json,
Csv,
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
fn make_process() -> ProcessInfo {
ProcessInfo {
pid: 1,
name: "test".into(),
path: None,
cmdline: None,
user: None,
parent_pid: None,
parent_name: None,
}
}
#[test]
fn local_port_returns_port_from_addr() {
let entry = PortEntry {
protocol: Protocol::Tcp,
local_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080),
remote_addr: None,
state: ConnectionState::Listen,
process: make_process(),
};
assert_eq!(entry.local_port(), 8080);
}
#[test]
fn sort_state_default_is_port_ascending() {
let s = SortState::default();
assert_eq!(s.column, SortColumn::Port);
assert!(s.ascending);
}
#[test]
fn sort_state_toggle_same_column_flips_direction() {
let mut s = SortState::default();
s.toggle(SortColumn::Port);
assert!(!s.ascending);
s.toggle(SortColumn::Port);
assert!(s.ascending);
}
#[test]
fn sort_state_toggle_different_column_resets_ascending() {
let mut s = SortState::default();
s.toggle(SortColumn::Port);
s.toggle(SortColumn::Pid);
assert_eq!(s.column, SortColumn::Pid);
assert!(s.ascending);
}
#[test]
fn protocol_display() {
assert_eq!(Protocol::Tcp.to_string(), "TCP");
assert_eq!(Protocol::Udp.to_string(), "UDP");
}
#[test]
fn connection_state_display() {
let cases = [
(ConnectionState::Listen, "LISTEN"),
(ConnectionState::Established, "ESTABLISHED"),
(ConnectionState::TimeWait, "TIME_WAIT"),
(ConnectionState::CloseWait, "CLOSE_WAIT"),
(ConnectionState::SynSent, "SYN_SENT"),
(ConnectionState::SynRecv, "SYN_RECV"),
(ConnectionState::FinWait1, "FIN_WAIT1"),
(ConnectionState::FinWait2, "FIN_WAIT2"),
(ConnectionState::Closing, "CLOSING"),
(ConnectionState::LastAck, "LAST_ACK"),
(ConnectionState::Closed, "CLOSED"),
(ConnectionState::Unknown, "UNKNOWN"),
];
for (state, expected) in cases {
assert_eq!(state.to_string(), expected, "state {:?}", state);
}
}
#[test]
fn detail_tab_next_cycles_forward() {
let cases = [
(DetailTab::Tree, DetailTab::Interface),
(DetailTab::Interface, DetailTab::Connection),
(DetailTab::Connection, DetailTab::Tree),
];
for (from, expected) in cases {
assert_eq!(from.next(), expected, "next of {:?}", from);
}
}
#[test]
fn detail_tab_prev_cycles_backward() {
let cases = [
(DetailTab::Tree, DetailTab::Connection),
(DetailTab::Interface, DetailTab::Tree),
(DetailTab::Connection, DetailTab::Interface),
];
for (from, expected) in cases {
assert_eq!(from.prev(), expected, "prev of {:?}", from);
}
}
#[test]
fn detail_tab_next_prev_roundtrip() {
for tab in DetailTab::ALL {
let tab = *tab;
assert_eq!(tab.next().prev(), tab, "roundtrip {:?}", tab);
assert_eq!(tab.prev().next(), tab, "reverse roundtrip {:?}", tab);
}
}
#[test]
fn detail_tab_all_contains_every_variant() {
let variant_count = {
let mut n = 0u8;
for tab in DetailTab::ALL {
match tab {
DetailTab::Tree => n += 1,
DetailTab::Interface => n += 1,
DetailTab::Connection => n += 1,
}
}
n as usize
};
assert_eq!(
DetailTab::ALL.len(),
variant_count,
"ALL must list every DetailTab variant exactly once"
);
}
#[test]
fn detail_tab_index_matches_position() {
for (i, &tab) in DetailTab::ALL.iter().enumerate() {
assert_eq!(tab.index(), i, "index of {:?}", tab);
}
}
#[test]
fn detail_tab_key_label() {
assert_eq!(DetailTab::Tree.key_label(), "1");
assert_eq!(DetailTab::Interface.key_label(), "2");
assert_eq!(DetailTab::Connection.key_label(), "3");
}
#[test]
fn view_mode_default_is_table() {
assert_eq!(ViewMode::default(), ViewMode::Table);
}
#[test]
fn sort_state_toggle_all_columns() {
let columns = [
SortColumn::Port,
SortColumn::Service,
SortColumn::Protocol,
SortColumn::State,
SortColumn::Pid,
SortColumn::ProcessName,
SortColumn::User,
];
for col in columns {
let mut s = SortState::default();
s.toggle(col);
if col == SortColumn::Port {
assert!(!s.ascending, "toggling same column should flip");
} else {
assert_eq!(s.column, col);
assert!(s.ascending, "switching to {:?} should be ascending", col);
}
}
}
}