nd_300/diagnostics/
arp.rs1use serde::Serialize;
2
3#[derive(Debug, Clone, Serialize)]
4pub struct ArpEntry {
5 pub ip: String,
6 pub mac: String,
7 pub interface: String,
8 pub entry_type: String,
9}
10
11pub async fn collect() -> Option<Vec<ArpEntry>> {
12 #[cfg(windows)]
13 {
14 collect_windows().await
15 }
16
17 #[cfg(target_os = "macos")]
18 {
19 collect_macos().await
20 }
21
22 #[cfg(target_os = "linux")]
23 {
24 collect_linux().await
25 }
26}
27
28#[cfg(windows)]
29async fn collect_windows() -> Option<Vec<ArpEntry>> {
30 let mut cmd = tokio::process::Command::new("arp");
31 cmd.args(["-a"]);
32 let output = super::util::run_with_timeout(cmd, super::util::QUICK).await?;
33
34 let text = String::from_utf8_lossy(&output.stdout);
35 let mut entries = Vec::new();
36 let mut current_iface = String::new();
37
38 for line in text.lines() {
39 let line = line.trim();
40 if line.starts_with("Interface:") {
41 current_iface = line
42 .split_whitespace()
43 .nth(1)
44 .unwrap_or("unknown")
45 .to_string();
46 } else if !line.is_empty() && !line.starts_with("Internet") {
47 let parts: Vec<&str> = line.split_whitespace().collect();
48 if parts.len() >= 3 {
49 entries.push(ArpEntry {
50 ip: parts[0].to_string(),
51 mac: parts[1].to_string(),
52 interface: current_iface.clone(),
53 entry_type: parts[2].to_string(),
54 });
55 }
56 }
57 }
58
59 Some(entries)
60}
61
62#[cfg(target_os = "macos")]
63async fn collect_macos() -> Option<Vec<ArpEntry>> {
64 let mut cmd = tokio::process::Command::new("arp");
65 cmd.args(["-a"]);
66 let output = super::util::run_with_timeout(cmd, super::util::QUICK).await?;
67
68 let text = String::from_utf8_lossy(&output.stdout);
69 let mut entries = Vec::new();
70
71 for line in text.lines() {
72 let parts: Vec<&str> = line.split_whitespace().collect();
74 if parts.len() >= 6 && parts[1].starts_with('(') {
75 let ip = parts[1].trim_matches(|c| c == '(' || c == ')').to_string();
76 let mac = parts[3].to_string();
77 let iface = parts.get(5).unwrap_or(&"unknown").to_string();
78
79 entries.push(ArpEntry {
80 ip,
81 mac,
82 interface: iface,
83 entry_type: "dynamic".to_string(),
84 });
85 }
86 }
87
88 Some(entries)
89}
90
91#[cfg(target_os = "linux")]
92async fn collect_linux() -> Option<Vec<ArpEntry>> {
93 if let Ok(content) = tokio::fs::read_to_string("/proc/net/arp").await {
95 let mut entries = Vec::new();
96 for line in content.lines().skip(1) {
97 let parts: Vec<&str> = line.split_whitespace().collect();
98 if parts.len() >= 6 {
99 entries.push(ArpEntry {
100 ip: parts[0].to_string(),
101 mac: parts[3].to_string(),
102 interface: parts[5].to_string(),
103 entry_type: if parts[2] == "0x2" {
104 "dynamic".to_string()
105 } else {
106 "static".to_string()
107 },
108 });
109 }
110 }
111 return Some(entries);
112 }
113
114 None
115}