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, 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, 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}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq)]
299pub enum ExportFormat {
300 Json,
301 Csv,
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
308
309 fn make_process() -> ProcessInfo {
310 ProcessInfo {
311 pid: 1,
312 name: "test".into(),
313 path: None,
314 cmdline: None,
315 user: None,
316 parent_pid: None,
317 parent_name: None,
318 }
319 }
320
321 #[test]
322 fn local_port_returns_port_from_addr() {
323 let entry = PortEntry {
324 protocol: Protocol::Tcp,
325 local_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080),
326 remote_addr: None,
327 state: ConnectionState::Listen,
328 process: make_process(),
329 };
330 assert_eq!(entry.local_port(), 8080);
331 }
332
333 #[test]
334 fn sort_state_default_is_port_ascending() {
335 let s = SortState::default();
336 assert_eq!(s.column, SortColumn::Port);
337 assert!(s.ascending);
338 }
339
340 #[test]
341 fn sort_state_toggle_same_column_flips_direction() {
342 let mut s = SortState::default();
343 s.toggle(SortColumn::Port);
344 assert!(!s.ascending);
345 s.toggle(SortColumn::Port);
346 assert!(s.ascending);
347 }
348
349 #[test]
350 fn sort_state_toggle_different_column_resets_ascending() {
351 let mut s = SortState::default();
352 s.toggle(SortColumn::Port);
353 s.toggle(SortColumn::Pid);
354 assert_eq!(s.column, SortColumn::Pid);
355 assert!(s.ascending);
356 }
357
358 #[test]
359 fn protocol_display() {
360 assert_eq!(Protocol::Tcp.to_string(), "TCP");
361 assert_eq!(Protocol::Udp.to_string(), "UDP");
362 }
363
364 #[test]
365 fn connection_state_display() {
366 let cases = [
367 (ConnectionState::Listen, "LISTEN"),
368 (ConnectionState::Established, "ESTABLISHED"),
369 (ConnectionState::TimeWait, "TIME_WAIT"),
370 (ConnectionState::CloseWait, "CLOSE_WAIT"),
371 (ConnectionState::SynSent, "SYN_SENT"),
372 (ConnectionState::SynRecv, "SYN_RECV"),
373 (ConnectionState::FinWait1, "FIN_WAIT1"),
374 (ConnectionState::FinWait2, "FIN_WAIT2"),
375 (ConnectionState::Closing, "CLOSING"),
376 (ConnectionState::LastAck, "LAST_ACK"),
377 (ConnectionState::Closed, "CLOSED"),
378 (ConnectionState::Unknown, "UNKNOWN"),
379 ];
380 for (state, expected) in cases {
381 assert_eq!(state.to_string(), expected, "state {:?}", state);
382 }
383 }
384
385 #[test]
388 fn detail_tab_next_cycles_forward() {
389 let cases = [
390 (DetailTab::Tree, DetailTab::Interface),
391 (DetailTab::Interface, DetailTab::Connection),
392 (DetailTab::Connection, DetailTab::Tree),
393 ];
394 for (from, expected) in cases {
395 assert_eq!(from.next(), expected, "next of {:?}", from);
396 }
397 }
398
399 #[test]
400 fn detail_tab_prev_cycles_backward() {
401 let cases = [
402 (DetailTab::Tree, DetailTab::Connection),
403 (DetailTab::Interface, DetailTab::Tree),
404 (DetailTab::Connection, DetailTab::Interface),
405 ];
406 for (from, expected) in cases {
407 assert_eq!(from.prev(), expected, "prev of {:?}", from);
408 }
409 }
410
411 #[test]
412 fn detail_tab_next_prev_roundtrip() {
413 for tab in DetailTab::ALL {
414 let tab = *tab;
415 assert_eq!(tab.next().prev(), tab, "roundtrip {:?}", tab);
416 assert_eq!(tab.prev().next(), tab, "reverse roundtrip {:?}", tab);
417 }
418 }
419
420 #[test]
421 fn detail_tab_all_contains_every_variant() {
422 let variant_count = {
423 let mut n = 0u8;
424 for tab in DetailTab::ALL {
425 match tab {
426 DetailTab::Tree => n += 1,
427 DetailTab::Interface => n += 1,
428 DetailTab::Connection => n += 1,
429 }
430 }
431 n as usize
432 };
433 assert_eq!(
434 DetailTab::ALL.len(),
435 variant_count,
436 "ALL must list every DetailTab variant exactly once"
437 );
438 }
439
440 #[test]
441 fn detail_tab_index_matches_position() {
442 for (i, &tab) in DetailTab::ALL.iter().enumerate() {
443 assert_eq!(tab.index(), i, "index of {:?}", tab);
444 }
445 }
446
447 #[test]
448 fn detail_tab_key_label() {
449 assert_eq!(DetailTab::Tree.key_label(), "1");
450 assert_eq!(DetailTab::Interface.key_label(), "2");
451 assert_eq!(DetailTab::Connection.key_label(), "3");
452 }
453
454 #[test]
455 fn view_mode_default_is_table() {
456 assert_eq!(ViewMode::default(), ViewMode::Table);
457 }
458
459 #[test]
462 fn sort_state_toggle_all_columns() {
463 let columns = [
464 SortColumn::Port,
465 SortColumn::Service,
466 SortColumn::Protocol,
467 SortColumn::State,
468 SortColumn::Pid,
469 SortColumn::ProcessName,
470 SortColumn::User,
471 ];
472 for col in columns {
473 let mut s = SortState::default();
474 s.toggle(col);
475 if col == SortColumn::Port {
476 assert!(!s.ascending, "toggling same column should flip");
477 } else {
478 assert_eq!(s.column, col);
479 assert!(s.ascending, "switching to {:?} should be ascending", col);
480 }
481 }
482 }
483}