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 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 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 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}