use anyhow::{Result, anyhow};
use std::net::{Ipv4Addr, IpAddr};
use std::time::{Duration, Instant};
use crate::common::cidr_to_ip_range;
pub fn run_arp_scan(interface: &str, network: &str, timeout_ms: u64) -> Result<()> {
if interface == "list-interfaces" {
return list_network_interfaces();
}
if network == "list-ranges" {
return list_common_network_ranges();
}
println!("ARP Scan on interface: {}", interface);
println!("Network range: {}", network);
let ip_range = cidr_to_ip_range(network)?;
println!("Scanning {} hosts...", ip_range.len());
let scan_start = Instant::now();
let mut found_hosts = 0;
for ip in ip_range {
let start_time = Instant::now();
let mut host_responded = false;
let mut responding_port = None;
let mut response_type = "";
let ports_to_try = vec![
20, 21, 22, 23, 25, 53, 80, 110, 115, 119, 135, 139, 143, 161, 194, 389, 443,
445, 465, 587, 636, 993, 995, 1080, 1433, 1521, 1720, 1723, 2049, 2086,
2087, 2095, 2096, 2121, 2200, 3306, 3389, 3478, 3690, 4444, 5000, 5060,
5061, 5432, 5900, 6000, 6379, 6667, 7000, 8000, 8008, 8080, 8443, 8888,
9000, 9001, 9090, 9100, 9418, 9999, 10000, 32768, 49152, 49153, 49154,
49155, 49156, 49157
];
for port in ports_to_try {
match std::net::TcpStream::connect_timeout(
&std::net::SocketAddr::from((ip, port)),
Duration::from_millis(timeout_ms)
) {
Ok(_) => {
host_responded = true;
responding_port = Some(port);
response_type = "Connected";
break;
}
Err(e) => {
let error_kind = e.kind();
if error_kind == std::io::ErrorKind::ConnectionRefused {
host_responded = true;
responding_port = Some(port);
response_type = "Connection refused (host exists)";
break;
} else if error_kind == std::io::ErrorKind::ConnectionReset {
host_responded = true;
responding_port = Some(port);
response_type = "Connection reset (host exists)";
break;
} else if error_kind == std::io::ErrorKind::TimedOut {
continue;
}
}
}
}
if host_responded {
found_hosts += 1;
let service_name = get_service_name(responding_port.unwrap_or(0));
println!(" {} - Host active ({} port {} {}) - {:.2}ms",
ip, service_name, responding_port.unwrap_or(0), response_type, start_time.elapsed().as_millis());
}
if !host_responded {
}
std::thread::sleep(Duration::from_millis(10));
}
let scan_duration = scan_start.elapsed();
println!("\nScan completed in {:.2?}. Found {} responsive hosts.", scan_duration, found_hosts);
println!("Note: This scan uses TCP port connectivity testing instead of true ARP packets.");
println!("Results may vary based on network configuration and firewall settings.");
println!("For true ARP scanning, platform-specific raw socket access would be required.");
println!("Use 'arp-scan list-interfaces any' to see available network interfaces.");
println!("Use 'arp-scan any list-ranges' to see common network ranges.");
Ok(())
}
fn list_network_interfaces() -> Result<()> {
println!("Available Network Interfaces:");
println!("=========================");
let mut interfaces = Vec::new();
match get_local_ip_addresses() {
Ok(ips) => {
for ip in ips {
let interface_info = NetworkInterfaceInfo {
name: guess_interface_name(&ip),
ip: Some(ip),
mac: None,
status: "Active",
interface_type: guess_interface_type_from_ip(&ip),
};
interfaces.push(interface_info);
}
}
Err(_) => {}
}
add_common_interfaces(&mut interfaces);
interfaces.push(NetworkInterfaceInfo {
name: "lo".to_string(),
ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
mac: Some(MacAddr::zero()),
status: "Active",
interface_type: "Loopback",
});
interfaces.sort_by(|a, b| a.name.cmp(&b.name));
interfaces.dedup_by(|a, b| a.name == b.name);
if interfaces.is_empty() {
println!("No network interfaces found.");
println!("This might be due to permissions or platform limitations.");
println!("Try running with administrator/root privileges for better results.");
} else {
println!("Found {} network interface(s):", interfaces.len());
println!();
for (index, interface) in interfaces.iter().enumerate() {
println!("{}. {}", index + 1, interface.name);
println!(" Status: {}", interface.status);
if let Some(ip) = &interface.ip {
println!(" IP Address: {}", ip);
if let IpAddr::V4(ipv4) = ip {
let suggested_range = suggest_network_range(*ipv4);
println!(" └─ Suggested range: {}", suggested_range);
if ipv4.octets()[0] == 192 && ipv4.octets()[1] == 168 {
let prev_subnet = ipv4.octets()[2].saturating_sub(1);
let current_subnet = ipv4.octets()[2];
println!(" └─ Test ranges: {}.{}.{}.0/24, {}.{}.{}.0/28",
ipv4.octets()[0], ipv4.octets()[1], prev_subnet,
ipv4.octets()[0], ipv4.octets()[1], current_subnet);
}
}
}
if let Some(mac) = &interface.mac {
if *mac != MacAddr::zero() {
println!(" MAC Address: {}", mac);
}
}
println!(" Type: {}", interface.interface_type);
println!();
}
}
println!("Platform-Specific Interface Names:");
println!(" Windows: Ethernet, Wi-Fi, \"Local Area Connection\", \"Wireless Network Connection\"");
println!(" Linux: eth0, wlan0, enp0s3, wlp2s0");
println!(" macOS: en0, en1, bridge0");
println!();
println!("Usage Examples:");
println!(" arp-scan eth0 192.168.1.0/24 # Scan using interface name");
println!(" arp-scan \"Wi-Fi\" 10.0.0.0/24 # Scan with quoted interface name (Windows)");
println!(" arp-scan any 172.20.0.0/24 # Use any available interface");
Ok(())
}
struct NetworkInterfaceInfo {
name: String,
ip: Option<IpAddr>,
mac: Option<MacAddr>,
status: &'static str,
interface_type: &'static str,
}
fn add_common_interfaces(interfaces: &mut Vec<NetworkInterfaceInfo>) {
let common_names = vec![
("eth0", "Ethernet"),
("wlan0", "Wireless/WiFi"),
("en0", "Ethernet"),
("en1", "Wireless/WiFi"),
("Ethernet0", "Ethernet"),
("Wi-Fi", "Wireless/WiFi"),
];
for (name, interface_type) in common_names {
if !interfaces.iter().any(|i| i.name == name) {
interfaces.push(NetworkInterfaceInfo {
name: name.to_string(),
ip: None,
mac: None,
status: "Unknown",
interface_type: interface_type,
});
}
}
}
fn guess_interface_name(ip: &IpAddr) -> String {
match ip {
IpAddr::V4(ipv4) if ipv4.is_loopback() => "lo".to_string(),
IpAddr::V4(ipv4) => {
match ipv4.octets() {
[192, 168, _, _] => "eth0".to_string(), [10, _, _, _] => "eth0".to_string(), [172, 16..=31, _, _] => "eth0".to_string(), [127, _, _, _] => "lo".to_string(), _ => "eth0".to_string(), }
}
IpAddr::V6(_) => "eth0".to_string(),
}
}
fn guess_interface_type_from_ip(ip: &IpAddr) -> &'static str {
match ip {
IpAddr::V4(ipv4) if ipv4.is_loopback() => "Loopback",
IpAddr::V4(ipv4) => {
match ipv4.octets() {
[192, 168, _, _] => "Home/Office LAN",
[10, _, _, _] => "Corporate/Private",
[172, 16..=31, _, _] => "Private Network",
[127, _, _, _] => "Loopback",
_ => "Unknown",
}
}
IpAddr::V6(_) => "IPv6",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct MacAddr {
bytes: [u8; 6],
}
impl MacAddr {
const fn zero() -> MacAddr {
MacAddr { bytes: [0, 0, 0, 0, 0, 0] }
}
}
impl std::fmt::Display for MacAddr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
self.bytes[0], self.bytes[1], self.bytes[2],
self.bytes[3], self.bytes[4], self.bytes[5])
}
}
fn get_interface_type(interface_name: &str) -> &'static str {
let name_lower = interface_name.to_lowercase();
if name_lower.contains("eth") || name_lower.contains("enp") {
"Ethernet"
} else if name_lower.contains("wlan") || name_lower.contains("wifi") || name_lower.contains("wl") {
"Wireless/WiFi"
} else if name_lower.contains("lo") || name_lower.contains("loopback") {
"Loopback"
} else if name_lower.contains("vm") || name_lower.contains("virt") {
"Virtual Machine"
} else if name_lower.contains("docker") || name_lower.contains("bridge") {
"Container/Bridge"
} else if name_lower.contains("tun") || name_lower.contains("tap") {
"VPN/Tunnel"
} else if name_lower.contains("ppp") {
"Point-to-Point"
} else {
"Unknown"
}
}
fn list_common_network_ranges() -> Result<()> {
println!("Common Network Ranges:");
println!("====================");
println!();
println!("Private Network Ranges (RFC 1918):");
println!(" 192.168.0.0/16 - Private networks (192.168.0.0 - 192.168.255.255)");
println!(" 192.168.1.0/24 - Common home/office network");
println!(" 192.168.0.0/24 - Another common home network");
println!(" 10.0.0.0/8 - Private networks (10.0.0.0 - 10.255.255.255)");
println!(" 10.0.0.0/24 - Large private networks");
println!(" 172.16.0.0/12 - Private networks (172.16.0.0 - 172.31.255.255)");
println!(" 172.16.0.0/16 - Medium private networks");
println!();
println!("Class C Subnets (small networks):");
println!(" 192.168.1.0/28 - 14 hosts (192.168.1.1 - 192.168.1.14)");
println!(" 192.168.1.0/27 - 30 hosts (192.168.1.1 - 192.168.1.30)");
println!(" 192.168.1.0/26 - 62 hosts (192.168.1.1 - 192.168.1.62)");
println!(" 192.168.1.0/25 - 126 hosts (192.168.1.1 - 192.168.1.126)");
println!();
println!("Public Network Ranges (for testing):");
println!(" 8.8.8.0/24 - Google DNS network");
println!(" 1.1.1.0/24 - Cloudflare DNS network");
println!(" 208.67.222.0/24 - OpenDNS network");
println!();
println!("Examples:");
println!(" arp-scan eth0 192.168.1.0/24 # Scan home network");
println!(" arp-scan wlan0 10.0.0.0/24 # Scan large private network");
println!(" arp-scan eth0 192.168.1.0/28 # Quick scan of 14 hosts");
Ok(())
}
fn get_local_ip_addresses() -> Result<Vec<IpAddr>> {
let mut ips = Vec::new();
match std::net::TcpStream::connect_timeout(
&std::net::SocketAddr::from(([8, 8, 8, 8], 80)),
Duration::from_secs(5)
) {
Ok(stream) => {
if let Ok(local_addr) = stream.local_addr() {
ips.push(local_addr.ip());
}
}
Err(_) => {
ips.push(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
}
}
match std::net::UdpSocket::bind("0.0.0.0:0") {
Ok(socket) => {
match socket.connect("8.8.8.8:80") {
Ok(_) => {
if let Ok(local_addr) = socket.local_addr() {
ips.push(local_addr.ip());
}
}
Err(_) => {}
}
}
Err(_) => {}
}
ips.sort();
ips.dedup();
Ok(ips)
}
fn suggest_network_range(ipv4: Ipv4Addr) -> String {
let octets = ipv4.octets();
match octets[0] {
192 if octets[1] == 168 => {
format!("{}.{}.0.0/24", octets[0], octets[1])
}
10 => {
format!("{}.0.0.0/24", octets[0])
}
172 if octets[1] >= 16 && octets[1] <= 31 => {
format!("{}.{}.0.0/24", octets[0], octets[1])
}
_ => {
format!("{}.{}.0.0/24", octets[0], octets[1])
}
}
}
fn get_service_name(port: u16) -> &'static str {
match port {
20 => "FTP data",
21 => "FTP control",
22 => "SSH",
23 => "Telnet",
25 => "SMTP",
53 => "DNS",
80 => "HTTP",
110 => "POP3",
115 => "SFTP",
119 => "NNTP",
135 => "RPC",
139 => "NetBIOS",
143 => "IMAP",
161 => "SNMP",
194 => "IRC",
389 => "LDAP",
443 => "HTTPS",
445 => "SMB",
465 => "SMTPS",
587 => "SMTP",
636 => "LDAPS",
993 => "IMAPS",
995 => "POP3S",
1080 => "SOCKS",
1433 => "MSSQL",
1521 => "Oracle",
1720 => "H.323",
1723 => "PPTP",
2049 => "NFS",
2086 => "WHM",
2087 => "WHM SSL",
2095 => "cPanel",
2096 => "cPanel SSL",
2121 => "CCCam",
2200 => "AMQP",
3306 => "MySQL",
3389 => "RDP",
3478 => "STUN",
3690 => "Subversion",
4444 => "KRB524",
5000 => "FLV",
5060 => "SIP",
5061 => "SIPS",
5432 => "PostgreSQL",
5900 => "VNC",
6000 => "X11",
6379 => "Redis",
6667 => "IRC",
7000 => "MSSQL",
8000 => "HTTP alt",
8008 => "HTTP alt",
8080 => "HTTP alt",
8443 => "HTTPS alt",
8888 => "HTTP alt",
9000 => "HTTP alt",
9001 => "HTTP alt",
9090 => "HTTP alt",
9100 => "JetDirect",
9418 => "Git",
9999 => "HTTP alt",
10000 => "SNC",
32768 => "FileMaker",
49152 => "Dynamic",
49153 => "Dynamic",
49154 => "Dynamic",
49155 => "Dynamic",
49156 => "Dynamic",
49157 => "Dynamic",
_ => "Unknown",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cidr_to_ip_range() {
let ips = cidr_to_ip_range("192.168.1.0/30").unwrap();
assert_eq!(ips.len(), 4);
assert_eq!(ips[0], Ipv4Addr::new(192, 168, 1, 0));
assert_eq!(ips[1], Ipv4Addr::new(192, 168, 1, 1));
assert_eq!(ips[2], Ipv4Addr::new(192, 168, 1, 2));
assert_eq!(ips[3], Ipv4Addr::new(192, 168, 1, 3));
}
}