1use serde::Serialize;
12use std::fmt;
13use std::net::SocketAddr;
14use std::path::PathBuf;
15use std::time::{Duration, Instant};
16
17pub const TICK_RATE: Duration = Duration::from_secs(2);
20
21pub const GONE_RETENTION: Duration = Duration::from_secs(5);
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
27pub enum Protocol {
28 Tcp,
29 Udp,
30}
31
32impl fmt::Display for Protocol {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 match self {
35 Protocol::Tcp => write!(f, "TCP"),
36 Protocol::Udp => write!(f, "UDP"),
37 }
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
46pub enum ConnectionState {
47 Listen,
48 Established,
49 TimeWait,
50 CloseWait,
51 SynSent,
52 SynRecv,
53 FinWait1,
54 FinWait2,
55 Closing,
56 LastAck,
57 Closed,
58 Unknown,
59}
60
61impl fmt::Display for ConnectionState {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 let s = match self {
64 ConnectionState::Listen => "LISTEN",
65 ConnectionState::Established => "ESTABLISHED",
66 ConnectionState::TimeWait => "TIME_WAIT",
67 ConnectionState::CloseWait => "CLOSE_WAIT",
68 ConnectionState::SynSent => "SYN_SENT",
69 ConnectionState::SynRecv => "SYN_RECV",
70 ConnectionState::FinWait1 => "FIN_WAIT1",
71 ConnectionState::FinWait2 => "FIN_WAIT2",
72 ConnectionState::Closing => "CLOSING",
73 ConnectionState::LastAck => "LAST_ACK",
74 ConnectionState::Closed => "CLOSED",
75 ConnectionState::Unknown => "UNKNOWN",
76 };
77 write!(f, "{s}")
78 }
79}
80
81#[derive(Debug, Clone, Serialize)]
87pub struct ProcessInfo {
88 pub pid: u32,
90 pub name: String,
92 pub path: Option<PathBuf>,
94 pub cmdline: Option<String>,
96 pub user: Option<String>,
98 pub parent_pid: Option<u32>,
100 pub parent_name: Option<String>,
102}
103
104#[derive(Debug, Clone, Serialize)]
109pub struct PortEntry {
110 pub protocol: Protocol,
112 pub local_addr: SocketAddr,
114 pub remote_addr: Option<SocketAddr>,
116 pub state: ConnectionState,
118 pub process: ProcessInfo,
120}
121
122impl PortEntry {
123 pub fn local_port(&self) -> u16 {
125 self.local_addr.port()
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum EntryStatus {
134 Unchanged,
136 New,
138 Gone,
141}
142
143#[derive(Debug, Clone)]
154pub struct TrackedEntry {
155 pub entry: PortEntry,
157 pub status: EntryStatus,
159 pub seen_at: Instant,
161
162 pub first_seen: Option<Instant>,
166 pub suspicious: Vec<SuspiciousReason>,
168 pub container_name: Option<String>,
170 pub service_name: Option<String>,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
176pub enum SuspiciousReason {
177 NonRootPrivileged,
179 ScriptOnSensitive,
181 RootHighPortOutgoing,
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187pub enum SortColumn {
188 Port,
189 Service,
190 Protocol,
191 State,
192 Pid,
193 ProcessName,
194 User,
195}
196
197#[derive(Debug, Clone, Copy)]
202pub struct SortState {
203 pub column: SortColumn,
205 pub ascending: bool,
207}
208
209impl Default for SortState {
210 fn default() -> Self {
211 Self {
212 column: SortColumn::Port,
213 ascending: true,
214 }
215 }
216}
217
218impl SortState {
219 pub fn toggle(&mut self, col: SortColumn) {
222 if self.column == col {
223 self.ascending = !self.ascending;
224 } else {
225 self.column = col;
226 self.ascending = true;
227 }
228 }
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum DetailTab {
237 Tree,
239 Interface,
241 Connection,
243}
244
245impl DetailTab {
246 pub const ALL: &[DetailTab] = &[DetailTab::Tree, DetailTab::Interface, DetailTab::Connection];
248
249 pub fn index(self) -> usize {
251 Self::ALL
252 .iter()
253 .position(|&t| t == self)
254 .expect("all DetailTab variants must be listed in ALL")
255 }
256
257 pub fn next(self) -> Self {
259 Self::ALL[(self.index() + 1) % Self::ALL.len()]
260 }
261
262 pub fn prev(self) -> Self {
264 Self::ALL[(self.index() + Self::ALL.len() - 1) % Self::ALL.len()]
265 }
266
267 pub fn key_label(self) -> String {
269 (self.index() + 1).to_string()
270 }
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
279pub enum ViewMode {
280 #[default]
282 Table,
283 Chart,
285 Topology,
287 ProcessDetail,
289 Namespaces,
291 SshHosts,
293 Tunnels,
295}
296
297#[derive(Debug, Clone, Copy, PartialEq, Eq)]
303pub enum ExportFormat {
304 Json,
305 Csv,
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
312
313 fn make_process() -> ProcessInfo {
314 ProcessInfo {
315 pid: 1,
316 name: "test".into(),
317 path: None,
318 cmdline: None,
319 user: None,
320 parent_pid: None,
321 parent_name: None,
322 }
323 }
324
325 #[test]
326 fn local_port_returns_port_from_addr() {
327 let entry = PortEntry {
328 protocol: Protocol::Tcp,
329 local_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080),
330 remote_addr: None,
331 state: ConnectionState::Listen,
332 process: make_process(),
333 };
334 assert_eq!(entry.local_port(), 8080);
335 }
336
337 #[test]
338 fn sort_state_default_is_port_ascending() {
339 let s = SortState::default();
340 assert_eq!(s.column, SortColumn::Port);
341 assert!(s.ascending);
342 }
343
344 #[test]
345 fn sort_state_toggle_same_column_flips_direction() {
346 let mut s = SortState::default();
347 s.toggle(SortColumn::Port);
348 assert!(!s.ascending);
349 s.toggle(SortColumn::Port);
350 assert!(s.ascending);
351 }
352
353 #[test]
354 fn sort_state_toggle_different_column_resets_ascending() {
355 let mut s = SortState::default();
356 s.toggle(SortColumn::Port);
357 s.toggle(SortColumn::Pid);
358 assert_eq!(s.column, SortColumn::Pid);
359 assert!(s.ascending);
360 }
361
362 #[test]
363 fn protocol_display() {
364 assert_eq!(Protocol::Tcp.to_string(), "TCP");
365 assert_eq!(Protocol::Udp.to_string(), "UDP");
366 }
367
368 #[test]
369 fn connection_state_display() {
370 let cases = [
371 (ConnectionState::Listen, "LISTEN"),
372 (ConnectionState::Established, "ESTABLISHED"),
373 (ConnectionState::TimeWait, "TIME_WAIT"),
374 (ConnectionState::CloseWait, "CLOSE_WAIT"),
375 (ConnectionState::SynSent, "SYN_SENT"),
376 (ConnectionState::SynRecv, "SYN_RECV"),
377 (ConnectionState::FinWait1, "FIN_WAIT1"),
378 (ConnectionState::FinWait2, "FIN_WAIT2"),
379 (ConnectionState::Closing, "CLOSING"),
380 (ConnectionState::LastAck, "LAST_ACK"),
381 (ConnectionState::Closed, "CLOSED"),
382 (ConnectionState::Unknown, "UNKNOWN"),
383 ];
384 for (state, expected) in cases {
385 assert_eq!(state.to_string(), expected, "state {:?}", state);
386 }
387 }
388
389 #[test]
392 fn detail_tab_next_cycles_forward() {
393 let cases = [
394 (DetailTab::Tree, DetailTab::Interface),
395 (DetailTab::Interface, DetailTab::Connection),
396 (DetailTab::Connection, DetailTab::Tree),
397 ];
398 for (from, expected) in cases {
399 assert_eq!(from.next(), expected, "next of {:?}", from);
400 }
401 }
402
403 #[test]
404 fn detail_tab_prev_cycles_backward() {
405 let cases = [
406 (DetailTab::Tree, DetailTab::Connection),
407 (DetailTab::Interface, DetailTab::Tree),
408 (DetailTab::Connection, DetailTab::Interface),
409 ];
410 for (from, expected) in cases {
411 assert_eq!(from.prev(), expected, "prev of {:?}", from);
412 }
413 }
414
415 #[test]
416 fn detail_tab_next_prev_roundtrip() {
417 for tab in DetailTab::ALL {
418 let tab = *tab;
419 assert_eq!(tab.next().prev(), tab, "roundtrip {:?}", tab);
420 assert_eq!(tab.prev().next(), tab, "reverse roundtrip {:?}", tab);
421 }
422 }
423
424 #[test]
425 fn detail_tab_all_contains_every_variant() {
426 let variant_count = {
427 let mut n = 0u8;
428 for tab in DetailTab::ALL {
429 match tab {
430 DetailTab::Tree => n += 1,
431 DetailTab::Interface => n += 1,
432 DetailTab::Connection => n += 1,
433 }
434 }
435 n as usize
436 };
437 assert_eq!(
438 DetailTab::ALL.len(),
439 variant_count,
440 "ALL must list every DetailTab variant exactly once"
441 );
442 }
443
444 #[test]
445 fn detail_tab_index_matches_position() {
446 for (i, &tab) in DetailTab::ALL.iter().enumerate() {
447 assert_eq!(tab.index(), i, "index of {:?}", tab);
448 }
449 }
450
451 #[test]
452 fn detail_tab_key_label() {
453 assert_eq!(DetailTab::Tree.key_label(), "1");
454 assert_eq!(DetailTab::Interface.key_label(), "2");
455 assert_eq!(DetailTab::Connection.key_label(), "3");
456 }
457
458 #[test]
459 fn view_mode_default_is_table() {
460 assert_eq!(ViewMode::default(), ViewMode::Table);
461 }
462
463 #[test]
466 fn sort_state_toggle_all_columns() {
467 let columns = [
468 SortColumn::Port,
469 SortColumn::Service,
470 SortColumn::Protocol,
471 SortColumn::State,
472 SortColumn::Pid,
473 SortColumn::ProcessName,
474 SortColumn::User,
475 ];
476 for col in columns {
477 let mut s = SortState::default();
478 s.toggle(col);
479 if col == SortColumn::Port {
480 assert!(!s.ascending, "toggling same column should flip");
481 } else {
482 assert_eq!(s.column, col);
483 assert!(s.ascending, "switching to {:?} should be ascending", col);
484 }
485 }
486 }
487}