Skip to main content

nd_300/diagnostics/
protocol_stats.rs

1use serde::Serialize;
2
3#[derive(Debug, Clone, Serialize)]
4pub struct ProtocolStatistics {
5    pub tcp: TcpStats,
6    pub udp: UdpStats,
7    pub icmp: IcmpStats,
8}
9
10#[derive(Debug, Clone, Serialize)]
11pub struct TcpStats {
12    pub active_opens: u64,
13    pub passive_opens: u64,
14    pub failed_connections: u64,
15    pub reset_connections: u64,
16    pub current_connections: u64,
17    pub segments_received: u64,
18    pub segments_sent: u64,
19    pub segments_retransmitted: u64,
20}
21
22#[derive(Debug, Clone, Serialize)]
23pub struct UdpStats {
24    pub datagrams_received: u64,
25    pub datagrams_sent: u64,
26    pub receive_errors: u64,
27    pub no_port_errors: u64,
28}
29
30#[derive(Debug, Clone, Serialize)]
31pub struct IcmpStats {
32    pub messages_received: u64,
33    pub messages_sent: u64,
34    pub errors_received: u64,
35    pub errors_sent: u64,
36}
37
38pub async fn collect() -> Option<ProtocolStatistics> {
39    #[cfg(windows)]
40    {
41        collect_windows().await
42    }
43
44    #[cfg(target_os = "macos")]
45    {
46        collect_macos().await
47    }
48
49    #[cfg(target_os = "linux")]
50    {
51        collect_linux().await
52    }
53}
54
55#[cfg(windows)]
56async fn collect_windows() -> Option<ProtocolStatistics> {
57    let mut cmd = tokio::process::Command::new("netstat");
58    cmd.args(["-s"]);
59    let output = super::util::run_with_timeout(cmd, super::util::QUICK).await?;
60
61    let text = String::from_utf8_lossy(&output.stdout);
62    let mut tcp = TcpStats {
63        active_opens: 0,
64        passive_opens: 0,
65        failed_connections: 0,
66        reset_connections: 0,
67        current_connections: 0,
68        segments_received: 0,
69        segments_sent: 0,
70        segments_retransmitted: 0,
71    };
72    let mut udp = UdpStats {
73        datagrams_received: 0,
74        datagrams_sent: 0,
75        receive_errors: 0,
76        no_port_errors: 0,
77    };
78    let mut icmp = IcmpStats {
79        messages_received: 0,
80        messages_sent: 0,
81        errors_received: 0,
82        errors_sent: 0,
83    };
84
85    let mut section = "";
86    for line in text.lines() {
87        let line = line.trim();
88        if line.contains("TCP Statistics") {
89            section = "tcp";
90            continue;
91        }
92        if line.contains("UDP Statistics") {
93            section = "udp";
94            continue;
95        }
96        if line.contains("ICMPv4 Statistics") || line.contains("ICMP Statistics") {
97            section = "icmp";
98            continue;
99        }
100        if line.contains("IPv4 Statistics") || line.contains("IPv6 Statistics") {
101            section = "";
102            continue;
103        }
104
105        let val = extract_stat_value(line).unwrap_or(0);
106
107        match section {
108            "tcp" => {
109                if line.contains("Active Opens") {
110                    tcp.active_opens = val;
111                } else if line.contains("Passive Opens") {
112                    tcp.passive_opens = val;
113                } else if line.contains("Failed") {
114                    tcp.failed_connections = val;
115                } else if line.contains("Reset") && line.contains("Connection") {
116                    tcp.reset_connections = val;
117                } else if line.contains("Current") {
118                    tcp.current_connections = val;
119                } else if line.contains("Segments Received") {
120                    tcp.segments_received = val;
121                } else if line.contains("Segments Sent") && !line.contains("Re") {
122                    tcp.segments_sent = val;
123                } else if line.contains("Retransmit") {
124                    tcp.segments_retransmitted = val;
125                }
126            }
127            "udp" => {
128                if line.contains("Datagrams Received") {
129                    udp.datagrams_received = val;
130                } else if line.contains("No Ports") {
131                    udp.no_port_errors = val;
132                } else if line.contains("Receive Errors") {
133                    udp.receive_errors = val;
134                } else if line.contains("Datagrams Sent") {
135                    udp.datagrams_sent = val;
136                }
137            }
138            "icmp" => {
139                if line.contains("Messages") && line.contains("Received") {
140                    icmp.messages_received = val;
141                } else if line.contains("Messages") && line.contains("Sent") {
142                    icmp.messages_sent = val;
143                } else if line.contains("Errors") && line.contains("Received") {
144                    icmp.errors_received = val;
145                } else if line.contains("Errors") && line.contains("Sent") {
146                    icmp.errors_sent = val;
147                }
148            }
149            _ => {}
150        }
151    }
152
153    Some(ProtocolStatistics { tcp, udp, icmp })
154}
155
156#[cfg(target_os = "macos")]
157async fn collect_macos() -> Option<ProtocolStatistics> {
158    let mut cmd = tokio::process::Command::new("netstat");
159    cmd.args(["-s"]);
160    let output = super::util::run_with_timeout(cmd, super::util::QUICK).await?;
161
162    let text = String::from_utf8_lossy(&output.stdout);
163
164    // macOS netstat -s output parsing (similar structure to Windows)
165    let mut tcp = TcpStats {
166        active_opens: 0,
167        passive_opens: 0,
168        failed_connections: 0,
169        reset_connections: 0,
170        current_connections: 0,
171        segments_received: 0,
172        segments_sent: 0,
173        segments_retransmitted: 0,
174    };
175    let mut udp = UdpStats {
176        datagrams_received: 0,
177        datagrams_sent: 0,
178        receive_errors: 0,
179        no_port_errors: 0,
180    };
181    let mut icmp = IcmpStats {
182        messages_received: 0,
183        messages_sent: 0,
184        errors_received: 0,
185        errors_sent: 0,
186    };
187
188    let mut section = "";
189    for line in text.lines() {
190        let trimmed = line.trim();
191        if trimmed == "tcp:" {
192            section = "tcp";
193            continue;
194        }
195        if trimmed == "udp:" {
196            section = "udp";
197            continue;
198        }
199        if trimmed == "icmp:" {
200            section = "icmp";
201            continue;
202        }
203
204        let val = extract_leading_number(trimmed).unwrap_or(0);
205
206        match section {
207            "tcp" => {
208                if trimmed.contains("connection request") {
209                    tcp.active_opens = val;
210                } else if trimmed.contains("connection accept") {
211                    tcp.passive_opens = val;
212                } else if trimmed.contains("bad connection") {
213                    tcp.failed_connections = val;
214                } else if trimmed.contains("reset") {
215                    tcp.reset_connections = val;
216                } else if trimmed.contains("packet") && trimmed.contains("sent") {
217                    tcp.segments_sent = val;
218                } else if trimmed.contains("packet") && trimmed.contains("received") {
219                    tcp.segments_received = val;
220                } else if trimmed.contains("retransmit") {
221                    tcp.segments_retransmitted = val;
222                }
223            }
224            "udp" => {
225                if trimmed.contains("datagram") && trimmed.contains("received") {
226                    udp.datagrams_received = val;
227                } else if trimmed.contains("datagram") && trimmed.contains("sent") {
228                    udp.datagrams_sent = val;
229                }
230            }
231            "icmp" => {
232                if trimmed.contains("response") && trimmed.contains("received") {
233                    icmp.messages_received = val;
234                } else if trimmed.contains("sent") {
235                    icmp.messages_sent = val;
236                }
237            }
238            _ => {}
239        }
240    }
241
242    Some(ProtocolStatistics { tcp, udp, icmp })
243}
244
245#[cfg(target_os = "linux")]
246async fn collect_linux() -> Option<ProtocolStatistics> {
247    let mut tcp = TcpStats {
248        active_opens: 0,
249        passive_opens: 0,
250        failed_connections: 0,
251        reset_connections: 0,
252        current_connections: 0,
253        segments_received: 0,
254        segments_sent: 0,
255        segments_retransmitted: 0,
256    };
257    let mut udp = UdpStats {
258        datagrams_received: 0,
259        datagrams_sent: 0,
260        receive_errors: 0,
261        no_port_errors: 0,
262    };
263    let mut icmp = IcmpStats {
264        messages_received: 0,
265        messages_sent: 0,
266        errors_received: 0,
267        errors_sent: 0,
268    };
269
270    // Read /proc/net/snmp
271    if let Ok(content) = tokio::fs::read_to_string("/proc/net/snmp").await {
272        let lines: Vec<&str> = content.lines().collect();
273        for i in (0..lines.len()).step_by(2) {
274            if i + 1 >= lines.len() {
275                break;
276            }
277            let headers: Vec<&str> = lines[i].split_whitespace().collect();
278            let values: Vec<&str> = lines[i + 1].split_whitespace().collect();
279
280            if headers.first() == Some(&"Tcp:") && headers.len() == values.len() {
281                for (j, header) in headers.iter().enumerate() {
282                    let val: u64 = values.get(j).and_then(|s| s.parse().ok()).unwrap_or(0);
283                    match *header {
284                        "ActiveOpens" => tcp.active_opens = val,
285                        "PassiveOpens" => tcp.passive_opens = val,
286                        "AttemptFails" => tcp.failed_connections = val,
287                        "EstabResets" => tcp.reset_connections = val,
288                        "CurrEstab" => tcp.current_connections = val,
289                        "InSegs" => tcp.segments_received = val,
290                        "OutSegs" => tcp.segments_sent = val,
291                        "RetransSegs" => tcp.segments_retransmitted = val,
292                        _ => {}
293                    }
294                }
295            } else if headers.first() == Some(&"Udp:") && headers.len() == values.len() {
296                for (j, header) in headers.iter().enumerate() {
297                    let val: u64 = values.get(j).and_then(|s| s.parse().ok()).unwrap_or(0);
298                    match *header {
299                        "InDatagrams" => udp.datagrams_received = val,
300                        "OutDatagrams" => udp.datagrams_sent = val,
301                        "InErrors" => udp.receive_errors = val,
302                        "NoPorts" => udp.no_port_errors = val,
303                        _ => {}
304                    }
305                }
306            } else if headers.first() == Some(&"Icmp:") && headers.len() == values.len() {
307                for (j, header) in headers.iter().enumerate() {
308                    let val: u64 = values.get(j).and_then(|s| s.parse().ok()).unwrap_or(0);
309                    match *header {
310                        "InMsgs" => icmp.messages_received = val,
311                        "OutMsgs" => icmp.messages_sent = val,
312                        "InErrors" => icmp.errors_received = val,
313                        "OutErrors" => icmp.errors_sent = val,
314                        _ => {}
315                    }
316                }
317            }
318        }
319    }
320
321    Some(ProtocolStatistics { tcp, udp, icmp })
322}
323
324#[cfg(windows)]
325fn extract_stat_value(line: &str) -> Option<u64> {
326    // "  Active Opens              = 12345"
327    if let Some(pos) = line.find('=') {
328        let after = line[pos + 1..].trim();
329        return after.parse().ok();
330    }
331    None
332}
333
334#[cfg(target_os = "macos")]
335fn extract_leading_number(line: &str) -> Option<u64> {
336    let num_str: String = line.chars().take_while(|c| c.is_ascii_digit()).collect();
337    num_str.parse().ok()
338}