Skip to main content

nd_300/diagnostics/
arp.rs

1use 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        // Format: ? (192.168.1.1) at aa:bb:cc:dd:ee:ff on en0 ifscope [ethernet]
73        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    // Try /proc/net/arp first
94    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}