use anyhow::{bail, Context, Result};
use futures_util::future::join_all;
use ipnetwork::IpNetwork;
use pnet::datalink::{self, Channel, Config, NetworkInterface as PnetNetworkInterface};
use pnet::packet::arp::{ArpHardwareTypes, ArpOperations, ArpPacket, MutableArpPacket};
use pnet::packet::ethernet::{EtherTypes, MutableEthernetPacket};
use pnet::packet::Packet;
use pnet::util::MacAddr;
use serde::Serialize;
use std::net::IpAddr;
use std::time::Duration;
use tracing::{info, warn};
#[derive(Serialize, Debug, Clone)]
pub struct DiscoveredDevice {
pub ip: String,
pub mac: String,
pub hostname: Option<String>,
}
impl DiscoveredDevice {
fn scan_with_pnet(
interface: PnetNetworkInterface,
network: IpNetwork,
source_ip: IpAddr,
source_mac: MacAddr,
) -> Result<Vec<DiscoveredDevice>> {
let source_ipv4 = match source_ip {
IpAddr::V4(ip) => ip,
_ => bail!("Only IPv4 is supported"),
};
let config = Config {
read_timeout: Some(Duration::from_secs(2)),
..Default::default()
};
let (mut tx, mut rx) = match datalink::channel(&interface, config) {
Ok(Channel::Ethernet(tx, rx)) => (tx, rx),
Ok(_) => bail!("Unsupported channel type"),
Err(e) => bail!(
"Failed to create raw network socket for ARP scanning: {}. \
This requires root/administrator privileges. \
Please run the application with 'sudo' or as administrator.",
e
),
};
for target_ip in network.iter() {
let target_ipv4 = match target_ip {
IpAddr::V4(ip) => ip,
_ => continue,
};
if target_ipv4 == source_ipv4 {
continue;
}
let mut ethernet_buffer = [0u8; 42];
let mut ethernet_packet = MutableEthernetPacket::new(&mut ethernet_buffer).unwrap();
ethernet_packet.set_destination(MacAddr::broadcast());
ethernet_packet.set_source(source_mac);
ethernet_packet.set_ethertype(EtherTypes::Arp);
let mut arp_buffer = [0u8; 28];
let mut arp_packet = MutableArpPacket::new(&mut arp_buffer).unwrap();
arp_packet.set_hardware_type(ArpHardwareTypes::Ethernet);
arp_packet.set_protocol_type(EtherTypes::Ipv4);
arp_packet.set_hw_addr_len(6);
arp_packet.set_proto_addr_len(4);
arp_packet.set_operation(ArpOperations::Request);
arp_packet.set_sender_hw_addr(source_mac);
arp_packet.set_sender_proto_addr(source_ipv4);
arp_packet.set_target_hw_addr(MacAddr::zero());
arp_packet.set_target_proto_addr(target_ipv4);
ethernet_packet.set_payload(arp_packet.packet());
tx.send_to(ethernet_packet.packet(), None);
}
drop(tx);
let mut devices = Vec::new();
let start_time = std::time::Instant::now();
let scan_duration = Duration::from_secs(5);
while start_time.elapsed() < scan_duration {
match rx.next() {
Ok(packet) => {
if let Some(ethernet_packet) =
pnet::packet::ethernet::EthernetPacket::new(packet)
{
if ethernet_packet.get_ethertype() == EtherTypes::Arp {
if let Some(arp_packet) = ArpPacket::new(ethernet_packet.payload()) {
if arp_packet.get_operation() == ArpOperations::Reply {
let device = DiscoveredDevice {
ip: arp_packet.get_sender_proto_addr().to_string(),
mac: arp_packet
.get_sender_hw_addr()
.to_string()
.to_uppercase(),
hostname: None,
};
if !devices
.iter()
.any(|d: &DiscoveredDevice| d.mac == device.mac)
{
devices.push(device);
}
}
}
}
}
}
Err(e) if e.kind() == std::io::ErrorKind::TimedOut => continue,
Err(e) => {
warn!("Error receiving packet: {}", e);
break;
}
}
}
Ok(devices)
}
}
#[derive(Serialize, Debug, Clone)]
pub struct NetworkInterface {
pub name: String,
pub ip: String,
pub mac: String,
pub is_up: bool,
}
impl NetworkInterface {
pub async fn list_interfaces() -> Result<Vec<NetworkInterface>> {
let interfaces = datalink::interfaces()
.into_iter()
.filter(|iface| {
iface.is_up()
&& !iface.is_loopback()
&& iface.mac.is_some()
&& iface.ips.iter().any(|ip| ip.is_ipv4())
})
.map(|iface| {
let ip = iface
.ips
.iter()
.find(|ip| ip.is_ipv4())
.map(|ip| ip.ip().to_string())
.unwrap_or_else(|| "No IPv4".to_string());
let mac = iface
.mac
.map(|mac| mac.to_string().to_uppercase())
.unwrap_or_else(|| "No MAC".to_string());
let is_up = iface.is_up();
let name = iface.name;
NetworkInterface {
name,
ip,
mac,
is_up,
}
})
.collect();
Ok(interfaces)
}
pub async fn scan_network_with_interface(
interface_name: Option<&str>,
) -> Result<Vec<DiscoveredDevice>> {
info!("Starting network scan on interface: {:?}", interface_name);
let pnet_iface = if let Some(name) = interface_name {
datalink::interfaces()
.into_iter()
.find(|iface| iface.name == name)
.ok_or_else(|| anyhow::anyhow!("Interface '{}' not found", name))?
} else {
datalink::interfaces()
.into_iter()
.filter(|iface| {
iface.is_up()
&& !iface.is_loopback()
&& iface.mac.is_some()
&& iface.ips.iter().any(|ip| ip.is_ipv4())
})
.find(|iface| {
let has_non_docker_ip = iface.ips.iter().any(|ip| {
if let std::net::IpAddr::V4(ipv4) = ip.ip() {
ipv4.octets()[0] != 172 } else {
false
}
});
if has_non_docker_ip {
return true;
}
!iface.name.starts_with("eth0")
})
.or_else(|| {
datalink::interfaces().into_iter().find(|iface| {
iface.is_up()
&& !iface.is_loopback()
&& iface.mac.is_some()
&& iface.ips.iter().any(|ip| ip.is_ipv4())
})
})
.ok_or_else(|| {
anyhow::anyhow!("No suitable network interface found for scanning.")
})?
};
if !pnet_iface.is_up() {
bail!("Selected interface '{}' is not up", pnet_iface.name);
}
if pnet_iface.is_loopback() {
bail!("Selected interface '{}' is loopback", pnet_iface.name);
}
if pnet_iface.mac.is_none() {
bail!(
"Selected interface '{}' has no MAC address",
pnet_iface.name
);
}
if !pnet_iface.ips.iter().any(|ip| ip.is_ipv4()) {
bail!(
"Selected interface '{}' has no IPv4 address",
pnet_iface.name
);
}
let ip_network = pnet_iface
.ips
.iter()
.find(|ip| ip.is_ipv4())
.ok_or_else(|| anyhow::anyhow!("Selected interface has no IPv4 address."))?;
let source_ip = ip_network.ip();
let network = IpNetwork::new(ip_network.ip(), ip_network.prefix())
.context("Failed to create IP network")?;
info!(
"Found network interface to scan: {} on {}",
network, pnet_iface.name
);
let source_mac = pnet_iface
.mac
.ok_or_else(|| anyhow::anyhow!("Interface has no MAC address"))?;
let discovered_devices_no_hostname = tokio::task::spawn_blocking(move || {
DiscoveredDevice::scan_with_pnet(pnet_iface, network, source_ip, source_mac)
})
.await
.context("Failed to join network scanning task")?
.context(
"Network scanning failed. ARP scanning requires root/administrator privileges \
to create raw network sockets. Please run the application with 'sudo' or as administrator. \
Alternative: You can try running as administrator User Account Control (UAC) on Windows, \
or use 'sudo' on macOS/Linux."
)?;
let lookups = discovered_devices_no_hostname
.into_iter()
.map(|mut device| {
tokio::spawn(async move {
if let Ok(ip_addr) = device.ip.parse::<IpAddr>() {
device.hostname = dns_lookup::lookup_addr(&ip_addr).ok();
}
device
})
});
let discovered_devices = join_all(lookups)
.await
.into_iter()
.filter_map(|r| r.ok())
.collect::<Vec<_>>();
info!(
"Network scan finished. Found {} devices.",
discovered_devices.len()
);
Ok(discovered_devices)
}
}