Skip to content

Commit fb014e6

Browse files
committed
Enabling local hostname lookup
1 parent 18ef400 commit fb014e6

File tree

6 files changed

+142
-35
lines changed

6 files changed

+142
-35
lines changed

Cargo.lock

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ categories = ["command-line-utilities"]
1515
pnet = "0.28.0"
1616
ipnetwork = "0.18.0"
1717
clap = "2.33.3"
18+
dns-lookup = "1.0.6"

README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,30 @@ Enhance the scan timeout to 15 seconds (by default, 5 seconds).
3535

3636
## Options
3737

38-
### `arp-scan -l`
38+
### Get help `-h`
39+
40+
Display the main help message with all commands and available ARP scan options.
41+
42+
### List interfaces `-l`
3943

4044
List all available network interfaces. Using this option will only print a list of interfaces and exit the process.
4145

42-
### `arp-scan -i eth0`
46+
### Select interface `-i eth0`
4347

4448
Perform a scan on the network interface `eth0`. The first valid IPv4 network on this interface will be used as scan target.
4549

46-
### `arp-scan -t 15`
50+
### Set timeout `-t 15`
4751

4852
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`.
4953

54+
### Numeric mode `-n`
55+
56+
Switch to numeric mode. This will skip the local hostname resolution process and will only display IP addresses.
57+
58+
### Show version `-n`
59+
60+
Display the ARP scan CLI version and exits the process.
61+
5062
## Contributing
5163

5264
Feel free to suggest an improvement, report a bug, or ask something: https://github.com/saluki/arp-scan-rs/issues

src/main.rs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
mod arp;
1+
mod network;
22
mod utils;
33

44
use std::net::{IpAddr};
@@ -9,6 +9,9 @@ use ipnetwork::NetworkSize;
99
use pnet::datalink;
1010
use clap::{Arg, App};
1111

12+
const FIVE_HOURS: u64 = 5 * 60 * 60;
13+
const TIMEOUT_DEFAULT: u64 = 5;
14+
1215
fn main() {
1316

1417
let matches = App::new("arp-scan")
@@ -20,6 +23,9 @@ fn main() {
2023
.arg(
2124
Arg::with_name("timeout").short("t").long("timeout").takes_value(true).value_name("TIMEOUT_SECONDS").help("ARP response timeout")
2225
)
26+
.arg(
27+
Arg::with_name("numeric").short("n").long("numeric").takes_value(false).help("Numeric mode, no hostname resolution")
28+
)
2329
.arg(
2430
Arg::with_name("list").short("l").long("list").takes_value(false).help("List network interfaces")
2531
)
@@ -53,11 +59,19 @@ fn main() {
5359
}
5460
};
5561

56-
let timeout_seconds: u64 = match matches.value_of("timeout") {
57-
Some(seconds) => seconds.parse().unwrap_or(5),
58-
None => 5
62+
let timeout_seconds: u64 = match matches.value_of("timeout").map(|seconds| seconds.parse::<u64>()) {
63+
Some(seconds) => seconds.unwrap_or(TIMEOUT_DEFAULT),
64+
None => TIMEOUT_DEFAULT
5965
};
6066

67+
if timeout_seconds > FIVE_HOURS {
68+
eprintln!("The timeout exceeds the limit (maximum {} seconds allowed)", FIVE_HOURS);
69+
process::exit(1);
70+
}
71+
72+
// Hostnames will not be resolved in numeric mode
73+
let resolve_hostname = !matches.is_present("numeric");
74+
6175
if !utils::is_root_user() {
6276
eprintln!("Should run this binary as root");
6377
process::exit(1);
@@ -98,7 +112,7 @@ fn main() {
98112
Err(error) => panic!(error)
99113
};
100114

101-
let arp_responses = thread::spawn(move || arp::receive_responses(&mut rx, timeout_seconds));
115+
let arp_responses = thread::spawn(move || network::receive_arp_responses(&mut rx, timeout_seconds, resolve_hostname));
102116

103117
let network_size: u128 = match ip_network.size() {
104118
NetworkSize::V4(x) => x.into(),
@@ -109,7 +123,7 @@ fn main() {
109123
for ip_address in ip_network.iter() {
110124

111125
if let IpAddr::V4(ipv4_address) = ip_address {
112-
arp::send_request(&mut tx, selected_interface, ipv4_address);
126+
network::send_arp_request(&mut tx, selected_interface, ipv4_address);
113127
}
114128
}
115129

@@ -118,5 +132,5 @@ fn main() {
118132
process::exit(1);
119133
});
120134

121-
utils::display_scan_results(final_result);
135+
utils::display_scan_results(final_result, resolve_hostname);
122136
}

src/arp.rs renamed to src/network.rs

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,25 @@ use std::process;
22
use std::net::{IpAddr, Ipv4Addr};
33
use std::time::Instant;
44
use std::collections::HashMap;
5+
use dns_lookup::lookup_addr;
56

67
use pnet::datalink::{MacAddr, NetworkInterface, DataLinkSender, DataLinkReceiver};
78
use pnet::packet::{MutablePacket, Packet};
89
use pnet::packet::ethernet::{EthernetPacket, MutableEthernetPacket, EtherTypes};
910
use pnet::packet::arp::{MutableArpPacket, ArpOperations, ArpHardwareTypes, ArpPacket};
1011

11-
pub fn send_request(tx: &mut Box<dyn DataLinkSender>, interface: &NetworkInterface, target_ip: Ipv4Addr) {
12+
pub struct TargetDetails {
13+
pub ipv4: Ipv4Addr,
14+
pub mac: MacAddr,
15+
pub hostname: Option<String>
16+
}
17+
18+
/**
19+
* Send a single ARP request - using a datalink-layer sender, a given network
20+
* interface and a target IPv4 address. The ARP request will be broadcasted to
21+
* the whole local network with the first valid IPv4 address on the interface.
22+
*/
23+
pub fn send_arp_request(tx: &mut Box<dyn DataLinkSender>, interface: &NetworkInterface, target_ip: Ipv4Addr) {
1224

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

54-
pub fn receive_responses(rx: &mut Box<dyn DataLinkReceiver>, timeout_seconds: u64) -> HashMap<Ipv4Addr, MacAddr> {
66+
/**
67+
* Wait at least N seconds and receive ARP network responses. The main
68+
* downside of this function is the blocking nature of the datalink receiver:
69+
* when the N seconds are elapsed, the receiver loop will therefore only stop
70+
* on the next received frame.
71+
*/
72+
pub fn receive_arp_responses(rx: &mut Box<dyn DataLinkReceiver>, timeout_seconds: u64, resolve_hostname: bool) -> Vec<TargetDetails> {
5573

56-
let mut discover_map: HashMap<Ipv4Addr, MacAddr> = HashMap::new();
74+
let mut discover_map: HashMap<Ipv4Addr, TargetDetails> = HashMap::new();
5775
let start_recording = Instant::now();
5876

5977
loop {
@@ -83,18 +101,44 @@ pub fn receive_responses(rx: &mut Box<dyn DataLinkReceiver>, timeout_seconds: u6
83101

84102
let arp_packet = ArpPacket::new(&arp_buffer[MutableEthernetPacket::minimum_packet_size()..]);
85103

86-
match arp_packet {
87-
Some(arp) => {
88-
89-
let sender_ipv4 = arp.get_sender_proto_addr();
90-
let sender_mac = arp.get_sender_hw_addr();
91-
92-
discover_map.insert(sender_ipv4, sender_mac);
104+
if let Some(arp) = arp_packet {
93105

94-
},
95-
_ => ()
106+
let sender_ipv4 = arp.get_sender_proto_addr();
107+
let sender_mac = arp.get_sender_hw_addr();
108+
109+
discover_map.insert(sender_ipv4, TargetDetails {
110+
ipv4: sender_ipv4,
111+
mac: sender_mac,
112+
hostname: None
113+
});
96114
}
97115
}
98116

99-
return discover_map;
100-
}
117+
discover_map.into_iter().map(|(_, mut target_details)| {
118+
119+
if resolve_hostname {
120+
target_details.hostname = find_hostname(target_details.ipv4);
121+
}
122+
123+
target_details
124+
125+
}).collect()
126+
}
127+
128+
fn find_hostname(ipv4: Ipv4Addr) -> Option<String> {
129+
130+
let ip: IpAddr = ipv4.into();
131+
match lookup_addr(&ip) {
132+
Ok(hostname) => {
133+
134+
// The 'lookup_addr' function returns an IP address if no hostname
135+
// was found. If this is the case, we prefer switching to None.
136+
if let Ok(_) = hostname.parse::<IpAddr>() {
137+
return None;
138+
}
139+
140+
Some(hostname)
141+
},
142+
Err(_) => None
143+
}
144+
}

src/utils.rs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
use std::net::Ipv4Addr;
2-
use std::collections::HashMap;
1+
use pnet::datalink::NetworkInterface;
32

4-
use pnet::datalink::{MacAddr, NetworkInterface};
3+
use crate::network::TargetDetails;
54

65
pub fn is_root_user() -> bool {
76
std::env::var("USER").unwrap_or(String::from("")) == String::from("root")
@@ -18,19 +17,27 @@ pub fn show_interfaces(interfaces: &Vec<NetworkInterface>) {
1817
Some(mac_address) => format!("{}", mac_address),
1918
None => "No MAC address".to_string()
2019
};
21-
println!("{: <12} {: <7} {}", interface.name, up_text, mac_text);
20+
println!("{: <17} {: <7} {}", interface.name, up_text, mac_text);
2221
}
2322
}
2423

25-
pub fn display_scan_results(final_result: HashMap<Ipv4Addr, MacAddr>) {
24+
pub fn display_scan_results(mut final_result: Vec<TargetDetails>, resolve_hostname: bool) {
25+
26+
final_result.sort_by_key(|item| item.ipv4);
2627

27-
let mut sorted_map: Vec<(Ipv4Addr, MacAddr)> = final_result.into_iter().collect();
28-
sorted_map.sort_by_key(|x| x.0);
2928
println!("");
30-
println!("| IPv4 | MAC |");
31-
println!("|-----------------|-------------------|");
32-
for (result_ipv4, result_mac) in sorted_map {
33-
println!("| {: <15} | {: <18} |", &result_ipv4, &result_mac);
29+
println!("| IPv4 | MAC | Hostname |");
30+
println!("|-----------------|-------------------|-----------------------|");
31+
32+
for result_item in final_result {
33+
34+
let hostname = match result_item.hostname {
35+
Some(hostname) => hostname,
36+
None if !resolve_hostname => String::from("(disabled)"),
37+
None => String::from("")
38+
};
39+
println!("| {: <15} | {: <18} | {: <21} |", result_item.ipv4, result_item.mac, hostname);
3440
}
41+
3542
println!("");
3643
}

0 commit comments

Comments
 (0)