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