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 let mut stats = parse_macos_protocol_stats(&text)?;
165 let mut connections_cmd = tokio::process::Command::new("netstat");
166 connections_cmd.args(["-anW", "-p", "tcp"]);
167 let connections = super::util::run_with_timeout(connections_cmd, super::util::QUICK).await?;
168 stats.tcp.current_connections = String::from_utf8_lossy(&connections.stdout)
169 .lines()
170 .filter(|line| line.split_whitespace().last() == Some("ESTABLISHED"))
171 .count() as u64;
172 Some(stats)
173}
174
175#[cfg(any(target_os = "macos", test))]
176fn parse_macos_protocol_stats(text: &str) -> Option<ProtocolStatistics> {
177 let mut tcp = TcpStats {
178 active_opens: 0,
179 passive_opens: 0,
180 failed_connections: 0,
181 reset_connections: 0,
182 current_connections: 0,
183 segments_received: 0,
184 segments_sent: 0,
185 segments_retransmitted: 0,
186 };
187 let mut udp = UdpStats {
188 datagrams_received: 0,
189 datagrams_sent: 0,
190 receive_errors: 0,
191 no_port_errors: 0,
192 };
193 let mut icmp = IcmpStats {
194 messages_received: 0,
195 messages_sent: 0,
196 errors_received: 0,
197 errors_sent: 0,
198 };
199
200 let mut section = "";
201 let mut icmp_input_histogram = false;
202 let mut icmp_input_total = 0u64;
203 for line in text.lines() {
204 let trimmed = line.trim();
205 if trimmed == "tcp:" {
206 section = "tcp";
207 continue;
208 }
209 if trimmed == "udp:" {
210 section = "udp";
211 continue;
212 }
213 if trimmed == "icmp:" {
214 section = "icmp";
215 icmp_input_histogram = false;
216 continue;
217 }
218 if trimmed.ends_with(':') && !trimmed.contains(' ') {
219 section = "";
220 continue;
221 }
222
223 let val = extract_leading_number(trimmed).unwrap_or(0);
224
225 match section {
226 "tcp" => {
227 if trimmed.contains("connection request") {
228 tcp.active_opens = val;
229 } else if trimmed.contains("connection accept") {
230 tcp.passive_opens = val;
231 } else if trimmed.contains("bad connection") {
232 tcp.failed_connections = val;
233 } else if trimmed.ends_with("bad reset") {
234 tcp.reset_connections = val;
235 } else if trimmed.contains("data packet") && trimmed.contains("retransmitted") {
236 tcp.segments_retransmitted = val;
237 } else if trimmed.ends_with("packet sent") || trimmed.ends_with("packets sent") {
238 tcp.segments_sent = val;
239 } else if trimmed.ends_with("packet received")
240 || trimmed.ends_with("packets received")
241 {
242 tcp.segments_received = val;
243 }
244 }
245 "udp" => {
246 if trimmed.contains("datagram") && trimmed.contains("received") {
247 udp.datagrams_received = val;
248 } else if trimmed.contains("datagram") && trimmed.contains("output") {
249 udp.datagrams_sent = val;
250 } else if trimmed.contains("dropped due to no socket") {
251 udp.no_port_errors = val;
252 } else if trimmed.contains("incomplete header")
253 || trimmed.contains("bad data length")
254 || trimmed.contains("bad checksum")
255 || trimmed.contains("full socket buffers")
256 {
257 udp.receive_errors = udp.receive_errors.saturating_add(val);
258 }
259 }
260 "icmp" => {
261 if trimmed == "Input histogram:" {
262 icmp_input_histogram = true;
263 } else if trimmed.ends_with("calls to icmp_error") {
264 icmp.errors_sent = val;
265 icmp.messages_sent = icmp.messages_sent.saturating_add(val);
266 } else if trimmed.ends_with("message response generated") {
267 icmp.messages_sent = icmp.messages_sent.saturating_add(val);
268 } else if icmp_input_histogram && trimmed.contains(':') {
269 let histogram_value = trimmed
270 .rsplit_once(':')
271 .and_then(|(_, value)| value.trim().parse::<u64>().ok())
272 .unwrap_or(0);
273 icmp_input_total = icmp_input_total.saturating_add(histogram_value);
274 } else if trimmed.contains("bad code")
275 || trimmed.contains("bad checksum")
276 || trimmed.contains("bad length")
277 || trimmed.contains("minimum length")
278 {
279 icmp.errors_received = icmp.errors_received.saturating_add(val);
280 }
281 }
282 _ => {}
283 }
284 }
285 icmp.messages_received = icmp_input_total;
286
287 let tcp_available = tcp.active_opens > 0
292 || tcp.passive_opens > 0
293 || tcp.failed_connections > 0
294 || tcp.reset_connections > 0
295 || tcp.current_connections > 0
296 || tcp.segments_received > 0
297 || tcp.segments_sent > 0
298 || tcp.segments_retransmitted > 0;
299 tcp_available.then_some(ProtocolStatistics { tcp, udp, icmp })
300}
301
302#[cfg(target_os = "linux")]
303async fn collect_linux() -> Option<ProtocolStatistics> {
304 let mut tcp = TcpStats {
305 active_opens: 0,
306 passive_opens: 0,
307 failed_connections: 0,
308 reset_connections: 0,
309 current_connections: 0,
310 segments_received: 0,
311 segments_sent: 0,
312 segments_retransmitted: 0,
313 };
314 let mut udp = UdpStats {
315 datagrams_received: 0,
316 datagrams_sent: 0,
317 receive_errors: 0,
318 no_port_errors: 0,
319 };
320 let mut icmp = IcmpStats {
321 messages_received: 0,
322 messages_sent: 0,
323 errors_received: 0,
324 errors_sent: 0,
325 };
326
327 if let Ok(content) = tokio::fs::read_to_string("/proc/net/snmp").await {
329 let lines: Vec<&str> = content.lines().collect();
330 for i in (0..lines.len()).step_by(2) {
331 if i + 1 >= lines.len() {
332 break;
333 }
334 let headers: Vec<&str> = lines[i].split_whitespace().collect();
335 let values: Vec<&str> = lines[i + 1].split_whitespace().collect();
336
337 if headers.first() == Some(&"Tcp:") && headers.len() == values.len() {
338 for (j, header) in headers.iter().enumerate() {
339 let val: u64 = values.get(j).and_then(|s| s.parse().ok()).unwrap_or(0);
340 match *header {
341 "ActiveOpens" => tcp.active_opens = val,
342 "PassiveOpens" => tcp.passive_opens = val,
343 "AttemptFails" => tcp.failed_connections = val,
344 "EstabResets" => tcp.reset_connections = val,
345 "CurrEstab" => tcp.current_connections = val,
346 "InSegs" => tcp.segments_received = val,
347 "OutSegs" => tcp.segments_sent = val,
348 "RetransSegs" => tcp.segments_retransmitted = val,
349 _ => {}
350 }
351 }
352 } else if headers.first() == Some(&"Udp:") && headers.len() == values.len() {
353 for (j, header) in headers.iter().enumerate() {
354 let val: u64 = values.get(j).and_then(|s| s.parse().ok()).unwrap_or(0);
355 match *header {
356 "InDatagrams" => udp.datagrams_received = val,
357 "OutDatagrams" => udp.datagrams_sent = val,
358 "InErrors" => udp.receive_errors = val,
359 "NoPorts" => udp.no_port_errors = val,
360 _ => {}
361 }
362 }
363 } else if headers.first() == Some(&"Icmp:") && headers.len() == values.len() {
364 for (j, header) in headers.iter().enumerate() {
365 let val: u64 = values.get(j).and_then(|s| s.parse().ok()).unwrap_or(0);
366 match *header {
367 "InMsgs" => icmp.messages_received = val,
368 "OutMsgs" => icmp.messages_sent = val,
369 "InErrors" => icmp.errors_received = val,
370 "OutErrors" => icmp.errors_sent = val,
371 _ => {}
372 }
373 }
374 }
375 }
376 }
377
378 Some(ProtocolStatistics { tcp, udp, icmp })
379}
380
381#[cfg(windows)]
382fn extract_stat_value(line: &str) -> Option<u64> {
383 if let Some(pos) = line.find('=') {
385 let after = line[pos + 1..].trim();
386 return after.parse().ok();
387 }
388 None
389}
390
391#[cfg(any(target_os = "macos", test))]
392fn extract_leading_number(line: &str) -> Option<u64> {
393 let num_str: String = line.chars().take_while(|c| c.is_ascii_digit()).collect();
394 num_str.parse().ok()
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 #[test]
402 fn current_macos_udp_wording_and_icmp_histogram_parse() {
403 let fixture = "tcp:\n\t12 packets sent\n\t3 data packets retransmitted\n\t15 packets received\n\t2 connection requests\n\t1 connection accept\nudp:\n\t409241 datagrams received\n\t\t7 dropped due to no socket\n\t\t3 dropped due to full socket buffers\n\t104267 datagrams output\nicmp:\n\t5 calls to icmp_error\n\tInput histogram:\n\t\techo reply: 8\n\t\tdestination unreachable: 2\n\t1 message response generated\n";
404 let stats = parse_macos_protocol_stats(fixture).unwrap();
405 assert_eq!(stats.tcp.segments_sent, 12);
406 assert_eq!(stats.tcp.segments_retransmitted, 3);
407 assert_eq!(stats.udp.datagrams_sent, 104267);
408 assert_eq!(stats.udp.no_port_errors, 7);
409 assert_eq!(stats.udp.receive_errors, 3);
410 assert_eq!(stats.icmp.messages_received, 10);
411 assert_eq!(stats.icmp.messages_sent, 6);
412 }
413
414 #[test]
415 fn suppressed_macos_tcp_counters_omit_optional_section() {
416 let fixture = "tcp:\n\t0 packet sent\n\t0 packet received\n\t0 connection request\n\t0 connection accept\nudp:\n\t12 datagrams received\n\t9 datagrams output\n";
417 assert!(parse_macos_protocol_stats(fixture).is_none());
418 }
419}