From fb014e6bb94e4e46a8ab5ff81210a86b967681ff Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 17 May 2021 22:50:56 +0200 Subject: [PATCH] Enabling local hostname lookup --- Cargo.lock | 29 +++++++++++++++ Cargo.toml | 1 + README.md | 18 ++++++++-- src/main.rs | 28 +++++++++++---- src/{arp.rs => network.rs} | 72 ++++++++++++++++++++++++++++++-------- src/utils.rs | 29 +++++++++------ 6 files changed, 142 insertions(+), 35 deletions(-) rename src/{arp.rs => network.rs} (57%) diff --git a/Cargo.lock b/Cargo.lock index ee62ebc..0b27318 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,7 @@ name = "arp-scan" version = "0.2.0" dependencies = [ "clap", + "dns-lookup", "ipnetwork", "pnet", ] @@ -44,6 +45,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "2.33.3" @@ -59,6 +66,18 @@ dependencies = [ "vec_map", ] +[[package]] +name = "dns-lookup" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb4c5ce3a7034c5eb66720bb16e9ac820e01b29032ddc06dd0fe47072acf7454" +dependencies = [ + "cfg-if", + "libc", + "socket2", + "winapi", +] + [[package]] name = "glob" version = "0.3.0" @@ -224,6 +243,16 @@ version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" +[[package]] +name = "socket2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "strsim" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index c6f44b3..0486625 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,4 @@ categories = ["command-line-utilities"] pnet = "0.28.0" ipnetwork = "0.18.0" clap = "2.33.3" +dns-lookup = "1.0.6" diff --git a/README.md b/README.md index 1756e19..353cbda 100644 --- a/README.md +++ b/README.md @@ -35,18 +35,30 @@ Enhance the scan timeout to 15 seconds (by default, 5 seconds). ## Options -### `arp-scan -l` +### Get help `-h` + +Display the main help message with all commands and available ARP scan options. + +### List interfaces `-l` List all available network interfaces. Using this option will only print a list of interfaces and exit the process. -### `arp-scan -i eth0` +### Select interface `-i eth0` Perform a scan on the network interface `eth0`. The first valid IPv4 network on this interface will be used as scan target. -### `arp-scan -t 15` +### Set timeout `-t 15` Enforce a timeout of at least 15 seconds. This timeout is a minimum value (scans may take a little more time). Default value is `5`. +### Numeric mode `-n` + +Switch to numeric mode. This will skip the local hostname resolution process and will only display IP addresses. + +### Show version `-n` + +Display the ARP scan CLI version and exits the process. + ## Contributing Feel free to suggest an improvement, report a bug, or ask something: https://github.com/saluki/arp-scan-rs/issues diff --git a/src/main.rs b/src/main.rs index 3e71722..1212e79 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -mod arp; +mod network; mod utils; use std::net::{IpAddr}; @@ -9,6 +9,9 @@ use ipnetwork::NetworkSize; use pnet::datalink; use clap::{Arg, App}; +const FIVE_HOURS: u64 = 5 * 60 * 60; +const TIMEOUT_DEFAULT: u64 = 5; + fn main() { let matches = App::new("arp-scan") @@ -20,6 +23,9 @@ fn main() { .arg( Arg::with_name("timeout").short("t").long("timeout").takes_value(true).value_name("TIMEOUT_SECONDS").help("ARP response timeout") ) + .arg( + Arg::with_name("numeric").short("n").long("numeric").takes_value(false).help("Numeric mode, no hostname resolution") + ) .arg( Arg::with_name("list").short("l").long("list").takes_value(false).help("List network interfaces") ) @@ -53,11 +59,19 @@ fn main() { } }; - let timeout_seconds: u64 = match matches.value_of("timeout") { - Some(seconds) => seconds.parse().unwrap_or(5), - None => 5 + let timeout_seconds: u64 = match matches.value_of("timeout").map(|seconds| seconds.parse::()) { + Some(seconds) => seconds.unwrap_or(TIMEOUT_DEFAULT), + None => TIMEOUT_DEFAULT }; + if timeout_seconds > FIVE_HOURS { + eprintln!("The timeout exceeds the limit (maximum {} seconds allowed)", FIVE_HOURS); + process::exit(1); + } + + // Hostnames will not be resolved in numeric mode + let resolve_hostname = !matches.is_present("numeric"); + if !utils::is_root_user() { eprintln!("Should run this binary as root"); process::exit(1); @@ -98,7 +112,7 @@ fn main() { Err(error) => panic!(error) }; - let arp_responses = thread::spawn(move || arp::receive_responses(&mut rx, timeout_seconds)); + let arp_responses = thread::spawn(move || network::receive_arp_responses(&mut rx, timeout_seconds, resolve_hostname)); let network_size: u128 = match ip_network.size() { NetworkSize::V4(x) => x.into(), @@ -109,7 +123,7 @@ fn main() { for ip_address in ip_network.iter() { if let IpAddr::V4(ipv4_address) = ip_address { - arp::send_request(&mut tx, selected_interface, ipv4_address); + network::send_arp_request(&mut tx, selected_interface, ipv4_address); } } @@ -118,5 +132,5 @@ fn main() { process::exit(1); }); - utils::display_scan_results(final_result); + utils::display_scan_results(final_result, resolve_hostname); } diff --git a/src/arp.rs b/src/network.rs similarity index 57% rename from src/arp.rs rename to src/network.rs index ea89317..2f8a63b 100644 --- a/src/arp.rs +++ b/src/network.rs @@ -2,13 +2,25 @@ use std::process; use std::net::{IpAddr, Ipv4Addr}; use std::time::Instant; use std::collections::HashMap; +use dns_lookup::lookup_addr; use pnet::datalink::{MacAddr, NetworkInterface, DataLinkSender, DataLinkReceiver}; use pnet::packet::{MutablePacket, Packet}; use pnet::packet::ethernet::{EthernetPacket, MutableEthernetPacket, EtherTypes}; use pnet::packet::arp::{MutableArpPacket, ArpOperations, ArpHardwareTypes, ArpPacket}; -pub fn send_request(tx: &mut Box, interface: &NetworkInterface, target_ip: Ipv4Addr) { +pub struct TargetDetails { + pub ipv4: Ipv4Addr, + pub mac: MacAddr, + pub hostname: Option +} + +/** + * Send a single ARP request - using a datalink-layer sender, a given network + * interface and a target IPv4 address. The ARP request will be broadcasted to + * the whole local network with the first valid IPv4 address on the interface. + */ +pub fn send_arp_request(tx: &mut Box, interface: &NetworkInterface, target_ip: Ipv4Addr) { let mut ethernet_buffer = [0u8; 42]; let mut ethernet_packet = MutableEthernetPacket::new(&mut ethernet_buffer).unwrap(); @@ -51,9 +63,15 @@ pub fn send_request(tx: &mut Box, interface: &NetworkInterfa tx.send_to(ðernet_packet.to_immutable().packet(), Some(interface.clone())); } -pub fn receive_responses(rx: &mut Box, timeout_seconds: u64) -> HashMap { +/** + * Wait at least N seconds and receive ARP network responses. The main + * downside of this function is the blocking nature of the datalink receiver: + * when the N seconds are elapsed, the receiver loop will therefore only stop + * on the next received frame. + */ +pub fn receive_arp_responses(rx: &mut Box, timeout_seconds: u64, resolve_hostname: bool) -> Vec { - let mut discover_map: HashMap = HashMap::new(); + let mut discover_map: HashMap = HashMap::new(); let start_recording = Instant::now(); loop { @@ -83,18 +101,44 @@ pub fn receive_responses(rx: &mut Box, timeout_seconds: u6 let arp_packet = ArpPacket::new(&arp_buffer[MutableEthernetPacket::minimum_packet_size()..]); - match arp_packet { - Some(arp) => { - - let sender_ipv4 = arp.get_sender_proto_addr(); - let sender_mac = arp.get_sender_hw_addr(); - - discover_map.insert(sender_ipv4, sender_mac); + if let Some(arp) = arp_packet { - }, - _ => () + let sender_ipv4 = arp.get_sender_proto_addr(); + let sender_mac = arp.get_sender_hw_addr(); + + discover_map.insert(sender_ipv4, TargetDetails { + ipv4: sender_ipv4, + mac: sender_mac, + hostname: None + }); } } - return discover_map; -} \ No newline at end of file + discover_map.into_iter().map(|(_, mut target_details)| { + + if resolve_hostname { + target_details.hostname = find_hostname(target_details.ipv4); + } + + target_details + + }).collect() +} + +fn find_hostname(ipv4: Ipv4Addr) -> Option { + + let ip: IpAddr = ipv4.into(); + match lookup_addr(&ip) { + Ok(hostname) => { + + // The 'lookup_addr' function returns an IP address if no hostname + // was found. If this is the case, we prefer switching to None. + if let Ok(_) = hostname.parse::() { + return None; + } + + Some(hostname) + }, + Err(_) => None + } +} diff --git a/src/utils.rs b/src/utils.rs index e1d2fdf..e49b173 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,7 +1,6 @@ -use std::net::Ipv4Addr; -use std::collections::HashMap; +use pnet::datalink::NetworkInterface; -use pnet::datalink::{MacAddr, NetworkInterface}; +use crate::network::TargetDetails; pub fn is_root_user() -> bool { std::env::var("USER").unwrap_or(String::from("")) == String::from("root") @@ -18,19 +17,27 @@ pub fn show_interfaces(interfaces: &Vec) { Some(mac_address) => format!("{}", mac_address), None => "No MAC address".to_string() }; - println!("{: <12} {: <7} {}", interface.name, up_text, mac_text); + println!("{: <17} {: <7} {}", interface.name, up_text, mac_text); } } -pub fn display_scan_results(final_result: HashMap) { +pub fn display_scan_results(mut final_result: Vec, resolve_hostname: bool) { + + final_result.sort_by_key(|item| item.ipv4); - let mut sorted_map: Vec<(Ipv4Addr, MacAddr)> = final_result.into_iter().collect(); - sorted_map.sort_by_key(|x| x.0); println!(""); - println!("| IPv4 | MAC |"); - println!("|-----------------|-------------------|"); - for (result_ipv4, result_mac) in sorted_map { - println!("| {: <15} | {: <18} |", &result_ipv4, &result_mac); + println!("| IPv4 | MAC | Hostname |"); + println!("|-----------------|-------------------|-----------------------|"); + + for result_item in final_result { + + let hostname = match result_item.hostname { + Some(hostname) => hostname, + None if !resolve_hostname => String::from("(disabled)"), + None => String::from("") + }; + println!("| {: <15} | {: <18} | {: <21} |", result_item.ipv4, result_item.mac, hostname); } + println!(""); } \ No newline at end of file