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, Default)]
237pub enum ViewMode {
238 #[default]
240 Connections,
241 Processes,
243 Ssh,
245}
246
247impl ViewMode {
248 pub const ALL: &[ViewMode] = &[ViewMode::Connections, ViewMode::Processes, ViewMode::Ssh];
249
250 fn index(self) -> usize {
251 Self::ALL
252 .iter()
253 .position(|&m| m == self)
254 .expect("all ViewMode variants must be listed in ALL")
255 }
256
257 pub fn next(self) -> Self {
258 Self::ALL[(self.index() + 1) % Self::ALL.len()]
259 }
260
261 pub fn prev(self) -> Self {
262 Self::ALL[(self.index() + Self::ALL.len() - 1) % Self::ALL.len()]
263 }
264}
265
266#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
268pub enum ProcessesTab {
269 #[default]
270 Detail,
271 Topology,
272}
273
274impl ProcessesTab {
275 pub const ALL: &[ProcessesTab] = &[ProcessesTab::Detail, ProcessesTab::Topology];
276
277 pub fn next(self) -> Self {
278 match self {
279 ProcessesTab::Detail => ProcessesTab::Topology,
280 ProcessesTab::Topology => ProcessesTab::Detail,
281 }
282 }
283
284 pub fn prev(self) -> Self {
285 self.next()
286 }
287}
288
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
294pub enum ActionItem {
295 Kill,
296 Copy,
297 CopyPid,
298 BlockIp,
299 Trace,
300 Forward,
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
305pub enum SshTab {
306 #[default]
307 Hosts,
308 Tunnels,
309}
310
311impl SshTab {
312 pub const ALL: &[SshTab] = &[SshTab::Hosts, SshTab::Tunnels];
313
314 pub fn next(self) -> Self {
315 match self {
316 SshTab::Hosts => SshTab::Tunnels,
317 SshTab::Tunnels => SshTab::Hosts,
318 }
319 }
320
321 pub fn prev(self) -> Self {
322 self.next()
323 }
324}
325
326#[derive(Debug, Clone, Copy, PartialEq, Eq)]
332pub enum ExportFormat {
333 Json,
334 Csv,
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
341
342 fn make_process() -> ProcessInfo {
343 ProcessInfo {
344 pid: 1,
345 name: "test".into(),
346 path: None,
347 cmdline: None,
348 user: None,
349 parent_pid: None,
350 parent_name: None,
351 }
352 }
353
354 #[test]
355 fn local_port_returns_port_from_addr() {
356 let entry = PortEntry {
357 protocol: Protocol::Tcp,
358 local_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080),
359 remote_addr: None,
360 state: ConnectionState::Listen,
361 process: make_process(),
362 };
363 assert_eq!(entry.local_port(), 8080);
364 }
365
366 #[test]
367 fn sort_state_default_is_port_ascending() {
368 let s = SortState::default();
369 assert_eq!(s.column, SortColumn::Port);
370 assert!(s.ascending);
371 }
372
373 #[test]
374 fn sort_state_toggle_same_column_flips_direction() {
375 let mut s = SortState::default();
376 s.toggle(SortColumn::Port);
377 assert!(!s.ascending);
378 s.toggle(SortColumn::Port);
379 assert!(s.ascending);
380 }
381
382 #[test]
383 fn sort_state_toggle_different_column_resets_ascending() {
384 let mut s = SortState::default();
385 s.toggle(SortColumn::Port);
386 s.toggle(SortColumn::Pid);
387 assert_eq!(s.column, SortColumn::Pid);
388 assert!(s.ascending);
389 }
390
391 #[test]
392 fn protocol_display() {
393 assert_eq!(Protocol::Tcp.to_string(), "TCP");
394 assert_eq!(Protocol::Udp.to_string(), "UDP");
395 }
396
397 #[test]
398 fn connection_state_display() {
399 let cases = [
400 (ConnectionState::Listen, "LISTEN"),
401 (ConnectionState::Established, "ESTABLISHED"),
402 (ConnectionState::TimeWait, "TIME_WAIT"),
403 (ConnectionState::CloseWait, "CLOSE_WAIT"),
404 (ConnectionState::SynSent, "SYN_SENT"),
405 (ConnectionState::SynRecv, "SYN_RECV"),
406 (ConnectionState::FinWait1, "FIN_WAIT1"),
407 (ConnectionState::FinWait2, "FIN_WAIT2"),
408 (ConnectionState::Closing, "CLOSING"),
409 (ConnectionState::LastAck, "LAST_ACK"),
410 (ConnectionState::Closed, "CLOSED"),
411 (ConnectionState::Unknown, "UNKNOWN"),
412 ];
413 for (state, expected) in cases {
414 assert_eq!(state.to_string(), expected, "state {:?}", state);
415 }
416 }
417
418 #[test]
419 fn view_mode_default_is_connections() {
420 assert_eq!(ViewMode::default(), ViewMode::Connections);
421 }
422
423 #[test]
424 fn view_mode_next_prev_cycle() {
425 let cases = [
426 (ViewMode::Connections, ViewMode::Processes),
427 (ViewMode::Processes, ViewMode::Ssh),
428 (ViewMode::Ssh, ViewMode::Connections),
429 ];
430 for (from, expected) in cases {
431 assert_eq!(from.next(), expected);
432 assert_eq!(expected.prev(), from);
433 }
434 }
435
436 #[test]
437 fn processes_tab_cycle() {
438 assert_eq!(ProcessesTab::Detail.next(), ProcessesTab::Topology);
439 assert_eq!(ProcessesTab::Topology.next(), ProcessesTab::Detail);
440 assert_eq!(ProcessesTab::default(), ProcessesTab::Detail);
441 }
442
443 #[test]
444 fn ssh_tab_cycle() {
445 assert_eq!(SshTab::Hosts.next(), SshTab::Tunnels);
446 assert_eq!(SshTab::Tunnels.next(), SshTab::Hosts);
447 assert_eq!(SshTab::default(), SshTab::Hosts);
448 }
449
450 #[test]
453 fn sort_state_toggle_all_columns() {
454 let columns = [
455 SortColumn::Port,
456 SortColumn::Service,
457 SortColumn::Protocol,
458 SortColumn::State,
459 SortColumn::Pid,
460 SortColumn::ProcessName,
461 SortColumn::User,
462 ];
463 for col in columns {
464 let mut s = SortState::default();
465 s.toggle(col);
466 if col == SortColumn::Port {
467 assert!(!s.ascending, "toggling same column should flip");
468 } else {
469 assert_eq!(s.column, col);
470 assert!(s.ascending, "switching to {:?} should be ascending", col);
471 }
472 }
473 }
474}