Skip to main content

netscli_core/
arp.rs

1use crate::oui::lookup_vendor;
2use ipnet::IpNet;
3#[cfg(not(windows))]
4use ipnetwork::IpNetwork;
5use mac_address::MacAddress;
6#[cfg(not(windows))]
7use pnet_datalink;
8use serde::Serialize;
9use std::net::IpAddr;
10
11#[derive(Debug, Clone, Serialize)]
12pub struct ArpEntry {
13    pub ip: IpAddr,
14    pub mac: MacAddress,
15    pub interface: String,
16    pub vendor: Option<String>,
17}
18
19#[derive(Debug, Clone, Serialize)]
20pub struct InterfaceInfo {
21    pub name: String,
22    pub mac: Option<MacAddress>,
23    pub ips: Vec<IpNet>,
24    pub is_up: bool,
25    pub is_loopback: bool,
26}
27
28pub struct NetworkManager;
29
30impl NetworkManager {
31    pub fn find_mac(ip: &IpAddr) -> Option<ArpEntry> {
32        let table = Self::get_arp_table().ok()?;
33        table.into_iter().find(|e| &e.ip == ip)
34    }
35
36    #[cfg(not(windows))]
37    pub fn get_interfaces() -> Vec<InterfaceInfo> {
38        pnet_datalink::interfaces()
39            .into_iter()
40            .map(|iface| {
41                let name = iface.name.clone();
42                let mac = iface.mac.map(|m| MacAddress::new(m.octets()));
43                let is_up = iface.is_up();
44                let is_loopback = iface.is_loopback();
45                let ips = iface
46                    .ips
47                    .iter()
48                    .filter_map(|n| convert_ipnetwork(*n))
49                    .collect();
50                InterfaceInfo {
51                    name,
52                    mac,
53                    ips,
54                    is_up,
55                    is_loopback,
56                }
57            })
58            .collect()
59    }
60
61    #[cfg(windows)]
62    pub fn get_interfaces() -> Vec<InterfaceInfo> {
63        use ipconfig::{IfType, OperStatus};
64
65        let adapters = match ipconfig::get_adapters() {
66            Ok(adapters) => adapters,
67            Err(_) => return Vec::new(),
68        };
69
70        adapters
71            .into_iter()
72            .map(|adapter| {
73                let name = adapter.friendly_name().to_string();
74                let mac = adapter.physical_address().and_then(mac_from_bytes);
75                let is_up = adapter.oper_status() == OperStatus::IfOperStatusUp;
76                let is_loopback = adapter.if_type() == IfType::SoftwareLoopback;
77                let prefixes = adapter.prefixes();
78                let ips = adapter
79                    .ip_addresses()
80                    .iter()
81                    .filter_map(|ip| ipnet_from_adapter(*ip, prefixes))
82                    .collect();
83                InterfaceInfo {
84                    name,
85                    mac,
86                    ips,
87                    is_up,
88                    is_loopback,
89                }
90            })
91            .collect()
92    }
93
94    #[cfg(target_os = "linux")]
95    pub fn get_arp_table() -> anyhow::Result<Vec<ArpEntry>> {
96        use std::fs::File;
97        use std::io::{BufRead, BufReader};
98        use std::str::FromStr;
99
100        let file = File::open("/proc/net/arp")?;
101        let reader = BufReader::new(file);
102        let mut entries = Vec::new();
103
104        for line in reader.lines().skip(1) {
105            let line = line?;
106            let parts: Vec<&str> = line.split_whitespace().collect();
107            if parts.len() >= 6 {
108                if let (Ok(ip), Ok(mac)) =
109                    (IpAddr::from_str(parts[0]), MacAddress::from_str(parts[3]))
110                {
111                    if parts[3] != "00:00:00:00:00:00" {
112                        let vendor = lookup_vendor(parts[3].to_string().as_str());
113                        entries.push(ArpEntry {
114                            ip,
115                            mac,
116                            interface: parts[5].to_string(),
117                            vendor,
118                        });
119                    }
120                }
121            }
122        }
123        Ok(entries)
124    }
125
126    #[cfg(target_os = "windows")]
127    pub fn get_arp_table() -> anyhow::Result<Vec<ArpEntry>> {
128        use std::process::Command;
129        use std::str::FromStr;
130        let output = Command::new("arp").arg("-a").output()?;
131        let text = String::from_utf8_lossy(&output.stdout);
132        let mut entries = Vec::new();
133
134        // `arp -a` on Windows groups entries by interface. Each group begins with
135        // a header like `Interface: 192.168.1.2 --- 0x5` followed by a column
136        // header (`Internet Address      Physical Address      Type`) and then
137        // rows. Track the current interface IP so entries are correctly attributed
138        // instead of all being flattened to "unknown".
139        let mut current_interface = String::from("unknown");
140
141        for line in text.lines() {
142            let trimmed = line.trim();
143            if trimmed.is_empty() {
144                continue;
145            }
146
147            // Section header: "Interface: <ip> --- <index>"
148            if let Some(rest) = trimmed.strip_prefix("Interface:") {
149                if let Some(iface_ip) = rest.split_whitespace().next() {
150                    current_interface = iface_ip.to_string();
151                }
152                continue;
153            }
154
155            // Skip the "Internet Address ..." column header rows.
156            if trimmed.starts_with("Internet Address") {
157                continue;
158            }
159
160            let parts: Vec<&str> = trimmed.split_whitespace().collect();
161            if parts.len() < 3 {
162                continue;
163            }
164
165            let Ok(ip) = IpAddr::from_str(parts[0]) else {
166                continue;
167            };
168            let Ok(mac) = MacAddress::from_str(parts[1]) else {
169                continue;
170            };
171
172            // `arp -a` emits MACs as `aa-bb-cc-dd-ee-ff` on Windows; the
173            // `MacAddress` parser accepts both formats, but pass the raw token
174            // to `lookup_vendor` since it already handles separators.
175            let vendor = lookup_vendor(parts[1]);
176            entries.push(ArpEntry {
177                ip,
178                mac,
179                interface: current_interface.clone(),
180                vendor,
181            });
182        }
183        Ok(entries)
184    }
185
186    #[cfg(target_os = "macos")]
187    pub fn get_arp_table() -> anyhow::Result<Vec<ArpEntry>> {
188        use std::process::Command;
189        use std::str::FromStr;
190        let output = Command::new("arp").arg("-an").output()?;
191        let text = String::from_utf8_lossy(&output.stdout);
192        let mut entries = Vec::new();
193
194        // Expected format:
195        //   `? (192.168.1.1) at aa:bb:cc:dd:ee:ff on en0 ifscope [ethernet]`
196        // Variants we must tolerate without panicking:
197        //   - `? (192.168.1.1) at (incomplete) on en0 ...`  (no MAC yet)
198        //   - Lines with `permanent` in place of the interface token
199        // Rather than indexing by position, walk the tokens and pick the
200        // IP (parenthesized), the MAC (after `at`), and the interface (after `on`).
201        for line in text.lines() {
202            let tokens: Vec<&str> = line.split_whitespace().collect();
203            let mut ip: Option<IpAddr> = None;
204            let mut mac: Option<MacAddress> = None;
205            let mut iface: Option<String> = None;
206            let mut mac_token: Option<&str> = None;
207
208            let mut i = 0;
209            while i < tokens.len() {
210                let tok = tokens[i];
211                if tok.starts_with('(') && tok.ends_with(')') {
212                    let cleaned = tok.trim_matches(&['(', ')'][..]);
213                    if let Ok(parsed) = IpAddr::from_str(cleaned) {
214                        ip = Some(parsed);
215                    }
216                } else if tok == "at" {
217                    if let Some(next) = tokens.get(i + 1) {
218                        if let Ok(parsed) = MacAddress::from_str(next) {
219                            mac = Some(parsed);
220                            mac_token = Some(*next);
221                        }
222                        i += 1;
223                    }
224                } else if tok == "on" {
225                    if let Some(next) = tokens.get(i + 1) {
226                        iface = Some((*next).to_string());
227                        i += 1;
228                    }
229                }
230                i += 1;
231            }
232
233            if let (Some(ip), Some(mac)) = (ip, mac) {
234                let vendor = mac_token.and_then(lookup_vendor);
235                entries.push(ArpEntry {
236                    ip,
237                    mac,
238                    interface: iface.unwrap_or_default(),
239                    vendor,
240                });
241            }
242        }
243        Ok(entries)
244    }
245
246    pub fn add_entry(ip: IpAddr, mac: MacAddress) -> anyhow::Result<()> {
247        use std::process::Command;
248        #[cfg(target_os = "windows")]
249        {
250            let status = Command::new("arp")
251                .args(["-s", &ip.to_string(), &mac.to_string().replace(":", "-")])
252                .status()?;
253            if !status.success() {
254                anyhow::bail!("Failed to add ARP entry (run as admin?)");
255            }
256        }
257        #[cfg(not(target_os = "windows"))]
258        {
259            let status = Command::new("arp")
260                .args(["-s", &ip.to_string(), &mac.to_string()])
261                .status()?;
262            if !status.success() {
263                anyhow::bail!("Failed to add ARP entry (requires root/admin privileges)");
264            }
265        }
266        Ok(())
267    }
268
269    pub fn delete_entry(ip: IpAddr) -> anyhow::Result<()> {
270        use std::process::Command;
271        #[cfg(target_os = "windows")]
272        {
273            let status = Command::new("arp").args(["-d", &ip.to_string()]).status()?;
274            if !status.success() {
275                anyhow::bail!("Failed to delete ARP entry (run as admin?)");
276            }
277        }
278        #[cfg(not(target_os = "windows"))]
279        {
280            let status = Command::new("arp").args(["-d", &ip.to_string()]).status()?;
281            if !status.success() {
282                anyhow::bail!("Failed to delete ARP entry (requires root/admin privileges)");
283            }
284        }
285        Ok(())
286    }
287
288    pub fn clear_table() -> anyhow::Result<()> {
289        use std::process::Command;
290        #[cfg(target_os = "windows")]
291        {
292            let status = Command::new("arp").args(["-d", "*"]).status()?;
293            if !status.success() {
294                anyhow::bail!("Failed to clear ARP table (run as admin?)");
295            }
296        }
297        #[cfg(any(target_os = "linux", target_os = "macos"))]
298        {
299            // On BSD/macOS 'arp -d -a', on Linux 'ip neigh flush all' is cleaner but 'arp -d' loop is common.
300            // We'll try a best effort 'arp -d -a' for mac, and iterating for linux if needed, but let's assume
301            // the user might just want to delete specific ones.
302            // For simplicity in this cross-platform shim:
303            #[cfg(target_os = "macos")]
304            {
305                let status = Command::new("arp").args(["-d", "-a"]).status()?;
306                if !status.success() {
307                    anyhow::bail!("Failed to clear ARP table");
308                }
309            }
310            #[cfg(target_os = "linux")]
311            {
312                let status = Command::new("ip")
313                    .args(["neigh", "flush", "all"])
314                    .status()?;
315                if !status.success() {
316                    anyhow::bail!("Failed to clear ARP table (iproute2 required)");
317                }
318            }
319        }
320        Ok(())
321    }
322}
323
324#[cfg(not(windows))]
325fn convert_ipnetwork(net: IpNetwork) -> Option<IpNet> {
326    match net {
327        IpNetwork::V4(v4) => IpNet::new(IpAddr::V4(v4.ip()), v4.prefix()).ok(),
328        IpNetwork::V6(v6) => IpNet::new(IpAddr::V6(v6.ip()), v6.prefix()).ok(),
329    }
330}
331
332#[cfg(windows)]
333fn ipnet_from_adapter(ip: IpAddr, prefixes: &[(IpAddr, u32)]) -> Option<IpNet> {
334    let prefix_len = prefixes
335        .iter()
336        .find(|(prefix_ip, _)| prefix_ip.is_ipv4() == ip.is_ipv4())
337        .map(|(_, len)| *len)
338        .unwrap_or_else(|| if ip.is_ipv4() { 32 } else { 128 });
339    IpNet::new(ip, prefix_len.min(128) as u8).ok()
340}
341
342#[cfg(windows)]
343fn mac_from_bytes(bytes: &[u8]) -> Option<MacAddress> {
344    if bytes.len() == 6 {
345        Some(MacAddress::new([
346            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5],
347        ]))
348    } else {
349        None
350    }
351}