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)]
149pub struct TrackedEntry {
150 pub entry: PortEntry,
152 pub status: EntryStatus,
154 pub seen_at: Instant,
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum SortColumn {
161 Port,
162 Protocol,
163 State,
164 Pid,
165 ProcessName,
166 User,
167}
168
169#[derive(Debug, Clone, Copy)]
174pub struct SortState {
175 pub column: SortColumn,
177 pub ascending: bool,
179}
180
181impl Default for SortState {
182 fn default() -> Self {
183 Self {
184 column: SortColumn::Port,
185 ascending: true,
186 }
187 }
188}
189
190impl SortState {
191 pub fn toggle(&mut self, col: SortColumn) {
194 if self.column == col {
195 self.ascending = !self.ascending;
196 } else {
197 self.column = col;
198 self.ascending = true;
199 }
200 }
201}
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205pub enum DetailTab {
206 Tree,
208 Interface,
210 Connection,
212}
213
214impl DetailTab {
215 pub fn next(self) -> Self {
217 match self {
218 Self::Tree => Self::Interface,
219 Self::Interface => Self::Connection,
220 Self::Connection => Self::Tree,
221 }
222 }
223
224 pub fn prev(self) -> Self {
226 match self {
227 Self::Tree => Self::Connection,
228 Self::Interface => Self::Tree,
229 Self::Connection => Self::Interface,
230 }
231 }
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub enum ExportFormat {
241 Json,
242 Csv,
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
249
250 fn make_process() -> ProcessInfo {
251 ProcessInfo {
252 pid: 1,
253 name: "test".into(),
254 path: None,
255 cmdline: None,
256 user: None,
257 parent_pid: None,
258 parent_name: None,
259 }
260 }
261
262 #[test]
263 fn local_port_returns_port_from_addr() {
264 let entry = PortEntry {
265 protocol: Protocol::Tcp,
266 local_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080),
267 remote_addr: None,
268 state: ConnectionState::Listen,
269 process: make_process(),
270 };
271 assert_eq!(entry.local_port(), 8080);
272 }
273
274 #[test]
275 fn sort_state_default_is_port_ascending() {
276 let s = SortState::default();
277 assert_eq!(s.column, SortColumn::Port);
278 assert!(s.ascending);
279 }
280
281 #[test]
282 fn sort_state_toggle_same_column_flips_direction() {
283 let mut s = SortState::default();
284 s.toggle(SortColumn::Port);
285 assert!(!s.ascending);
286 s.toggle(SortColumn::Port);
287 assert!(s.ascending);
288 }
289
290 #[test]
291 fn sort_state_toggle_different_column_resets_ascending() {
292 let mut s = SortState::default();
293 s.toggle(SortColumn::Port);
294 s.toggle(SortColumn::Pid);
295 assert_eq!(s.column, SortColumn::Pid);
296 assert!(s.ascending);
297 }
298
299 #[test]
300 fn protocol_display() {
301 assert_eq!(Protocol::Tcp.to_string(), "TCP");
302 assert_eq!(Protocol::Udp.to_string(), "UDP");
303 }
304
305 #[test]
306 fn connection_state_display() {
307 let cases = [
308 (ConnectionState::Listen, "LISTEN"),
309 (ConnectionState::Established, "ESTABLISHED"),
310 (ConnectionState::TimeWait, "TIME_WAIT"),
311 (ConnectionState::CloseWait, "CLOSE_WAIT"),
312 (ConnectionState::SynSent, "SYN_SENT"),
313 (ConnectionState::SynRecv, "SYN_RECV"),
314 (ConnectionState::FinWait1, "FIN_WAIT1"),
315 (ConnectionState::FinWait2, "FIN_WAIT2"),
316 (ConnectionState::Closing, "CLOSING"),
317 (ConnectionState::LastAck, "LAST_ACK"),
318 (ConnectionState::Closed, "CLOSED"),
319 (ConnectionState::Unknown, "UNKNOWN"),
320 ];
321 for (state, expected) in cases {
322 assert_eq!(state.to_string(), expected, "state {:?}", state);
323 }
324 }
325
326 #[test]
329 fn detail_tab_next_cycles_forward() {
330 let cases = [
331 (DetailTab::Tree, DetailTab::Interface),
332 (DetailTab::Interface, DetailTab::Connection),
333 (DetailTab::Connection, DetailTab::Tree),
334 ];
335 for (from, expected) in cases {
336 assert_eq!(from.next(), expected, "next of {:?}", from);
337 }
338 }
339
340 #[test]
341 fn detail_tab_prev_cycles_backward() {
342 let cases = [
343 (DetailTab::Tree, DetailTab::Connection),
344 (DetailTab::Interface, DetailTab::Tree),
345 (DetailTab::Connection, DetailTab::Interface),
346 ];
347 for (from, expected) in cases {
348 assert_eq!(from.prev(), expected, "prev of {:?}", from);
349 }
350 }
351
352 #[test]
353 fn detail_tab_next_prev_roundtrip() {
354 for tab in [DetailTab::Tree, DetailTab::Interface, DetailTab::Connection] {
355 assert_eq!(tab.next().prev(), tab, "roundtrip {:?}", tab);
356 assert_eq!(tab.prev().next(), tab, "reverse roundtrip {:?}", tab);
357 }
358 }
359
360 #[test]
363 fn sort_state_toggle_all_columns() {
364 let columns = [
365 SortColumn::Port,
366 SortColumn::Protocol,
367 SortColumn::State,
368 SortColumn::Pid,
369 SortColumn::ProcessName,
370 SortColumn::User,
371 ];
372 for col in columns {
373 let mut s = SortState::default();
374 s.toggle(col);
375 if col == SortColumn::Port {
376 assert!(!s.ascending, "toggling same column should flip");
377 } else {
378 assert_eq!(s.column, col);
379 assert!(s.ascending, "switching to {:?} should be ascending", col);
380 }
381 }
382 }
383}