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