Skip to content

Commit

Permalink
Enabling local hostname lookup
Browse files Browse the repository at this point in the history
  • Loading branch information
Saluki committed May 17, 2021
1 parent 18ef400 commit fb014e6
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 35 deletions.
29 changes: 29 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
28 changes: 21 additions & 7 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
mod arp;
mod network;
mod utils;

use std::net::{IpAddr};
Expand All @@ -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")
Expand All @@ -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")
)
Expand Down Expand Up @@ -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::<u64>()) {
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);
Expand Down Expand Up @@ -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(),
Expand All @@ -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);
}
}

Expand All @@ -118,5 +132,5 @@ fn main() {
process::exit(1);
});

utils::display_scan_results(final_result);
utils::display_scan_results(final_result, resolve_hostname);
}
72 changes: 58 additions & 14 deletions src/arp.rs → src/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn DataLinkSender>, interface: &NetworkInterface, target_ip: Ipv4Addr) {
pub struct TargetDetails {
pub ipv4: Ipv4Addr,
pub mac: MacAddr,
pub hostname: Option<String>
}

/**
* 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<dyn DataLinkSender>, interface: &NetworkInterface, target_ip: Ipv4Addr) {

let mut ethernet_buffer = [0u8; 42];
let mut ethernet_packet = MutableEthernetPacket::new(&mut ethernet_buffer).unwrap();
Expand Down Expand Up @@ -51,9 +63,15 @@ pub fn send_request(tx: &mut Box<dyn DataLinkSender>, interface: &NetworkInterfa
tx.send_to(&ethernet_packet.to_immutable().packet(), Some(interface.clone()));
}

pub fn receive_responses(rx: &mut Box<dyn DataLinkReceiver>, timeout_seconds: u64) -> HashMap<Ipv4Addr, MacAddr> {
/**
* 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<dyn DataLinkReceiver>, timeout_seconds: u64, resolve_hostname: bool) -> Vec<TargetDetails> {

let mut discover_map: HashMap<Ipv4Addr, MacAddr> = HashMap::new();
let mut discover_map: HashMap<Ipv4Addr, TargetDetails> = HashMap::new();
let start_recording = Instant::now();

loop {
Expand Down Expand Up @@ -83,18 +101,44 @@ pub fn receive_responses(rx: &mut Box<dyn DataLinkReceiver>, 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;
}
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<String> {

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::<IpAddr>() {
return None;
}

Some(hostname)
},
Err(_) => None
}
}
29 changes: 18 additions & 11 deletions src/utils.rs
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -18,19 +17,27 @@ pub fn show_interfaces(interfaces: &Vec<NetworkInterface>) {
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<Ipv4Addr, MacAddr>) {
pub fn display_scan_results(mut final_result: Vec<TargetDetails>, 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!("");
}

0 comments on commit fb014e6

Please sign in to comment.