Skip to main content

netscli_core/
arp.rs

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