Skip to main content

turmoil_net/
netstat.rs

1//! Per-host socket inspection, Linux `netstat`-style.
2//!
3//! [`netstat`] returns a snapshot of one host's socket table as a
4//! plain [`Netstat`] struct. The [`Display`] impl renders the same
5//! columns Linux does, so tests can `println!("{}", netstat("h1"))`
6//! and eyeball connection state during a failure.
7//!
8//! Snapshot — the entries are a copy of socket state at call time.
9//! Poking at the returned struct later won't reflect ongoing
10//! activity.
11//!
12//! [`Display`]: std::fmt::Display
13
14use std::fmt::{self, Display};
15use std::net::SocketAddr;
16
17use crate::kernel::{Kernel, ListenState, Socket, Tcb, TcpState, Type};
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct Netstat {
21    pub entries: Vec<NetstatEntry>,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct NetstatEntry {
26    pub proto: Proto,
27    pub recv_q: usize,
28    pub send_q: usize,
29    pub local: SocketAddr,
30    /// `None` renders as `*:*` — an unbound peer (listener, plain UDP).
31    pub peer: Option<SocketAddr>,
32    /// `None` for UDP sockets without a TCB.
33    pub state: Option<NetstatState>,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum Proto {
38    Tcp,
39    Udp,
40}
41
42/// Renderable TCP state. Mirrors the state names Linux `netstat`
43/// prints (`LISTEN`, `SYN_SENT`, `ESTABLISHED`, …).
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum NetstatState {
46    Listen,
47    SynSent,
48    SynReceived,
49    Established,
50    FinWait1,
51    FinWait2,
52    CloseWait,
53    LastAck,
54    Closing,
55    /// Terminal. `netstat` filters these entries out — the socket is a
56    /// tick away from being reaped, and real Linux would show
57    /// `TIME_WAIT` (which our kernel doesn't model). Kept in the enum
58    /// so [`From<TcpState>`](From) stays total.
59    Closed,
60}
61
62impl From<TcpState> for NetstatState {
63    fn from(s: TcpState) -> Self {
64        match s {
65            TcpState::SynSent => NetstatState::SynSent,
66            TcpState::SynReceived => NetstatState::SynReceived,
67            TcpState::Established => NetstatState::Established,
68            TcpState::FinWait1 => NetstatState::FinWait1,
69            TcpState::FinWait2 => NetstatState::FinWait2,
70            TcpState::CloseWait => NetstatState::CloseWait,
71            TcpState::LastAck => NetstatState::LastAck,
72            TcpState::Closing => NetstatState::Closing,
73            TcpState::Closed => NetstatState::Closed,
74        }
75    }
76}
77
78pub fn snapshot(kernel: &Kernel) -> Netstat {
79    let mut entries = Vec::new();
80    for (_fd, sock) in kernel.sockets() {
81        if let Some(entry) = entry_for(sock) {
82            entries.push(entry);
83        }
84    }
85    Netstat { entries }
86}
87
88fn entry_for(sock: &Socket) -> Option<NetstatEntry> {
89    let bound = sock.bound.as_ref()?;
90    let local = SocketAddr::new(bound.local_addr, bound.local_port);
91
92    match sock.ty {
93        Type::Stream => tcp_entry(sock, local),
94        Type::Dgram => Some(udp_entry(sock, local)),
95        Type::SeqPacket => None,
96    }
97}
98
99fn tcp_entry(sock: &Socket, local: SocketAddr) -> Option<NetstatEntry> {
100    if let Some(tcb) = sock.tcb.as_ref() {
101        // `Closed` is a transient pre-reap state — the socket is one
102        // egress pass away from leaving the table. Real Linux would
103        // show TIME_WAIT here, but our kernel doesn't model it (no
104        // 2×MSL timer, no bind-conflict hold), so rendering either
105        // would be a lie. Hide it instead.
106        if tcb.state == TcpState::Closed {
107            return None;
108        }
109        return Some(tcb_entry(tcb, local));
110    }
111    if let Some(listen) = sock.listen.as_ref() {
112        return Some(listen_entry(listen, local));
113    }
114    None
115}
116
117fn tcb_entry(tcb: &Tcb, local: SocketAddr) -> NetstatEntry {
118    // send_buf holds `[in-flight | queued]` — both count as Send-Q in
119    // real netstat, which reports unACK'd + unsent together.
120    let send_q = tcb.send_buf.len();
121    let recv_q = tcb.recv_buf.len();
122    NetstatEntry {
123        proto: Proto::Tcp,
124        recv_q,
125        send_q,
126        local,
127        peer: Some(tcb.peer),
128        state: Some(NetstatState::from(tcb.state)),
129    }
130}
131
132fn listen_entry(listen: &ListenState, local: SocketAddr) -> NetstatEntry {
133    // Linux convention for LISTEN: Recv-Q is the current accept-queue
134    // depth (completed handshakes waiting on accept()), Send-Q is the
135    // configured backlog. Half-open (SYN_RCVD) connections aren't
136    // counted here — they render as their own rows.
137    NetstatEntry {
138        proto: Proto::Tcp,
139        recv_q: listen.ready.len(),
140        send_q: listen.backlog,
141        local,
142        peer: None,
143        state: Some(NetstatState::Listen),
144    }
145}
146
147fn udp_entry(sock: &Socket, local: SocketAddr) -> NetstatEntry {
148    let recv_q: usize = sock.recv_queue.iter().map(|(_, b)| b.len()).sum();
149    NetstatEntry {
150        proto: Proto::Udp,
151        recv_q,
152        send_q: 0,
153        local,
154        peer: None,
155        state: None,
156    }
157}
158
159impl Display for Proto {
160    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        f.write_str(match self {
162            Proto::Tcp => "tcp",
163            Proto::Udp => "udp",
164        })
165    }
166}
167
168impl Display for NetstatState {
169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170        f.write_str(match self {
171            NetstatState::Listen => "LISTEN",
172            NetstatState::SynSent => "SYN_SENT",
173            NetstatState::SynReceived => "SYN_RCVD",
174            NetstatState::Established => "ESTABLISHED",
175            NetstatState::FinWait1 => "FIN_WAIT1",
176            NetstatState::FinWait2 => "FIN_WAIT2",
177            NetstatState::CloseWait => "CLOSE_WAIT",
178            NetstatState::LastAck => "LAST_ACK",
179            NetstatState::Closing => "CLOSING",
180            NetstatState::Closed => "CLOSED",
181        })
182    }
183}
184
185impl Display for Netstat {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        // Linux netstat column order: Proto, Recv-Q, Send-Q, Local
188        // Address, Foreign Address, State.
189        const HEADERS: [&str; 6] = [
190            "Proto",
191            "Recv-Q",
192            "Send-Q",
193            "Local Address",
194            "Foreign Address",
195            "State",
196        ];
197
198        let rows: Vec<[String; 6]> = self
199            .entries
200            .iter()
201            .map(|e| {
202                [
203                    e.proto.to_string(),
204                    e.recv_q.to_string(),
205                    e.send_q.to_string(),
206                    e.local.to_string(),
207                    e.peer
208                        .map(|p| p.to_string())
209                        .unwrap_or_else(|| "*:*".into()),
210                    e.state.map(|s| s.to_string()).unwrap_or_default(),
211                ]
212            })
213            .collect();
214
215        let mut widths = HEADERS.map(str::len);
216        for row in &rows {
217            for (i, cell) in row.iter().enumerate() {
218                widths[i] = widths[i].max(cell.len());
219            }
220        }
221
222        write_row(f, &HEADERS.map(String::from), &widths)?;
223        for row in &rows {
224            write_row(f, row, &widths)?;
225        }
226        Ok(())
227    }
228}
229
230fn write_row(f: &mut fmt::Formatter<'_>, row: &[String; 6], widths: &[usize; 6]) -> fmt::Result {
231    // Recv-Q / Send-Q are right-aligned (numeric columns in Linux
232    // netstat); everything else is left-aligned. The last column gets
233    // no trailing padding.
234    for (i, cell) in row.iter().enumerate() {
235        if i > 0 {
236            f.write_str(" ")?;
237        }
238        let w = widths[i];
239        let right_align = matches!(i, 1 | 2);
240        if i == row.len() - 1 {
241            f.write_str(cell)?;
242        } else if right_align {
243            write!(f, "{cell:>w$}")?;
244        } else {
245            write!(f, "{cell:<w$}")?;
246        }
247    }
248    writeln!(f)
249}