From f876bb39c68f51402a488a303b26f04d552b67c4 Mon Sep 17 00:00:00 2001 From: Saluki Date: Fri, 7 May 2021 01:11:29 +0200 Subject: [PATCH 001/121] Adding rust dependencies --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c324985..530b0e2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # ARP scanner CLI [![Build Status](https://saluki.semaphoreci.com/badges/arp-scan-rs/branches/master.svg?style=shields)](https://saluki.semaphoreci.com/projects/arp-scan-rs) +[![dependency status](https://deps.rs/repo/github/Saluki/arp-scan-rs/status.svg)](https://deps.rs/repo/github/Saluki/arp-scan-rs) Find all hosts in your local network using this fast ARP scanner. The CLI is written in Rust and provides a minimal scanner that finds all hosts using the ARP protocol. Inspired by the awesome [arp-scan project](https://github.com/royhills/arp-scan). From a0ec05fb3c558a0c977688387acc0d01bbdfeeaa Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 10 May 2021 18:48:33 +0200 Subject: [PATCH 002/121] Unique scan results in unified table --- src/main.rs | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5d4f460..af84e35 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use pnet::packet::ethernet::{MutableEthernetPacket, EtherTypes}; use pnet::packet::MutablePacket; use pnet::packet::ethernet::EthernetPacket; use pnet::packet::Packet; +use pnet::util::MacAddr; use clap::{Arg, App}; @@ -11,6 +12,7 @@ use std::net::{IpAddr, Ipv4Addr}; use std::process; use std::thread; use std::time::Instant; +use std::collections::HashMap; fn is_root_user() -> bool { std::env::var("USER").unwrap_or(String::from("")) == String::from("root") @@ -39,8 +41,8 @@ fn main() { }; let timeout_seconds: u64 = match matches.value_of("timeout") { - Some(seconds) => seconds.parse().unwrap_or(10), - None => 10 + Some(seconds) => seconds.parse().unwrap_or(5), + None => 5 }; // ---------------------- @@ -67,6 +69,7 @@ fn main() { process::exit(1); } + println!(""); println!("Selected interface {} with IP {}", selected_interface.name, ip_network); // ----------------------- @@ -79,6 +82,7 @@ fn main() { let responses = thread::spawn(move || { + let mut discover_map: HashMap = HashMap::new(); let start_recording = Instant::now(); loop { @@ -109,14 +113,23 @@ fn main() { let arp_packet = ArpPacket::new(&arp_buffer[MutableEthernetPacket::minimum_packet_size()..]); match arp_packet { - Some(arp) => println!("{} - {}", arp.get_sender_proto_addr(), arp.get_sender_hw_addr()), + 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); + + }, _ => () } } + return discover_map; + }); - println!("Sending {:?} ARP requests to network", ip_network.size()); + println!("Sending {:?} ARP requests to network ({}s timeout)", ip_network.size(), timeout_seconds); for ip_address in ip_network.iter() { if let IpAddr::V4(ipv4_address) = ip_address { @@ -126,10 +139,20 @@ fn main() { // ------------------ - responses.join().unwrap_or_else(|error| { + let final_result = responses.join().unwrap_or_else(|error| { eprintln!("Failed to close receive thread ({:?})", error); process::exit(1); }); + + 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!(""); } fn send_arp_request(tx: &mut Box, interface: &datalink::NetworkInterface, target_ip: Ipv4Addr) { From 078cc86869d46c0e902d3a6718ededa1e935b362 Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 10 May 2021 18:58:14 +0200 Subject: [PATCH 003/121] Update Cargo details before publish --- Cargo.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index ae7551a..1da9eae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,15 @@ [package] name = "arp-scan" +description = "A minimalistic ARP scan tool" license = "AGPL-3.0-or-later" version = "0.1.0" authors = ["Saluki"] edition = "2018" +readme = "README.md" +homepage = "https://github.com/Saluki/arp-scan-rs" +repository = "https://github.com/Saluki/arp-scan-rs" +keywords = ["arp", "scan", "network", "security"] +categories = ["command-line-utilities"] [dependencies] pnet = "0.27.2" From 5db5e0ed4b11a86059342e3327ea9a03be346103 Mon Sep 17 00:00:00 2001 From: Saluki Date: Wed, 12 May 2021 12:27:46 +0200 Subject: [PATCH 004/121] Allow to show network interfaces --- src/main.rs | 53 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/src/main.rs b/src/main.rs index af84e35..fe00a95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use pnet::packet::MutablePacket; use pnet::packet::ethernet::EthernetPacket; use pnet::packet::Packet; use pnet::util::MacAddr; +use pnet::datalink::NetworkInterface; use clap::{Arg, App}; @@ -18,24 +19,53 @@ fn is_root_user() -> bool { std::env::var("USER").unwrap_or(String::from("")) == String::from("root") } -fn main() { - - if !is_root_user() { - eprintln!("Should run this binary as root"); - process::exit(1); +fn show_interfaces(interfaces: &Vec) { + + for interface in interfaces.iter() { + let up_text = match interface.is_up() { + true => "UP", + false => "DOWN" + }; + let mac_text = match interface.mac { + Some(mac_address) => format!("{}", mac_address), + None => "No MAC address".to_string() + }; + println!("{: <12} {: <7} {}", interface.name, up_text, mac_text); } +} + +fn main() { let matches = App::new("arp-scan") .version("0.1") .about("A minimalistic ARP scan tool written in Rust") - .arg(Arg::with_name("interface").short("i").long("interface").takes_value(true).value_name("INTERFACE_NAME").help("Network interface")) - .arg(Arg::with_name("timeout").short("t").long("timeout").takes_value(true).value_name("TIMEOUT_SECONDS").help("ARP response timeout")) + .arg( + Arg::with_name("interface").short("i").long("interface").takes_value(true).value_name("INTERFACE_NAME").help("Network interface") + ) + .arg( + Arg::with_name("timeout").short("t").long("timeout").takes_value(true).value_name("TIMEOUT_SECONDS").help("ARP response timeout") + ) + .arg( + Arg::with_name("list").short("l").long("list").takes_value(false).help("List network interfaces") + ) .get_matches(); + // ---------------------- + + let interfaces = datalink::interfaces(); + + if matches.is_present("list") { + show_interfaces(&interfaces); + process::exit(0); + } + + // ---------------------- + let interface_name = match matches.value_of("interface") { Some(name) => name, None => { - eprintln!("Interface name required"); + eprintln!("Network interface name required"); + eprintln!("Use 'arp scan -l' to list available interfaces"); process::exit(1); } }; @@ -45,9 +75,10 @@ fn main() { None => 5 }; - // ---------------------- - - let interfaces = datalink::interfaces(); + if !is_root_user() { + eprintln!("Should run this binary as root"); + process::exit(1); + } let selected_interface: &datalink::NetworkInterface = interfaces.iter() .find(|interface| { interface.name == interface_name && interface.is_up() && !interface.is_loopback() }) From 137d7568db767d4d9932731e0c3d842362176b9d Mon Sep 17 00:00:00 2001 From: Saluki Date: Wed, 12 May 2021 12:42:19 +0200 Subject: [PATCH 005/121] Updating README with some examples --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 530b0e2..cdbc13a 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,23 @@ [![dependency status](https://deps.rs/repo/github/Saluki/arp-scan-rs/status.svg)](https://deps.rs/repo/github/Saluki/arp-scan-rs) Find all hosts in your local network using this fast ARP scanner. The CLI is written in Rust and provides a minimal scanner that finds all hosts using the ARP protocol. Inspired by the awesome [arp-scan project](https://github.com/royhills/arp-scan). + +## Gettings started + +List all available network interfaces. + +``` +arp-scan -l +``` + +Launch a scan on interface `wlp1s0`. + +``` +arp-scan -i wlp1s0 +``` + +Enhance the scan timeout to 15 seconds (by default, 5 seconds). + +``` +arp-scan -i wlp1s0 -t 15 +``` \ No newline at end of file From 1e425cdd42423332adf01e055cffaa9f8b0d38af Mon Sep 17 00:00:00 2001 From: Saluki Date: Wed, 12 May 2021 13:18:34 +0200 Subject: [PATCH 006/121] Code organization using ARP & utils --- src/arp.rs | 100 ++++++++++++++++++++++++++++++ src/main.rs | 168 +++++++++------------------------------------------ src/utils.rs | 36 +++++++++++ 3 files changed, 163 insertions(+), 141 deletions(-) create mode 100644 src/arp.rs create mode 100644 src/utils.rs diff --git a/src/arp.rs b/src/arp.rs new file mode 100644 index 0000000..ea89317 --- /dev/null +++ b/src/arp.rs @@ -0,0 +1,100 @@ +use std::process; +use std::net::{IpAddr, Ipv4Addr}; +use std::time::Instant; +use std::collections::HashMap; + +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) { + + let mut ethernet_buffer = [0u8; 42]; + let mut ethernet_packet = MutableEthernetPacket::new(&mut ethernet_buffer).unwrap(); + + let target_mac = MacAddr::broadcast(); + let source_mac = interface.mac.unwrap_or_else(|| { + eprintln!("Interface should have a MAC address"); + process::exit(1); + }); + + ethernet_packet.set_destination(target_mac); + 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(); + + let source_ip = interface.ips.first().unwrap_or_else(|| { + eprintln!("Interface should have an IP address"); + process::exit(1); + }).ip(); + + let source_ipv4 = match source_ip { + IpAddr::V4(ipv4_addr) => Some(ipv4_addr), + IpAddr::V6(_ipv6_addr) => None + }; + + 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.unwrap()); + arp_packet.set_target_hw_addr(target_mac); + arp_packet.set_target_proto_addr(target_ip); + + ethernet_packet.set_payload(arp_packet.packet_mut()); + + tx.send_to(ðernet_packet.to_immutable().packet(), Some(interface.clone())); +} + +pub fn receive_responses(rx: &mut Box, timeout_seconds: u64) -> HashMap { + + let mut discover_map: HashMap = HashMap::new(); + let start_recording = Instant::now(); + + loop { + + if start_recording.elapsed().as_secs() > timeout_seconds { + break; + } + + let arp_buffer = rx.next().unwrap_or_else(|error| { + eprintln!("Failed to receive ARP requests ({})", error); + process::exit(1); + }); + + let ethernet_packet = match EthernetPacket::new(&arp_buffer[..]) { + Some(packet) => packet, + None => continue + }; + + let is_arp = match ethernet_packet.get_ethertype() { + EtherTypes::Arp => true, + _ => false + }; + + if !is_arp { + continue; + } + + 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); + + }, + _ => () + } + } + + return discover_map; +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index fe00a95..c837b4b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,38 +1,12 @@ -use pnet::datalink; -use pnet::packet::arp::{MutableArpPacket, ArpPacket, ArpOperations, ArpHardwareTypes}; -use pnet::packet::ethernet::{MutableEthernetPacket, EtherTypes}; -use pnet::packet::MutablePacket; -use pnet::packet::ethernet::EthernetPacket; -use pnet::packet::Packet; -use pnet::util::MacAddr; -use pnet::datalink::NetworkInterface; - -use clap::{Arg, App}; +mod arp; +mod utils; -use std::net::{IpAddr, Ipv4Addr}; +use std::net::{IpAddr}; use std::process; use std::thread; -use std::time::Instant; -use std::collections::HashMap; - -fn is_root_user() -> bool { - std::env::var("USER").unwrap_or(String::from("")) == String::from("root") -} -fn show_interfaces(interfaces: &Vec) { - - for interface in interfaces.iter() { - let up_text = match interface.is_up() { - true => "UP", - false => "DOWN" - }; - let mac_text = match interface.mac { - Some(mac_address) => format!("{}", mac_address), - None => "No MAC address".to_string() - }; - println!("{: <12} {: <7} {}", interface.name, up_text, mac_text); - } -} +use pnet::datalink; +use clap::{Arg, App}; fn main() { @@ -50,16 +24,24 @@ fn main() { ) .get_matches(); - // ---------------------- + // Find interfaces & list them if requested + // ---------------------------------------- + // All network interfaces are retrieved and will be listed if the '--list' + // flag has been given in the request. Note that this can be done without + // using a root account (this will be verified later). let interfaces = datalink::interfaces(); if matches.is_present("list") { - show_interfaces(&interfaces); + utils::show_interfaces(&interfaces); process::exit(0); } - // ---------------------- + // Assert requirements for a local network scan + // -------------------------------------------- + // Ensure all requirements are met to perform an ARP scan on the local + // network for the given interface. ARP scans require an active interface + // with an IPv4 address and root permissions (for crafting ARP packets). let interface_name = match matches.value_of("interface") { Some(name) => name, @@ -75,7 +57,7 @@ fn main() { None => 5 }; - if !is_root_user() { + if !utils::is_root_user() { eprintln!("Should run this binary as root"); process::exit(1); } @@ -103,128 +85,32 @@ fn main() { println!(""); println!("Selected interface {} with IP {}", selected_interface.name, ip_network); - // ----------------------- + // Start ARP scan operation + // ------------------------ + // ARP responses on the interface will be collected in a separate thread, + // while the main thread sends a batch of ARP requests for each IP in the + // local network. let (mut tx, mut rx) = match datalink::channel(selected_interface, Default::default()) { Ok(datalink::Channel::Ethernet(tx, rx)) => (tx, rx), - Ok(_) => panic!("unknown type"), + Ok(_) => panic!("unknown interface type, expected Ethernet"), Err(error) => panic!(error) }; - let responses = thread::spawn(move || { - - let mut discover_map: HashMap = HashMap::new(); - let start_recording = Instant::now(); - - loop { - - if start_recording.elapsed().as_secs() > timeout_seconds { - break; - } - - let arp_buffer = rx.next().unwrap_or_else(|error| { - eprintln!("Failed to receive ARP requests ({})", error); - process::exit(1); - }); - - let ethernet_packet = match EthernetPacket::new(&arp_buffer[..]) { - Some(packet) => packet, - None => continue - }; - - let is_arp = match ethernet_packet.get_ethertype() { - EtherTypes::Arp => true, - _ => false - }; - - if !is_arp { - continue; - } - - 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); - - }, - _ => () - } - } - - return discover_map; - - }); + let arp_responses = thread::spawn(move || arp::receive_responses(&mut rx, timeout_seconds)); println!("Sending {:?} ARP requests to network ({}s timeout)", ip_network.size(), timeout_seconds); for ip_address in ip_network.iter() { if let IpAddr::V4(ipv4_address) = ip_address { - send_arp_request(&mut tx, selected_interface, ipv4_address); + arp::send_request(&mut tx, selected_interface, ipv4_address); } } - // ------------------ - - let final_result = responses.join().unwrap_or_else(|error| { + let final_result = arp_responses.join().unwrap_or_else(|error| { eprintln!("Failed to close receive thread ({:?})", error); process::exit(1); }); - 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!(""); -} - -fn send_arp_request(tx: &mut Box, interface: &datalink::NetworkInterface, target_ip: Ipv4Addr) { - - let mut ethernet_buffer = [0u8; 42]; - let mut ethernet_packet = MutableEthernetPacket::new(&mut ethernet_buffer).unwrap(); - - let target_mac = datalink::MacAddr::broadcast(); - let source_mac = interface.mac.unwrap_or_else(|| { - eprintln!("Interface should have a MAC address"); - process::exit(1); - }); - - ethernet_packet.set_destination(target_mac); - 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(); - - let source_ip = interface.ips.first().unwrap_or_else(|| { - eprintln!("Interface should have an IP address"); - process::exit(1); - }).ip(); - - let source_ipv4 = match source_ip { - IpAddr::V4(ipv4_addr) => Some(ipv4_addr), - IpAddr::V6(_ipv6_addr) => None - }; - - 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.unwrap()); - arp_packet.set_target_hw_addr(target_mac); - arp_packet.set_target_proto_addr(target_ip); - - ethernet_packet.set_payload(arp_packet.packet_mut()); - - tx.send_to(ðernet_packet.to_immutable().packet(), Some(interface.clone())); + utils::display_scan_results(final_result); } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..e1d2fdf --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,36 @@ +use std::net::Ipv4Addr; +use std::collections::HashMap; + +use pnet::datalink::{MacAddr, NetworkInterface}; + +pub fn is_root_user() -> bool { + std::env::var("USER").unwrap_or(String::from("")) == String::from("root") +} + +pub fn show_interfaces(interfaces: &Vec) { + + for interface in interfaces.iter() { + let up_text = match interface.is_up() { + true => "UP", + false => "DOWN" + }; + let mac_text = match interface.mac { + Some(mac_address) => format!("{}", mac_address), + None => "No MAC address".to_string() + }; + println!("{: <12} {: <7} {}", interface.name, up_text, mac_text); + } +} + +pub fn display_scan_results(final_result: HashMap) { + + 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!(""); +} \ No newline at end of file From be8d79b8a0bc00fcd5ffe53e0b8fd5cefdb5aa6a Mon Sep 17 00:00:00 2001 From: Corentin B Date: Wed, 12 May 2021 21:40:50 +0000 Subject: [PATCH 007/121] Update Semaphore configuration --- .semaphore/semaphore.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index f981ff8..a688075 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -3,9 +3,10 @@ name: Rust agent: machine: type: e1-standard-2 + os_image: ubuntu1804 containers: - name: main - image: registry.semaphoreci.com/rust:1.47 + image: 'registry.semaphoreci.com/rust:1.47' blocks: - name: Test task: @@ -15,3 +16,9 @@ blocks: - checkout - cargo build --verbose - cargo test --verbose + - name: Build release + task: + jobs: + - name: Configure + commands: + - rustup target add x86_64-unknown-linux-musl From aedcfa05004a8653c059df368afbbe52b00bf0a2 Mon Sep 17 00:00:00 2001 From: Corentin B Date: Wed, 12 May 2021 21:48:15 +0000 Subject: [PATCH 008/121] Update Semaphore configuration --- .semaphore/semaphore.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index a688075..3b20283 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -8,10 +8,10 @@ agent: - name: main image: 'registry.semaphoreci.com/rust:1.47' blocks: - - name: Test + - name: Test release task: jobs: - - name: cargo test + - name: Cargo test commands: - checkout - cargo build --verbose @@ -22,3 +22,9 @@ blocks: - name: Configure commands: - rustup target add x86_64-unknown-linux-musl + - name: Build + commands: + - cargo build --target=x86_64-unknown-linux-musl --locked + - name: Push artificact + commands: + - artifact push job target/x86_64-unknown-linux-musl/release/arp-scan From 4314aa21f252ea44b05a57983cc91fcc74929e91 Mon Sep 17 00:00:00 2001 From: Corentin B Date: Wed, 12 May 2021 21:51:53 +0000 Subject: [PATCH 009/121] Update Semaphore configuration --- .semaphore/semaphore.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 3b20283..1f4b009 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -21,10 +21,7 @@ blocks: jobs: - name: Configure commands: + - checkout - rustup target add x86_64-unknown-linux-musl - - name: Build - commands: - cargo build --target=x86_64-unknown-linux-musl --locked - - name: Push artificact - commands: - artifact push job target/x86_64-unknown-linux-musl/release/arp-scan From cc2356bac9de2beb580eb4fc23a58ac8e9b13c9f Mon Sep 17 00:00:00 2001 From: Corentin B Date: Wed, 12 May 2021 22:10:17 +0000 Subject: [PATCH 010/121] Update Semaphore configuration --- .semaphore/semaphore.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 1f4b009..59df300 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -16,12 +16,3 @@ blocks: - checkout - cargo build --verbose - cargo test --verbose - - name: Build release - task: - jobs: - - name: Configure - commands: - - checkout - - rustup target add x86_64-unknown-linux-musl - - cargo build --target=x86_64-unknown-linux-musl --locked - - artifact push job target/x86_64-unknown-linux-musl/release/arp-scan From bad6531564e096acadd3cb594276f2d669c7ddd6 Mon Sep 17 00:00:00 2001 From: Saluki Date: Thu, 13 May 2021 00:12:46 +0200 Subject: [PATCH 011/121] Release 0.2.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 81ff51d..b458117 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,7 +20,7 @@ dependencies = [ [[package]] name = "arp-scan" -version = "0.1.0" +version = "0.2.0" dependencies = [ "clap", "pnet", diff --git a/Cargo.toml b/Cargo.toml index 1da9eae..0d17dcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "arp-scan" description = "A minimalistic ARP scan tool" license = "AGPL-3.0-or-later" -version = "0.1.0" +version = "0.2.0" authors = ["Saluki"] edition = "2018" readme = "README.md" From e49754e8846ea6a563d7f5e19f725ca712e1ba77 Mon Sep 17 00:00:00 2001 From: Saluki Date: Thu, 13 May 2021 00:20:08 +0200 Subject: [PATCH 012/121] Update documentation with download --- README.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index cdbc13a..ffe861b 100644 --- a/README.md +++ b/README.md @@ -7,20 +7,28 @@ Find all hosts in your local network using this fast ARP scanner. The CLI is wri ## Gettings started -List all available network interfaces. +Download the `arp-scan` binary for Linux. + +```bash +wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.2.0/arp-scan-v0.2.0-x86_64-unknown-linux-musl +chmod +x arp-scan ``` -arp-scan -l + +List all available network interfaces. + +```bash +./arp-scan -l ``` Launch a scan on interface `wlp1s0`. -``` -arp-scan -i wlp1s0 +```bash +./arp-scan -i wlp1s0 ``` Enhance the scan timeout to 15 seconds (by default, 5 seconds). -``` -arp-scan -i wlp1s0 -t 15 +```bash +./arp-scan -i wlp1s0 -t 15 ``` \ No newline at end of file From 7d931d9bfd3c303cd2400925f3f9f2aa4b91763e Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 17 May 2021 13:31:45 +0200 Subject: [PATCH 013/121] Updating dependencies with ipnetwork --- Cargo.lock | 203 +++++++++++++++------------------------------------- Cargo.toml | 3 +- src/main.rs | 8 ++- 3 files changed, 67 insertions(+), 147 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b458117..ee62ebc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,7 +15,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -23,6 +23,7 @@ name = "arp-scan" version = "0.2.0" dependencies = [ "clap", + "ipnetwork", "pnet", ] @@ -34,27 +35,15 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", - "winapi 0.3.9", + "winapi", ] -[[package]] -name = "bitflags" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f67931368edf3a9a51d29886d245f1c3db2f1ef0dcc9e35ff70341b78c10d23" - [[package]] name = "bitflags" 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" @@ -63,7 +52,7 @@ checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" dependencies = [ "ansi_term", "atty", - "bitflags 1.2.1", + "bitflags", "strsim", "textwrap", "unicode-width", @@ -72,9 +61,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.2.11" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "hermit-abi" @@ -87,46 +76,18 @@ dependencies = [ [[package]] name = "ipnetwork" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c3eaab3ac0ede60ffa41add21970a7df7d91772c03383aac6c2c3d53cc716b" +checksum = "4088d739b183546b239688ddbc79891831df421773df95e236daf7867866d355" dependencies = [ "serde", ] -[[package]] -name = "kernel32-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" -dependencies = [ - "winapi 0.2.8", - "winapi-build", -] - [[package]] name = "libc" -version = "0.2.93" +version = "0.2.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41" - -[[package]] -name = "log" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" -dependencies = [ - "log 0.4.14", -] - -[[package]] -name = "log" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" -dependencies = [ - "cfg-if", -] +checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" [[package]] name = "memchr" @@ -136,9 +97,9 @@ checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" [[package]] name = "pnet" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b657d5b9a98a2c81b82549922b8b15984e49f8120cd130b11a09f81b9b55d633" +checksum = "4b6d2a0409666964722368ef5fb74b9f93fac11c18bef3308693c16c6733f103" dependencies = [ "ipnetwork", "pnet_base", @@ -150,71 +111,71 @@ dependencies = [ [[package]] name = "pnet_base" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4688aa497ef62129f302a5800ebde67825f8ff129f43690ca84099f6620bed" +checksum = "25488cd551a753dcaaa6fffc9f69a7610a412dd8954425bf7ffad5f7d1156fb8" [[package]] name = "pnet_datalink" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59001c9c4d9d23bf2f61afaaf134a766fd6932ba2557c606b9112157053b9ac7" +checksum = "d4d1f8ab1ef6c914cf51dc5dfe0be64088ea5f3b08bbf5a31abc70356d271198" dependencies = [ "ipnetwork", "libc", "pnet_base", "pnet_sys", - "winapi 0.3.9", + "winapi", ] [[package]] name = "pnet_macros" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d894a90dbdbe976e624453fc31b1912f658083778329442dda1cca94f76a3e76" +checksum = "30490e0852e58402b8fae0d39897b08a24f493023a4d6cf56b2e30f31ed57548" dependencies = [ + "proc-macro2", + "quote", "regex", - "syntex", - "syntex_syntax", + "syn", ] [[package]] name = "pnet_macros_support" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b99269a458570bc06a9132254349f6543d9abc92e88b68d8de934aac9481f6c" +checksum = "d4714e10f30cab023005adce048f2d30dd4ac4f093662abf2220855655ef8f90" dependencies = [ "pnet_base", ] [[package]] name = "pnet_packet" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33f8238f4eb897a55ca06510cd71afb5b5ca7b4ff2d7188f1ca855fc1710133e" +checksum = "8588067671d03c9f4254b2e66fecb4d8b93b5d3e703195b84f311cd137e32130" dependencies = [ "glob", "pnet_base", "pnet_macros", "pnet_macros_support", - "syntex", ] [[package]] name = "pnet_sys" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7589e4c4e7ed72a3ffdff8a65d3bea84e8c3a23e19d0a10e8f45efdf632fff15" +checksum = "d9a3f32b0df45515befd19eed04616f6b56a488da92afc61164ef455e955f07f" dependencies = [ "libc", - "winapi 0.3.9", + "winapi", ] [[package]] name = "pnet_transport" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "326abdfd2e70e8e943bd58087b59686de170cac050a3b19c9fcc84db01690af5" +checksum = "932b2916d693bcc5fa18443dc99142e0a6fd31a6ce75a511868f7174c17e2bce" dependencies = [ "libc", "pnet_base", @@ -222,11 +183,29 @@ dependencies = [ "pnet_sys", ] +[[package]] +name = "proc-macro2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + [[package]] name = "regex" -version = "1.5.3" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce5f1ceb7f74abbce32601642fcf8e8508a8a8991e0621c7d750295b9095702b" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" dependencies = [ "aho-corasick", "memchr", @@ -239,12 +218,6 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" -[[package]] -name = "rustc-serialize" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" - [[package]] name = "serde" version = "1.0.125" @@ -258,64 +231,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] -name = "syntex" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a30b08a6b383a22e5f6edc127d169670d48f905bb00ca79a00ea3e442ebe317" -dependencies = [ - "syntex_errors", - "syntex_syntax", -] - -[[package]] -name = "syntex_errors" -version = "0.42.0" +name = "syn" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04c48f32867b6114449155b2a82114b86d4b09e1bddb21c47ff104ab9172b646" +checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" dependencies = [ - "libc", - "log 0.3.9", - "rustc-serialize", - "syntex_pos", - "term", + "proc-macro2", + "quote", "unicode-xid", ] -[[package]] -name = "syntex_pos" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fd49988e52451813c61fecbe9abb5cfd4e1b7bb6cdbb980a6fbcbab859171a6" -dependencies = [ - "rustc-serialize", -] - -[[package]] -name = "syntex_syntax" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7628a0506e8f9666fdabb5f265d0059b059edac9a3f810bda077abb5d826bd8d" -dependencies = [ - "bitflags 0.5.0", - "libc", - "log 0.3.9", - "rustc-serialize", - "syntex_errors", - "syntex_pos", - "term", - "unicode-xid", -] - -[[package]] -name = "term" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa63644f74ce96fbeb9b794f66aff2a52d601cbd5e80f4b97123e3899f4570f1" -dependencies = [ - "kernel32-sys", - "winapi 0.2.8", -] - [[package]] name = "textwrap" version = "0.11.0" @@ -333,9 +258,9 @@ checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" [[package]] name = "unicode-xid" -version = "0.0.3" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36dff09cafb4ec7c8cf0023eb0b686cb6ce65499116a12201c9e11840ca01beb" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "vec_map" @@ -343,12 +268,6 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" -[[package]] -name = "winapi" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" - [[package]] name = "winapi" version = "0.3.9" @@ -359,12 +278,6 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" - [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 0d17dcb..c6f44b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,5 +12,6 @@ keywords = ["arp", "scan", "network", "security"] categories = ["command-line-utilities"] [dependencies] -pnet = "0.27.2" +pnet = "0.28.0" +ipnetwork = "0.18.0" clap = "2.33.3" diff --git a/src/main.rs b/src/main.rs index c837b4b..3e71722 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use std::net::{IpAddr}; use std::process; use std::thread; +use ipnetwork::NetworkSize; use pnet::datalink; use clap::{Arg, App}; @@ -99,7 +100,12 @@ fn main() { let arp_responses = thread::spawn(move || arp::receive_responses(&mut rx, timeout_seconds)); - println!("Sending {:?} ARP requests to network ({}s timeout)", ip_network.size(), timeout_seconds); + let network_size: u128 = match ip_network.size() { + NetworkSize::V4(x) => x.into(), + NetworkSize::V6(y) => y + }; + println!("Sending {} ARP requests to network ({}s timeout)", network_size, timeout_seconds); + for ip_address in ip_network.iter() { if let IpAddr::V4(ipv4_address) = ip_address { From 35508fa78edefb85ca23576dfc486fff8969b862 Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 17 May 2021 21:24:16 +0200 Subject: [PATCH 014/121] Cleaning README --- README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ffe861b..1756e19 100644 --- a/README.md +++ b/README.md @@ -31,4 +31,22 @@ Enhance the scan timeout to 15 seconds (by default, 5 seconds). ```bash ./arp-scan -i wlp1s0 -t 15 -``` \ No newline at end of file +``` + +## Options + +### `arp-scan -l` + +List all available network interfaces. Using this option will only print a list of interfaces and exit the process. + +### `arp-scan -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` + +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`. + +## Contributing + +Feel free to suggest an improvement, report a bug, or ask something: https://github.com/saluki/arp-scan-rs/issues From 109a703f0507b3a304550b576a9e881ebb61e2f7 Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 17 May 2021 22:50:56 +0200 Subject: [PATCH 015/121] 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 From e8b06623c068271f3630d5b895a7d6e82f706e6a Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 17 May 2021 23:26:00 +0200 Subject: [PATCH 016/121] Releasing 0.3.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0b27318..d68f89b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,7 +20,7 @@ dependencies = [ [[package]] name = "arp-scan" -version = "0.2.0" +version = "0.3.0" dependencies = [ "clap", "dns-lookup", diff --git a/Cargo.toml b/Cargo.toml index 0486625..e7f44ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "arp-scan" description = "A minimalistic ARP scan tool" license = "AGPL-3.0-or-later" -version = "0.2.0" +version = "0.3.0" authors = ["Saluki"] edition = "2018" readme = "README.md" diff --git a/README.md b/README.md index 353cbda..5e8200a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Find all hosts in your local network using this fast ARP scanner. The CLI is wri Download the `arp-scan` binary for Linux. ```bash -wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.2.0/arp-scan-v0.2.0-x86_64-unknown-linux-musl +wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.3.0/arp-scan-v0.2.0-x86_64-unknown-linux-musl chmod +x arp-scan ``` From 8fae19e14d0ccb72d31b3c01c2a73cc9a65d278b Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 17 May 2021 23:30:58 +0200 Subject: [PATCH 017/121] Fixing last release tag --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e8200a..bd3ddae 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Find all hosts in your local network using this fast ARP scanner. The CLI is wri Download the `arp-scan` binary for Linux. ```bash -wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.3.0/arp-scan-v0.2.0-x86_64-unknown-linux-musl +wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.3.0/arp-scan-v0.3.0-x86_64-unknown-linux-musl chmod +x arp-scan ``` From 875ef4653599a43aa7cd92975e35e9e99924fcac Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 17 May 2021 23:34:46 +0200 Subject: [PATCH 018/121] Fixing release number --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 1212e79..4f4af42 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,7 @@ const TIMEOUT_DEFAULT: u64 = 5; fn main() { let matches = App::new("arp-scan") - .version("0.1") + .version("0.3.0") .about("A minimalistic ARP scan tool written in Rust") .arg( Arg::with_name("interface").short("i").long("interface").takes_value(true).value_name("INTERFACE_NAME").help("Network interface") From ad9eef31337cbe08d37fee11aa98150373f0f784 Mon Sep 17 00:00:00 2001 From: Saluki Date: Tue, 18 May 2021 00:14:49 +0200 Subject: [PATCH 019/121] Crafting basic test suite --- README.md | 16 +++++++--------- src/network.rs | 31 +++++++++++++++++++++++++++++++ src/utils.rs | 31 ++++++++++++++++++++++++++++++- 3 files changed, 68 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index bd3ddae..e127612 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,7 @@ Find all hosts in your local network using this fast ARP scanner. The CLI is wri Download the `arp-scan` binary for Linux. ```bash -wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.3.0/arp-scan-v0.3.0-x86_64-unknown-linux-musl - -chmod +x arp-scan +wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.3.0/arp-scan-v0.3.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan ``` List all available network interfaces. @@ -35,27 +33,27 @@ Enhance the scan timeout to 15 seconds (by default, 5 seconds). ## Options -### Get help `-h` +#### Get help `-h` Display the main help message with all commands and available ARP scan options. -### List interfaces `-l` +#### List interfaces `-l` List all available network interfaces. Using this option will only print a list of interfaces and exit the process. -### Select interface `-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. -### Set timeout `-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` +#### Numeric mode `-n` Switch to numeric mode. This will skip the local hostname resolution process and will only display IP addresses. -### Show version `-n` +#### Show version `-n` Display the ARP scan CLI version and exits the process. diff --git a/src/network.rs b/src/network.rs index 2f8a63b..8ae5c3c 100644 --- a/src/network.rs +++ b/src/network.rs @@ -142,3 +142,34 @@ fn find_hostname(ipv4: Ipv4Addr) -> Option { Err(_) => None } } + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn should_resolve_public_ip() { + + let ipv4 = Ipv4Addr::new(1,1,1,1); + + assert_eq!(find_hostname(ipv4), Some("one.one.one.one".to_string())); + } + + #[test] + fn should_resolve_localhost() { + + let ipv4 = Ipv4Addr::new(127,0,0,1); + + assert_eq!(find_hostname(ipv4), Some("localhost".to_string())); + } + + #[test] + fn should_not_resolve_unknown_ip() { + + let ipv4 = Ipv4Addr::new(10,254,254,254); + + assert_eq!(find_hostname(ipv4), None); + } + +} diff --git a/src/utils.rs b/src/utils.rs index e49b173..0f4f75c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -40,4 +40,33 @@ pub fn display_scan_results(mut final_result: Vec, resolve_hostna } println!(""); -} \ No newline at end of file +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn should_detect_root_user() { + + let old_user = std::env::var("USER").unwrap(); + std::env::set_var("USER", "root"); + + assert_eq!(is_root_user(), true); + + std::env::set_var("USER", old_user); + } + + #[test] + fn should_detect_standard_user() { + + let old_user = std::env::var("USER").unwrap(); + std::env::set_var("USER", "john"); + + assert_eq!(is_root_user(), false); + + std::env::set_var("USER", old_user); + } + +} From 573d33e7bf52712594d7d813d8fd8181eb18cdd4 Mon Sep 17 00:00:00 2001 From: Saluki Date: Tue, 18 May 2021 00:17:51 +0200 Subject: [PATCH 020/121] Pruning tests not compatible with Semaphore CI --- src/utils.rs | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 0f4f75c..0997e68 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -41,32 +41,3 @@ pub fn display_scan_results(mut final_result: Vec, resolve_hostna println!(""); } - -#[cfg(test)] -mod tests { - - use super::*; - - #[test] - fn should_detect_root_user() { - - let old_user = std::env::var("USER").unwrap(); - std::env::set_var("USER", "root"); - - assert_eq!(is_root_user(), true); - - std::env::set_var("USER", old_user); - } - - #[test] - fn should_detect_standard_user() { - - let old_user = std::env::var("USER").unwrap(); - std::env::set_var("USER", "john"); - - assert_eq!(is_root_user(), false); - - std::env::set_var("USER", old_user); - } - -} From 3b347bdcd6c17f12b378c22494a58d8889f53422 Mon Sep 17 00:00:00 2001 From: Saluki Date: Tue, 18 May 2021 03:20:56 +0200 Subject: [PATCH 021/121] Adding source IPv4 bypass --- README.md | 4 ++++ src/main.rs | 23 +++++++++++++++++++++-- src/network.rs | 33 ++++++++++++++++++++++----------- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index e127612..9f42dc8 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,10 @@ Enforce a timeout of at least 15 seconds. This timeout is a minimum value (scans Switch to numeric mode. This will skip the local hostname resolution process and will only display IP addresses. +#### Change source IPv4 `-S` + +Change or force the IPv4 address sent as source in the broadcasted ARP packets. This may be useful for isolated hosts. + #### Show version `-n` Display the ARP scan CLI version and exits the process. diff --git a/src/main.rs b/src/main.rs index 4f4af42..0f74a64 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ mod network; mod utils; -use std::net::{IpAddr}; +use std::net::{IpAddr, Ipv4Addr}; use std::process; use std::thread; @@ -23,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("source_ip").short("S").long("source-ip").takes_value(true).value_name("SOURCE_IPV4").help("Source IPv4 address for requests") + ) .arg( Arg::with_name("numeric").short("n").long("numeric").takes_value(false).help("Numeric mode, no hostname resolution") ) @@ -71,6 +74,19 @@ fn main() { // Hostnames will not be resolved in numeric mode let resolve_hostname = !matches.is_present("numeric"); + + let source_ipv4: Option = match matches.value_of("source_ip").map(|source| source.parse::()) { + Some(parsed_source) => { + + if let Err(_) = parsed_source { + eprintln!("Expected valid IPv4 as source IP"); + process::exit(1); + } + + Some(parsed_source.unwrap()) + }, + None => None + }; if !utils::is_root_user() { eprintln!("Should run this binary as root"); @@ -99,6 +115,9 @@ fn main() { println!(""); println!("Selected interface {} with IP {}", selected_interface.name, ip_network); + if let Some(forced_source_ipv4) = source_ipv4 { + println!("The ARP source IPv4 will be forced to {}", forced_source_ipv4); + } // Start ARP scan operation // ------------------------ @@ -123,7 +142,7 @@ fn main() { for ip_address in ip_network.iter() { if let IpAddr::V4(ipv4_address) = ip_address { - network::send_arp_request(&mut tx, selected_interface, ipv4_address); + network::send_arp_request(&mut tx, selected_interface, ipv4_address, source_ipv4); } } diff --git a/src/network.rs b/src/network.rs index 8ae5c3c..144e579 100644 --- a/src/network.rs +++ b/src/network.rs @@ -20,7 +20,7 @@ pub struct TargetDetails { * 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) { +pub fn send_arp_request(tx: &mut Box, interface: &NetworkInterface, target_ip: Ipv4Addr, forced_source_ipv4: Option) { let mut ethernet_buffer = [0u8; 42]; let mut ethernet_packet = MutableEthernetPacket::new(&mut ethernet_buffer).unwrap(); @@ -38,15 +38,7 @@ pub fn send_arp_request(tx: &mut Box, interface: &NetworkInt let mut arp_buffer = [0u8; 28]; let mut arp_packet = MutableArpPacket::new(&mut arp_buffer).unwrap(); - let source_ip = interface.ips.first().unwrap_or_else(|| { - eprintln!("Interface should have an IP address"); - process::exit(1); - }).ip(); - - let source_ipv4 = match source_ip { - IpAddr::V4(ipv4_addr) => Some(ipv4_addr), - IpAddr::V6(_ipv6_addr) => None - }; + let source_ipv4 = find_source_ip(interface, forced_source_ipv4); arp_packet.set_hardware_type(ArpHardwareTypes::Ethernet); arp_packet.set_protocol_type(EtherTypes::Ipv4); @@ -54,7 +46,7 @@ pub fn send_arp_request(tx: &mut Box, interface: &NetworkInt 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.unwrap()); + arp_packet.set_sender_proto_addr(source_ipv4); arp_packet.set_target_hw_addr(target_mac); arp_packet.set_target_proto_addr(target_ip); @@ -63,6 +55,25 @@ pub fn send_arp_request(tx: &mut Box, interface: &NetworkInt tx.send_to(ðernet_packet.to_immutable().packet(), Some(interface.clone())); } +fn find_source_ip(interface: &NetworkInterface, forced_source_ipv4: Option) -> Ipv4Addr { + + if let Some(forced_ipv4) = forced_source_ipv4 { + return forced_ipv4; + } + + let source_ip = interface.ips.first().unwrap_or_else(|| { + eprintln!("Interface should have an IP address"); + process::exit(1); + }).ip(); + + let source_ipv4 = match source_ip { + IpAddr::V4(ipv4_addr) => Some(ipv4_addr), + IpAddr::V6(_ipv6_addr) => None + }; + + source_ipv4.unwrap() +} + /** * Wait at least N seconds and receive ARP network responses. The main * downside of this function is the blocking nature of the datalink receiver: From 5f987e92c4270d0381aa0474c3c6dfcfd7308c63 Mon Sep 17 00:00:00 2001 From: Saluki Date: Tue, 18 May 2021 19:14:28 +0200 Subject: [PATCH 022/121] Working on common documentation --- README.md | 15 ++++++++++----- src/network.rs | 14 ++++++++++++++ src/utils.rs | 13 +++++++++++++ 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9f42dc8..fc6e07c 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,14 @@ Find all hosts in your local network using this fast ARP scanner. The CLI is written in Rust and provides a minimal scanner that finds all hosts using the ARP protocol. Inspired by the awesome [arp-scan project](https://github.com/royhills/arp-scan). -## Gettings started +:heavy_check_mark: Minimal Rust binary +:heavy_check_mark: Fast ARP scans +:heavy_check_mark: Scan customization (timeout, interface, ...) +:heavy_check_mark: Force request source IP -Download the `arp-scan` binary for Linux. +## Getting started + +Download the `arp-scan` binary for Linux (Ubuntu, Fedora, Debian, ...). See the [releases page](https://github.com/Saluki/arp-scan-rs/releases) for other binaries. ```bash wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.3.0/arp-scan-v0.3.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan @@ -25,7 +30,7 @@ Launch a scan on interface `wlp1s0`. ./arp-scan -i wlp1s0 ``` -Enhance the scan timeout to 15 seconds (by default, 5 seconds). +Enhance the minimum scan timeout to 15 seconds (by default, 5 seconds). ```bash ./arp-scan -i wlp1s0 -t 15 @@ -53,11 +58,11 @@ Enforce a timeout of at least 15 seconds. This timeout is a minimum value (scans Switch to numeric mode. This will skip the local hostname resolution process and will only display IP addresses. -#### Change source IPv4 `-S` +#### Change source IPv4 `-S 192.168.1.130` Change or force the IPv4 address sent as source in the broadcasted ARP packets. This may be useful for isolated hosts. -#### Show version `-n` +#### Show version `-v` Display the ARP scan CLI version and exits the process. diff --git a/src/network.rs b/src/network.rs index 144e579..5026252 100644 --- a/src/network.rs +++ b/src/network.rs @@ -9,6 +9,11 @@ use pnet::packet::{MutablePacket, Packet}; use pnet::packet::ethernet::{EthernetPacket, MutableEthernetPacket, EtherTypes}; use pnet::packet::arp::{MutableArpPacket, ArpOperations, ArpHardwareTypes, ArpPacket}; +/** + * A target detail represents a single host on the local network with an IPv4 + * address and a linked MAC address. Hostnames are optional since some hosts + * does not respond to the resolve call (or the numeric mode may be enabled). + */ pub struct TargetDetails { pub ipv4: Ipv4Addr, pub mac: MacAddr, @@ -55,6 +60,11 @@ pub fn send_arp_request(tx: &mut Box, interface: &NetworkInt tx.send_to(ðernet_packet.to_immutable().packet(), Some(interface.clone())); } +/** + * Find the most adequate IPv4 address on a given network interface for sending + * ARP requests. If the 'forced_source_ipv4' parameter is set, it will take + * the priority over the network interface address. + */ fn find_source_ip(interface: &NetworkInterface, forced_source_ipv4: Option) -> Ipv4Addr { if let Some(forced_ipv4) = forced_source_ipv4 { @@ -136,6 +146,10 @@ pub fn receive_arp_responses(rx: &mut Box, timeout_seconds }).collect() } +/** + * Find the local hostname linked to an IPv4 address. This will perform a + * reverse DNS request in the local network to find the IPv4 hostname. + */ fn find_hostname(ipv4: Ipv4Addr) -> Option { let ip: IpAddr = ipv4.into(); diff --git a/src/utils.rs b/src/utils.rs index 0997e68..dfd2555 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,10 +2,19 @@ use pnet::datalink::NetworkInterface; use crate::network::TargetDetails; +/** + * Based on the current UNIX environment, find if the process is run as root + * user. This approach only supports Linux-like systems (Ubuntu, Fedore, ...). + */ pub fn is_root_user() -> bool { std::env::var("USER").unwrap_or(String::from("")) == String::from("root") } +/** + * Prints on stdout a list of all available network interfaces with some + * technical details. The goal is to present the most useful technical details + * to pick the right network interface for scans. + */ pub fn show_interfaces(interfaces: &Vec) { for interface in interfaces.iter() { @@ -21,6 +30,10 @@ pub fn show_interfaces(interfaces: &Vec) { } } +/** + * Display the scan results on stdout with a table. The 'final_result' vector + * contains all items that will be displayed. + */ pub fn display_scan_results(mut final_result: Vec, resolve_hostname: bool) { final_result.sort_by_key(|item| item.ipv4); From 177987013f85c5aa85ececacb94039bec283822a Mon Sep 17 00:00:00 2001 From: Saluki Date: Tue, 18 May 2021 19:16:09 +0200 Subject: [PATCH 023/121] Fixing a documentation typo --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fc6e07c..9714939 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,12 @@ Find all hosts in your local network using this fast ARP scanner. The CLI is written in Rust and provides a minimal scanner that finds all hosts using the ARP protocol. Inspired by the awesome [arp-scan project](https://github.com/royhills/arp-scan). :heavy_check_mark: Minimal Rust binary + :heavy_check_mark: Fast ARP scans -:heavy_check_mark: Scan customization (timeout, interface, ...) -:heavy_check_mark: Force request source IP + +:heavy_check_mark: Scan customization (timeout, interface, DNS, ...) + +:heavy_check_mark: Force ARP source IP ## Getting started From babca7a7571f1e16e2c03f20f994ec5010f4589c Mon Sep 17 00:00:00 2001 From: Saluki Date: Wed, 19 May 2021 12:50:24 +0200 Subject: [PATCH 024/121] Adding a force MAC destination option --- README.md | 12 ++++++++---- src/main.rs | 24 ++++++++++++++++++++++-- src/network.rs | 7 +++++-- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9714939..6f4881e 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,10 @@ Launch a scan on interface `wlp1s0`. ./arp-scan -i wlp1s0 ``` -Enhance the minimum scan timeout to 15 seconds (by default, 5 seconds). +Enhance the minimum scan timeout to 5 seconds (by default, 2 seconds). ```bash -./arp-scan -i wlp1s0 -t 15 +./arp-scan -i wlp1s0 -t 5 ``` ## Options @@ -55,7 +55,7 @@ Perform a scan on the network interface `eth0`. The first valid IPv4 network on #### 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`. +Enforce a timeout of at least 15 seconds. This timeout is a minimum value (scans may take a little more time). Default value is `2`. #### Numeric mode `-n` @@ -63,7 +63,11 @@ Switch to numeric mode. This will skip the local hostname resolution process and #### Change source IPv4 `-S 192.168.1.130` -Change or force the IPv4 address sent as source in the broadcasted ARP packets. This may be useful for isolated hosts. +Change or force the IPv4 address sent as source in the broadcasted ARP packets. By default, a valid IPv4 address on the network interface will be used. This option may be useful for isolated hosts and security checks. + +#### Change destination MAC `-M 55:44:33:22:11:00` + +Change or force the MAC address sent as destination ARP request. By default, a broadcast destination (`00:00:00:00:00:00`) will be set. #### Show version `-v` diff --git a/src/main.rs b/src/main.rs index 0f74a64..e49f97e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,10 +7,11 @@ use std::thread; use ipnetwork::NetworkSize; use pnet::datalink; +use pnet::datalink::{MacAddr}; use clap::{Arg, App}; const FIVE_HOURS: u64 = 5 * 60 * 60; -const TIMEOUT_DEFAULT: u64 = 5; +const TIMEOUT_DEFAULT: u64 = 2; fn main() { @@ -26,6 +27,9 @@ fn main() { .arg( Arg::with_name("source_ip").short("S").long("source-ip").takes_value(true).value_name("SOURCE_IPV4").help("Source IPv4 address for requests") ) + .arg( + Arg::with_name("destination_mac").short("M").long("dest-mac").takes_value(true).value_name("DESTINATION_MAC").help("Destination MAC address for requests") + ) .arg( Arg::with_name("numeric").short("n").long("numeric").takes_value(false).help("Numeric mode, no hostname resolution") ) @@ -87,6 +91,19 @@ fn main() { }, None => None }; + + let destination_mac: Option = match matches.value_of("destination_mac").map(|dest| dest.parse::()) { + Some(mac_address) => { + + if let Err(_) = mac_address { + eprintln!("Expected valid MAC address as destination"); + process::exit(1); + } + + Some(mac_address.unwrap()) + }, + None => None + }; if !utils::is_root_user() { eprintln!("Should run this binary as root"); @@ -118,6 +135,9 @@ fn main() { if let Some(forced_source_ipv4) = source_ipv4 { println!("The ARP source IPv4 will be forced to {}", forced_source_ipv4); } + if let Some(forced_destination_mac) = destination_mac { + println!("The ARP destination MAC will be forced to {}", forced_destination_mac); + } // Start ARP scan operation // ------------------------ @@ -142,7 +162,7 @@ fn main() { for ip_address in ip_network.iter() { if let IpAddr::V4(ipv4_address) = ip_address { - network::send_arp_request(&mut tx, selected_interface, ipv4_address, source_ipv4); + network::send_arp_request(&mut tx, selected_interface, ipv4_address, source_ipv4, destination_mac); } } diff --git a/src/network.rs b/src/network.rs index 5026252..6b91d84 100644 --- a/src/network.rs +++ b/src/network.rs @@ -25,12 +25,15 @@ pub struct TargetDetails { * 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, forced_source_ipv4: Option) { +pub fn send_arp_request(tx: &mut Box, interface: &NetworkInterface, target_ip: Ipv4Addr, forced_source_ipv4: Option, forced_destination_mac: Option) { let mut ethernet_buffer = [0u8; 42]; let mut ethernet_packet = MutableEthernetPacket::new(&mut ethernet_buffer).unwrap(); - let target_mac = MacAddr::broadcast(); + let target_mac = match forced_destination_mac { + Some(forced_mac) => forced_mac, + None => MacAddr::broadcast() + }; let source_mac = interface.mac.unwrap_or_else(|| { eprintln!("Interface should have a MAC address"); process::exit(1); From 1f653e6c261f1cc04e6601fe4ba0feb5bbf2ccb5 Mon Sep 17 00:00:00 2001 From: Saluki Date: Wed, 19 May 2021 12:57:40 +0200 Subject: [PATCH 025/121] Release version 0.4.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 4 ++-- src/main.rs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d68f89b..1369bf2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,7 +20,7 @@ dependencies = [ [[package]] name = "arp-scan" -version = "0.3.0" +version = "0.4.0" dependencies = [ "clap", "dns-lookup", diff --git a/Cargo.toml b/Cargo.toml index e7f44ec..ef26fc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "arp-scan" description = "A minimalistic ARP scan tool" license = "AGPL-3.0-or-later" -version = "0.3.0" +version = "0.4.0" authors = ["Saluki"] edition = "2018" readme = "README.md" diff --git a/README.md b/README.md index 6f4881e..086a9e9 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Find all hosts in your local network using this fast ARP scanner. The CLI is wri Download the `arp-scan` binary for Linux (Ubuntu, Fedora, Debian, ...). See the [releases page](https://github.com/Saluki/arp-scan-rs/releases) for other binaries. ```bash -wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.3.0/arp-scan-v0.3.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan +wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.4.0/arp-scan-v0.4.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan ``` List all available network interfaces. @@ -53,7 +53,7 @@ List all available network interfaces. Using this option will only print a list Perform a scan on the network interface `eth0`. The first valid IPv4 network on this interface will be used as scan target. -#### Set timeout `-t 15` +#### Set global scan 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 `2`. diff --git a/src/main.rs b/src/main.rs index e49f97e..67c552d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ const TIMEOUT_DEFAULT: u64 = 2; fn main() { let matches = App::new("arp-scan") - .version("0.3.0") + .version("0.4.0") .about("A minimalistic ARP scan tool written in Rust") .arg( Arg::with_name("interface").short("i").long("interface").takes_value(true).value_name("INTERFACE_NAME").help("Network interface") From 1b8cb367e21ee6d7717f9c0dd4a65295247bcd02 Mon Sep 17 00:00:00 2001 From: Saluki Date: Wed, 19 May 2021 13:03:45 +0200 Subject: [PATCH 026/121] Fixed version tag --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 086a9e9..e45de02 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Change or force the IPv4 address sent as source in the broadcasted ARP packets. Change or force the MAC address sent as destination ARP request. By default, a broadcast destination (`00:00:00:00:00:00`) will be set. -#### Show version `-v` +#### Show version `--version` Display the ARP scan CLI version and exits the process. From a22a0a9912ababda54d829838b6afc5b914ce575 Mon Sep 17 00:00:00 2001 From: Saluki Date: Fri, 21 May 2021 12:53:22 +0200 Subject: [PATCH 027/121] Adding network interface auto-discovery --- README.md | 2 +- src/main.rs | 19 +++++++++++++------ src/utils.rs | 29 ++++++++++++++++++++++++++++- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e45de02..e6a8b5c 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ List all available network interfaces. Using this option will only print a list #### 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. +Perform a scan on the network interface `eth0`. The first valid IPv4 network on this interface will be used as scan target. By default, the first network interface with an `up` status and a valid IPv4 will be selected. #### Set global scan timeout `-t 15` diff --git a/src/main.rs b/src/main.rs index 67c552d..142bf4a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,8 +6,7 @@ use std::process; use std::thread; use ipnetwork::NetworkSize; -use pnet::datalink; -use pnet::datalink::{MacAddr}; +use pnet::datalink::{self, MacAddr}; use clap::{Arg, App}; const FIVE_HOURS: u64 = 5 * 60 * 60; @@ -58,11 +57,19 @@ fn main() { // with an IPv4 address and root permissions (for crafting ARP packets). let interface_name = match matches.value_of("interface") { - Some(name) => name, + Some(name) => String::from(name), None => { - eprintln!("Network interface name required"); - eprintln!("Use 'arp scan -l' to list available interfaces"); - process::exit(1); + + match utils::select_default_interface() { + Some(default_interface) => { + String::from(default_interface.name) + }, + None => { + eprintln!("Network interface name required"); + eprintln!("Use 'arp scan -l' to list available interfaces"); + process::exit(1); + } + } } }; diff --git a/src/utils.rs b/src/utils.rs index dfd2555..5f35a55 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,4 @@ -use pnet::datalink::NetworkInterface; +use pnet::datalink::{self, NetworkInterface}; use crate::network::TargetDetails; @@ -30,6 +30,33 @@ pub fn show_interfaces(interfaces: &Vec) { } } +/** + * Find a default network interface for scans, based on the operating system + * priority and some interface technical details. + */ +pub fn select_default_interface() -> Option { + + let interfaces = datalink::interfaces(); + + interfaces.into_iter().find(|interface| { + + if let None = interface.mac { + return false; + } + + if interface.ips.len() == 0 || !interface.is_up() || interface.is_loopback() { + return false; + } + + let potential_ipv4 = interface.ips.iter().find(|ip| ip.is_ipv4()); + if let None = potential_ipv4 { + return false; + } + + true + }) +} + /** * Display the scan results on stdout with a table. The 'final_result' vector * contains all items that will be displayed. From 1b0d59d84cc03b6ff6a9350e3f7bbf1ea07a05c2 Mon Sep 17 00:00:00 2001 From: Saluki Date: Fri, 21 May 2021 18:48:51 +0200 Subject: [PATCH 028/121] Add a new VLAN sending option --- README.md | 4 ++++ src/main.rs | 19 ++++++++++++++++++- src/network.rs | 41 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e6a8b5c..f5da190 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,10 @@ Change or force the IPv4 address sent as source in the broadcasted ARP packets. Change or force the MAC address sent as destination ARP request. By default, a broadcast destination (`00:00:00:00:00:00`) will be set. +#### Set VLAN ID `-Q 540` + +Add a 802.1Q field in the Ethernet frame. This fields contains the given VLAN ID for outgoing ARP requests. By default, the Ethernet frame is sent without 802.1Q fields (no VLAN). + #### Show version `--version` Display the ARP scan CLI version and exits the process. diff --git a/src/main.rs b/src/main.rs index 142bf4a..664b216 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,6 +32,9 @@ fn main() { .arg( Arg::with_name("numeric").short("n").long("numeric").takes_value(false).help("Numeric mode, no hostname resolution") ) + .arg( + Arg::with_name("vlan").short("Q").long("vlan").takes_value(true).value_name("VLAN_ID").help("Send using 802.1Q with VLAN ID") + ) .arg( Arg::with_name("list").short("l").long("list").takes_value(false).help("List network interfaces") ) @@ -111,6 +114,20 @@ fn main() { }, None => None }; + + let vlan_id: Option = match matches.value_of("vlan") { + Some(vlan) => { + + match vlan.parse::() { + Ok(vlan_number) => Some(vlan_number), + Err(_) => { + eprintln!("Expected valid VLAN identifier"); + process::exit(1); + } + } + }, + None => None + }; if !utils::is_root_user() { eprintln!("Should run this binary as root"); @@ -169,7 +186,7 @@ fn main() { for ip_address in ip_network.iter() { if let IpAddr::V4(ipv4_address) = ip_address { - network::send_arp_request(&mut tx, selected_interface, ipv4_address, source_ipv4, destination_mac); + network::send_arp_request(&mut tx, selected_interface, ipv4_address, source_ipv4, destination_mac, vlan_id); } } diff --git a/src/network.rs b/src/network.rs index 6b91d84..a532ecb 100644 --- a/src/network.rs +++ b/src/network.rs @@ -8,6 +8,14 @@ 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}; +use pnet::packet::vlan::{ClassOfService, MutableVlanPacket}; + +const VLAN_QOS_DEFAULT: u8 = 1; +const ARP_PACKET_SIZE: usize = 28; +const VLAN_PACKET_SIZE: usize = 32; + +const ETHERNET_STD_PACKET_SIZE: usize = 42; +const ETHERNET_VLAN_PACKET_SIZE: usize = 46; /** * A target detail represents a single host on the local network with an IPv4 @@ -25,9 +33,12 @@ pub struct TargetDetails { * 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, forced_source_ipv4: Option, forced_destination_mac: Option) { +pub fn send_arp_request(tx: &mut Box, interface: &NetworkInterface, target_ip: Ipv4Addr, forced_source_ipv4: Option, forced_destination_mac: Option, forced_vlan_id: Option) { - let mut ethernet_buffer = [0u8; 42]; + let mut ethernet_buffer = match forced_vlan_id { + Some(_) => vec![0u8; ETHERNET_VLAN_PACKET_SIZE], + None => vec![0u8; ETHERNET_STD_PACKET_SIZE] + }; let mut ethernet_packet = MutableEthernetPacket::new(&mut ethernet_buffer).unwrap(); let target_mac = match forced_destination_mac { @@ -41,9 +52,14 @@ pub fn send_arp_request(tx: &mut Box, interface: &NetworkInt ethernet_packet.set_destination(target_mac); ethernet_packet.set_source(source_mac); - ethernet_packet.set_ethertype(EtherTypes::Arp); - let mut arp_buffer = [0u8; 28]; + let selected_ethertype = match forced_vlan_id { + Some(_) => EtherTypes::Vlan, + None => EtherTypes::Arp + }; + ethernet_packet.set_ethertype(selected_ethertype); + + let mut arp_buffer = [0u8; ARP_PACKET_SIZE]; let mut arp_packet = MutableArpPacket::new(&mut arp_buffer).unwrap(); let source_ipv4 = find_source_ip(interface, forced_source_ipv4); @@ -58,7 +74,22 @@ pub fn send_arp_request(tx: &mut Box, interface: &NetworkInt arp_packet.set_target_hw_addr(target_mac); arp_packet.set_target_proto_addr(target_ip); - ethernet_packet.set_payload(arp_packet.packet_mut()); + if let Some(vlan_id) = forced_vlan_id { + + let mut vlan_buffer = [0u8; VLAN_PACKET_SIZE]; + let mut vlan_packet = MutableVlanPacket::new(&mut vlan_buffer).unwrap(); + vlan_packet.set_vlan_identifier(vlan_id); + vlan_packet.set_priority_code_point(ClassOfService::new(VLAN_QOS_DEFAULT)); + vlan_packet.set_drop_eligible_indicator(0); + vlan_packet.set_ethertype(EtherTypes::Arp); + + vlan_packet.set_payload(arp_packet.packet_mut()); + + ethernet_packet.set_payload(vlan_packet.packet_mut()); + } + else { + ethernet_packet.set_payload(arp_packet.packet_mut()); + } tx.send_to(ðernet_packet.to_immutable().packet(), Some(interface.clone())); } From 9567b914f53cfc7d15a145af2ace33ec0d27595f Mon Sep 17 00:00:00 2001 From: Saluki Date: Fri, 21 May 2021 18:50:47 +0200 Subject: [PATCH 029/121] Release 0.5.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 2 +- src/main.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1369bf2..ef4e9cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,7 +20,7 @@ dependencies = [ [[package]] name = "arp-scan" -version = "0.4.0" +version = "0.5.0" dependencies = [ "clap", "dns-lookup", diff --git a/Cargo.toml b/Cargo.toml index ef26fc6..9aa82be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "arp-scan" description = "A minimalistic ARP scan tool" license = "AGPL-3.0-or-later" -version = "0.4.0" +version = "0.5.0" authors = ["Saluki"] edition = "2018" readme = "README.md" diff --git a/README.md b/README.md index f5da190..b117d95 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Find all hosts in your local network using this fast ARP scanner. The CLI is wri Download the `arp-scan` binary for Linux (Ubuntu, Fedora, Debian, ...). See the [releases page](https://github.com/Saluki/arp-scan-rs/releases) for other binaries. ```bash -wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.4.0/arp-scan-v0.4.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan +wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.5.0/arp-scan-v0.5.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan ``` List all available network interfaces. diff --git a/src/main.rs b/src/main.rs index 664b216..bd05233 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,7 @@ const TIMEOUT_DEFAULT: u64 = 2; fn main() { let matches = App::new("arp-scan") - .version("0.4.0") + .version("0.5.0") .about("A minimalistic ARP scan tool written in Rust") .arg( Arg::with_name("interface").short("i").long("interface").takes_value(true).value_name("INTERFACE_NAME").help("Network interface") From 0317478681eae543ce20683b08fbc6d5303edaff Mon Sep 17 00:00:00 2001 From: Saluki Date: Sat, 22 May 2021 13:55:55 +0200 Subject: [PATCH 030/121] New dedicated scan options structure --- src/args.rs | 134 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/args.rs diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..a222e91 --- /dev/null +++ b/src/args.rs @@ -0,0 +1,134 @@ +use std::net::Ipv4Addr; +use std::process; + +use clap::{Arg, ArgMatches, App}; +use pnet::datalink::MacAddr; + +const FIVE_HOURS: u64 = 5 * 60 * 60; +const TIMEOUT_DEFAULT: u64 = 2; + +pub fn build_args<'a, 'b>() -> App<'a, 'b> { + + App::new("arp-scan") + .version("0.5.0") + .about("A minimalistic ARP scan tool written in Rust") + .arg( + Arg::with_name("interface").short("i").long("interface").takes_value(true).value_name("INTERFACE_NAME").help("Network interface") + ) + .arg( + Arg::with_name("timeout").short("t").long("timeout").takes_value(true).value_name("TIMEOUT_SECONDS").help("ARP response timeout") + ) + .arg( + Arg::with_name("source_ip").short("S").long("source-ip").takes_value(true).value_name("SOURCE_IPV4").help("Source IPv4 address for requests") + ) + .arg( + Arg::with_name("destination_mac").short("M").long("dest-mac").takes_value(true).value_name("DESTINATION_MAC").help("Destination MAC address for requests") + ) + .arg( + Arg::with_name("numeric").short("n").long("numeric").takes_value(false).help("Numeric mode, no hostname resolution") + ) + .arg( + Arg::with_name("vlan").short("Q").long("vlan").takes_value(true).value_name("VLAN_ID").help("Send using 802.1Q with VLAN ID") + ) + .arg( + Arg::with_name("list").short("l").long("list").takes_value(false).help("List network interfaces") + ) +} + +#[derive(Clone)] +pub struct ScanOptions { + pub interface_name: String, + pub timeout_seconds: u64, + pub resolve_hostname: bool, + pub source_ipv4: Option, + pub destination_mac: Option, + pub vlan_id: Option +} + +impl ScanOptions { + + pub fn new(matches: &ArgMatches) -> Self { + + let interface_name = match matches.value_of("interface") { + Some(name) => String::from(name), + None => { + + match super::utils::select_default_interface() { + Some(default_interface) => { + String::from(default_interface.name) + }, + None => { + eprintln!("Network interface name required"); + eprintln!("Use 'arp scan -l' to list available interfaces"); + process::exit(1); + } + } + } + }; + + 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"); + + let source_ipv4: Option = match matches.value_of("source_ip") { + Some(source_ip) => { + + match source_ip.parse::() { + Ok(parsed_ipv4) => Some(parsed_ipv4), + Err(_) => { + eprintln!("Expected valid IPv4 as source IP"); + process::exit(1); + } + } + }, + None => None + }; + + let destination_mac: Option = match matches.value_of("destination_mac") { + Some(mac_address) => { + + match mac_address.parse::() { + Ok(parsed_mac) => Some(parsed_mac), + Err(_) => { + eprintln!("Expected valid MAC address as destination"); + process::exit(1); + } + } + }, + None => None + }; + + let vlan_id: Option = match matches.value_of("vlan") { + Some(vlan) => { + + match vlan.parse::() { + Ok(vlan_number) => Some(vlan_number), + Err(_) => { + eprintln!("Expected valid VLAN identifier"); + process::exit(1); + } + } + }, + None => None + }; + + ScanOptions { + interface_name, + timeout_seconds, + resolve_hostname, + source_ipv4, + destination_mac, + vlan_id + } + } + +} From 07c66b65d262c07e7619a97d691c6a6d6c9f0c0d Mon Sep 17 00:00:00 2001 From: Saluki Date: Sat, 22 May 2021 13:56:11 +0200 Subject: [PATCH 031/121] Refactor scan options in CLI --- src/main.rs | 132 +++++++++---------------------------------------- src/network.rs | 56 ++++++++++++--------- src/utils.rs | 5 +- 3 files changed, 58 insertions(+), 135 deletions(-) diff --git a/src/main.rs b/src/main.rs index bd05233..3ca8167 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,44 +1,19 @@ +mod args; mod network; mod utils; -use std::net::{IpAddr, Ipv4Addr}; +use std::net::IpAddr; use std::process; use std::thread; use ipnetwork::NetworkSize; -use pnet::datalink::{self, MacAddr}; -use clap::{Arg, App}; +use pnet::datalink; -const FIVE_HOURS: u64 = 5 * 60 * 60; -const TIMEOUT_DEFAULT: u64 = 2; +use args::ScanOptions; fn main() { - let matches = App::new("arp-scan") - .version("0.5.0") - .about("A minimalistic ARP scan tool written in Rust") - .arg( - Arg::with_name("interface").short("i").long("interface").takes_value(true).value_name("INTERFACE_NAME").help("Network interface") - ) - .arg( - Arg::with_name("timeout").short("t").long("timeout").takes_value(true).value_name("TIMEOUT_SECONDS").help("ARP response timeout") - ) - .arg( - Arg::with_name("source_ip").short("S").long("source-ip").takes_value(true).value_name("SOURCE_IPV4").help("Source IPv4 address for requests") - ) - .arg( - Arg::with_name("destination_mac").short("M").long("dest-mac").takes_value(true).value_name("DESTINATION_MAC").help("Destination MAC address for requests") - ) - .arg( - Arg::with_name("numeric").short("n").long("numeric").takes_value(false).help("Numeric mode, no hostname resolution") - ) - .arg( - Arg::with_name("vlan").short("Q").long("vlan").takes_value(true).value_name("VLAN_ID").help("Send using 802.1Q with VLAN ID") - ) - .arg( - Arg::with_name("list").short("l").long("list").takes_value(false).help("List network interfaces") - ) - .get_matches(); + let matches = args::build_args().get_matches(); // Find interfaces & list them if requested // ---------------------------------------- @@ -59,75 +34,7 @@ fn main() { // network for the given interface. ARP scans require an active interface // with an IPv4 address and root permissions (for crafting ARP packets). - let interface_name = match matches.value_of("interface") { - Some(name) => String::from(name), - None => { - - match utils::select_default_interface() { - Some(default_interface) => { - String::from(default_interface.name) - }, - None => { - eprintln!("Network interface name required"); - eprintln!("Use 'arp scan -l' to list available interfaces"); - process::exit(1); - } - } - } - }; - - 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"); - - let source_ipv4: Option = match matches.value_of("source_ip").map(|source| source.parse::()) { - Some(parsed_source) => { - - if let Err(_) = parsed_source { - eprintln!("Expected valid IPv4 as source IP"); - process::exit(1); - } - - Some(parsed_source.unwrap()) - }, - None => None - }; - - let destination_mac: Option = match matches.value_of("destination_mac").map(|dest| dest.parse::()) { - Some(mac_address) => { - - if let Err(_) = mac_address { - eprintln!("Expected valid MAC address as destination"); - process::exit(1); - } - - Some(mac_address.unwrap()) - }, - None => None - }; - - let vlan_id: Option = match matches.value_of("vlan") { - Some(vlan) => { - - match vlan.parse::() { - Ok(vlan_number) => Some(vlan_number), - Err(_) => { - eprintln!("Expected valid VLAN identifier"); - process::exit(1); - } - } - }, - None => None - }; + let scan_options = ScanOptions::new(&matches); if !utils::is_root_user() { eprintln!("Should run this binary as root"); @@ -135,9 +42,9 @@ fn main() { } let selected_interface: &datalink::NetworkInterface = interfaces.iter() - .find(|interface| { interface.name == interface_name && interface.is_up() && !interface.is_loopback() }) + .find(|interface| { interface.name == scan_options.interface_name && interface.is_up() && !interface.is_loopback() }) .unwrap_or_else(|| { - eprintln!("Could not find interface with name {}", interface_name); + eprintln!("Could not find interface with name {}", scan_options.interface_name); process::exit(1); }); @@ -156,10 +63,10 @@ fn main() { println!(""); println!("Selected interface {} with IP {}", selected_interface.name, ip_network); - if let Some(forced_source_ipv4) = source_ipv4 { + if let Some(forced_source_ipv4) = scan_options.source_ipv4 { println!("The ARP source IPv4 will be forced to {}", forced_source_ipv4); } - if let Some(forced_destination_mac) = destination_mac { + if let Some(forced_destination_mac) = scan_options.destination_mac { println!("The ARP destination MAC will be forced to {}", forced_destination_mac); } @@ -171,22 +78,29 @@ fn main() { let (mut tx, mut rx) = match datalink::channel(selected_interface, Default::default()) { Ok(datalink::Channel::Ethernet(tx, rx)) => (tx, rx), - Ok(_) => panic!("unknown interface type, expected Ethernet"), - Err(error) => panic!(error) + Ok(_) => { + eprintln!("Expected an Ethernet datalink channel"); + process::exit(1); + }, + Err(error) => { + eprintln!("Datalink channel creation failed ({})", error); + process::exit(1); + } }; - let arp_responses = thread::spawn(move || network::receive_arp_responses(&mut rx, timeout_seconds, resolve_hostname)); + let cloned_options = scan_options.clone(); + let arp_responses = thread::spawn(move || network::receive_arp_responses(&mut rx, &cloned_options)); let network_size: u128 = match ip_network.size() { NetworkSize::V4(x) => x.into(), NetworkSize::V6(y) => y }; - println!("Sending {} ARP requests to network ({}s timeout)", network_size, timeout_seconds); + println!("Sending {} ARP requests to network ({}s timeout)", network_size, scan_options.timeout_seconds); for ip_address in ip_network.iter() { if let IpAddr::V4(ipv4_address) = ip_address { - network::send_arp_request(&mut tx, selected_interface, ipv4_address, source_ipv4, destination_mac, vlan_id); + network::send_arp_request(&mut tx, selected_interface, &ip_network, ipv4_address, &scan_options); } } @@ -195,5 +109,5 @@ fn main() { process::exit(1); }); - utils::display_scan_results(final_result, resolve_hostname); + utils::display_scan_results(final_result, &scan_options); } diff --git a/src/network.rs b/src/network.rs index a532ecb..1331541 100644 --- a/src/network.rs +++ b/src/network.rs @@ -4,12 +4,15 @@ use std::time::Instant; use std::collections::HashMap; use dns_lookup::lookup_addr; +use ipnetwork::IpNetwork; 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}; use pnet::packet::vlan::{ClassOfService, MutableVlanPacket}; +use crate::args::ScanOptions; + const VLAN_QOS_DEFAULT: u8 = 1; const ARP_PACKET_SIZE: usize = 28; const VLAN_PACKET_SIZE: usize = 32; @@ -33,15 +36,18 @@ pub struct TargetDetails { * 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, forced_source_ipv4: Option, forced_destination_mac: Option, forced_vlan_id: Option) { +pub fn send_arp_request(tx: &mut Box, interface: &NetworkInterface, ip_network: &IpNetwork, target_ip: Ipv4Addr, options: &ScanOptions) { - let mut ethernet_buffer = match forced_vlan_id { + let mut ethernet_buffer = match options.vlan_id { Some(_) => vec![0u8; ETHERNET_VLAN_PACKET_SIZE], None => vec![0u8; ETHERNET_STD_PACKET_SIZE] }; - let mut ethernet_packet = MutableEthernetPacket::new(&mut ethernet_buffer).unwrap(); + let mut ethernet_packet = MutableEthernetPacket::new(&mut ethernet_buffer).unwrap_or_else(|| { + eprintln!("Could not build Ethernet packet"); + process::exit(1); + }); - let target_mac = match forced_destination_mac { + let target_mac = match options.destination_mac { Some(forced_mac) => forced_mac, None => MacAddr::broadcast() }; @@ -53,16 +59,19 @@ pub fn send_arp_request(tx: &mut Box, interface: &NetworkInt ethernet_packet.set_destination(target_mac); ethernet_packet.set_source(source_mac); - let selected_ethertype = match forced_vlan_id { + let selected_ethertype = match options.vlan_id { Some(_) => EtherTypes::Vlan, None => EtherTypes::Arp }; ethernet_packet.set_ethertype(selected_ethertype); let mut arp_buffer = [0u8; ARP_PACKET_SIZE]; - let mut arp_packet = MutableArpPacket::new(&mut arp_buffer).unwrap(); + let mut arp_packet = MutableArpPacket::new(&mut arp_buffer).unwrap_or_else(|| { + eprintln!("Could not build ARP packet"); + process::exit(1); + }); - let source_ipv4 = find_source_ip(interface, forced_source_ipv4); + let source_ipv4 = find_source_ip(ip_network, options.source_ipv4); arp_packet.set_hardware_type(ArpHardwareTypes::Ethernet); arp_packet.set_protocol_type(EtherTypes::Ipv4); @@ -74,10 +83,13 @@ pub fn send_arp_request(tx: &mut Box, interface: &NetworkInt arp_packet.set_target_hw_addr(target_mac); arp_packet.set_target_proto_addr(target_ip); - if let Some(vlan_id) = forced_vlan_id { + if let Some(vlan_id) = options.vlan_id { let mut vlan_buffer = [0u8; VLAN_PACKET_SIZE]; - let mut vlan_packet = MutableVlanPacket::new(&mut vlan_buffer).unwrap(); + let mut vlan_packet = MutableVlanPacket::new(&mut vlan_buffer).unwrap_or_else(|| { + eprintln!("Could not build VLAN packet"); + process::exit(1); + }); vlan_packet.set_vlan_identifier(vlan_id); vlan_packet.set_priority_code_point(ClassOfService::new(VLAN_QOS_DEFAULT)); vlan_packet.set_drop_eligible_indicator(0); @@ -99,23 +111,19 @@ pub fn send_arp_request(tx: &mut Box, interface: &NetworkInt * ARP requests. If the 'forced_source_ipv4' parameter is set, it will take * the priority over the network interface address. */ -fn find_source_ip(interface: &NetworkInterface, forced_source_ipv4: Option) -> Ipv4Addr { +fn find_source_ip(ip_network: &IpNetwork, forced_source_ipv4: Option) -> Ipv4Addr { if let Some(forced_ipv4) = forced_source_ipv4 { return forced_ipv4; } - let source_ip = interface.ips.first().unwrap_or_else(|| { - eprintln!("Interface should have an IP address"); - process::exit(1); - }).ip(); - - let source_ipv4 = match source_ip { - IpAddr::V4(ipv4_addr) => Some(ipv4_addr), - IpAddr::V6(_ipv6_addr) => None - }; - - source_ipv4.unwrap() + match ip_network.ip() { + IpAddr::V4(ipv4_addr) => ipv4_addr, + IpAddr::V6(_ipv6_addr) => { + eprintln!("Expected IPv4 address on network interface, found IPv6"); + process::exit(1); + } + } } /** @@ -124,14 +132,14 @@ fn find_source_ip(interface: &NetworkInterface, forced_source_ipv4: Option, timeout_seconds: u64, resolve_hostname: bool) -> Vec { +pub fn receive_arp_responses(rx: &mut Box, options: &ScanOptions) -> Vec { let mut discover_map: HashMap = HashMap::new(); let start_recording = Instant::now(); loop { - if start_recording.elapsed().as_secs() > timeout_seconds { + if start_recording.elapsed().as_secs() > options.timeout_seconds { break; } @@ -171,7 +179,7 @@ pub fn receive_arp_responses(rx: &mut Box, timeout_seconds discover_map.into_iter().map(|(_, mut target_details)| { - if resolve_hostname { + if options.resolve_hostname { target_details.hostname = find_hostname(target_details.ipv4); } diff --git a/src/utils.rs b/src/utils.rs index 5f35a55..59ce920 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,7 @@ use pnet::datalink::{self, NetworkInterface}; use crate::network::TargetDetails; +use crate::args::ScanOptions; /** * Based on the current UNIX environment, find if the process is run as root @@ -61,7 +62,7 @@ pub fn select_default_interface() -> Option { * Display the scan results on stdout with a table. The 'final_result' vector * contains all items that will be displayed. */ -pub fn display_scan_results(mut final_result: Vec, resolve_hostname: bool) { +pub fn display_scan_results(mut final_result: Vec, options: &ScanOptions) { final_result.sort_by_key(|item| item.ipv4); @@ -73,7 +74,7 @@ pub fn display_scan_results(mut final_result: Vec, resolve_hostna let hostname = match result_item.hostname { Some(hostname) => hostname, - None if !resolve_hostname => String::from("(disabled)"), + None if !options.resolve_hostname => String::from("(disabled)"), None => String::from("") }; println!("| {: <15} | {: <18} | {: <21} |", result_item.ipv4, result_item.mac, hostname); From bcac5814340ce5f5ff439cd4168a36433e0bd42f Mon Sep 17 00:00:00 2001 From: Saluki Date: Sat, 22 May 2021 14:17:49 +0200 Subject: [PATCH 032/121] Adding a new retry count per host --- README.md | 4 ++++ src/args.rs | 24 ++++++++++++++++++++++-- src/main.rs | 12 ++++++++---- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b117d95..8251e03 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,10 @@ Enforce a timeout of at least 15 seconds. This timeout is a minimum value (scans Switch to numeric mode. This will skip the local hostname resolution process and will only display IP addresses. +#### Host retry count `-r 3` + +Send 3 ARP requests to the targets (retry count). By default, a single ARP request will be sent to each host. + #### Change source IPv4 `-S 192.168.1.130` Change or force the IPv4 address sent as source in the broadcasted ARP packets. By default, a valid IPv4 address on the network interface will be used. This option may be useful for isolated hosts and security checks. diff --git a/src/args.rs b/src/args.rs index a222e91..e2159a7 100644 --- a/src/args.rs +++ b/src/args.rs @@ -6,6 +6,7 @@ use pnet::datalink::MacAddr; const FIVE_HOURS: u64 = 5 * 60 * 60; const TIMEOUT_DEFAULT: u64 = 2; +const HOST_RETRY_DEFAULT: usize = 1; pub fn build_args<'a, 'b>() -> App<'a, 'b> { @@ -30,6 +31,9 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name("vlan").short("Q").long("vlan").takes_value(true).value_name("VLAN_ID").help("Send using 802.1Q with VLAN ID") ) + .arg( + Arg::with_name("retry_count").short("r").long("retry").takes_value(true).value_name("RETRY_COUNT").help("Host retry attempt count") + ) .arg( Arg::with_name("list").short("l").long("list").takes_value(false).help("List network interfaces") ) @@ -42,7 +46,8 @@ pub struct ScanOptions { pub resolve_hostname: bool, pub source_ipv4: Option, pub destination_mac: Option, - pub vlan_id: Option + pub vlan_id: Option, + pub retry_count: usize } impl ScanOptions { @@ -120,6 +125,20 @@ impl ScanOptions { }, None => None }; + + let retry_count = match matches.value_of("retry_count") { + Some(retry_count) => { + + match retry_count.parse::() { + Ok(retry_number) => retry_number, + Err(_) => { + eprintln!("Expected positive number for host retry count"); + process::exit(1); + } + } + }, + None => HOST_RETRY_DEFAULT + }; ScanOptions { interface_name, @@ -127,7 +146,8 @@ impl ScanOptions { resolve_hostname, source_ipv4, destination_mac, - vlan_id + vlan_id, + retry_count } } diff --git a/src/main.rs b/src/main.rs index 3ca8167..e7a257e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -97,10 +97,14 @@ fn main() { }; println!("Sending {} ARP requests to network ({}s timeout)", network_size, scan_options.timeout_seconds); - for ip_address in ip_network.iter() { - - if let IpAddr::V4(ipv4_address) = ip_address { - network::send_arp_request(&mut tx, selected_interface, &ip_network, ipv4_address, &scan_options); + // The retry count does right now use a 'brute-force' strategy without + // synchronization process with the already known hosts. + for _ in 0..scan_options.retry_count { + for ip_address in ip_network.iter() { + + if let IpAddr::V4(ipv4_address) = ip_address { + network::send_arp_request(&mut tx, selected_interface, &ip_network, ipv4_address, &scan_options); + } } } From 32ffba44241a4b432c42f3dfb0c643fa52918d0f Mon Sep 17 00:00:00 2001 From: Saluki Date: Sun, 23 May 2021 17:54:33 +0200 Subject: [PATCH 033/121] Global code cleaning with response summary --- src/args.rs | 13 ++++++++++++- src/main.rs | 26 +++++++++++++++----------- src/network.rs | 26 +++++++++++++++++++++----- src/utils.rs | 23 +++++++++++++++++------ 4 files changed, 65 insertions(+), 23 deletions(-) diff --git a/src/args.rs b/src/args.rs index e2159a7..16fc6e3 100644 --- a/src/args.rs +++ b/src/args.rs @@ -8,10 +8,16 @@ const FIVE_HOURS: u64 = 5 * 60 * 60; const TIMEOUT_DEFAULT: u64 = 2; const HOST_RETRY_DEFAULT: usize = 1; +const CLI_VERSION: &'static str = env!("CARGO_PKG_VERSION"); + +/** + * This function groups together all exposed CLI arguments to the end-users + * with clap. Other CLI details (version, ...) should be grouped there as well. + */ pub fn build_args<'a, 'b>() -> App<'a, 'b> { App::new("arp-scan") - .version("0.5.0") + .version(CLI_VERSION) .about("A minimalistic ARP scan tool written in Rust") .arg( Arg::with_name("interface").short("i").long("interface").takes_value(true).value_name("INTERFACE_NAME").help("Network interface") @@ -52,6 +58,11 @@ pub struct ScanOptions { impl ScanOptions { + /** + * Build a new 'ScanOptions' struct that will be used in the whole CLI such + * as the network level, the display details and more. The scan options reflect + * user requests for the CLI and should not be mutated. + */ pub fn new(matches: &ArgMatches) -> Self { let interface_name = match matches.value_of("interface") { diff --git a/src/main.rs b/src/main.rs index e7a257e..f455cec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,18 +49,17 @@ fn main() { }); let ip_network = match selected_interface.ips.first() { - Some(ip_network) => ip_network, + Some(ip_network) if ip_network.is_ipv4() => ip_network, + Some(_) => { + eprintln!("Only IPv4 networks supported"); + process::exit(1); + }, None => { - eprintln!("Expects a valid IP on the interface"); + eprintln!("Expects a valid IP on the interface, none found"); process::exit(1); } }; - if !ip_network.is_ipv4() { - eprintln!("Only IPv4 supported"); - process::exit(1); - } - println!(""); println!("Selected interface {} with IP {}", selected_interface.name, ip_network); if let Some(forced_source_ipv4) = scan_options.source_ipv4 { @@ -88,12 +87,17 @@ fn main() { } }; + // The options are right now cloned, since they are moved in the response + // -catching thread, while also being used in the main thread after. let cloned_options = scan_options.clone(); let arp_responses = thread::spawn(move || network::receive_arp_responses(&mut rx, &cloned_options)); let network_size: u128 = match ip_network.size() { - NetworkSize::V4(x) => x.into(), - NetworkSize::V6(y) => y + NetworkSize::V4(ipv4_network_size) => ipv4_network_size.into(), + NetworkSize::V6(_) => { + eprintln!("IPv6 networks are not supported by the ARP protocol"); + process::exit(1); + } }; println!("Sending {} ARP requests to network ({}s timeout)", network_size, scan_options.timeout_seconds); @@ -108,10 +112,10 @@ fn main() { } } - let final_result = arp_responses.join().unwrap_or_else(|error| { + let (response_summary, target_details) = arp_responses.join().unwrap_or_else(|error| { eprintln!("Failed to close receive thread ({:?})", error); process::exit(1); }); - utils::display_scan_results(final_result, &scan_options); + utils::display_scan_results(response_summary, target_details, &scan_options); } diff --git a/src/network.rs b/src/network.rs index 1331541..0b0a9de 100644 --- a/src/network.rs +++ b/src/network.rs @@ -20,6 +20,15 @@ const VLAN_PACKET_SIZE: usize = 32; const ETHERNET_STD_PACKET_SIZE: usize = 42; const ETHERNET_VLAN_PACKET_SIZE: usize = 46; +/** + * Gives high-level details about the scan response. This may include Ethernet + * details (packet count, size, ...) and other technical network aspects. + */ +pub struct ResponseSummary { + pub packet_count: usize, + pub arp_count: usize +} + /** * A target detail represents a single host on the local network with an IPv4 * address and a linked MAC address. Hostnames are optional since some hosts @@ -132,11 +141,14 @@ fn find_source_ip(ip_network: &IpNetwork, forced_source_ipv4: Option) * 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, options: &ScanOptions) -> Vec { +pub fn receive_arp_responses(rx: &mut Box, options: &ScanOptions) -> (ResponseSummary, Vec) { let mut discover_map: HashMap = HashMap::new(); let start_recording = Instant::now(); + let mut packet_count = 0; + let mut arp_count = 0; + loop { if start_recording.elapsed().as_secs() > options.timeout_seconds { @@ -147,6 +159,7 @@ pub fn receive_arp_responses(rx: &mut Box, options: &ScanO eprintln!("Failed to receive ARP requests ({})", error); process::exit(1); }); + packet_count += 1; let ethernet_packet = match EthernetPacket::new(&arp_buffer[..]) { Some(packet) => packet, @@ -163,6 +176,7 @@ pub fn receive_arp_responses(rx: &mut Box, options: &ScanO } let arp_packet = ArpPacket::new(&arp_buffer[MutableEthernetPacket::minimum_packet_size()..]); + arp_count += 1; if let Some(arp) = arp_packet { @@ -177,15 +191,17 @@ pub fn receive_arp_responses(rx: &mut Box, options: &ScanO } } - discover_map.into_iter().map(|(_, mut target_details)| { + let target_details = discover_map.into_iter().map(|(_, mut target_detail)| { if options.resolve_hostname { - target_details.hostname = find_hostname(target_details.ipv4); + target_detail.hostname = find_hostname(target_detail.ipv4); } - target_details + target_detail + + }).collect(); - }).collect() + (ResponseSummary { packet_count, arp_count }, target_details) } /** diff --git a/src/utils.rs b/src/utils.rs index 59ce920..c150fcb 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,6 @@ use pnet::datalink::{self, NetworkInterface}; -use crate::network::TargetDetails; +use crate::network::{ResponseSummary, TargetDetails}; use crate::args::ScanOptions; /** @@ -62,23 +62,34 @@ pub fn select_default_interface() -> Option { * Display the scan results on stdout with a table. The 'final_result' vector * contains all items that will be displayed. */ -pub fn display_scan_results(mut final_result: Vec, options: &ScanOptions) { +pub fn display_scan_results(response_summary: ResponseSummary, mut target_details: Vec, options: &ScanOptions) { - final_result.sort_by_key(|item| item.ipv4); + target_details.sort_by_key(|item| item.ipv4); println!(""); println!("| IPv4 | MAC | Hostname |"); println!("|-----------------|-------------------|-----------------------|"); - for result_item in final_result { + for detail in target_details { - let hostname = match result_item.hostname { + let hostname = match detail.hostname { Some(hostname) => hostname, None if !options.resolve_hostname => String::from("(disabled)"), None => String::from("") }; - println!("| {: <15} | {: <18} | {: <21} |", result_item.ipv4, result_item.mac, hostname); + println!("| {: <15} | {: <18} | {: <21} |", detail.ipv4, detail.mac, hostname); } println!(""); + match response_summary.packet_count { + 0 => print!("No packets received, "), + 1 => print!("1 packet received, "), + _ => print!("{} packets received, ", response_summary.packet_count) + }; + match response_summary.arp_count { + 0 => println!("no ARP packets filtered"), + 1 => println!("1 ARP packet filtered"), + _ => println!("{} ARP packets filtered", response_summary.arp_count) + }; + println!(""); } From 09ddd28a1ff78625f5e6e470aa71983cb51b2d6c Mon Sep 17 00:00:00 2001 From: Saluki Date: Sun, 23 May 2021 18:15:25 +0200 Subject: [PATCH 034/121] Including scan duration in summary --- src/main.rs | 2 +- src/network.rs | 10 ++++++++-- src/utils.rs | 16 +++++++++++++--- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index f455cec..6a71aca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -99,7 +99,7 @@ fn main() { process::exit(1); } }; - println!("Sending {} ARP requests to network ({}s timeout)", network_size, scan_options.timeout_seconds); + println!("Sending {} ARP requests to network (waiting at least {}s)", network_size, scan_options.timeout_seconds); // The retry count does right now use a 'brute-force' strategy without // synchronization process with the already known hosts. diff --git a/src/network.rs b/src/network.rs index 0b0a9de..cfa20d1 100644 --- a/src/network.rs +++ b/src/network.rs @@ -26,7 +26,8 @@ const ETHERNET_VLAN_PACKET_SIZE: usize = 46; */ pub struct ResponseSummary { pub packet_count: usize, - pub arp_count: usize + pub arp_count: usize, + pub duration_ms: u128 } /** @@ -201,7 +202,12 @@ pub fn receive_arp_responses(rx: &mut Box, options: &ScanO }).collect(); - (ResponseSummary { packet_count, arp_count }, target_details) + let response_summary = ResponseSummary { + packet_count, + arp_count, + duration_ms: start_recording.elapsed().as_millis() + }; + (response_summary, target_details) } /** diff --git a/src/utils.rs b/src/utils.rs index c150fcb..aeba91c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -70,10 +70,10 @@ pub fn display_scan_results(response_summary: ResponseSummary, mut target_detail println!("| IPv4 | MAC | Hostname |"); println!("|-----------------|-------------------|-----------------------|"); - for detail in target_details { + for detail in target_details.iter() { - let hostname = match detail.hostname { - Some(hostname) => hostname, + let hostname = match &detail.hostname { + Some(hostname) => hostname.clone(), None if !options.resolve_hostname => String::from("(disabled)"), None => String::from("") }; @@ -81,6 +81,16 @@ pub fn display_scan_results(response_summary: ResponseSummary, mut target_detail } println!(""); + print!("ARP scan finished, "); + let target_count = target_details.len(); + match target_count { + 0 => print!("no hosts found"), + 1 => print!("1 host found"), + _ => print!("{} hosts found", target_count) + } + let seconds_duration = (response_summary.duration_ms as f32) / (1000 as f32); + println!(" in {:.3} seconds", seconds_duration); + match response_summary.packet_count { 0 => print!("No packets received, "), 1 => print!("1 packet received, "), From aaed521e56d92fc25cfb173a39b785e0cd706624 Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 24 May 2021 13:33:59 +0200 Subject: [PATCH 035/121] Using Arc instead of clones for config --- src/network.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/network.rs b/src/network.rs index cfa20d1..3e48581 100644 --- a/src/network.rs +++ b/src/network.rs @@ -2,8 +2,9 @@ use std::process; use std::net::{IpAddr, Ipv4Addr}; use std::time::Instant; use std::collections::HashMap; -use dns_lookup::lookup_addr; +use std::sync::Arc; +use dns_lookup::lookup_addr; use ipnetwork::IpNetwork; use pnet::datalink::{MacAddr, NetworkInterface, DataLinkSender, DataLinkReceiver}; use pnet::packet::{MutablePacket, Packet}; @@ -46,7 +47,7 @@ pub struct TargetDetails { * 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, ip_network: &IpNetwork, target_ip: Ipv4Addr, options: &ScanOptions) { +pub fn send_arp_request(tx: &mut Box, interface: &NetworkInterface, ip_network: &IpNetwork, target_ip: Ipv4Addr, options: Arc) { let mut ethernet_buffer = match options.vlan_id { Some(_) => vec![0u8; ETHERNET_VLAN_PACKET_SIZE], @@ -142,7 +143,7 @@ fn find_source_ip(ip_network: &IpNetwork, forced_source_ipv4: Option) * 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, options: &ScanOptions) -> (ResponseSummary, Vec) { +pub fn receive_arp_responses(rx: &mut Box, options: Arc) -> (ResponseSummary, Vec) { let mut discover_map: HashMap = HashMap::new(); let start_recording = Instant::now(); From dd93853f7933fc53a18845cff156d159acbd998a Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 24 May 2021 13:34:36 +0200 Subject: [PATCH 036/121] Randomize the target list with new -R option --- src/args.rs | 19 +++++++++++++------ src/main.rs | 27 +++++++++++++++++++++------ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/args.rs b/src/args.rs index 16fc6e3..8d6640c 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,5 +1,6 @@ use std::net::Ipv4Addr; use std::process; +use std::sync::Arc; use clap::{Arg, ArgMatches, App}; use pnet::datalink::MacAddr; @@ -40,12 +41,14 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name("retry_count").short("r").long("retry").takes_value(true).value_name("RETRY_COUNT").help("Host retry attempt count") ) + .arg( + Arg::with_name("random").short("R").long("random").takes_value(false).help("Randomize the target list") + ) .arg( Arg::with_name("list").short("l").long("list").takes_value(false).help("List network interfaces") ) } -#[derive(Clone)] pub struct ScanOptions { pub interface_name: String, pub timeout_seconds: u64, @@ -53,7 +56,8 @@ pub struct ScanOptions { pub source_ipv4: Option, pub destination_mac: Option, pub vlan_id: Option, - pub retry_count: usize + pub retry_count: usize, + pub randomize_targets: bool } impl ScanOptions { @@ -63,7 +67,7 @@ impl ScanOptions { * as the network level, the display details and more. The scan options reflect * user requests for the CLI and should not be mutated. */ - pub fn new(matches: &ArgMatches) -> Self { + pub fn new(matches: &ArgMatches) -> Arc { let interface_name = match matches.value_of("interface") { Some(name) => String::from(name), @@ -150,16 +154,19 @@ impl ScanOptions { }, None => HOST_RETRY_DEFAULT }; + + let randomize_targets = matches.is_present("random"); - ScanOptions { + Arc::new(ScanOptions { interface_name, timeout_seconds, resolve_hostname, source_ipv4, destination_mac, vlan_id, - retry_count - } + retry_count, + randomize_targets + }) } } diff --git a/src/main.rs b/src/main.rs index 6a71aca..eb23fd0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,9 +5,11 @@ mod utils; use std::net::IpAddr; use std::process; use std::thread; +use std::sync::Arc; use ipnetwork::NetworkSize; use pnet::datalink; +use rand::prelude::*; use args::ScanOptions; @@ -87,10 +89,8 @@ fn main() { } }; - // The options are right now cloned, since they are moved in the response - // -catching thread, while also being used in the main thread after. - let cloned_options = scan_options.clone(); - let arp_responses = thread::spawn(move || network::receive_arp_responses(&mut rx, &cloned_options)); + let cloned_options = Arc::clone(&scan_options); + let arp_responses = thread::spawn(move || network::receive_arp_responses(&mut rx, cloned_options)); let network_size: u128 = match ip_network.size() { NetworkSize::V4(ipv4_network_size) => ipv4_network_size.into(), @@ -104,10 +104,25 @@ fn main() { // The retry count does right now use a 'brute-force' strategy without // synchronization process with the already known hosts. for _ in 0..scan_options.retry_count { - for ip_address in ip_network.iter() { + + // The random approach has one major drawback, compared with the native + // network iterator exposed by 'ipnetwork': memory usage. Instead of + // using a small memory footprint iterator, we have to store all IP + // addresses in memory at once. This can cause problems on large ranges. + let ip_addresses: Vec = match scan_options.randomize_targets { + true => { + let mut rng = rand::thread_rng(); + let mut shuffled_addresses: Vec = ip_network.iter().collect(); + shuffled_addresses.shuffle(&mut rng); + shuffled_addresses + }, + false => ip_network.iter().collect() + }; + + for ip_address in ip_addresses { if let IpAddr::V4(ipv4_address) = ip_address { - network::send_arp_request(&mut tx, selected_interface, &ip_network, ipv4_address, &scan_options); + network::send_arp_request(&mut tx, selected_interface, &ip_network, ipv4_address, Arc::clone(&scan_options)); } } } From 1ccfb541119ff8f3cb6561e1b0514c0bfb2673d3 Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 24 May 2021 13:36:05 +0200 Subject: [PATCH 037/121] Release 0.6.0 --- Cargo.lock | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++---- Cargo.toml | 3 ++- README.md | 6 ++++- 3 files changed, 76 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef4e9cd..04aa2a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,12 +20,13 @@ dependencies = [ [[package]] name = "arp-scan" -version = "0.5.0" +version = "0.6.0" dependencies = [ "clap", "dns-lookup", "ipnetwork", "pnet", + "rand", ] [[package]] @@ -78,6 +79,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "glob" version = "0.3.0" @@ -202,11 +214,17 @@ dependencies = [ "pnet_sys", ] +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + [[package]] name = "proc-macro2" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" +checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" dependencies = [ "unicode-xid", ] @@ -220,6 +238,46 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" +dependencies = [ + "rand_core", +] + [[package]] name = "regex" version = "1.5.4" @@ -239,9 +297,9 @@ checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] name = "serde" -version = "1.0.125" +version = "1.0.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" [[package]] name = "socket2" @@ -297,6 +355,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 9aa82be..0e834dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "arp-scan" description = "A minimalistic ARP scan tool" license = "AGPL-3.0-or-later" -version = "0.5.0" +version = "0.6.0" authors = ["Saluki"] edition = "2018" readme = "README.md" @@ -16,3 +16,4 @@ pnet = "0.28.0" ipnetwork = "0.18.0" clap = "2.33.3" dns-lookup = "1.0.6" +rand = "0.8.3" diff --git a/README.md b/README.md index 8251e03..8a3f01e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Find all hosts in your local network using this fast ARP scanner. The CLI is wri Download the `arp-scan` binary for Linux (Ubuntu, Fedora, Debian, ...). See the [releases page](https://github.com/Saluki/arp-scan-rs/releases) for other binaries. ```bash -wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.5.0/arp-scan-v0.5.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan +wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.6.0/arp-scan-v0.6.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan ``` List all available network interfaces. @@ -73,6 +73,10 @@ Change or force the IPv4 address sent as source in the broadcasted ARP packets. Change or force the MAC address sent as destination ARP request. By default, a broadcast destination (`00:00:00:00:00:00`) will be set. +#### Randomize target list `-R` + +Randomize the IPv4 target list before sending ARP requests. By default, all ARP requests are sent in ascending order by IPv4 address. + #### Set VLAN ID `-Q 540` Add a 802.1Q field in the Ethernet frame. This fields contains the given VLAN ID for outgoing ARP requests. By default, the Ethernet frame is sent without 802.1Q fields (no VLAN). From 5bdbc3fe5b9bb9975ae27bdc78c733e6c1ba00b0 Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 24 May 2021 13:59:55 +0200 Subject: [PATCH 038/121] Release utility & README roadmap --- .gitignore | 1 + README.md | 15 +++++++++++++++ release.sh | 22 ++++++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100755 release.sh diff --git a/.gitignore b/.gitignore index 53eaa21..1a1b907 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target +/builds **/*.rs.bk diff --git a/README.md b/README.md index 8a3f01e..2cdda25 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,21 @@ Add a 802.1Q field in the Ethernet frame. This fields contains the given VLAN ID Display the ARP scan CLI version and exits the process. +## Roadmap + +The features below will be shipped in the next releases of the project. + +- Make ARP scans faster + - with a per-host retry approach + - by closing the response thread faster +- Scan profiles (standard, attacker, light, ...) +- Complete VLAN support +- Exports (JSON, CSV, YAML, ...) +- Full ARP packet customization (Ethernet protocol, ARP operation, ...) +- MAC vendor lookup in the results +- Fine-grained scan timings (interval, ...) +- Wide network range support + ## Contributing Feel free to suggest an improvement, report a bug, or ask something: https://github.com/saluki/arp-scan-rs/issues diff --git a/release.sh b/release.sh new file mode 100755 index 0000000..d553ca7 --- /dev/null +++ b/release.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# RELEASE UTILITY +# This script helps with the release process on Github (musl & glibc builds for Linux) + +mkdir -p ./builds +rm ./builds/* + +CLI_VERSION=$(/usr/bin/cat Cargo.toml | egrep "version = (.*)" | egrep -o --color=never "([0-9]+\.?){3}") +echo "Releasing v$CLI_VERSION for GNU & musl targets" + +# Build a 'musl' release for Linux x86_64 +cargo build --release --target=x86_64-unknown-linux-musl --locked +cp -p ./target/x86_64-unknown-linux-musl/release/arp-scan ./builds/arp-scan-v$CLI_VERSION-x86_64-unknown-linux-musl +./builds/arp-scan-v$CLI_VERSION-x86_64-unknown-linux-musl --version + +# Build a 'glibc' (GNU) release for Linux x86_64 +cargo build --release --target=x86_64-unknown-linux-gnu --locked +cp -p ./target/x86_64-unknown-linux-gnu/release/arp-scan ./builds/arp-scan-v$CLI_VERSION-x86_64-unknown-linux-glibc +./builds/arp-scan-v$CLI_VERSION-x86_64-unknown-linux-glibc --version + +echo "Update the README instructions for v$CLI_VERSION" \ No newline at end of file From 2e092cf0b4dadf3b1d8f4017f6a54fe91dc04bab Mon Sep 17 00:00:00 2001 From: Saluki Date: Thu, 27 May 2021 22:36:30 +0200 Subject: [PATCH 039/121] Add a new output option --- src/args.rs | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/args.rs b/src/args.rs index 8d6640c..05972f9 100644 --- a/src/args.rs +++ b/src/args.rs @@ -47,6 +47,14 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name("list").short("l").long("list").takes_value(false).help("List network interfaces") ) + .arg( + Arg::with_name("output").short("o").long("output").takes_value(true).value_name("FORMAT").help("Define output format") + ) +} + +pub enum OutputFormat { + Plain, + Json } pub struct ScanOptions { @@ -57,7 +65,8 @@ pub struct ScanOptions { pub destination_mac: Option, pub vlan_id: Option, pub retry_count: usize, - pub randomize_targets: bool + pub randomize_targets: bool, + pub output: OutputFormat } impl ScanOptions { @@ -155,6 +164,21 @@ impl ScanOptions { None => HOST_RETRY_DEFAULT }; + let output = match matches.value_of("output") { + Some(output_request) => { + + match output_request { + "json" => OutputFormat::Json, + "plain" | "text" => OutputFormat::Plain, + _ => { + eprintln!("Expected correct output format (json/plain)"); + process::exit(1); + } + } + }, + None => OutputFormat::Plain + }; + let randomize_targets = matches.is_present("random"); Arc::new(ScanOptions { @@ -165,8 +189,17 @@ impl ScanOptions { destination_mac, vlan_id, retry_count, - randomize_targets + randomize_targets, + output }) } + pub fn is_plain_output(&self) -> bool { + + match &self.output { + OutputFormat::Plain => true, + _ => false + } + } + } From 51050edec3e55744d05b17567c18276b4481d44d Mon Sep 17 00:00:00 2001 From: Saluki Date: Thu, 27 May 2021 22:36:59 +0200 Subject: [PATCH 040/121] Add JSON export to the output formats --- Cargo.lock | 39 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ src/main.rs | 29 +++++++++++++++++++---------- src/utils.rs | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04aa2a5..178e371 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,6 +27,8 @@ dependencies = [ "ipnetwork", "pnet", "rand", + "serde", + "serde_json", ] [[package]] @@ -114,6 +116,12 @@ dependencies = [ "serde", ] +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + [[package]] name = "libc" version = "0.2.94" @@ -295,11 +303,42 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + [[package]] name = "serde" version = "1.0.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +dependencies = [ + "itoa", + "ryu", + "serde", +] [[package]] name = "socket2" diff --git a/Cargo.toml b/Cargo.toml index 0e834dd..e714d12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,5 @@ ipnetwork = "0.18.0" clap = "2.33.3" dns-lookup = "1.0.6" rand = "0.8.3" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/src/main.rs b/src/main.rs index eb23fd0..e8bec76 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use ipnetwork::NetworkSize; use pnet::datalink; use rand::prelude::*; -use args::ScanOptions; +use args::{ScanOptions, OutputFormat}; fn main() { @@ -62,13 +62,16 @@ fn main() { } }; - println!(""); - println!("Selected interface {} with IP {}", selected_interface.name, ip_network); - if let Some(forced_source_ipv4) = scan_options.source_ipv4 { - println!("The ARP source IPv4 will be forced to {}", forced_source_ipv4); - } - if let Some(forced_destination_mac) = scan_options.destination_mac { - println!("The ARP destination MAC will be forced to {}", forced_destination_mac); + if scan_options.is_plain_output() { + + println!(""); + println!("Selected interface {} with IP {}", selected_interface.name, ip_network); + if let Some(forced_source_ipv4) = scan_options.source_ipv4 { + println!("The ARP source IPv4 will be forced to {}", forced_source_ipv4); + } + if let Some(forced_destination_mac) = scan_options.destination_mac { + println!("The ARP destination MAC will be forced to {}", forced_destination_mac); + } } // Start ARP scan operation @@ -99,7 +102,10 @@ fn main() { process::exit(1); } }; - println!("Sending {} ARP requests to network (waiting at least {}s)", network_size, scan_options.timeout_seconds); + + if scan_options.is_plain_output() { + println!("Sending {} ARP requests to network (waiting at least {}s)", network_size, scan_options.timeout_seconds); + } // The retry count does right now use a 'brute-force' strategy without // synchronization process with the already known hosts. @@ -132,5 +138,8 @@ fn main() { process::exit(1); }); - utils::display_scan_results(response_summary, target_details, &scan_options); + match &scan_options.output { + OutputFormat::Plain => utils::display_scan_results(response_summary, target_details, &scan_options), + OutputFormat::Json => println!("{}", utils::export_to_json(response_summary, target_details)) + } } diff --git a/src/utils.rs b/src/utils.rs index aeba91c..48b8b2e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,5 @@ use pnet::datalink::{self, NetworkInterface}; +use serde::Serialize; use crate::network::{ResponseSummary, TargetDetails}; use crate::args::ScanOptions; @@ -103,3 +104,52 @@ pub fn display_scan_results(response_summary: ResponseSummary, mut target_detail }; println!(""); } + +#[derive(Serialize)] +struct JsonResultItem { + ipv4: String, + mac: String, + hostname: String +} + +#[derive(Serialize)] +struct JsonGlobalResult { + packet_count: usize, + arp_count: usize, + duration_ms: u128, + results: Vec +} + +/** + * Export the scan results as a JSON string with response details (timings, ...) + * and ARP results from the local network. + */ +pub fn export_to_json(response_summary: ResponseSummary, mut target_details: Vec) -> String { + + target_details.sort_by_key(|item| item.ipv4); + + let exportable_results: Vec = target_details.into_iter() + .map(|detail| { + + let hostname = match &detail.hostname { + Some(hostname) => hostname.clone(), + None => String::from("") + }; + + JsonResultItem { + ipv4: format!("{}", detail.ipv4), + mac: format!("{}", detail.mac), + hostname + } + }) + .collect(); + + let global_result = JsonGlobalResult { + packet_count: response_summary.packet_count, + arp_count: response_summary.arp_count, + duration_ms: response_summary.duration_ms, + results: exportable_results + }; + + serde_json::to_string(&global_result).unwrap() +} \ No newline at end of file From 11b170909ecd539c9cdd514e6386044d28d0a1ce Mon Sep 17 00:00:00 2001 From: Saluki Date: Thu, 27 May 2021 22:51:28 +0200 Subject: [PATCH 041/121] Adding YAML to export formats --- Cargo.lock | 34 ++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/args.rs | 6 ++++-- src/main.rs | 3 ++- src/utils.rs | 48 +++++++++++++++++++++++++++++++++--------------- 5 files changed, 74 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 178e371..c447a42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,7 @@ dependencies = [ "rand", "serde", "serde_json", + "serde_yaml", ] [[package]] @@ -81,6 +82,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + [[package]] name = "getrandom" version = "0.2.3" @@ -128,6 +135,12 @@ version = "0.2.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + [[package]] name = "memchr" version = "2.4.0" @@ -340,6 +353,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23" +dependencies = [ + "dtoa", + "linked-hash-map", + "serde", + "yaml-rust", +] + [[package]] name = "socket2" version = "0.4.0" @@ -421,3 +446,12 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml index e714d12..f9ca761 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,4 @@ dns-lookup = "1.0.6" rand = "0.8.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +serde_yaml = "0.8" diff --git a/src/args.rs b/src/args.rs index 05972f9..25333fd 100644 --- a/src/args.rs +++ b/src/args.rs @@ -54,7 +54,8 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> { pub enum OutputFormat { Plain, - Json + Json, + Yaml } pub struct ScanOptions { @@ -169,9 +170,10 @@ impl ScanOptions { match output_request { "json" => OutputFormat::Json, + "yaml" => OutputFormat::Yaml, "plain" | "text" => OutputFormat::Plain, _ => { - eprintln!("Expected correct output format (json/plain)"); + eprintln!("Expected correct output format (json/yaml/plain)"); process::exit(1); } } diff --git a/src/main.rs b/src/main.rs index e8bec76..f1cf21e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -140,6 +140,7 @@ fn main() { match &scan_options.output { OutputFormat::Plain => utils::display_scan_results(response_summary, target_details, &scan_options), - OutputFormat::Json => println!("{}", utils::export_to_json(response_summary, target_details)) + OutputFormat::Json => println!("{}", utils::export_to_json(response_summary, target_details)), + OutputFormat::Yaml => println!("{}", utils::export_to_yaml(response_summary, target_details)) } } diff --git a/src/utils.rs b/src/utils.rs index 48b8b2e..f821479 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -106,29 +106,23 @@ pub fn display_scan_results(response_summary: ResponseSummary, mut target_detail } #[derive(Serialize)] -struct JsonResultItem { +struct SerializableResultItem { ipv4: String, mac: String, hostname: String } #[derive(Serialize)] -struct JsonGlobalResult { +struct SerializableGlobalResult { packet_count: usize, arp_count: usize, duration_ms: u128, - results: Vec + results: Vec } -/** - * Export the scan results as a JSON string with response details (timings, ...) - * and ARP results from the local network. - */ -pub fn export_to_json(response_summary: ResponseSummary, mut target_details: Vec) -> String { - - target_details.sort_by_key(|item| item.ipv4); +fn get_serializable_result(response_summary: ResponseSummary, target_details: Vec) -> SerializableGlobalResult { - let exportable_results: Vec = target_details.into_iter() + let exportable_results: Vec = target_details.into_iter() .map(|detail| { let hostname = match &detail.hostname { @@ -136,7 +130,7 @@ pub fn export_to_json(response_summary: ResponseSummary, mut target_details: Vec None => String::from("") }; - JsonResultItem { + SerializableResultItem { ipv4: format!("{}", detail.ipv4), mac: format!("{}", detail.mac), hostname @@ -144,12 +138,36 @@ pub fn export_to_json(response_summary: ResponseSummary, mut target_details: Vec }) .collect(); - let global_result = JsonGlobalResult { + SerializableGlobalResult { packet_count: response_summary.packet_count, arp_count: response_summary.arp_count, duration_ms: response_summary.duration_ms, results: exportable_results - }; + } +} + +/** + * Export the scan results as a JSON string with response details (timings, ...) + * and ARP results from the local network. + */ +pub fn export_to_json(response_summary: ResponseSummary, mut target_details: Vec) -> String { + + target_details.sort_by_key(|item| item.ipv4); + + let global_result = get_serializable_result(response_summary, target_details); serde_json::to_string(&global_result).unwrap() -} \ No newline at end of file +} + +/** + * Export the scan results as a YAML string with response details (timings, ...) + * and ARP results from the local network. + */ +pub fn export_to_yaml(response_summary: ResponseSummary, mut target_details: Vec) -> String { + + target_details.sort_by_key(|item| item.ipv4); + + let global_result = get_serializable_result(response_summary, target_details); + + serde_yaml::to_string(&global_result).unwrap() +} From 716a3adedf151803d63ca49fa44d61d38b9cf6ef Mon Sep 17 00:00:00 2001 From: Saluki Date: Thu, 27 May 2021 23:01:49 +0200 Subject: [PATCH 042/121] Prepare release 0.7.0 --- Cargo.lock | 6 +++--- Cargo.toml | 8 ++++---- README.md | 14 ++++++++++++-- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c447a42..0d2dc39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,7 +20,7 @@ dependencies = [ [[package]] name = "arp-scan" -version = "0.6.0" +version = "0.7.0" dependencies = [ "clap", "dns-lookup", @@ -131,9 +131,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" [[package]] name = "libc" -version = "0.2.94" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" +checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36" [[package]] name = "linked-hash-map" diff --git a/Cargo.toml b/Cargo.toml index f9ca761..06f318e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "arp-scan" description = "A minimalistic ARP scan tool" license = "AGPL-3.0-or-later" -version = "0.6.0" +version = "0.7.0" authors = ["Saluki"] edition = "2018" readme = "README.md" @@ -17,6 +17,6 @@ ipnetwork = "0.18.0" clap = "2.33.3" dns-lookup = "1.0.6" rand = "0.8.3" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -serde_yaml = "0.8" +serde = { version = "1.0.126", features = ["derive"] } +serde_json = "1.0.64" +serde_yaml = "0.8.17" diff --git a/README.md b/README.md index 2cdda25..e0895fc 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Find all hosts in your local network using this fast ARP scanner. The CLI is wri Download the `arp-scan` binary for Linux (Ubuntu, Fedora, Debian, ...). See the [releases page](https://github.com/Saluki/arp-scan-rs/releases) for other binaries. ```bash -wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.6.0/arp-scan-v0.6.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan +wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.7.0/arp-scan-v0.7.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan ``` List all available network interfaces. @@ -39,6 +39,12 @@ Enhance the minimum scan timeout to 5 seconds (by default, 2 seconds). ./arp-scan -i wlp1s0 -t 5 ``` +Perform an ARP scan on the default network interface, VLAN 45 and JSON output. + +```bash +./arp-scan -Q 45 -o json +``` + ## Options #### Get help `-h` @@ -81,6 +87,10 @@ Randomize the IPv4 target list before sending ARP requests. By default, all ARP Add a 802.1Q field in the Ethernet frame. This fields contains the given VLAN ID for outgoing ARP requests. By default, the Ethernet frame is sent without 802.1Q fields (no VLAN). +#### Set output format `-o json` + +Set the output format to either `plain` (a full-text output with tables), `json` or `yaml`. + #### Show version `--version` Display the ARP scan CLI version and exits the process. @@ -94,7 +104,7 @@ The features below will be shipped in the next releases of the project. - by closing the response thread faster - Scan profiles (standard, attacker, light, ...) - Complete VLAN support -- Exports (JSON, CSV, YAML, ...) +- ~~Exports (JSON & YAML)~~ - released in 0.7.0 - Full ARP packet customization (Ethernet protocol, ARP operation, ...) - MAC vendor lookup in the results - Fine-grained scan timings (interval, ...) From e309203356ad1f9a9ee7dfcf78a96fab96d44a43 Mon Sep 17 00:00:00 2001 From: Saluki Date: Thu, 27 May 2021 23:04:44 +0200 Subject: [PATCH 043/121] Updating serde_yaml for bugfix --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 06f318e..e4dc319 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,4 +19,4 @@ dns-lookup = "1.0.6" rand = "0.8.3" serde = { version = "1.0.126", features = ["derive"] } serde_json = "1.0.64" -serde_yaml = "0.8.17" +serde_yaml = "~0.8.17" From 84a63bc1357fde96d7120abfecf1a282a7c6bcbb Mon Sep 17 00:00:00 2001 From: Saluki Date: Thu, 27 May 2021 23:21:05 +0200 Subject: [PATCH 044/121] Fixing release script --- release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.sh b/release.sh index d553ca7..fddc382 100755 --- a/release.sh +++ b/release.sh @@ -6,7 +6,7 @@ mkdir -p ./builds rm ./builds/* -CLI_VERSION=$(/usr/bin/cat Cargo.toml | egrep "version = (.*)" | egrep -o --color=never "([0-9]+\.?){3}") +CLI_VERSION=$(/usr/bin/cat Cargo.toml | egrep "version = (.*)" | egrep -o --color=never "([0-9]+\.?){3}" | head -n 1) echo "Releasing v$CLI_VERSION for GNU & musl targets" # Build a 'musl' release for Linux x86_64 From e85a560bde8669a8bfd65ebd91ba7b03989c1a6a Mon Sep 17 00:00:00 2001 From: Saluki Date: Sat, 29 May 2021 13:32:05 +0200 Subject: [PATCH 045/121] Making scans faster with global read timeout --- src/main.rs | 6 +++++- src/network.rs | 19 ++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index f1cf21e..8e372ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use std::net::IpAddr; use std::process; use std::thread; use std::sync::Arc; +use std::time::Duration; use ipnetwork::NetworkSize; use pnet::datalink; @@ -80,7 +81,10 @@ fn main() { // while the main thread sends a batch of ARP requests for each IP in the // local network. - let (mut tx, mut rx) = match datalink::channel(selected_interface, Default::default()) { + let mut channel_config = datalink::Config::default(); + channel_config.read_timeout = Some(Duration::from_millis(500)); + + let (mut tx, mut rx) = match datalink::channel(selected_interface, channel_config) { Ok(datalink::Channel::Ethernet(tx, rx)) => (tx, rx), Ok(_) => { eprintln!("Expected an Ethernet datalink channel"); diff --git a/src/network.rs b/src/network.rs index 3e48581..400c227 100644 --- a/src/network.rs +++ b/src/network.rs @@ -3,6 +3,7 @@ use std::net::{IpAddr, Ipv4Addr}; use std::time::Instant; use std::collections::HashMap; use std::sync::Arc; +use std::io::ErrorKind::TimedOut; use dns_lookup::lookup_addr; use ipnetwork::IpNetwork; @@ -156,11 +157,19 @@ pub fn receive_arp_responses(rx: &mut Box, options: Arc options.timeout_seconds { break; } - - let arp_buffer = rx.next().unwrap_or_else(|error| { - eprintln!("Failed to receive ARP requests ({})", error); - process::exit(1); - }); + + let arp_buffer = match rx.next() { + Ok(buffer) => buffer, + Err(error) => { + match error.kind() { + TimedOut => continue, + _ => { + eprintln!("Failed to receive ARP requests ({})", error); + process::exit(1); + } + }; + } + }; packet_count += 1; let ethernet_packet = match EthernetPacket::new(&arp_buffer[..]) { From f1013492ccd2ccdd25b52facf714c8e09bc2d0c0 Mon Sep 17 00:00:00 2001 From: Saluki Date: Sun, 30 May 2021 19:22:42 +0200 Subject: [PATCH 046/121] Move time-out management to main thread with intervals --- src/args.rs | 20 ++++++++++++++++++++ src/main.rs | 22 +++++++++++++++++++--- src/network.rs | 18 ++++++++++++++---- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/args.rs b/src/args.rs index 25333fd..82f0135 100644 --- a/src/args.rs +++ b/src/args.rs @@ -8,6 +8,7 @@ use pnet::datalink::MacAddr; const FIVE_HOURS: u64 = 5 * 60 * 60; const TIMEOUT_DEFAULT: u64 = 2; const HOST_RETRY_DEFAULT: usize = 1; +const REQUEST_MS_INTERVAL: u64 = 10; const CLI_VERSION: &'static str = env!("CARGO_PKG_VERSION"); @@ -44,6 +45,9 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name("random").short("R").long("random").takes_value(false).help("Randomize the target list") ) + .arg( + Arg::with_name("interval").short("I").long("interval").takes_value(true).value_name("MS_INTERVAL").help("Milliseconds between ARP requests") + ) .arg( Arg::with_name("list").short("l").long("list").takes_value(false).help("List network interfaces") ) @@ -66,6 +70,7 @@ pub struct ScanOptions { pub destination_mac: Option, pub vlan_id: Option, pub retry_count: usize, + pub interval: u64, pub randomize_targets: bool, pub output: OutputFormat } @@ -165,6 +170,20 @@ impl ScanOptions { None => HOST_RETRY_DEFAULT }; + let interval = match matches.value_of("interval") { + Some(interval_text) => { + + match interval_text.parse::() { + Ok(interval_number) => interval_number, + Err(_) => { + eprintln!("Expected positive interval"); + process::exit(1); + } + } + }, + None => REQUEST_MS_INTERVAL + }; + let output = match matches.value_of("output") { Some(output_request) => { @@ -191,6 +210,7 @@ impl ScanOptions { destination_mac, vlan_id, retry_count, + interval, randomize_targets, output }) diff --git a/src/main.rs b/src/main.rs index 8e372ac..d05fde7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ mod utils; use std::net::IpAddr; use std::process; use std::thread; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use std::time::Duration; use ipnetwork::NetworkSize; @@ -96,8 +96,14 @@ fn main() { } }; + // The 'timed_out' mutex is shared accross the main thread (which performs + // ARP packet sending) and the response thread (which receives and stores + // all ARP responses). + let timed_out = Arc::new(Mutex::new(false)); + let cloned_timed_out = Arc::clone(&timed_out); + let cloned_options = Arc::clone(&scan_options); - let arp_responses = thread::spawn(move || network::receive_arp_responses(&mut rx, cloned_options)); + let arp_responses = thread::spawn(move || network::receive_arp_responses(&mut rx, cloned_options, cloned_timed_out)); let network_size: u128 = match ip_network.size() { NetworkSize::V4(ipv4_network_size) => ipv4_network_size.into(), @@ -108,7 +114,7 @@ fn main() { }; if scan_options.is_plain_output() { - println!("Sending {} ARP requests to network (waiting at least {}s)", network_size, scan_options.timeout_seconds); + println!("Sending {} ARP requests (waiting at least {}s, {}ms request interval)", network_size, scan_options.timeout_seconds, scan_options.interval); } // The retry count does right now use a 'brute-force' strategy without @@ -133,10 +139,20 @@ fn main() { if let IpAddr::V4(ipv4_address) = ip_address { network::send_arp_request(&mut tx, selected_interface, &ip_network, ipv4_address, Arc::clone(&scan_options)); + thread::sleep(Duration::from_millis(scan_options.interval)); } } } + // Once the ARP packets are sent, the main thread will sleep for T seconds + // (where T is the timeout option). After the sleep phase, the response + // thread will receive a stop request through the 'timed_out' mutex. + thread::sleep(Duration::from_secs(scan_options.timeout_seconds)); + { + let mut locked_timed_out = timed_out.lock().unwrap(); + *locked_timed_out = true; + } + let (response_summary, target_details) = arp_responses.join().unwrap_or_else(|error| { eprintln!("Failed to close receive thread ({:?})", error); process::exit(1); diff --git a/src/network.rs b/src/network.rs index 400c227..4eff40a 100644 --- a/src/network.rs +++ b/src/network.rs @@ -2,7 +2,7 @@ use std::process; use std::net::{IpAddr, Ipv4Addr}; use std::time::Instant; use std::collections::HashMap; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use std::io::ErrorKind::TimedOut; use dns_lookup::lookup_addr; @@ -144,7 +144,7 @@ fn find_source_ip(ip_network: &IpNetwork, forced_source_ipv4: Option) * 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, options: Arc) -> (ResponseSummary, Vec) { +pub fn receive_arp_responses(rx: &mut Box, options: Arc, timed_out: Arc>) -> (ResponseSummary, Vec) { let mut discover_map: HashMap = HashMap::new(); let start_recording = Instant::now(); @@ -154,14 +154,24 @@ pub fn receive_arp_responses(rx: &mut Box, options: Arc options.timeout_seconds { - break; + { + let is_timed_out = timed_out.lock().unwrap_or_else(|err| { + eprintln!("Could not lock time-out mutex ({})", err); + process::exit(1); + }); + + if *is_timed_out == true { + break; + } } let arp_buffer = match rx.next() { Ok(buffer) => buffer, Err(error) => { match error.kind() { + // The 'next' call will only block the thread for a given + // amount of microseconds. The goal is to avoid long blocks + // due to the lack of packets received. TimedOut => continue, _ => { eprintln!("Failed to receive ARP requests ({})", error); From 646407c730bc071974a140d135c5eb9fa482234e Mon Sep 17 00:00:00 2001 From: Saluki Date: Sun, 30 May 2021 19:33:03 +0200 Subject: [PATCH 047/121] Update docs & release 0.8.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 18 ++++++++++-------- release.sh | 4 +++- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d2dc39..bbb9f35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,7 +20,7 @@ dependencies = [ [[package]] name = "arp-scan" -version = "0.7.0" +version = "0.8.0" dependencies = [ "clap", "dns-lookup", diff --git a/Cargo.toml b/Cargo.toml index e4dc319..905d0f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "arp-scan" description = "A minimalistic ARP scan tool" license = "AGPL-3.0-or-later" -version = "0.7.0" +version = "0.8.0" authors = ["Saluki"] edition = "2018" readme = "README.md" diff --git a/README.md b/README.md index e0895fc..32d4be3 100644 --- a/README.md +++ b/README.md @@ -5,20 +5,18 @@ Find all hosts in your local network using this fast ARP scanner. The CLI is written in Rust and provides a minimal scanner that finds all hosts using the ARP protocol. Inspired by the awesome [arp-scan project](https://github.com/royhills/arp-scan). -:heavy_check_mark: Minimal Rust binary +:heavy_check_mark: Minimal Rust binary & fast ARP scans -:heavy_check_mark: Fast ARP scans +:heavy_check_mark: Scan customization (ARP, timings, interface, DNS, ...) -:heavy_check_mark: Scan customization (timeout, interface, DNS, ...) - -:heavy_check_mark: Force ARP source IP +:heavy_check_mark: JSON & YAML exports ## Getting started Download the `arp-scan` binary for Linux (Ubuntu, Fedora, Debian, ...). See the [releases page](https://github.com/Saluki/arp-scan-rs/releases) for other binaries. ```bash -wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.7.0/arp-scan-v0.7.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan +wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.8.0/arp-scan-v0.8.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan ``` List all available network interfaces. @@ -63,6 +61,10 @@ Perform a scan on the network interface `eth0`. The first valid IPv4 network on Enforce a timeout of at least 15 seconds. This timeout is a minimum value (scans may take a little more time). Default value is `2`. +#### Change ARP request interval `-I 30` + +By default, a 10ms gap will be set between ARP requests to avoid an ARP storm on the network. This value can be changed to reduce or increase the milliseconds between each ARP request. + #### Numeric mode `-n` Switch to numeric mode. This will skip the local hostname resolution process and will only display IP addresses. @@ -101,13 +103,13 @@ The features below will be shipped in the next releases of the project. - Make ARP scans faster - with a per-host retry approach - - by closing the response thread faster + - ~~by closing the response thread faster~~ - released in 0.8.0 - Scan profiles (standard, attacker, light, ...) - Complete VLAN support - ~~Exports (JSON & YAML)~~ - released in 0.7.0 - Full ARP packet customization (Ethernet protocol, ARP operation, ...) - MAC vendor lookup in the results -- Fine-grained scan timings (interval, ...) +- ~~Fine-grained scan timings (interval)~~ - released in 0.8.0 - Wide network range support ## Contributing diff --git a/release.sh b/release.sh index fddc382..5fba853 100755 --- a/release.sh +++ b/release.sh @@ -19,4 +19,6 @@ cargo build --release --target=x86_64-unknown-linux-gnu --locked cp -p ./target/x86_64-unknown-linux-gnu/release/arp-scan ./builds/arp-scan-v$CLI_VERSION-x86_64-unknown-linux-glibc ./builds/arp-scan-v$CLI_VERSION-x86_64-unknown-linux-glibc --version -echo "Update the README instructions for v$CLI_VERSION" \ No newline at end of file +echo "Update the README instructions for v$CLI_VERSION" +echo " ✓ Publish on crates.io" +echo " ✓ Release on Github with Git tag v$CLI_VERSION" \ No newline at end of file From 9164004e0143ad985395e29bf6a545bad4125c99 Mon Sep 17 00:00:00 2001 From: Saluki Date: Sun, 30 May 2021 20:11:34 +0200 Subject: [PATCH 048/121] Clean code with clippy findings --- src/args.rs | 9 +++------ src/main.rs | 18 +++++++++--------- src/network.rs | 26 ++++++++------------------ src/utils.rs | 18 +++++++++--------- 4 files changed, 29 insertions(+), 42 deletions(-) diff --git a/src/args.rs b/src/args.rs index 82f0135..06589f6 100644 --- a/src/args.rs +++ b/src/args.rs @@ -10,7 +10,7 @@ const TIMEOUT_DEFAULT: u64 = 2; const HOST_RETRY_DEFAULT: usize = 1; const REQUEST_MS_INTERVAL: u64 = 10; -const CLI_VERSION: &'static str = env!("CARGO_PKG_VERSION"); +const CLI_VERSION: &str = env!("CARGO_PKG_VERSION"); /** * This function groups together all exposed CLI arguments to the end-users @@ -90,7 +90,7 @@ impl ScanOptions { match super::utils::select_default_interface() { Some(default_interface) => { - String::from(default_interface.name) + default_interface.name }, None => { eprintln!("Network interface name required"); @@ -218,10 +218,7 @@ impl ScanOptions { pub fn is_plain_output(&self) -> bool { - match &self.output { - OutputFormat::Plain => true, - _ => false - } + matches!(&self.output, OutputFormat::Plain) } } diff --git a/src/main.rs b/src/main.rs index d05fde7..b0bc6c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,8 +5,9 @@ mod utils; use std::net::IpAddr; use std::process; use std::thread; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::time::Duration; +use std::sync::atomic::{AtomicBool, Ordering}; use ipnetwork::NetworkSize; use pnet::datalink; @@ -65,7 +66,7 @@ fn main() { if scan_options.is_plain_output() { - println!(""); + println!(); println!("Selected interface {} with IP {}", selected_interface.name, ip_network); if let Some(forced_source_ipv4) = scan_options.source_ipv4 { println!("The ARP source IPv4 will be forced to {}", forced_source_ipv4); @@ -81,8 +82,10 @@ fn main() { // while the main thread sends a batch of ARP requests for each IP in the // local network. - let mut channel_config = datalink::Config::default(); - channel_config.read_timeout = Some(Duration::from_millis(500)); + let channel_config = datalink::Config { + read_timeout: Some(Duration::from_millis(500)), + ..datalink::Config::default() + }; let (mut tx, mut rx) = match datalink::channel(selected_interface, channel_config) { Ok(datalink::Channel::Ethernet(tx, rx)) => (tx, rx), @@ -99,7 +102,7 @@ fn main() { // The 'timed_out' mutex is shared accross the main thread (which performs // ARP packet sending) and the response thread (which receives and stores // all ARP responses). - let timed_out = Arc::new(Mutex::new(false)); + let timed_out = Arc::new(AtomicBool::new(false)); let cloned_timed_out = Arc::clone(&timed_out); let cloned_options = Arc::clone(&scan_options); @@ -148,10 +151,7 @@ fn main() { // (where T is the timeout option). After the sleep phase, the response // thread will receive a stop request through the 'timed_out' mutex. thread::sleep(Duration::from_secs(scan_options.timeout_seconds)); - { - let mut locked_timed_out = timed_out.lock().unwrap(); - *locked_timed_out = true; - } + timed_out.store(true, Ordering::Relaxed); let (response_summary, target_details) = arp_responses.join().unwrap_or_else(|error| { eprintln!("Failed to close receive thread ({:?})", error); diff --git a/src/network.rs b/src/network.rs index 4eff40a..9f10cbe 100644 --- a/src/network.rs +++ b/src/network.rs @@ -2,7 +2,8 @@ use std::process; use std::net::{IpAddr, Ipv4Addr}; use std::time::Instant; use std::collections::HashMap; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::io::ErrorKind::TimedOut; use dns_lookup::lookup_addr; @@ -144,7 +145,7 @@ fn find_source_ip(ip_network: &IpNetwork, forced_source_ipv4: Option) * 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, options: Arc, timed_out: Arc>) -> (ResponseSummary, Vec) { +pub fn receive_arp_responses(rx: &mut Box, options: Arc, timed_out: Arc) -> (ResponseSummary, Vec) { let mut discover_map: HashMap = HashMap::new(); let start_recording = Instant::now(); @@ -154,15 +155,8 @@ pub fn receive_arp_responses(rx: &mut Box, options: Arc, options: Arc continue }; - let is_arp = match ethernet_packet.get_ethertype() { - EtherTypes::Arp => true, - _ => false - }; - - if !is_arp { + let is_arp_type = matches!(ethernet_packet.get_ethertype(), EtherTypes::Arp); + if !is_arp_type { continue; } @@ -242,7 +232,7 @@ fn find_hostname(ipv4: Ipv4Addr) -> Option { // 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::() { + if hostname.parse::().is_ok() { return None; } diff --git a/src/utils.rs b/src/utils.rs index f821479..87405c1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -9,7 +9,7 @@ use crate::args::ScanOptions; * user. This approach only supports Linux-like systems (Ubuntu, Fedore, ...). */ pub fn is_root_user() -> bool { - std::env::var("USER").unwrap_or(String::from("")) == String::from("root") + std::env::var("USER").unwrap_or_else(|_| String::from("")) == *"root" } /** @@ -17,7 +17,7 @@ pub fn is_root_user() -> bool { * technical details. The goal is to present the most useful technical details * to pick the right network interface for scans. */ -pub fn show_interfaces(interfaces: &Vec) { +pub fn show_interfaces(interfaces: &[NetworkInterface]) { for interface in interfaces.iter() { let up_text = match interface.is_up() { @@ -42,16 +42,16 @@ pub fn select_default_interface() -> Option { interfaces.into_iter().find(|interface| { - if let None = interface.mac { + if interface.mac.is_none() { return false; } - if interface.ips.len() == 0 || !interface.is_up() || interface.is_loopback() { + if interface.ips.is_empty() || !interface.is_up() || interface.is_loopback() { return false; } let potential_ipv4 = interface.ips.iter().find(|ip| ip.is_ipv4()); - if let None = potential_ipv4 { + if potential_ipv4.is_none() { return false; } @@ -67,7 +67,7 @@ pub fn display_scan_results(response_summary: ResponseSummary, mut target_detail target_details.sort_by_key(|item| item.ipv4); - println!(""); + println!(); println!("| IPv4 | MAC | Hostname |"); println!("|-----------------|-------------------|-----------------------|"); @@ -81,7 +81,7 @@ pub fn display_scan_results(response_summary: ResponseSummary, mut target_detail println!("| {: <15} | {: <18} | {: <21} |", detail.ipv4, detail.mac, hostname); } - println!(""); + println!(); print!("ARP scan finished, "); let target_count = target_details.len(); match target_count { @@ -89,7 +89,7 @@ pub fn display_scan_results(response_summary: ResponseSummary, mut target_detail 1 => print!("1 host found"), _ => print!("{} hosts found", target_count) } - let seconds_duration = (response_summary.duration_ms as f32) / (1000 as f32); + let seconds_duration = (response_summary.duration_ms as f32) / (1000_f32); println!(" in {:.3} seconds", seconds_duration); match response_summary.packet_count { @@ -102,7 +102,7 @@ pub fn display_scan_results(response_summary: ResponseSummary, mut target_detail 1 => println!("1 ARP packet filtered"), _ => println!("{} ARP packets filtered", response_summary.arp_count) }; - println!(""); + println!(); } #[derive(Serialize)] From 5543e2b31f616d55d2a189cfc2ba2f06a1fb037b Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 31 May 2021 01:24:01 +0200 Subject: [PATCH 049/121] Enabling CSV crate --- Cargo.lock | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + 2 files changed, 57 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index bbb9f35..17f0789 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,7 @@ name = "arp-scan" version = "0.8.0" dependencies = [ "clap", + "csv", "dns-lookup", "ipnetwork", "pnet", @@ -49,6 +50,24 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "bstr" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "cfg-if" version = "1.0.0" @@ -70,6 +89,28 @@ dependencies = [ "vec_map", ] +[[package]] +name = "csv" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" +dependencies = [ + "bstr", + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + [[package]] name = "dns-lookup" version = "1.0.6" @@ -129,6 +170,12 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.95" @@ -310,6 +357,15 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" +dependencies = [ + "byteorder", +] + [[package]] name = "regex-syntax" version = "0.6.25" diff --git a/Cargo.toml b/Cargo.toml index 905d0f2..48e65b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ keywords = ["arp", "scan", "network", "security"] categories = ["command-line-utilities"] [dependencies] +csv = "1.1" pnet = "0.28.0" ipnetwork = "0.18.0" clap = "2.33.3" From 67c977fa3e8e55bce3fc5228582dc909e1e7af23 Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 31 May 2021 01:25:45 +0200 Subject: [PATCH 050/121] First MAC vendor IEEE database support --- src/main.rs | 8 +++++-- src/network.rs | 13 ++++++++--- src/utils.rs | 29 ++++++++++++++++-------- src/vendor.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 src/vendor.rs diff --git a/src/main.rs b/src/main.rs index b0bc6c9..ab2fda4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod args; mod network; mod utils; +mod vendor; use std::net::IpAddr; use std::process; @@ -13,7 +14,8 @@ use ipnetwork::NetworkSize; use pnet::datalink; use rand::prelude::*; -use args::{ScanOptions, OutputFormat}; +use crate::args::{ScanOptions, OutputFormat}; +use crate::vendor::Vendor; fn main() { @@ -105,8 +107,10 @@ fn main() { let timed_out = Arc::new(AtomicBool::new(false)); let cloned_timed_out = Arc::clone(&timed_out); + let mut vendor_list = Vendor::new(); + let cloned_options = Arc::clone(&scan_options); - let arp_responses = thread::spawn(move || network::receive_arp_responses(&mut rx, cloned_options, cloned_timed_out)); + let arp_responses = thread::spawn(move || network::receive_arp_responses(&mut rx, cloned_options, cloned_timed_out, &mut vendor_list)); let network_size: u128 = match ip_network.size() { NetworkSize::V4(ipv4_network_size) => ipv4_network_size.into(), diff --git a/src/network.rs b/src/network.rs index 9f10cbe..1a8561c 100644 --- a/src/network.rs +++ b/src/network.rs @@ -15,6 +15,7 @@ use pnet::packet::arp::{MutableArpPacket, ArpOperations, ArpHardwareTypes, ArpPa use pnet::packet::vlan::{ClassOfService, MutableVlanPacket}; use crate::args::ScanOptions; +use crate::vendor::Vendor; const VLAN_QOS_DEFAULT: u8 = 1; const ARP_PACKET_SIZE: usize = 28; @@ -41,7 +42,8 @@ pub struct ResponseSummary { pub struct TargetDetails { pub ipv4: Ipv4Addr, pub mac: MacAddr, - pub hostname: Option + pub hostname: Option, + pub vendor: Option } /** @@ -145,7 +147,7 @@ fn find_source_ip(ip_network: &IpNetwork, forced_source_ipv4: Option) * 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, options: Arc, timed_out: Arc) -> (ResponseSummary, Vec) { +pub fn receive_arp_responses(rx: &mut Box, options: Arc, timed_out: Arc, vendor_list: &mut Vendor) -> (ResponseSummary, Vec) { let mut discover_map: HashMap = HashMap::new(); let start_recording = Instant::now(); @@ -197,7 +199,8 @@ pub fn receive_arp_responses(rx: &mut Box, options: Arc, options: Arc hostname.clone(), - None if !options.resolve_hostname => String::from("(disabled)"), - None => String::from("") + let hostname: &str = match &detail.hostname { + Some(hostname) => &hostname, + None if !options.resolve_hostname => &"(disabled)", + None => &"" }; - println!("| {: <15} | {: <18} | {: <21} |", detail.ipv4, detail.mac, hostname); + let vendor: &str = match &detail.vendor { + Some(vendor) => &vendor, + None => &"" + }; + println!("| {: <15} | {: <18} | {: <21} | {: <26} |", detail.ipv4, detail.mac, hostname, vendor); } println!(); @@ -109,7 +113,8 @@ pub fn display_scan_results(response_summary: ResponseSummary, mut target_detail struct SerializableResultItem { ipv4: String, mac: String, - hostname: String + hostname: String, + vendor: String } #[derive(Serialize)] @@ -130,10 +135,16 @@ fn get_serializable_result(response_summary: ResponseSummary, target_details: Ve None => String::from("") }; + let vendor = match &detail.vendor { + Some(vendor) => vendor.clone(), + None => String::from("") + }; + SerializableResultItem { ipv4: format!("{}", detail.ipv4), mac: format!("{}", detail.mac), - hostname + hostname, + vendor } }) .collect(); diff --git a/src/vendor.rs b/src/vendor.rs new file mode 100644 index 0000000..9a4d945 --- /dev/null +++ b/src/vendor.rs @@ -0,0 +1,60 @@ +use std::fs::File; +use std::process; + +use pnet::datalink::MacAddr; +use csv::{Position, Reader}; + +pub struct Vendor { + reader: Option> +} + +impl Vendor { + + pub fn new() -> Self { + + let file_result = File::open("/usr/share/arp-scan/ieee-oui.csv"); + + match file_result { + Ok(file) => Vendor { reader: Some(Reader::from_reader(file)) }, + Err(_) => Vendor { reader: None } + } + } + + pub fn has_vendor_db(&self) -> bool { + self.reader.is_some() + } + + pub fn search_by_mac(&mut self, mac_address: &MacAddr) -> Option { + + match &mut self.reader { + Some(reader) => { + + let vendor_oui = format!("{:X}{:X}{:X}", mac_address.0, mac_address.1, mac_address.2); + + // Since we share a common instance of the CSV reader, it must be reset + // before each read (internal buffers will be cleared). + reader.seek(Position::new()).unwrap_or_else(|err| { + eprintln!("Could not reset the CSV reader ({})", err); + process::exit(1); + }); + + for vendor_result in reader.records() { + + let record = vendor_result.unwrap_or_else(|err| { + eprintln!("Could not read CSV record ({})", err); + process::exit(1); + }); + let potential_oui = record.get(1).unwrap_or(&""); + + if vendor_oui.eq(potential_oui) { + return Some(record.get(2).unwrap_or(&"(no vendor)").to_string()) + } + } + + None + } + None => None + } + } + +} From faba323c2c70ce403807af8e5c21af6d9d032941 Mon Sep 17 00:00:00 2001 From: Saluki Date: Wed, 2 Jun 2021 23:08:52 +0200 Subject: [PATCH 051/121] Handling multiple time units in args --- src/args.rs | 88 +++++++++++++++++++++++++++++++++++------------------ src/main.rs | 8 ++--- 2 files changed, 62 insertions(+), 34 deletions(-) diff --git a/src/args.rs b/src/args.rs index 06589f6..c3b3093 100644 --- a/src/args.rs +++ b/src/args.rs @@ -5,8 +5,7 @@ use std::sync::Arc; use clap::{Arg, ArgMatches, App}; use pnet::datalink::MacAddr; -const FIVE_HOURS: u64 = 5 * 60 * 60; -const TIMEOUT_DEFAULT: u64 = 2; +const TIMEOUT_MS_DEFAULT: u64 = 2000; const HOST_RETRY_DEFAULT: usize = 1; const REQUEST_MS_INTERVAL: u64 = 10; @@ -25,7 +24,7 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> { Arg::with_name("interface").short("i").long("interface").takes_value(true).value_name("INTERFACE_NAME").help("Network interface") ) .arg( - Arg::with_name("timeout").short("t").long("timeout").takes_value(true).value_name("TIMEOUT_SECONDS").help("ARP response timeout") + Arg::with_name("timeout").short("t").long("timeout").takes_value(true).value_name("TIMEOUT_DURATION").help("ARP response timeout") ) .arg( Arg::with_name("source_ip").short("S").long("source-ip").takes_value(true).value_name("SOURCE_IPV4").help("Source IPv4 address for requests") @@ -46,7 +45,7 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> { Arg::with_name("random").short("R").long("random").takes_value(false).help("Randomize the target list") ) .arg( - Arg::with_name("interval").short("I").long("interval").takes_value(true).value_name("MS_INTERVAL").help("Milliseconds between ARP requests") + Arg::with_name("interval").short("I").long("interval").takes_value(true).value_name("INTERVAL_DURATION").help("Milliseconds between ARP requests") ) .arg( Arg::with_name("list").short("l").long("list").takes_value(false).help("List network interfaces") @@ -64,13 +63,13 @@ pub enum OutputFormat { pub struct ScanOptions { pub interface_name: String, - pub timeout_seconds: u64, + pub timeout_ms: u64, pub resolve_hostname: bool, pub source_ipv4: Option, pub destination_mac: Option, pub vlan_id: Option, pub retry_count: usize, - pub interval: u64, + pub interval_ms: u64, pub randomize_targets: bool, pub output: OutputFormat } @@ -101,15 +100,9 @@ impl ScanOptions { } }; - 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); - } + let timeout_ms: u64 = matches.value_of("timeout") + .map(|timeout_text| parse_to_milliseconds(timeout_text)) + .unwrap_or(TIMEOUT_MS_DEFAULT); // Hostnames will not be resolved in numeric mode let resolve_hostname = !matches.is_present("numeric"); @@ -170,19 +163,9 @@ impl ScanOptions { None => HOST_RETRY_DEFAULT }; - let interval = match matches.value_of("interval") { - Some(interval_text) => { - - match interval_text.parse::() { - Ok(interval_number) => interval_number, - Err(_) => { - eprintln!("Expected positive interval"); - process::exit(1); - } - } - }, - None => REQUEST_MS_INTERVAL - }; + let interval_ms: u64 = matches.value_of("interval") + .map(|interval_text| parse_to_milliseconds(interval_text)) + .unwrap_or(REQUEST_MS_INTERVAL); let output = match matches.value_of("output") { Some(output_request) => { @@ -204,13 +187,13 @@ impl ScanOptions { Arc::new(ScanOptions { interface_name, - timeout_seconds, + timeout_ms, resolve_hostname, source_ipv4, destination_mac, vlan_id, retry_count, - interval, + interval_ms, randomize_targets, output }) @@ -222,3 +205,48 @@ impl ScanOptions { } } + +/** + * Parse a given time string into milliseconds. This can be used to convert a + * string such as '20ms', '10s' or '1h' into adequate milliseconds. Without + * suffix, the default behavior is to parse into milliseconds. + */ +fn parse_to_milliseconds(time_arg: &str) -> u64 { + + let len = time_arg.len(); + + if time_arg.ends_with("ms") { + let milliseconds_text = &time_arg[0..len-2]; + return milliseconds_text.parse::() + .unwrap_or_else(|err| { + eprintln!("Expected valid milliseconds ({})", err); + process::exit(1); + }); + } + + if time_arg.ends_with('s') { + let seconds_text = &time_arg[0..len-1]; + return seconds_text.parse::() + .map(|value| value * 1000) + .unwrap_or_else(|err| { + eprintln!("Expected valid seconds ({})", err); + process::exit(1); + }); + } + + if time_arg.ends_with('h') { + let hour_text = &time_arg[0..len-1]; + return hour_text.parse::() + .map(|value| value * 1000 * 60) + .unwrap_or_else(|err| { + eprintln!("Expected valid hours ({})", err); + process::exit(1); + }); + } + + time_arg.parse::() + .unwrap_or_else(|err| { + eprintln!("Expected valid milliseconds ({})", err); + process::exit(1); + }) +} diff --git a/src/main.rs b/src/main.rs index ab2fda4..1bacceb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,7 @@ use crate::args::{ScanOptions, OutputFormat}; use crate::vendor::Vendor; fn main() { - + let matches = args::build_args().get_matches(); // Find interfaces & list them if requested @@ -121,7 +121,7 @@ fn main() { }; if scan_options.is_plain_output() { - println!("Sending {} ARP requests (waiting at least {}s, {}ms request interval)", network_size, scan_options.timeout_seconds, scan_options.interval); + println!("Sending {} ARP requests (waiting at least {}ms, {}ms request interval)", network_size, scan_options.timeout_ms, scan_options.interval_ms); } // The retry count does right now use a 'brute-force' strategy without @@ -146,7 +146,7 @@ fn main() { if let IpAddr::V4(ipv4_address) = ip_address { network::send_arp_request(&mut tx, selected_interface, &ip_network, ipv4_address, Arc::clone(&scan_options)); - thread::sleep(Duration::from_millis(scan_options.interval)); + thread::sleep(Duration::from_millis(scan_options.interval_ms)); } } } @@ -154,7 +154,7 @@ fn main() { // Once the ARP packets are sent, the main thread will sleep for T seconds // (where T is the timeout option). After the sleep phase, the response // thread will receive a stop request through the 'timed_out' mutex. - thread::sleep(Duration::from_secs(scan_options.timeout_seconds)); + thread::sleep(Duration::from_millis(scan_options.timeout_ms)); timed_out.store(true, Ordering::Relaxed); let (response_summary, target_details) = arp_responses.join().unwrap_or_else(|error| { From cc383a93e2d8d77c3479276f34498204ce400579 Mon Sep 17 00:00:00 2001 From: Saluki Date: Wed, 2 Jun 2021 23:09:17 +0200 Subject: [PATCH 052/121] Add MAC vendor table space --- src/utils.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 2dc008e..79a9159 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -68,8 +68,8 @@ pub fn display_scan_results(response_summary: ResponseSummary, mut target_detail target_details.sort_by_key(|item| item.ipv4); println!(); - println!("| IPv4 | MAC | Hostname | Vendor |"); - println!("|-----------------|-------------------|-----------------------|----------------------------|"); + println!("| IPv4 | MAC | Hostname | Vendor |"); + println!("|-----------------|-------------------|-----------------------|---------------------------------|"); for detail in target_details.iter() { @@ -82,7 +82,7 @@ pub fn display_scan_results(response_summary: ResponseSummary, mut target_detail Some(vendor) => &vendor, None => &"" }; - println!("| {: <15} | {: <18} | {: <21} | {: <26} |", detail.ipv4, detail.mac, hostname, vendor); + println!("| {: <15} | {: <18} | {: <21} | {: <31} |", detail.ipv4, detail.mac, hostname, vendor); } println!(); From 0af4c400b620d594c033200b4188b977ae10ed96 Mon Sep 17 00:00:00 2001 From: Saluki Date: Sat, 5 Jun 2021 12:43:27 +0200 Subject: [PATCH 053/121] Enhancing vendor resolver with padding & tests --- src/vendor.rs | 87 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 6 deletions(-) diff --git a/src/vendor.rs b/src/vendor.rs index 9a4d945..431dfc4 100644 --- a/src/vendor.rs +++ b/src/vendor.rs @@ -5,18 +5,22 @@ use pnet::datalink::MacAddr; use csv::{Position, Reader}; pub struct Vendor { - reader: Option> + reader: Option>, } impl Vendor { - pub fn new() -> Self { + pub fn new(path: &str) -> Self { - let file_result = File::open("/usr/share/arp-scan/ieee-oui.csv"); + let file_result = File::open(path); match file_result { - Ok(file) => Vendor { reader: Some(Reader::from_reader(file)) }, - Err(_) => Vendor { reader: None } + Ok(file) => Vendor { + reader: Some(Reader::from_reader(file)), + }, + Err(_) => Vendor { + reader: None, + } } } @@ -29,7 +33,10 @@ impl Vendor { match &mut self.reader { Some(reader) => { - let vendor_oui = format!("{:X}{:X}{:X}", mac_address.0, mac_address.1, mac_address.2); + // The {:02X} syntax forces to pad all numbers with zero values. + // This ensures that a MAC 002272... will not be printed as + // 02272 and therefore fails the search process. + let vendor_oui = format!("{:02X}{:02X}{:02X}", mac_address.0, mac_address.1, mac_address.2); // Since we share a common instance of the CSV reader, it must be reset // before each read (internal buffers will be cleared). @@ -58,3 +65,71 @@ impl Vendor { } } + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn should_create_vendor_resolver() { + + let vendor = Vendor::new("./data/ieee-oui.csv"); + + assert_eq!(vendor.has_vendor_db(), true); + } + + #[test] + fn should_handle_unresolved_database() { + + let vendor = Vendor::new("./unknown.csv"); + + assert_eq!(vendor.has_vendor_db(), false); + } + + #[test] + fn should_find_specific_mac_vendor() { + + let mut vendor = Vendor::new("./data/ieee-oui.csv"); + let mac = MacAddr::new(0x40, 0x55, 0x82, 0xc3, 0xe5, 0x5b); + + assert_eq!(vendor.search_by_mac(&mac), Some("Nokia".to_string())); + } + + #[test] + fn should_find_first_mac_vendor() { + + let mut vendor = Vendor::new("./data/ieee-oui.csv"); + let mac = MacAddr::new(0x00, 0x22, 0x72, 0xd7, 0xb5, 0x23); + + assert_eq!(vendor.search_by_mac(&mac), Some("American Micro-Fuel Device Corp.".to_string())); + } + + #[test] + fn should_find_last_mac_vendor() { + + let mut vendor = Vendor::new("./data/ieee-oui.csv"); + let mac = MacAddr::new(0xcc, 0x9d, 0xa2, 0x14, 0x2e, 0x6f); + + assert_eq!(vendor.search_by_mac(&mac), Some("Eltex Enterprise Ltd.".to_string())); + } + + #[test] + fn should_handle_unknown_mac_vendor() { + + let mut vendor = Vendor::new("./data/ieee-oui.csv"); + let mac = MacAddr::new(0xbb, 0xbb, 0xbb, 0xd2, 0xf5, 0xb6); + + assert_eq!(vendor.search_by_mac(&mac), None); + } + + #[test] + fn should_pad_correctly_with_zeroes() { + + let mut vendor = Vendor::new("./data/ieee-oui.csv"); + let mac = MacAddr::new(0x01, 0x01, 0x01, 0x67, 0xb2, 0x1d); + + assert_eq!(vendor.search_by_mac(&mac), Some("SomeCorp".to_string())); + } + +} From 0e52815be32c1693ca5f4d1adb94cf76564a0816 Mon Sep 17 00:00:00 2001 From: Saluki Date: Sat, 5 Jun 2021 12:44:08 +0200 Subject: [PATCH 054/121] New option for custom IEEE OUI file (CSV format) --- data/ieee-oui.csv | 33 +++++++++++++++++++++++++++++++++ src/args.rs | 16 ++++++++++++++-- src/main.rs | 2 +- 3 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 data/ieee-oui.csv diff --git a/data/ieee-oui.csv b/data/ieee-oui.csv new file mode 100644 index 0000000..bb5d762 --- /dev/null +++ b/data/ieee-oui.csv @@ -0,0 +1,33 @@ +Registry,Assignment,Organization Name,Organization Address +MA-L,002272,American Micro-Fuel Device Corp.,2181 Buchanan Loop Ferndale WA US 98248 +MA-L,00D0EF,IGT,9295 PROTOTYPE DRIVE RENO NV US 89511 +MA-L,086195,Rockwell Automation,1 Allen-Bradley Dr. Mayfield Heights OH US 44124-6118 +MA-L,F4BD9E,"Cisco Systems, Inc",80 West Tasman Drive San Jose CA US 94568 +MA-L,5885E9,Realme Chongqing MobileTelecommunications Corp Ltd,"No.24 Nichang Boulevard, Huixing Block, Yubei District, Chongqing. Chongqing China CN 401120 " +MA-L,BC2392,BYD Precision Manufacture Company Ltd.,"No.3001, Bao He Road, Baolong Industrial, Longgang Street,Longgang Zone, Shenzhen shenzhen CN 518116 " +MA-L,405582,Nokia,600 March Road Kanata Ontario CA K2K 2E6 +MA-L,A4E31B,Nokia,600 March Road Kanata Ontario CA K2K 2E6 +MA-L,D48660,Arcadyan Corporation,"No.8, Sec.2, Guangfu Rd. Hsinchu City Hsinchu TW 30071 " +MA-L,C489ED,Solid Optics EU N.V.,De Huchtstraat 35 Almere Flevoland NL 1327 EC +MA-L,60F43A,Edifier International,"Suit 2207, 22nd floor, Tower II, Lippo centre, 89 Queensway Hong Kong CN 070 " +MA-L,58A87B,"Fitbit, Inc.","199 Fremont Street, 14th Fl San Francisco CA US 94105 " +MA-L,5C6BD7,Foshan VIOMI Electric Appliance Technology Co. Ltd.,"No.2 North Xinxi Fourth Road, Xiashi Village Committee,Lunjiao Sub-district Office, Shunde District Foshan Guandong CN 528308 " +MA-L,1848CA,"Murata Manufacturing Co., Ltd.","1-10-1, Higashikotari Nagaokakyo-shi Kyoto JP 617-8555 " +MA-L,90EEC7,"Samsung Electronics Co.,Ltd","#94-1, Imsoo-Dong Gumi Gyeongbuk KR 730-350 " +MA-L,1029AB,"Samsung Electronics Co.,Ltd","#94-1, Imsoo-Dong Gumi Gyeongbuk KR 730-350 " +MA-L,184ECB,"Samsung Electronics Co.,Ltd","#94-1, Imsoo-Dong Gumi Gyeongbuk KR 730-350 " +MA-L,010101,SomeCorp,Unknown address +MA-L,8022A7,"NEC Platforms, Ltd.",2-3 Kandatsukasamachi Chiyodaku Tokyo JP 101-8532 +MA-L,B83BCC,Xiaomi Communications Co Ltd,"#019, 9th Floor, Building 6, 33 Xi'erqi Middle Road Beijing Haidian District CN 100085 " +MA-L,88D199,"Vencer Co., Ltd.","14F-12, No. 79, Section 1, Hsin Tai Wu Road, Hsi-Chih District, New Taipei City Taiwan TW 22101 " +MA-L,CCE236,Hangzhou Yaguan Technology Co. LTD,"33rd Floor, T4 US Center, European and American Financial City, Yuhang District, Hangzhou, Zhejiang Hangzhou Zhejiang CN 311100 " +MA-L,204181,ESYSE GmbH Embedded Systems Engineering,Ruth-Niehaus Str. 8 Meerbusch Nordrhein-Westfalen DE 40667 +MA-L,DCBB96,Full Solution Telecom,"Calle 130A #59C-42, Barrio Ciudad Jardin Norte Bogota Distrito Capital de Bogota CO 111111 " +MA-L,74765B,"Quectel Wireless Solutions Co.,Ltd.","7th Floor, Hongye Building, No.1801 Hongmei Road, Xuhui District Shanghai CN 200233 " +MA-L,B437D8,D-Link (Shanghai) Limited Corp.,"Room 612, Floor 6, No.88, Taigu Road, Shanghai CN 200131 " +MA-L,9CD57D,"Cisco Systems, Inc",80 West Tasman Drive San Jose CA US 94568 +MA-L,941F3A,Ambiq,"6500 River Place Blvd., Building 7, Suite 200 Austin TX US 78730 " +MA-L,2C3557,"ELIIY Power CO., Ltd.","1-6-4, Osaki Shinagawa-ku TOKYO US 141-0032 " +MA-L,7066E1,dnt Innovation GmbH,Maiburger Straße 29 Leer DE 26789 +MA-L,F8CE72,Wistron Corporation," NO.5, HSIN AN ROAD, SCIENCE-BASED INDUSTRIAL PARK, HSINCHU, TAIWAN, R.O.C. Hsinchu County Taiwan TW 303036 " +MA-L,CC9DA2,Eltex Enterprise Ltd.,Okruzhnaya st. 29v Novosibirsk RU 630020 diff --git a/src/args.rs b/src/args.rs index c3b3093..2239423 100644 --- a/src/args.rs +++ b/src/args.rs @@ -47,6 +47,9 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name("interval").short("I").long("interval").takes_value(true).value_name("INTERVAL_DURATION").help("Milliseconds between ARP requests") ) + .arg( + Arg::with_name("oui-file").long("oui-file").takes_value(true).value_name("FILE_PATH").help("Path to custom IEEE OUI CSV file") + ) .arg( Arg::with_name("list").short("l").long("list").takes_value(false).help("List network interfaces") ) @@ -71,7 +74,8 @@ pub struct ScanOptions { pub retry_count: usize, pub interval_ms: u64, pub randomize_targets: bool, - pub output: OutputFormat + pub output: OutputFormat, + pub oui_file: String } impl ScanOptions { @@ -184,6 +188,13 @@ impl ScanOptions { }; let randomize_targets = matches.is_present("random"); + + + + let oui_file: String = match matches.value_of("oui-file") { + Some(file) => file.to_string(), + None => "/usr/share/arp-scan/ieee-oui.csv".to_string() + }; Arc::new(ScanOptions { interface_name, @@ -195,7 +206,8 @@ impl ScanOptions { retry_count, interval_ms, randomize_targets, - output + output, + oui_file }) } diff --git a/src/main.rs b/src/main.rs index 1bacceb..9147b79 100644 --- a/src/main.rs +++ b/src/main.rs @@ -107,7 +107,7 @@ fn main() { let timed_out = Arc::new(AtomicBool::new(false)); let cloned_timed_out = Arc::clone(&timed_out); - let mut vendor_list = Vendor::new(); + let mut vendor_list = Vendor::new(&scan_options.oui_file); let cloned_options = Arc::clone(&scan_options); let arp_responses = thread::spawn(move || network::receive_arp_responses(&mut rx, cloned_options, cloned_timed_out, &mut vendor_list)); From ca210ff3229825a0b43eaf61f42d0fc95b6e5be8 Mon Sep 17 00:00:00 2001 From: Saluki Date: Sat, 12 Jun 2021 12:36:15 +0200 Subject: [PATCH 055/121] Prepare release 0.9.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17f0789..460d04d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,7 +20,7 @@ dependencies = [ [[package]] name = "arp-scan" -version = "0.8.0" +version = "0.9.0" dependencies = [ "clap", "csv", diff --git a/Cargo.toml b/Cargo.toml index 48e65b0..54cfafe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "arp-scan" description = "A minimalistic ARP scan tool" license = "AGPL-3.0-or-later" -version = "0.8.0" +version = "0.9.0" authors = ["Saluki"] edition = "2018" readme = "README.md" diff --git a/README.md b/README.md index 32d4be3..f4363dc 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Find all hosts in your local network using this fast ARP scanner. The CLI is wri Download the `arp-scan` binary for Linux (Ubuntu, Fedora, Debian, ...). See the [releases page](https://github.com/Saluki/arp-scan-rs/releases) for other binaries. ```bash -wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.8.0/arp-scan-v0.8.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan +wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.9.0/arp-scan-v0.9.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan ``` List all available network interfaces. @@ -85,6 +85,10 @@ Change or force the MAC address sent as destination ARP request. By default, a b Randomize the IPv4 target list before sending ARP requests. By default, all ARP requests are sent in ascending order by IPv4 address. +#### Use custom MAC OUI file `--oui-file` + +Use a [custom OUI MAC file](http://standards-oui.ieee.org/oui/oui.csv), the default path will be set to `/usr/share/arp-scan/ieee-oui.csv"`. + #### Set VLAN ID `-Q 540` Add a 802.1Q field in the Ethernet frame. This fields contains the given VLAN ID for outgoing ARP requests. By default, the Ethernet frame is sent without 802.1Q fields (no VLAN). @@ -108,7 +112,7 @@ The features below will be shipped in the next releases of the project. - Complete VLAN support - ~~Exports (JSON & YAML)~~ - released in 0.7.0 - Full ARP packet customization (Ethernet protocol, ARP operation, ...) -- MAC vendor lookup in the results +- ~~MAC vendor lookup in the results~~ - released in 0.9.0 - ~~Fine-grained scan timings (interval)~~ - released in 0.8.0 - Wide network range support From 832fd8fa2131b35454f8eb8e97942d181a7bc2a6 Mon Sep 17 00:00:00 2001 From: Saluki Date: Sat, 12 Jun 2021 12:47:52 +0200 Subject: [PATCH 056/121] Fixing docs with human-friendly time units --- README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f4363dc..b861d53 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,13 @@ Find all hosts in your local network using this fast ARP scanner. The CLI is written in Rust and provides a minimal scanner that finds all hosts using the ARP protocol. Inspired by the awesome [arp-scan project](https://github.com/royhills/arp-scan). -:heavy_check_mark: Minimal Rust binary & fast ARP scans +✔ Minimal Rust binary & fast ARP scans -:heavy_check_mark: Scan customization (ARP, timings, interface, DNS, ...) +✔ Scan customization (ARP, timings, interface, DNS, ...) -:heavy_check_mark: JSON & YAML exports +✔ MAC vendor search + +✔ JSON & YAML exports ## Getting started @@ -34,7 +36,7 @@ Launch a scan on interface `wlp1s0`. Enhance the minimum scan timeout to 5 seconds (by default, 2 seconds). ```bash -./arp-scan -i wlp1s0 -t 5 +./arp-scan -i wlp1s0 -t 5s ``` Perform an ARP scan on the default network interface, VLAN 45 and JSON output. @@ -57,13 +59,13 @@ List all available network interfaces. Using this option will only print a list Perform a scan on the network interface `eth0`. The first valid IPv4 network on this interface will be used as scan target. By default, the first network interface with an `up` status and a valid IPv4 will be selected. -#### Set global scan timeout `-t 15` +#### Set global scan timeout `-t 15s` -Enforce a timeout of at least 15 seconds. This timeout is a minimum value (scans may take a little more time). Default value is `2`. +Enforce a timeout of at least 15 seconds. This timeout is a minimum value (scans may take a little more time). Default value is `2000ms`. -#### Change ARP request interval `-I 30` +#### Change ARP request interval `-I 30ms` -By default, a 10ms gap will be set between ARP requests to avoid an ARP storm on the network. This value can be changed to reduce or increase the milliseconds between each ARP request. +By default, a `10ms` gap will be set between ARP requests to avoid an ARP storm on the network. This value can be changed to reduce or increase the milliseconds between each ARP request. #### Numeric mode `-n` From 3a0aaafa5bc6ace69e9cfbb2ca6135b27474dc88 Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 14 Jun 2021 18:42:47 +0200 Subject: [PATCH 057/121] Utils code cleaning --- src/utils.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 79a9159..173ec49 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,6 @@ use pnet::datalink::{self, NetworkInterface}; use serde::Serialize; +use std::process; use crate::network::{ResponseSummary, TargetDetails}; use crate::args::ScanOptions; @@ -167,7 +168,10 @@ pub fn export_to_json(response_summary: ResponseSummary, mut target_details: Vec let global_result = get_serializable_result(response_summary, target_details); - serde_json::to_string(&global_result).unwrap() + serde_json::to_string(&global_result).unwrap_or_else(|err| { + eprintln!("Could not export JSON results ({})", err); + process::exit(1); + }) } /** @@ -180,5 +184,8 @@ pub fn export_to_yaml(response_summary: ResponseSummary, mut target_details: Vec let global_result = get_serializable_result(response_summary, target_details); - serde_yaml::to_string(&global_result).unwrap() + serde_yaml::to_string(&global_result).unwrap_or_else(|err| { + eprintln!("Could not export YAML results ({})", err); + process::exit(1); + }) } From 4469f53792497b58837b899388a0df9abf992edf Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 14 Jun 2021 18:43:00 +0200 Subject: [PATCH 058/121] New ARP customization options --- src/args.rs | 127 +++++++++++++++++++++++++++++++++++++++++++++++-- src/network.rs | 38 ++++++++++----- 2 files changed, 148 insertions(+), 17 deletions(-) diff --git a/src/args.rs b/src/args.rs index 2239423..a20a94a 100644 --- a/src/args.rs +++ b/src/args.rs @@ -4,6 +4,8 @@ use std::sync::Arc; use clap::{Arg, ArgMatches, App}; use pnet::datalink::MacAddr; +use pnet::packet::arp::{ArpHardwareType, ArpOperation}; +use pnet::packet::ethernet::EtherType; const TIMEOUT_MS_DEFAULT: u64 = 2000; const HOST_RETRY_DEFAULT: usize = 1; @@ -32,6 +34,9 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name("destination_mac").short("M").long("dest-mac").takes_value(true).value_name("DESTINATION_MAC").help("Destination MAC address for requests") ) + .arg( + Arg::with_name("source_mac").long("source-mac").takes_value(true).value_name("SOURCE_MAC").help("Source MAC address for requests") + ) .arg( Arg::with_name("numeric").short("n").long("numeric").takes_value(false).help("Numeric mode, no hostname resolution") ) @@ -56,6 +61,21 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name("output").short("o").long("output").takes_value(true).value_name("FORMAT").help("Define output format") ) + .arg( + Arg::with_name("hw_type").long("hw-type").takes_value(true).value_name("HW_TYPE").help("Custom ARP hardware field") + ) + .arg( + Arg::with_name("hw_addr").long("hw-addr").takes_value(true).value_name("ADDRESS_LEN").help("Custom ARP hardware address length") + ) + .arg( + Arg::with_name("proto_type").long("proto-type").takes_value(true).value_name("PROTO_TYPE").help("Custom ARP proto type") + ) + .arg( + Arg::with_name("proto_addr").long("proto-addr").takes_value(true).value_name("ADDRESS_LEN").help("Custom ARP proto address length") + ) + .arg( + Arg::with_name("arp_operation").long("arp-op").takes_value(true).value_name("OPERATION_ID").help("Custom ARP operation ID") + ) } pub enum OutputFormat { @@ -69,13 +89,19 @@ pub struct ScanOptions { pub timeout_ms: u64, pub resolve_hostname: bool, pub source_ipv4: Option, + pub source_mac: Option, pub destination_mac: Option, pub vlan_id: Option, pub retry_count: usize, pub interval_ms: u64, pub randomize_targets: bool, pub output: OutputFormat, - pub oui_file: String + pub oui_file: String, + pub hw_type: Option, + pub hw_addr: Option, + pub proto_type: Option, + pub proto_addr: Option, + pub arp_operation: Option } impl ScanOptions { @@ -138,6 +164,20 @@ impl ScanOptions { }, None => None }; + + let source_mac: Option = match matches.value_of("source_mac") { + Some(mac_address) => { + + match mac_address.parse::() { + Ok(parsed_mac) => Some(parsed_mac), + Err(_) => { + eprintln!("Expected valid MAC address as source"); + process::exit(1); + } + } + }, + None => None + }; let vlan_id: Option = match matches.value_of("vlan") { Some(vlan) => { @@ -189,12 +229,80 @@ impl ScanOptions { let randomize_targets = matches.is_present("random"); - - let oui_file: String = match matches.value_of("oui-file") { Some(file) => file.to_string(), None => "/usr/share/arp-scan/ieee-oui.csv".to_string() }; + + let hw_type = match matches.value_of("hw-type") { + Some(hw_type_text) => { + + match hw_type_text.parse::() { + Ok(type_number) => Some(ArpHardwareType::new(type_number)), + Err(_) => { + eprintln!("Expected valid ARP hardware type number"); + process::exit(1); + } + } + }, + None => None + }; + + let hw_addr = match matches.value_of("hw-addr") { + Some(hw_addr_text) => { + + match hw_addr_text.parse::() { + Ok(addr_length) => Some(addr_length), + Err(_) => { + eprintln!("Expected valid ARP hardware address length"); + process::exit(1); + } + } + }, + None => None + }; + + let proto_type = match matches.value_of("proto-type") { + Some(proto_type_text) => { + + match proto_type_text.parse::() { + Ok(type_number) => Some(EtherType::new(type_number)), + Err(_) => { + eprintln!("Expected valid ARP proto type number"); + process::exit(1); + } + } + }, + None => None + }; + + let proto_addr = match matches.value_of("proto-addr") { + Some(proto_addr_text) => { + + match proto_addr_text.parse::() { + Ok(addr_length) => Some(addr_length), + Err(_) => { + eprintln!("Expected valid ARP hardware address length"); + process::exit(1); + } + } + }, + None => None + }; + + let arp_operation = match matches.value_of("arp-op") { + Some(arp_op_text) => { + + match arp_op_text.parse::() { + Ok(op_number) => Some(ArpOperation::new(op_number)), + Err(_) => { + eprintln!("Expected valid ARP operation number"); + process::exit(1); + } + } + }, + None => None + }; Arc::new(ScanOptions { interface_name, @@ -202,12 +310,18 @@ impl ScanOptions { resolve_hostname, source_ipv4, destination_mac, + source_mac, vlan_id, retry_count, interval_ms, randomize_targets, output, - oui_file + oui_file, + hw_type, + hw_addr, + proto_type, + proto_addr, + arp_operation }) } @@ -216,6 +330,11 @@ impl ScanOptions { matches!(&self.output, OutputFormat::Plain) } + pub fn has_vlan(&self) -> bool { + + matches!(&self.vlan_id, Some(_)) + } + } /** diff --git a/src/network.rs b/src/network.rs index 1a8561c..7e446ca 100644 --- a/src/network.rs +++ b/src/network.rs @@ -53,9 +53,9 @@ pub struct TargetDetails { */ pub fn send_arp_request(tx: &mut Box, interface: &NetworkInterface, ip_network: &IpNetwork, target_ip: Ipv4Addr, options: Arc) { - let mut ethernet_buffer = match options.vlan_id { - Some(_) => vec![0u8; ETHERNET_VLAN_PACKET_SIZE], - None => vec![0u8; ETHERNET_STD_PACKET_SIZE] + let mut ethernet_buffer = match options.has_vlan() { + true => vec![0u8; ETHERNET_VLAN_PACKET_SIZE], + false => vec![0u8; ETHERNET_STD_PACKET_SIZE] }; let mut ethernet_packet = MutableEthernetPacket::new(&mut ethernet_buffer).unwrap_or_else(|| { eprintln!("Could not build Ethernet packet"); @@ -66,10 +66,13 @@ pub fn send_arp_request(tx: &mut Box, interface: &NetworkInt Some(forced_mac) => forced_mac, None => MacAddr::broadcast() }; - let source_mac = interface.mac.unwrap_or_else(|| { - eprintln!("Interface should have a MAC address"); - process::exit(1); - }); + let source_mac = match options.source_mac { + Some(forced_source_mac) => forced_source_mac, + None => interface.mac.unwrap_or_else(|| { + eprintln!("Interface should have a MAC address"); + process::exit(1); + }) + }; ethernet_packet.set_destination(target_mac); ethernet_packet.set_source(source_mac); @@ -88,11 +91,11 @@ pub fn send_arp_request(tx: &mut Box, interface: &NetworkInt let source_ipv4 = find_source_ip(ip_network, options.source_ipv4); - 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_hardware_type(options.hw_type.unwrap_or(ArpHardwareTypes::Ethernet)); + arp_packet.set_protocol_type(options.proto_type.unwrap_or(EtherTypes::Ipv4)); + arp_packet.set_hw_addr_len(options.hw_addr.unwrap_or(6)); + arp_packet.set_proto_addr_len(options.proto_addr.unwrap_or(4)); + arp_packet.set_operation(options.arp_operation.unwrap_or(ArpOperations::Request)); arp_packet.set_sender_hw_addr(source_mac); arp_packet.set_sender_proto_addr(source_ipv4); arp_packet.set_target_hw_addr(target_mac); @@ -145,7 +148,8 @@ fn find_source_ip(ip_network: &IpNetwork, forced_source_ipv4: Option) * 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. + * on the next received frame. Therefore, the receiver should have been + * configured to stop at certain intervals (500ms for example). */ pub fn receive_arp_responses(rx: &mut Box, options: Arc, timed_out: Arc, vendor_list: &mut Vendor) -> (ResponseSummary, Vec) { @@ -191,6 +195,10 @@ pub fn receive_arp_responses(rx: &mut Box, options: Arc, options: Arc, options: Arc Date: Mon, 14 Jun 2021 21:23:20 +0200 Subject: [PATCH 059/121] Writing tests for time args parser --- README.md | 4 ++ src/args.rs | 146 +++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 121 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index b861d53..0a7e942 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,10 @@ Change or force the IPv4 address sent as source in the broadcasted ARP packets. Change or force the MAC address sent as destination ARP request. By default, a broadcast destination (`00:00:00:00:00:00`) will be set. +#### Change source MAC `-M 11:24:71:29:21:76` + +Change or force the MAC address sent as source in the ARP request. By default, the network interface MAC will be used. + #### Randomize target list `-R` Randomize the IPv4 target list before sending ARP requests. By default, all ARP requests are sent in ascending order by IPv4 address. diff --git a/src/args.rs b/src/args.rs index a20a94a..f18e8a5 100644 --- a/src/args.rs +++ b/src/args.rs @@ -22,6 +22,9 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> { App::new("arp-scan") .version(CLI_VERSION) .about("A minimalistic ARP scan tool written in Rust") + .arg( + Arg::with_name("profile").short("p").long("profile").takes_value(true).value_name("PROFILE_NAME").help("Scan profile") + ) .arg( Arg::with_name("interface").short("i").long("interface").takes_value(true).value_name("INTERFACE_NAME").help("Network interface") ) @@ -84,7 +87,15 @@ pub enum OutputFormat { Yaml } +pub enum ProfileType { + Default, + Fast, + Stealth, + Chaos +} + pub struct ScanOptions { + pub profile: ProfileType, pub interface_name: String, pub timeout_ms: u64, pub resolve_hostname: bool, @@ -113,6 +124,23 @@ impl ScanOptions { */ pub fn new(matches: &ArgMatches) -> Arc { + let profile = match matches.value_of("profile") { + Some(output_request) => { + + match output_request { + "default" | "d" => ProfileType::Default, + "fast" | "f" => ProfileType::Fast, + "stealth" | "s" => ProfileType::Stealth, + "chaos" | "c" => ProfileType::Chaos, + _ => { + eprintln!("Expected correct profile name (default/fast/stealth/chaos)"); + process::exit(1); + } + } + }, + None => ProfileType::Default + }; + let interface_name = match matches.value_of("interface") { Some(name) => String::from(name), None => { @@ -130,9 +158,13 @@ impl ScanOptions { } }; - let timeout_ms: u64 = matches.value_of("timeout") - .map(|timeout_text| parse_to_milliseconds(timeout_text)) - .unwrap_or(TIMEOUT_MS_DEFAULT); + let timeout_ms: u64 = match matches.value_of("timeout") { + Some(timeout_text) => parse_to_milliseconds(timeout_text).unwrap_or_else(|err| { + eprintln!("Expected correct timeout, {}", err); + process::exit(1); + }), + None => TIMEOUT_MS_DEFAULT + }; // Hostnames will not be resolved in numeric mode let resolve_hostname = !matches.is_present("numeric"); @@ -207,9 +239,13 @@ impl ScanOptions { None => HOST_RETRY_DEFAULT }; - let interval_ms: u64 = matches.value_of("interval") - .map(|interval_text| parse_to_milliseconds(interval_text)) - .unwrap_or(REQUEST_MS_INTERVAL); + let interval_ms: u64 = match matches.value_of("interval") { + Some(interval_text) => parse_to_milliseconds(interval_text).unwrap_or_else(|err| { + eprintln!("Expected correct interval, {}", err); + process::exit(1); + }), + None => REQUEST_MS_INTERVAL + }; let output = match matches.value_of("output") { Some(output_request) => { @@ -305,6 +341,7 @@ impl ScanOptions { }; Arc::new(ScanOptions { + profile, interface_name, timeout_ms, resolve_hostname, @@ -342,42 +379,93 @@ impl ScanOptions { * string such as '20ms', '10s' or '1h' into adequate milliseconds. Without * suffix, the default behavior is to parse into milliseconds. */ -fn parse_to_milliseconds(time_arg: &str) -> u64 { +fn parse_to_milliseconds(time_arg: &str) -> Result { let len = time_arg.len(); if time_arg.ends_with("ms") { let milliseconds_text = &time_arg[0..len-2]; - return milliseconds_text.parse::() - .unwrap_or_else(|err| { - eprintln!("Expected valid milliseconds ({})", err); - process::exit(1); - }); + return match milliseconds_text.parse::() { + Ok(ms_value) => Ok(ms_value), + Err(_) => Err("invalid milliseconds") + }; } if time_arg.ends_with('s') { let seconds_text = &time_arg[0..len-1]; - return seconds_text.parse::() - .map(|value| value * 1000) - .unwrap_or_else(|err| { - eprintln!("Expected valid seconds ({})", err); - process::exit(1); - }); + return match seconds_text.parse::().map(|value| value * 1000) { + Ok(ms_value) => Ok(ms_value), + Err(_) => Err("invalid seconds") + }; + } + + if time_arg.ends_with('m') { + let seconds_text = &time_arg[0..len-1]; + return match seconds_text.parse::().map(|value| value * 1000 * 60) { + Ok(ms_value) => Ok(ms_value), + Err(_) => Err("invalid minutes") + }; } if time_arg.ends_with('h') { let hour_text = &time_arg[0..len-1]; - return hour_text.parse::() - .map(|value| value * 1000 * 60) - .unwrap_or_else(|err| { - eprintln!("Expected valid hours ({})", err); - process::exit(1); - }); + return match hour_text.parse::().map(|value| value * 1000 * 60 * 60) { + Ok(ms_value) => Ok(ms_value), + Err(_) => Err("invalid hours") + }; + } + + match time_arg.parse::() { + Ok(ms_value) => Ok(ms_value), + Err(_) => Err("invalid milliseconds") + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn should_parse_milliseconds() { + + assert_eq!(parse_to_milliseconds("1000"), Ok(1000)); + } + + #[test] + fn should_parse_seconds() { + + assert_eq!(parse_to_milliseconds("5s"), Ok(5000)); + } + + #[test] + fn should_parse_minutes() { + + assert_eq!(parse_to_milliseconds("3m"), Ok(180_000)); + } + + #[test] + fn should_parse_hours() { + + assert_eq!(parse_to_milliseconds("2h"), Ok(7_200_000)); + } + + #[test] + fn should_deny_negative() { + + assert_eq!(parse_to_milliseconds("-45"), Err("invalid milliseconds")); + } + + #[test] + fn should_deny_floating_numbers() { + + assert_eq!(parse_to_milliseconds("3.235"), Err("invalid milliseconds")); + } + + #[test] + fn should_deny_invalid_characters() { + + assert_eq!(parse_to_milliseconds("3z"), Err("invalid milliseconds")); } - time_arg.parse::() - .unwrap_or_else(|err| { - eprintln!("Expected valid milliseconds ({})", err); - process::exit(1); - }) } From 303303db43d0df26a4b40dcf91df8a3f9b910ecf Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 14 Jun 2021 22:32:36 +0200 Subject: [PATCH 060/121] First profile type implementation --- src/args.rs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/args.rs b/src/args.rs index f18e8a5..bbf0ff2 100644 --- a/src/args.rs +++ b/src/args.rs @@ -7,7 +7,9 @@ use pnet::datalink::MacAddr; use pnet::packet::arp::{ArpHardwareType, ArpOperation}; use pnet::packet::ethernet::EtherType; +const TIMEOUT_MS_FAST: u64 = 800; const TIMEOUT_MS_DEFAULT: u64 = 2000; + const HOST_RETRY_DEFAULT: usize = 1; const REQUEST_MS_INTERVAL: u64 = 10; @@ -163,11 +165,14 @@ impl ScanOptions { eprintln!("Expected correct timeout, {}", err); process::exit(1); }), - None => TIMEOUT_MS_DEFAULT + None => match profile { + ProfileType::Fast => TIMEOUT_MS_FAST, + _ => TIMEOUT_MS_DEFAULT + } }; - // Hostnames will not be resolved in numeric mode - let resolve_hostname = !matches.is_present("numeric"); + // Hostnames will not be resolved in numeric mode or stealth profile + let resolve_hostname = !matches.is_present("numeric") && !matches!(profile, ProfileType::Stealth); let source_ipv4: Option = match matches.value_of("source_ip") { Some(source_ip) => { @@ -236,7 +241,10 @@ impl ScanOptions { } } }, - None => HOST_RETRY_DEFAULT + None => match profile { + ProfileType::Chaos => HOST_RETRY_DEFAULT * 2, + _ => HOST_RETRY_DEFAULT + } }; let interval_ms: u64 = match matches.value_of("interval") { @@ -244,7 +252,10 @@ impl ScanOptions { eprintln!("Expected correct interval, {}", err); process::exit(1); }), - None => REQUEST_MS_INTERVAL + None => match profile { + ProfileType::Stealth => REQUEST_MS_INTERVAL * 2, + _ => REQUEST_MS_INTERVAL + } }; let output = match matches.value_of("output") { @@ -263,7 +274,7 @@ impl ScanOptions { None => OutputFormat::Plain }; - let randomize_targets = matches.is_present("random"); + let randomize_targets = matches.is_present("random") || matches!(profile, ProfileType::Stealth | ProfileType::Chaos); let oui_file: String = match matches.value_of("oui-file") { Some(file) => file.to_string(), From 5f7fa33fa796e963812c1a4a3c6a0b1cc29ca34b Mon Sep 17 00:00:00 2001 From: Saluki Date: Tue, 15 Jun 2021 08:06:02 +0200 Subject: [PATCH 061/121] Scan estimations for plain text output --- src/args.rs | 76 +++++++++++++++++++++++++++++++++++++------------- src/main.rs | 15 ++++------ src/network.rs | 44 +++++++++++++++++++++++++++++ src/utils.rs | 12 ++++++++ 4 files changed, 119 insertions(+), 28 deletions(-) diff --git a/src/args.rs b/src/args.rs index bbf0ff2..dca9a73 100644 --- a/src/args.rs +++ b/src/args.rs @@ -25,61 +25,99 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> { .version(CLI_VERSION) .about("A minimalistic ARP scan tool written in Rust") .arg( - Arg::with_name("profile").short("p").long("profile").takes_value(true).value_name("PROFILE_NAME").help("Scan profile") + Arg::with_name("profile").short("p").long("profile") + .takes_value(true).value_name("PROFILE_NAME") + .help("Scan profile") ) .arg( - Arg::with_name("interface").short("i").long("interface").takes_value(true).value_name("INTERFACE_NAME").help("Network interface") + Arg::with_name("interface").short("i").long("interface") + .takes_value(true).value_name("INTERFACE_NAME") + .help("Network interface") ) .arg( - Arg::with_name("timeout").short("t").long("timeout").takes_value(true).value_name("TIMEOUT_DURATION").help("ARP response timeout") + Arg::with_name("timeout").short("t").long("timeout") + .takes_value(true).value_name("TIMEOUT_DURATION") + .help("ARP response timeout") ) .arg( - Arg::with_name("source_ip").short("S").long("source-ip").takes_value(true).value_name("SOURCE_IPV4").help("Source IPv4 address for requests") + Arg::with_name("source_ip").short("S").long("source-ip") + .takes_value(true).value_name("SOURCE_IPV4") + .help("Source IPv4 address for requests") ) .arg( - Arg::with_name("destination_mac").short("M").long("dest-mac").takes_value(true).value_name("DESTINATION_MAC").help("Destination MAC address for requests") + Arg::with_name("destination_mac").short("M").long("dest-mac") + .takes_value(true).value_name("DESTINATION_MAC") + .help("Destination MAC address for requests") ) .arg( - Arg::with_name("source_mac").long("source-mac").takes_value(true).value_name("SOURCE_MAC").help("Source MAC address for requests") + Arg::with_name("source_mac").long("source-mac") + .takes_value(true).value_name("SOURCE_MAC") + .help("Source MAC address for requests") ) .arg( - Arg::with_name("numeric").short("n").long("numeric").takes_value(false).help("Numeric mode, no hostname resolution") + Arg::with_name("numeric").short("n").long("numeric") + .takes_value(false) + .help("Numeric mode, no hostname resolution") ) .arg( - Arg::with_name("vlan").short("Q").long("vlan").takes_value(true).value_name("VLAN_ID").help("Send using 802.1Q with VLAN ID") + Arg::with_name("vlan").short("Q").long("vlan") + .takes_value(true).value_name("VLAN_ID") + .help("Send using 802.1Q with VLAN ID") ) .arg( - Arg::with_name("retry_count").short("r").long("retry").takes_value(true).value_name("RETRY_COUNT").help("Host retry attempt count") + Arg::with_name("retry_count").short("r").long("retry") + .takes_value(true).value_name("RETRY_COUNT") + .help("Host retry attempt count") ) .arg( - Arg::with_name("random").short("R").long("random").takes_value(false).help("Randomize the target list") + Arg::with_name("random").short("R").long("random") + .takes_value(false) + .help("Randomize the target list") ) .arg( - Arg::with_name("interval").short("I").long("interval").takes_value(true).value_name("INTERVAL_DURATION").help("Milliseconds between ARP requests") + Arg::with_name("interval").short("I").long("interval") + .takes_value(true).value_name("INTERVAL_DURATION") + .help("Milliseconds between ARP requests") ) .arg( - Arg::with_name("oui-file").long("oui-file").takes_value(true).value_name("FILE_PATH").help("Path to custom IEEE OUI CSV file") + Arg::with_name("oui-file").long("oui-file") + .takes_value(true).value_name("FILE_PATH") + .help("Path to custom IEEE OUI CSV file") ) .arg( - Arg::with_name("list").short("l").long("list").takes_value(false).help("List network interfaces") + Arg::with_name("list").short("l").long("list") + .takes_value(false) + .help("List network interfaces") ) .arg( - Arg::with_name("output").short("o").long("output").takes_value(true).value_name("FORMAT").help("Define output format") + Arg::with_name("output").short("o").long("output") + .takes_value(true).value_name("FORMAT") + .help("Define output format") ) .arg( - Arg::with_name("hw_type").long("hw-type").takes_value(true).value_name("HW_TYPE").help("Custom ARP hardware field") + Arg::with_name("hw_type").long("hw-type") + .takes_value(true).value_name("HW_TYPE") + .help("Custom ARP hardware field") ) .arg( - Arg::with_name("hw_addr").long("hw-addr").takes_value(true).value_name("ADDRESS_LEN").help("Custom ARP hardware address length") + Arg::with_name("hw_addr").long("hw-addr") + .takes_value(true).value_name("ADDRESS_LEN") + .help("Custom ARP hardware address length") ) .arg( - Arg::with_name("proto_type").long("proto-type").takes_value(true).value_name("PROTO_TYPE").help("Custom ARP proto type") + Arg::with_name("proto_type").long("proto-type") + .takes_value(true).value_name("PROTO_TYPE") + .help("Custom ARP proto type") ) .arg( - Arg::with_name("proto_addr").long("proto-addr").takes_value(true).value_name("ADDRESS_LEN").help("Custom ARP proto address length") + Arg::with_name("proto_addr").long("proto-addr") + .takes_value(true).value_name("ADDRESS_LEN") + .help("Custom ARP proto address length") ) .arg( - Arg::with_name("arp_operation").long("arp-op").takes_value(true).value_name("OPERATION_ID").help("Custom ARP operation ID") + Arg::with_name("arp_operation").long("arp-op") + .takes_value(true).value_name("OPERATION_ID") + .help("Custom ARP operation ID") ) } diff --git a/src/main.rs b/src/main.rs index 9147b79..5197265 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,6 @@ use std::sync::Arc; use std::time::Duration; use std::sync::atomic::{AtomicBool, Ordering}; -use ipnetwork::NetworkSize; use pnet::datalink; use rand::prelude::*; @@ -85,7 +84,7 @@ fn main() { // local network. let channel_config = datalink::Config { - read_timeout: Some(Duration::from_millis(500)), + read_timeout: Some(Duration::from_millis(network::DATALINK_RCV_TIMEOUT)), ..datalink::Config::default() }; @@ -112,15 +111,13 @@ fn main() { let cloned_options = Arc::clone(&scan_options); let arp_responses = thread::spawn(move || network::receive_arp_responses(&mut rx, cloned_options, cloned_timed_out, &mut vendor_list)); - let network_size: u128 = match ip_network.size() { - NetworkSize::V4(ipv4_network_size) => ipv4_network_size.into(), - NetworkSize::V6(_) => { - eprintln!("IPv6 networks are not supported by the ARP protocol"); - process::exit(1); - } - }; + let network_size = utils::compute_network_size(&ip_network); if scan_options.is_plain_output() { + + let estimations = network::compute_scan_estimation(network_size, &scan_options); + println!("Estimated scan time {}ms (sending {} bytes, {} bytes/s)", estimations.duration_ms, estimations.request_size, estimations.bandwidth); + println!("Sending {} ARP requests (waiting at least {}ms, {}ms request interval)", network_size, scan_options.timeout_ms, scan_options.interval_ms); } diff --git a/src/network.rs b/src/network.rs index 7e446ca..bd0d26c 100644 --- a/src/network.rs +++ b/src/network.rs @@ -5,6 +5,7 @@ use std::collections::HashMap; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::io::ErrorKind::TimedOut; +use std::convert::TryInto; use dns_lookup::lookup_addr; use ipnetwork::IpNetwork; @@ -17,6 +18,8 @@ use pnet::packet::vlan::{ClassOfService, MutableVlanPacket}; use crate::args::ScanOptions; use crate::vendor::Vendor; +pub const DATALINK_RCV_TIMEOUT: u64 = 500; + const VLAN_QOS_DEFAULT: u8 = 1; const ARP_PACKET_SIZE: usize = 28; const VLAN_PACKET_SIZE: usize = 32; @@ -24,6 +27,16 @@ const VLAN_PACKET_SIZE: usize = 32; const ETHERNET_STD_PACKET_SIZE: usize = 42; const ETHERNET_VLAN_PACKET_SIZE: usize = 46; +/** + * Contains scan estimation records. This will be computed before the scan + * starts and should give insights about the scan. + */ +pub struct ScanEstimation { + pub duration_ms: u128, + pub request_size: u128, + pub bandwidth: u128 +} + /** * Gives high-level details about the scan response. This may include Ethernet * details (packet count, size, ...) and other technical network aspects. @@ -46,6 +59,37 @@ pub struct TargetDetails { pub vendor: Option } +/** + * Based on the network size and given scan options, this function performs an + * estimation of the scan impact (timing, bandwidth, ...). Keep in mind that + * this is only an estimation, real results may vary based on the network. + */ +pub fn compute_scan_estimation(network_size: u128, options: &Arc) -> ScanEstimation { + + let interval: u128 = options.interval_ms.into(); + let timeout: u128 = options.timeout_ms.into(); + let packet_size: u128 = match options.has_vlan() { + true => ETHERNET_VLAN_PACKET_SIZE.try_into().unwrap(), + false => ETHERNET_STD_PACKET_SIZE.try_into().unwrap() + }; + let retry_count: u128 = options.retry_count.try_into().unwrap(); + + let avg_arp_request_ms = 3; + let avg_resolve_ms = 500; + + let request_duration_ms: u128 = (network_size * (interval+avg_arp_request_ms)) * retry_count; + let duration_ms = request_duration_ms + timeout + avg_resolve_ms; + let request_size: u128 = network_size * packet_size; + + let bandwidth = (request_size / request_duration_ms) * 1000; + + ScanEstimation { + duration_ms, + request_size, + bandwidth + } +} + /** * 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 diff --git a/src/utils.rs b/src/utils.rs index 173ec49..3ea5c14 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,5 @@ use pnet::datalink::{self, NetworkInterface}; +use ipnetwork::{IpNetwork, NetworkSize}; use serde::Serialize; use std::process; @@ -60,6 +61,17 @@ pub fn select_default_interface() -> Option { }) } +pub fn compute_network_size(ip_network: &IpNetwork) -> u128 { + + match ip_network.size() { + NetworkSize::V4(ipv4_network_size) => ipv4_network_size.into(), + NetworkSize::V6(_) => { + eprintln!("IPv6 networks are not supported by the ARP protocol"); + process::exit(1); + } + } +} + /** * Display the scan results on stdout with a table. The 'final_result' vector * contains all items that will be displayed. From 08defc44579070595ba5d4fb180990f0cff446dd Mon Sep 17 00:00:00 2001 From: Saluki Date: Sun, 20 Jun 2021 22:33:21 +0200 Subject: [PATCH 062/121] Fine-tuning estimations --- README.md | 1 + src/args.rs | 1 + src/main.rs | 2 +- src/network.rs | 16 +++++++++------- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 0a7e942..47eb0d8 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ The features below will be shipped in the next releases of the project. - Complete VLAN support - ~~Exports (JSON & YAML)~~ - released in 0.7.0 - Full ARP packet customization (Ethernet protocol, ARP operation, ...) +- Time estimations & bandwidth - ~~MAC vendor lookup in the results~~ - released in 0.9.0 - ~~Fine-grained scan timings (interval)~~ - released in 0.8.0 - Wide network range support diff --git a/src/args.rs b/src/args.rs index dca9a73..f6d31d2 100644 --- a/src/args.rs +++ b/src/args.rs @@ -292,6 +292,7 @@ impl ScanOptions { }), None => match profile { ProfileType::Stealth => REQUEST_MS_INTERVAL * 2, + ProfileType::Fast => 0, _ => REQUEST_MS_INTERVAL } }; diff --git a/src/main.rs b/src/main.rs index 5197265..5b84678 100644 --- a/src/main.rs +++ b/src/main.rs @@ -116,7 +116,7 @@ fn main() { if scan_options.is_plain_output() { let estimations = network::compute_scan_estimation(network_size, &scan_options); - println!("Estimated scan time {}ms (sending {} bytes, {} bytes/s)", estimations.duration_ms, estimations.request_size, estimations.bandwidth); + println!("Estimated scan time {}ms ({} bytes, {} bytes/s)", estimations.duration_ms, estimations.request_size, estimations.bandwidth); println!("Sending {} ARP requests (waiting at least {}ms, {}ms request interval)", network_size, scan_options.timeout_ms, scan_options.interval_ms); } diff --git a/src/network.rs b/src/network.rs index bd0d26c..f508f98 100644 --- a/src/network.rs +++ b/src/network.rs @@ -64,24 +64,26 @@ pub struct TargetDetails { * estimation of the scan impact (timing, bandwidth, ...). Keep in mind that * this is only an estimation, real results may vary based on the network. */ -pub fn compute_scan_estimation(network_size: u128, options: &Arc) -> ScanEstimation { +pub fn compute_scan_estimation(host_count: u128, options: &Arc) -> ScanEstimation { let interval: u128 = options.interval_ms.into(); let timeout: u128 = options.timeout_ms.into(); let packet_size: u128 = match options.has_vlan() { - true => ETHERNET_VLAN_PACKET_SIZE.try_into().unwrap(), - false => ETHERNET_STD_PACKET_SIZE.try_into().unwrap() + true => ETHERNET_VLAN_PACKET_SIZE.try_into().expect("Internal number conversion failed for VLAN packet size"), + false => ETHERNET_STD_PACKET_SIZE.try_into().expect("Internal number conversion failed for Ethernet packet size") }; let retry_count: u128 = options.retry_count.try_into().unwrap(); + // The values below are averages based on an amount of performed network + // scans. This may of course vary based on network configurations. let avg_arp_request_ms = 3; let avg_resolve_ms = 500; - let request_duration_ms: u128 = (network_size * (interval+avg_arp_request_ms)) * retry_count; - let duration_ms = request_duration_ms + timeout + avg_resolve_ms; - let request_size: u128 = network_size * packet_size; + let request_phase_ms = (host_count * (avg_arp_request_ms+ interval)) * retry_count; + let duration_ms = request_phase_ms + timeout + avg_resolve_ms; + let request_size = host_count * packet_size; - let bandwidth = (request_size / request_duration_ms) * 1000; + let bandwidth = (request_size * 1000) / request_phase_ms; ScanEstimation { duration_ms, From 6c23d1a832eb96f6a99c245c19fe9de631412927 Mon Sep 17 00:00:00 2001 From: Saluki Date: Sun, 20 Jun 2021 22:35:25 +0200 Subject: [PATCH 063/121] Prepare release 0.10.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 460d04d..acf7d18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,7 +20,7 @@ dependencies = [ [[package]] name = "arp-scan" -version = "0.9.0" +version = "0.10.0" dependencies = [ "clap", "csv", diff --git a/Cargo.toml b/Cargo.toml index 54cfafe..874eeb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "arp-scan" description = "A minimalistic ARP scan tool" license = "AGPL-3.0-or-later" -version = "0.9.0" +version = "0.10.0" authors = ["Saluki"] edition = "2018" readme = "README.md" diff --git a/README.md b/README.md index 47eb0d8..31eeda6 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Find all hosts in your local network using this fast ARP scanner. The CLI is wri Download the `arp-scan` binary for Linux (Ubuntu, Fedora, Debian, ...). See the [releases page](https://github.com/Saluki/arp-scan-rs/releases) for other binaries. ```bash -wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.9.0/arp-scan-v0.9.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan +wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.10.0/arp-scan-v0.10.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan ``` List all available network interfaces. @@ -114,11 +114,11 @@ The features below will be shipped in the next releases of the project. - Make ARP scans faster - with a per-host retry approach - ~~by closing the response thread faster~~ - released in 0.8.0 -- Scan profiles (standard, attacker, light, ...) +- ~~Scan profiles (standard, attacker, light, ...)~~ - released in 0.10.0 - Complete VLAN support - ~~Exports (JSON & YAML)~~ - released in 0.7.0 -- Full ARP packet customization (Ethernet protocol, ARP operation, ...) -- Time estimations & bandwidth +- ~~Full ARP packet customization (Ethernet protocol, ARP operation, ...)~~ - released in 0.10.0 +- ~~Time estimations & bandwidth~~ - released in 0.10.0 - ~~MAC vendor lookup in the results~~ - released in 0.9.0 - ~~Fine-grained scan timings (interval)~~ - released in 0.8.0 - Wide network range support From 7f9faa218b8f68549da0929aef013feff8c6beed Mon Sep 17 00:00:00 2001 From: Saluki Date: Wed, 28 Jul 2021 07:49:56 +0200 Subject: [PATCH 064/121] CLI result table is now responsive --- src/utils.rs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 3ea5c14..0781273 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -80,9 +80,26 @@ pub fn display_scan_results(response_summary: ResponseSummary, mut target_detail target_details.sort_by_key(|item| item.ipv4); + let mut hostname_len = 15; + let mut vendor_len = 15; + for detail in target_details.iter() { + + if let Some(hostname) = &detail.hostname { + if hostname.len() > hostname_len { + hostname_len = hostname.len(); + } + } + + if let Some(vendor) = &detail.vendor { + if vendor.len() > vendor_len { + vendor_len = vendor.len(); + } + } + } + println!(); - println!("| IPv4 | MAC | Hostname | Vendor |"); - println!("|-----------------|-------------------|-----------------------|---------------------------------|"); + println!("| IPv4 | MAC | {: &vendor, None => &"" }; - println!("| {: <15} | {: <18} | {: <21} | {: <31} |", detail.ipv4, detail.mac, hostname, vendor); + println!("| {: <15} | {: <18} | {: Date: Sun, 8 Aug 2021 14:56:54 +0200 Subject: [PATCH 065/121] Cleaned dependencies with new ansi_term crate --- Cargo.lock | 12 +++++++++++- Cargo.toml | 17 ++++++++++++----- src/args.rs | 14 ++++++++++++++ src/utils.rs | 32 ++++++++++++++++++++++++++++---- 4 files changed, 65 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index acf7d18..b59f66a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,10 +18,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "arp-scan" version = "0.10.0" dependencies = [ + "ansi_term 0.12.1", "clap", "csv", "dns-lookup", @@ -80,7 +90,7 @@ version = "2.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" dependencies = [ - "ansi_term", + "ansi_term 0.11.0", "atty", "bitflags", "strsim", diff --git a/Cargo.toml b/Cargo.toml index 874eeb6..e19cf31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,12 +12,19 @@ keywords = ["arp", "scan", "network", "security"] categories = ["command-line-utilities"] [dependencies] + +# CLI & utilities +clap = "2.33" +ansi_term = "0.12" +rand = "0.8" + +# Network +pnet = "0.28" +ipnetwork = "0.18" +dns-lookup = "1.0" + +# Parsing & exports csv = "1.1" -pnet = "0.28.0" -ipnetwork = "0.18.0" -clap = "2.33.3" -dns-lookup = "1.0.6" -rand = "0.8.3" serde = { version = "1.0.126", features = ["derive"] } serde_json = "1.0.64" serde_yaml = "~0.8.17" diff --git a/src/args.rs b/src/args.rs index f6d31d2..15de25b 100644 --- a/src/args.rs +++ b/src/args.rs @@ -15,6 +15,19 @@ const REQUEST_MS_INTERVAL: u64 = 10; const CLI_VERSION: &str = env!("CARGO_PKG_VERSION"); +const EXAMPLES_HELP: &'static str = "EXAMPLES: + + List network interfaces + arp-scan -l + + Launch a scan on WiFi interface with fake IP and stealth profile + arp-scan -i wlp1s0 -source-ip 192.168.0.42 -profile stealth + + Launch a scan on VLAN 45 with JSON output + arp-scan -Q 45 -o json + +"; + /** * This function groups together all exposed CLI arguments to the end-users * with clap. Other CLI details (version, ...) should be grouped there as well. @@ -119,6 +132,7 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> { .takes_value(true).value_name("OPERATION_ID") .help("Custom ARP operation ID") ) + .after_help(EXAMPLES_HELP) } pub enum OutputFormat { diff --git a/src/utils.rs b/src/utils.rs index 0781273..684fb28 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,7 +1,9 @@ +use std::process; + use pnet::datalink::{self, NetworkInterface}; use ipnetwork::{IpNetwork, NetworkSize}; use serde::Serialize; -use std::process; +use ansi_term::Color::{Green, Red}; use crate::network::{ResponseSummary, TargetDetails}; use crate::args::ScanOptions; @@ -21,17 +23,39 @@ pub fn is_root_user() -> bool { */ pub fn show_interfaces(interfaces: &[NetworkInterface]) { + let mut interface_count = 0; + let mut interface_up_count = 0; + + println!(); for interface in interfaces.iter() { + let up_text = match interface.is_up() { - true => "UP", - false => "DOWN" + true => format!("{} UP", Green.paint("✔")), + false => format!("{} DOWN", Red.paint("✖")) }; let mac_text = match interface.mac { Some(mac_address) => format!("{}", mac_address), None => "No MAC address".to_string() }; - println!("{: <17} {: <7} {}", interface.name, up_text, mac_text); + let first_ip = match interface.ips.get(0) { + Some(ip_address) => format!("{}", ip_address), + None => "".to_string() + }; + + println!("{: <20} {: <18} {: <20} {}", interface.name, up_text, mac_text, first_ip); + + interface_count += 1; + if interface.is_up() { + interface_up_count += 1; + } } + + println!(); + println!("Found {} network interfaces, {} seems up for ARP scan", interface_count, interface_up_count); + if let Some(default_interface) = select_default_interface() { + println!("Default network interface will be {}", default_interface.name); + } + println!(); } /** From b1cf3d9f38b31e13041d263474a302ae4ca5c20e Mon Sep 17 00:00:00 2001 From: Saluki Date: Sun, 8 Aug 2021 15:15:08 +0200 Subject: [PATCH 066/121] Enhanced README documentation & profile examples --- README.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/README.md b/README.md index 31eeda6..b62d31a 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,51 @@ Find all hosts in your local network using this fast ARP scanner. The CLI is wri ✔ JSON & YAML exports +✔ Pre-defined scan profiles (default, fast, stealth & chaos) + +## Examples + +Start by listing all network interfaces on the host. + +```bash +# List all network interfaces +$ arp-scan -l + +lo ✔ UP 00:00:00:00:00:00 127.0.0.1/8 +enp3s0f0 ✔ UP 4f:6e:cd:78:bb:5a +enp4s0 ✖ DOWN d0:c5:e9:40:00:4a +wlp1s0 ✔ UP d2:71:d8:29:a8:72 192.168.1.21/24 +docker0 ✔ UP 49:fd:cd:60:73:77 172.17.0.1/16 +br-fa6dc54a91ee ✔ UP 61:ab:c1:a7:50:79 172.18.0.1/16 + +Found 6 network interfaces, 5 seems up for ARP scan +Default network interface will be wlp1s0 + +``` + +Perform a default ARP scan on the local network with safe defaults. + +```bash +# Perform a scan on the default network interface +$ arp-scan + +Selected interface wlp1s0 with IP 192.168.1.21/24 +Estimated scan time 2068ms (10752 bytes, 14000 bytes/s) +Sending 256 ARP requests (waiting at least 800ms, 0ms request interval) + +| IPv4 | MAC | Hostname | Vendor | +|-----------------|-------------------|--------------|--------------| +| 192.168.1.1 | 91:10:fb:30:06:04 | router.home | Vendor, Inc. | +| 192.168.1.11 | 45:2e:99:bc:22:b6 | host-a.home | | +| 192.168.1.15 | bc:03:c2:92:47:df | host-b.home | Vendor, Inc. | +| 192.168.1.18 | 8d:eb:56:17:b8:e1 | host-c.home | Vendor, Inc. | +| 192.168.1.34 | 35:e0:6c:1e:e3:fe | | Vendor, Inc. | + +ARP scan finished, 5 hosts found in 1.623 seconds +7 packets received, 5 ARP packets filtered + +``` + ## Getting started Download the `arp-scan` binary for Linux (Ubuntu, Fedora, Debian, ...). See the [releases page](https://github.com/Saluki/arp-scan-rs/releases) for other binaries. @@ -55,6 +100,15 @@ Display the main help message with all commands and available ARP scan options. List all available network interfaces. Using this option will only print a list of interfaces and exit the process. +#### Select scan profile `-p stealth` + +A scan profile groups together a set of ARP scan options to perform a specific scan. The scan profiles are listed below: + +- `default` : default option, this is enabled if the `-p` option is not used +- `fast` : fast ARP scans, the results may be less accurate +- `stealth` : slower scans that minimize the network impact +- `chaos` : randomly-selected values for the ARP scan + #### 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. By default, the first network interface with an `up` status and a valid IPv4 will be selected. From 541159a07a448c1619c66520a43aadf313e32ca6 Mon Sep 17 00:00:00 2001 From: Saluki Date: Sun, 8 Aug 2021 15:38:09 +0200 Subject: [PATCH 067/121] Finished documentation on options & next features --- README.md | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b62d31a..f1846ec 100644 --- a/README.md +++ b/README.md @@ -145,14 +145,34 @@ Change or force the MAC address sent as source in the ARP request. By default, t Randomize the IPv4 target list before sending ARP requests. By default, all ARP requests are sent in ascending order by IPv4 address. -#### Use custom MAC OUI file `--oui-file` +#### Use custom MAC OUI file `--oui-file ./my-file.csv` Use a [custom OUI MAC file](http://standards-oui.ieee.org/oui/oui.csv), the default path will be set to `/usr/share/arp-scan/ieee-oui.csv"`. -#### Set VLAN ID `-Q 540` +#### Set VLAN ID `-Q 42` Add a 802.1Q field in the Ethernet frame. This fields contains the given VLAN ID for outgoing ARP requests. By default, the Ethernet frame is sent without 802.1Q fields (no VLAN). +#### Customize ARP operation ID `--arp-op 1` + +Change the ARP protocol operation field, this can cause scan failure. + +#### Customize ARP hardware type `--hw-type 1` + +Change the ARP hardware type field, this can cause scan failure. + +#### Customize ARP hardware address length `--hw-addr 6` + +Change the ARP hardware address length field, this can cause scan failure. + +#### Customize ARP protocol type `--proto-type 2048` + +Change the ARP protocol type field, this can cause scan failure. + +#### Customize ARP protocol adress length `--proto-addr 4` + +Change the ARP protocol address length field, this can cause scan failure. + #### Set output format `-o json` Set the output format to either `plain` (a full-text output with tables), `json` or `yaml`. @@ -161,7 +181,7 @@ Set the output format to either `plain` (a full-text output with tables), `json` Display the ARP scan CLI version and exits the process. -## Roadmap +## Roadmap & features The features below will be shipped in the next releases of the project. @@ -175,7 +195,11 @@ The features below will be shipped in the next releases of the project. - ~~Time estimations & bandwidth~~ - released in 0.10.0 - ~~MAC vendor lookup in the results~~ - released in 0.9.0 - ~~Fine-grained scan timings (interval)~~ - released in 0.8.0 -- Wide network range support +- Wide network range support & partial results on SIGINT +- Read network targets from file +- Adding advanced packet options (padding, LLC, ...) +- Enable bandwith control (exclusive with interval) +- Stronger profile defaults (chaos & stealth) ## Contributing From aa975da4b0b2008b93bf8eac563bbf744492f729 Mon Sep 17 00:00:00 2001 From: Saluki Date: Wed, 22 Sep 2021 13:04:57 +0200 Subject: [PATCH 068/121] Fixing "up" interfaces to "ready" interfaces" --- src/utils.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 684fb28..6c1cb50 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -24,7 +24,7 @@ pub fn is_root_user() -> bool { pub fn show_interfaces(interfaces: &[NetworkInterface]) { let mut interface_count = 0; - let mut interface_up_count = 0; + let mut ready_count = 0; println!(); for interface in interfaces.iter() { @@ -45,13 +45,13 @@ pub fn show_interfaces(interfaces: &[NetworkInterface]) { println!("{: <20} {: <18} {: <20} {}", interface.name, up_text, mac_text, first_ip); interface_count += 1; - if interface.is_up() { - interface_up_count += 1; + if interface.is_up() && !interface.is_loopback() && interface.ips.len() > 0 { + ready_count += 1; } } println!(); - println!("Found {} network interfaces, {} seems up for ARP scan", interface_count, interface_up_count); + println!("Found {} network interfaces, {} seems ready for ARP scans", interface_count, ready_count); if let Some(default_interface) = select_default_interface() { println!("Default network interface will be {}", default_interface.name); } From f748359e00b581331c5bd26a81704f7990b45aee Mon Sep 17 00:00:00 2001 From: Saluki Date: Wed, 22 Sep 2021 13:09:05 +0200 Subject: [PATCH 069/121] Adding partial scan interruption --- Cargo.lock | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 1 + src/main.rs | 25 ++++++++++++++++++++++++- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b59f66a..b27ff49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,6 +34,7 @@ dependencies = [ "ansi_term 0.12.1", "clap", "csv", + "ctrlc", "dns-lookup", "ipnetwork", "pnet", @@ -54,6 +55,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + [[package]] name = "bitflags" version = "1.2.1" @@ -78,6 +85,12 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "cc" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" + [[package]] name = "cfg-if" version = "1.0.0" @@ -121,6 +134,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctrlc" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "377c9b002a72a0b2c1a18c62e2f3864bdfea4a015e3683a96e24aa45dd6c02d1" +dependencies = [ + "nix", + "winapi", +] + [[package]] name = "dns-lookup" version = "1.0.6" @@ -188,9 +211,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.95" +version = "0.2.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36" +checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103" [[package]] name = "linked-hash-map" @@ -204,6 +227,28 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" +[[package]] +name = "memoffset" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nix" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7555d6c7164cc913be1ce7f95cbecdabda61eb2ccd89008524af306fb7f5031" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "memoffset", +] + [[package]] name = "pnet" version = "0.28.0" diff --git a/Cargo.toml b/Cargo.toml index e19cf31..d4255d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ categories = ["command-line-utilities"] clap = "2.33" ansi_term = "0.12" rand = "0.8" +ctrlc = "3.2" # Network pnet = "0.28" diff --git a/src/main.rs b/src/main.rs index 5b84678..08e3927 100644 --- a/src/main.rs +++ b/src/main.rs @@ -121,10 +121,24 @@ fn main() { println!("Sending {} ARP requests (waiting at least {}ms, {}ms request interval)", network_size, scan_options.timeout_ms, scan_options.interval_ms); } + let finish_sleep = Arc::new(AtomicBool::new(false)); + let cloned_finish_sleep = Arc::clone(&finish_sleep); + + ctrlc::set_handler(move || { + cloned_finish_sleep.store(true, Ordering::Relaxed); + }).unwrap_or_else(|err| { + eprintln!("Could not set CTRL+C handler ({})", err); + process::exit(1); + }); + // The retry count does right now use a 'brute-force' strategy without // synchronization process with the already known hosts. for _ in 0..scan_options.retry_count { + if finish_sleep.load(Ordering::Relaxed) { + break; + } + // The random approach has one major drawback, compared with the native // network iterator exposed by 'ipnetwork': memory usage. Instead of // using a small memory footprint iterator, we have to store all IP @@ -141,6 +155,10 @@ fn main() { for ip_address in ip_addresses { + if finish_sleep.load(Ordering::Relaxed) { + break; + } + if let IpAddr::V4(ipv4_address) = ip_address { network::send_arp_request(&mut tx, selected_interface, &ip_network, ipv4_address, Arc::clone(&scan_options)); thread::sleep(Duration::from_millis(scan_options.interval_ms)); @@ -151,7 +169,12 @@ fn main() { // Once the ARP packets are sent, the main thread will sleep for T seconds // (where T is the timeout option). After the sleep phase, the response // thread will receive a stop request through the 'timed_out' mutex. - thread::sleep(Duration::from_millis(scan_options.timeout_ms)); + let mut sleep_ms_mount: u64 = 0; + while finish_sleep.load(Ordering::Relaxed) == false && sleep_ms_mount < scan_options.timeout_ms { + + thread::sleep(Duration::from_millis(500)); + sleep_ms_mount = sleep_ms_mount + 500; + } timed_out.store(true, Ordering::Relaxed); let (response_summary, target_details) = arp_responses.join().unwrap_or_else(|error| { From 2c75586b5750938420dcd8ffa1d5219d361ea682 Mon Sep 17 00:00:00 2001 From: Saluki Date: Wed, 22 Sep 2021 13:15:40 +0200 Subject: [PATCH 070/121] Updating versions and docs --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b27ff49..941577e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,7 +29,7 @@ dependencies = [ [[package]] name = "arp-scan" -version = "0.10.0" +version = "0.11.0" dependencies = [ "ansi_term 0.12.1", "clap", diff --git a/Cargo.toml b/Cargo.toml index d4255d7..bf06c27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "arp-scan" description = "A minimalistic ARP scan tool" license = "AGPL-3.0-or-later" -version = "0.10.0" +version = "0.11.0" authors = ["Saluki"] edition = "2018" readme = "README.md" diff --git a/README.md b/README.md index f1846ec..b9bebb4 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ ARP scan finished, 5 hosts found in 1.623 seconds Download the `arp-scan` binary for Linux (Ubuntu, Fedora, Debian, ...). See the [releases page](https://github.com/Saluki/arp-scan-rs/releases) for other binaries. ```bash -wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.10.0/arp-scan-v0.10.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan +wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.11.0/arp-scan-v0.11.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan ``` List all available network interfaces. @@ -195,7 +195,8 @@ The features below will be shipped in the next releases of the project. - ~~Time estimations & bandwidth~~ - released in 0.10.0 - ~~MAC vendor lookup in the results~~ - released in 0.9.0 - ~~Fine-grained scan timings (interval)~~ - released in 0.8.0 -- Wide network range support & partial results on SIGINT +- Wide network range support +- ~~Partial results on SIGINT~~ - released in 0.11.0 - Read network targets from file - Adding advanced packet options (padding, LLC, ...) - Enable bandwith control (exclusive with interval) From 7c06006080647b4a83d1053904ff9052b756daeb Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 18 Oct 2021 06:22:03 +0200 Subject: [PATCH 071/121] Fixing args example --- src/args.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/args.rs b/src/args.rs index 15de25b..2bf8c5e 100644 --- a/src/args.rs +++ b/src/args.rs @@ -21,7 +21,7 @@ const EXAMPLES_HELP: &'static str = "EXAMPLES: arp-scan -l Launch a scan on WiFi interface with fake IP and stealth profile - arp-scan -i wlp1s0 -source-ip 192.168.0.42 -profile stealth + arp-scan -i wlp1s0 --source-ip 192.168.0.42 --profile stealth Launch a scan on VLAN 45 with JSON output arp-scan -Q 45 -o json From bd56ac95badde24c3521926992eb04a985bc0bff Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 18 Oct 2021 06:46:36 +0200 Subject: [PATCH 072/121] Optimize datalink::interfaces calls --- src/args.rs | 19 ++----------------- src/main.rs | 23 ++++++++++++++++++++--- src/utils.rs | 14 +++++++------- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/args.rs b/src/args.rs index 2bf8c5e..6526ea2 100644 --- a/src/args.rs +++ b/src/args.rs @@ -150,7 +150,7 @@ pub enum ProfileType { pub struct ScanOptions { pub profile: ProfileType, - pub interface_name: String, + pub interface_name: Option, pub timeout_ms: u64, pub resolve_hostname: bool, pub source_ipv4: Option, @@ -195,22 +195,7 @@ impl ScanOptions { None => ProfileType::Default }; - let interface_name = match matches.value_of("interface") { - Some(name) => String::from(name), - None => { - - match super::utils::select_default_interface() { - Some(default_interface) => { - default_interface.name - }, - None => { - eprintln!("Network interface name required"); - eprintln!("Use 'arp scan -l' to list available interfaces"); - process::exit(1); - } - } - } - }; + let interface_name = matches.value_of("interface").map(|name| String::from(name)); let timeout_ms: u64 = match matches.value_of("timeout") { Some(timeout_text) => parse_to_milliseconds(timeout_text).unwrap_or_else(|err| { diff --git a/src/main.rs b/src/main.rs index 08e3927..d8a8d24 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,10 +46,28 @@ fn main() { process::exit(1); } + let interface_name = match &scan_options.interface_name { + Some(name) => String::from(name), + None => { + + let name = utils::select_default_interface(&interfaces).map(|interface| interface.name); + + match name { + Some(name) => name, + None => { + eprintln!("Could not find a default network interface"); + eprintln!("Use 'arp scan -l' to list available interfaces"); + process::exit(1); + } + } + } + }; + let selected_interface: &datalink::NetworkInterface = interfaces.iter() - .find(|interface| { interface.name == scan_options.interface_name && interface.is_up() && !interface.is_loopback() }) + .find(|interface| { interface.name == interface_name && interface.is_up() && !interface.is_loopback() }) .unwrap_or_else(|| { - eprintln!("Could not find interface with name {}", scan_options.interface_name); + eprintln!("Could not find interface with name {}", interface_name); + eprintln!("Make sure the interface is up, not loopback and has a valid IPv4"); process::exit(1); }); @@ -117,7 +135,6 @@ fn main() { let estimations = network::compute_scan_estimation(network_size, &scan_options); println!("Estimated scan time {}ms ({} bytes, {} bytes/s)", estimations.duration_ms, estimations.request_size, estimations.bandwidth); - println!("Sending {} ARP requests (waiting at least {}ms, {}ms request interval)", network_size, scan_options.timeout_ms, scan_options.interval_ms); } diff --git a/src/utils.rs b/src/utils.rs index 6c1cb50..b9888bc 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,6 @@ use std::process; -use pnet::datalink::{self, NetworkInterface}; +use pnet::datalink::NetworkInterface; use ipnetwork::{IpNetwork, NetworkSize}; use serde::Serialize; use ansi_term::Color::{Green, Red}; @@ -52,7 +52,7 @@ pub fn show_interfaces(interfaces: &[NetworkInterface]) { println!(); println!("Found {} network interfaces, {} seems ready for ARP scans", interface_count, ready_count); - if let Some(default_interface) = select_default_interface() { + if let Some(default_interface) = select_default_interface(interfaces) { println!("Default network interface will be {}", default_interface.name); } println!(); @@ -62,11 +62,9 @@ pub fn show_interfaces(interfaces: &[NetworkInterface]) { * Find a default network interface for scans, based on the operating system * priority and some interface technical details. */ -pub fn select_default_interface() -> Option { +pub fn select_default_interface(interfaces: &[NetworkInterface]) -> Option { - let interfaces = datalink::interfaces(); - - interfaces.into_iter().find(|interface| { + let default_interface = interfaces.iter().find(|interface| { if interface.mac.is_none() { return false; @@ -82,7 +80,9 @@ pub fn select_default_interface() -> Option { } true - }) + }); + + default_interface.map(|interface| interface.clone()) } pub fn compute_network_size(ip_network: &IpNetwork) -> u128 { From 0101ab126fb174bebf0c4b8bd654463ca7849d23 Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 18 Oct 2021 07:31:48 +0200 Subject: [PATCH 073/121] Enhance code readability --- src/args.rs | 4 ++-- src/main.rs | 4 ++-- src/utils.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/args.rs b/src/args.rs index 6526ea2..da7b149 100644 --- a/src/args.rs +++ b/src/args.rs @@ -15,7 +15,7 @@ const REQUEST_MS_INTERVAL: u64 = 10; const CLI_VERSION: &str = env!("CARGO_PKG_VERSION"); -const EXAMPLES_HELP: &'static str = "EXAMPLES: +const EXAMPLES_HELP: &str = "EXAMPLES: List network interfaces arp-scan -l @@ -195,7 +195,7 @@ impl ScanOptions { None => ProfileType::Default }; - let interface_name = matches.value_of("interface").map(|name| String::from(name)); + let interface_name = matches.value_of("interface").map(String::from); let timeout_ms: u64 = match matches.value_of("timeout") { Some(timeout_text) => parse_to_milliseconds(timeout_text).unwrap_or_else(|err| { diff --git a/src/main.rs b/src/main.rs index d8a8d24..b1f90f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -187,10 +187,10 @@ fn main() { // (where T is the timeout option). After the sleep phase, the response // thread will receive a stop request through the 'timed_out' mutex. let mut sleep_ms_mount: u64 = 0; - while finish_sleep.load(Ordering::Relaxed) == false && sleep_ms_mount < scan_options.timeout_ms { + while !finish_sleep.load(Ordering::Relaxed) && sleep_ms_mount < scan_options.timeout_ms { thread::sleep(Duration::from_millis(500)); - sleep_ms_mount = sleep_ms_mount + 500; + sleep_ms_mount += 500; } timed_out.store(true, Ordering::Relaxed); diff --git a/src/utils.rs b/src/utils.rs index b9888bc..854d096 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -45,7 +45,7 @@ pub fn show_interfaces(interfaces: &[NetworkInterface]) { println!("{: <20} {: <18} {: <20} {}", interface.name, up_text, mac_text, first_ip); interface_count += 1; - if interface.is_up() && !interface.is_loopback() && interface.ips.len() > 0 { + if interface.is_up() && !interface.is_loopback() && !interface.ips.is_empty() { ready_count += 1; } } @@ -82,7 +82,7 @@ pub fn select_default_interface(interfaces: &[NetworkInterface]) -> Option u128 { From d262aae67d2d2517a82ddf0629e7d41cb025082a Mon Sep 17 00:00:00 2001 From: Saluki Date: Sat, 6 Nov 2021 18:46:54 +0100 Subject: [PATCH 074/121] Dedicated time utilities module --- src/args.rs | 98 +----------------------------------- src/time.rs | 141 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 96 deletions(-) create mode 100644 src/time.rs diff --git a/src/args.rs b/src/args.rs index da7b149..854c1fc 100644 --- a/src/args.rs +++ b/src/args.rs @@ -7,6 +7,8 @@ use pnet::datalink::MacAddr; use pnet::packet::arp::{ArpHardwareType, ArpOperation}; use pnet::packet::ethernet::EtherType; +use crate::time::parse_to_milliseconds; + const TIMEOUT_MS_FAST: u64 = 800; const TIMEOUT_MS_DEFAULT: u64 = 2000; @@ -422,99 +424,3 @@ impl ScanOptions { } } - -/** - * Parse a given time string into milliseconds. This can be used to convert a - * string such as '20ms', '10s' or '1h' into adequate milliseconds. Without - * suffix, the default behavior is to parse into milliseconds. - */ -fn parse_to_milliseconds(time_arg: &str) -> Result { - - let len = time_arg.len(); - - if time_arg.ends_with("ms") { - let milliseconds_text = &time_arg[0..len-2]; - return match milliseconds_text.parse::() { - Ok(ms_value) => Ok(ms_value), - Err(_) => Err("invalid milliseconds") - }; - } - - if time_arg.ends_with('s') { - let seconds_text = &time_arg[0..len-1]; - return match seconds_text.parse::().map(|value| value * 1000) { - Ok(ms_value) => Ok(ms_value), - Err(_) => Err("invalid seconds") - }; - } - - if time_arg.ends_with('m') { - let seconds_text = &time_arg[0..len-1]; - return match seconds_text.parse::().map(|value| value * 1000 * 60) { - Ok(ms_value) => Ok(ms_value), - Err(_) => Err("invalid minutes") - }; - } - - if time_arg.ends_with('h') { - let hour_text = &time_arg[0..len-1]; - return match hour_text.parse::().map(|value| value * 1000 * 60 * 60) { - Ok(ms_value) => Ok(ms_value), - Err(_) => Err("invalid hours") - }; - } - - match time_arg.parse::() { - Ok(ms_value) => Ok(ms_value), - Err(_) => Err("invalid milliseconds") - } -} - -#[cfg(test)] -mod tests { - - use super::*; - - #[test] - fn should_parse_milliseconds() { - - assert_eq!(parse_to_milliseconds("1000"), Ok(1000)); - } - - #[test] - fn should_parse_seconds() { - - assert_eq!(parse_to_milliseconds("5s"), Ok(5000)); - } - - #[test] - fn should_parse_minutes() { - - assert_eq!(parse_to_milliseconds("3m"), Ok(180_000)); - } - - #[test] - fn should_parse_hours() { - - assert_eq!(parse_to_milliseconds("2h"), Ok(7_200_000)); - } - - #[test] - fn should_deny_negative() { - - assert_eq!(parse_to_milliseconds("-45"), Err("invalid milliseconds")); - } - - #[test] - fn should_deny_floating_numbers() { - - assert_eq!(parse_to_milliseconds("3.235"), Err("invalid milliseconds")); - } - - #[test] - fn should_deny_invalid_characters() { - - assert_eq!(parse_to_milliseconds("3z"), Err("invalid milliseconds")); - } - -} diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 0000000..024b9c7 --- /dev/null +++ b/src/time.rs @@ -0,0 +1,141 @@ +/** + * Parse a given time string into milliseconds. This can be used to convert a + * string such as '20ms', '10s' or '1h' into adequate milliseconds. Without + * suffix, the default behavior is to parse into milliseconds. + */ +pub fn parse_to_milliseconds(time_arg: &str) -> Result { + + let len = time_arg.len(); + + if time_arg.ends_with("ms") { + let milliseconds_text = &time_arg[0..len-2]; + return match milliseconds_text.parse::() { + Ok(ms_value) => Ok(ms_value), + Err(_) => Err("invalid milliseconds") + }; + } + + if time_arg.ends_with('s') { + let seconds_text = &time_arg[0..len-1]; + return match seconds_text.parse::().map(|value| value * 1000) { + Ok(ms_value) => Ok(ms_value), + Err(_) => Err("invalid seconds") + }; + } + + if time_arg.ends_with('m') { + let seconds_text = &time_arg[0..len-1]; + return match seconds_text.parse::().map(|value| value * 1000 * 60) { + Ok(ms_value) => Ok(ms_value), + Err(_) => Err("invalid minutes") + }; + } + + if time_arg.ends_with('h') { + let hour_text = &time_arg[0..len-1]; + return match hour_text.parse::().map(|value| value * 1000 * 60 * 60) { + Ok(ms_value) => Ok(ms_value), + Err(_) => Err("invalid hours") + }; + } + + match time_arg.parse::() { + Ok(ms_value) => Ok(ms_value), + Err(_) => Err("invalid milliseconds") + } +} + +pub fn format_milliseconds(milliseconds: u128) -> String { + + if milliseconds < 1000 { + return format!("{}ms", milliseconds); + } + + if milliseconds < 1000*60 { + let seconds = milliseconds / 1000; + return format!("{}s", seconds); + } + + if milliseconds < 1000*60*60 { + let minutes = milliseconds / 1000 / 60; + return format!("{}m", minutes); + } + + let hours: u128 = milliseconds / 1000 / 60 / 60; + return format!("{}h", hours); +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn should_parse_milliseconds() { + + assert_eq!(parse_to_milliseconds("1000"), Ok(1000)); + } + + #[test] + fn should_parse_seconds() { + + assert_eq!(parse_to_milliseconds("5s"), Ok(5000)); + } + + #[test] + fn should_parse_minutes() { + + assert_eq!(parse_to_milliseconds("3m"), Ok(180_000)); + } + + #[test] + fn should_parse_hours() { + + assert_eq!(parse_to_milliseconds("2h"), Ok(7_200_000)); + } + + #[test] + fn should_deny_negative() { + + assert_eq!(parse_to_milliseconds("-45"), Err("invalid milliseconds")); + } + + #[test] + fn should_deny_floating_numbers() { + + assert_eq!(parse_to_milliseconds("3.235"), Err("invalid milliseconds")); + } + + #[test] + fn should_deny_invalid_characters() { + + assert_eq!(parse_to_milliseconds("3z"), Err("invalid milliseconds")); + } + + // --- + + #[test] + fn should_display_milliseconds() { + + assert_eq!(format_milliseconds(500), "500ms".to_string()); + } + + #[test] + fn should_display_seconds() { + + assert_eq!(format_milliseconds(2500), "2s".to_string()); + } + + #[test] + fn should_display_minutes() { + + assert_eq!(format_milliseconds(300_000), "5m".to_string()); + } + + #[test] + fn should_display_hours() { + + assert_eq!(format_milliseconds(4_200_000), "1h".to_string()); + } + +} From 657c52324ecf5dae4a194b306b74df58a3439048 Mon Sep 17 00:00:00 2001 From: Saluki Date: Sat, 6 Nov 2021 18:47:18 +0100 Subject: [PATCH 075/121] Better CLI message details --- src/main.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index b1f90f9..985ea20 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod args; mod network; +mod time; mod utils; mod vendor; @@ -134,7 +135,8 @@ fn main() { if scan_options.is_plain_output() { let estimations = network::compute_scan_estimation(network_size, &scan_options); - println!("Estimated scan time {}ms ({} bytes, {} bytes/s)", estimations.duration_ms, estimations.request_size, estimations.bandwidth); + let formatted_ms = time::format_milliseconds(estimations.duration_ms); + println!("Estimated scan time {} ({} bytes, {} bytes/s)", formatted_ms, estimations.request_size, estimations.bandwidth); println!("Sending {} ARP requests (waiting at least {}ms, {}ms request interval)", network_size, scan_options.timeout_ms, scan_options.interval_ms); } @@ -142,6 +144,7 @@ fn main() { let cloned_finish_sleep = Arc::clone(&finish_sleep); ctrlc::set_handler(move || { + eprintln!("[warn] Receiving halt signal, ending scan with partial results"); cloned_finish_sleep.store(true, Ordering::Relaxed); }).unwrap_or_else(|err| { eprintln!("Could not set CTRL+C handler ({})", err); From 6fedd88ea9da6a931916253165d276aa998dbae4 Mon Sep 17 00:00:00 2001 From: Saluki Date: Sat, 6 Nov 2021 18:47:38 +0100 Subject: [PATCH 076/121] Working on vendor module docs --- src/vendor.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/vendor.rs b/src/vendor.rs index 431dfc4..b2154f7 100644 --- a/src/vendor.rs +++ b/src/vendor.rs @@ -4,12 +4,20 @@ use std::process; use pnet::datalink::MacAddr; use csv::{Position, Reader}; +// The Vendor structure performs search operations on a vendor database to find +// which MAC address belongs to a specific vendor. All network vendors have a +// dedicated MAC address range that is registered by the IEEE and maintained in +// the OUI database. An OUI is a 24-bit globally unique assigned number +// referenced by various standards. pub struct Vendor { reader: Option>, } impl Vendor { + // Create a new MAC vendor search instance based on the given datebase path + // (absolute or relative). A failure will not throw an error, but leave the + // vendor search instance without database reader. pub fn new(path: &str) -> Self { let file_result = File::open(path); @@ -28,6 +36,8 @@ impl Vendor { self.reader.is_some() } + // Find a vendor name based on a given MAC address. A vendor search + // operation will perform a whole read on the database for now. pub fn search_by_mac(&mut self, mac_address: &MacAddr) -> Option { match &mut self.reader { From 9eecb922909e1e2e7256e1c66f5ca14251b09765 Mon Sep 17 00:00:00 2001 From: Saluki Date: Sun, 7 Nov 2021 11:27:04 +0100 Subject: [PATCH 077/121] Refactoring with new --network option --- README.md | 6 +++++- src/args.rs | 21 ++++++++++++++++++- src/main.rs | 37 +-------------------------------- src/network.rs | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index b9bebb4..9954d9a 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,10 @@ A scan profile groups together a set of ARP scan options to perform a specific s Perform a scan on the network interface `eth0`. The first valid IPv4 network on this interface will be used as scan target. By default, the first network interface with an `up` status and a valid IPv4 will be selected. +#### Set IPv4 network range `-n 172.17.0.0/24` + +By default, the scan process will select the first IPv4 network on the interface and start a scan on the whole range. With the `--network` option, an IPv4 network can be defined _(this may be used for specific scans on a subset of network targets)_. + #### Set global scan timeout `-t 15s` Enforce a timeout of at least 15 seconds. This timeout is a minimum value (scans may take a little more time). Default value is `2000ms`. @@ -121,7 +125,7 @@ Enforce a timeout of at least 15 seconds. This timeout is a minimum value (scans By default, a `10ms` gap will be set between ARP requests to avoid an ARP storm on the network. This value can be changed to reduce or increase the milliseconds between each ARP request. -#### Numeric mode `-n` +#### Numeric mode `--numeric` Switch to numeric mode. This will skip the local hostname resolution process and will only display IP addresses. diff --git a/src/args.rs b/src/args.rs index 854c1fc..b04dd46 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,8 +1,10 @@ +use std::str::FromStr; use std::net::Ipv4Addr; use std::process; use std::sync::Arc; use clap::{Arg, ArgMatches, App}; +use ipnetwork::IpNetwork; use pnet::datalink::MacAddr; use pnet::packet::arp::{ArpHardwareType, ArpOperation}; use pnet::packet::ethernet::EtherType; @@ -49,6 +51,11 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> { .takes_value(true).value_name("INTERFACE_NAME") .help("Network interface") ) + .arg( + Arg::with_name("network").short("n").long("network") + .takes_value(true).value_name("NETWORK_RANGE") + .help("Network range to scan") + ) .arg( Arg::with_name("timeout").short("t").long("timeout") .takes_value(true).value_name("TIMEOUT_DURATION") @@ -70,7 +77,7 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> { .help("Source MAC address for requests") ) .arg( - Arg::with_name("numeric").short("n").long("numeric") + Arg::with_name("numeric").long("numeric") .takes_value(false) .help("Numeric mode, no hostname resolution") ) @@ -153,6 +160,7 @@ pub enum ProfileType { pub struct ScanOptions { pub profile: ProfileType, pub interface_name: Option, + pub network_range: Option, pub timeout_ms: u64, pub resolve_hostname: bool, pub source_ipv4: Option, @@ -199,6 +207,16 @@ impl ScanOptions { let interface_name = matches.value_of("interface").map(String::from); + let network_range: Option = matches.value_of("network").map(|raw_range: &str| { + match IpNetwork::from_str(raw_range) { + Ok(parsed_network) => parsed_network, + Err(err) => { + eprintln!("Expected valid IPv4 network range ({})", err); + process::exit(1); + } + } + }); + let timeout_ms: u64 = match matches.value_of("timeout") { Some(timeout_text) => parse_to_milliseconds(timeout_text).unwrap_or_else(|err| { eprintln!("Expected correct timeout, {}", err); @@ -394,6 +412,7 @@ impl ScanOptions { Arc::new(ScanOptions { profile, interface_name, + network_range, timeout_ms, resolve_hostname, source_ipv4, diff --git a/src/main.rs b/src/main.rs index 985ea20..d324740 100644 --- a/src/main.rs +++ b/src/main.rs @@ -47,42 +47,7 @@ fn main() { process::exit(1); } - let interface_name = match &scan_options.interface_name { - Some(name) => String::from(name), - None => { - - let name = utils::select_default_interface(&interfaces).map(|interface| interface.name); - - match name { - Some(name) => name, - None => { - eprintln!("Could not find a default network interface"); - eprintln!("Use 'arp scan -l' to list available interfaces"); - process::exit(1); - } - } - } - }; - - let selected_interface: &datalink::NetworkInterface = interfaces.iter() - .find(|interface| { interface.name == interface_name && interface.is_up() && !interface.is_loopback() }) - .unwrap_or_else(|| { - eprintln!("Could not find interface with name {}", interface_name); - eprintln!("Make sure the interface is up, not loopback and has a valid IPv4"); - process::exit(1); - }); - - let ip_network = match selected_interface.ips.first() { - Some(ip_network) if ip_network.is_ipv4() => ip_network, - Some(_) => { - eprintln!("Only IPv4 networks supported"); - process::exit(1); - }, - None => { - eprintln!("Expects a valid IP on the interface, none found"); - process::exit(1); - } - }; + let (selected_interface, ip_network) = network::compute_network_configuration(&interfaces, &scan_options); if scan_options.is_plain_output() { diff --git a/src/network.rs b/src/network.rs index f508f98..948bf4e 100644 --- a/src/network.rs +++ b/src/network.rs @@ -17,6 +17,7 @@ use pnet::packet::vlan::{ClassOfService, MutableVlanPacket}; use crate::args::ScanOptions; use crate::vendor::Vendor; +use crate::utils; pub const DATALINK_RCV_TIMEOUT: u64 = 500; @@ -59,6 +60,61 @@ pub struct TargetDetails { pub vendor: Option } +/** + * Compute a network configuration based on the scan options and available + * interfaces. This configuration will be used in the scan process to target a + * specific network on a network interfaces. + */ +pub fn compute_network_configuration<'a>(interfaces: &'a [NetworkInterface], scan_options: &'a Arc) -> (&'a NetworkInterface, &'a IpNetwork) { + + let interface_name = match &scan_options.interface_name { + Some(name) => String::from(name), + None => { + + let name = utils::select_default_interface(&interfaces).map(|interface| interface.name); + + match name { + Some(name) => name, + None => { + eprintln!("Could not find a default network interface"); + eprintln!("Use 'arp scan -l' to list available interfaces"); + process::exit(1); + } + } + } + }; + + let selected_interface: &NetworkInterface = interfaces.iter() + .find(|interface| { interface.name == interface_name && interface.is_up() && !interface.is_loopback() }) + .unwrap_or_else(|| { + eprintln!("Could not find interface with name {}", interface_name); + eprintln!("Make sure the interface is up, not loopback and has a valid IPv4"); + process::exit(1); + }); + + let ip_network = match &scan_options.network_range { + Some(network_range) => network_range, + None => { + + // If no network range given on the CLI, take the first IPv4 network available + // on the default network interface. + match selected_interface.ips.first() { + Some(ip_network) if ip_network.is_ipv4() => ip_network, + Some(_) => { + eprintln!("Only IPv4 networks supported"); + process::exit(1); + }, + None => { + eprintln!("Expects a valid IP on the interface, none found"); + process::exit(1); + } + } + } + }; + + (selected_interface, ip_network) +} + /** * Based on the network size and given scan options, this function performs an * estimation of the scan impact (timing, bandwidth, ...). Keep in mind that From 580dbb069fd5e0383f93b3f2c2435d08f5a7a073 Mon Sep 17 00:00:00 2001 From: Saluki Date: Sun, 7 Nov 2021 12:29:47 +0100 Subject: [PATCH 078/121] Refactor to support multiple IPv4 addresses --- src/args.rs | 22 ++++++++++------ src/main.rs | 22 ++++++---------- src/network.rs | 68 +++++++++++++++++++++++++++++--------------------- src/utils.rs | 20 +++++++++------ 4 files changed, 74 insertions(+), 58 deletions(-) diff --git a/src/args.rs b/src/args.rs index b04dd46..41e8255 100644 --- a/src/args.rs +++ b/src/args.rs @@ -160,7 +160,7 @@ pub enum ProfileType { pub struct ScanOptions { pub profile: ProfileType, pub interface_name: Option, - pub network_range: Option, + pub network_range: Option>, pub timeout_ms: u64, pub resolve_hostname: bool, pub source_ipv4: Option, @@ -207,14 +207,20 @@ impl ScanOptions { let interface_name = matches.value_of("interface").map(String::from); - let network_range: Option = matches.value_of("network").map(|raw_range: &str| { - match IpNetwork::from_str(raw_range) { - Ok(parsed_network) => parsed_network, - Err(err) => { - eprintln!("Expected valid IPv4 network range ({})", err); - process::exit(1); + let network_range: Option> = matches.value_of("network").map(|raw_ranges: &str| { + + let splitted_ranged = raw_ranges.split(','); + splitted_ranged.map(|raw_range| { + + match IpNetwork::from_str(raw_range) { + Ok(parsed_network) => parsed_network, + Err(err) => { + eprintln!("Expected valid IPv4 network range ({})", err); + process::exit(1); + } } - } + + }).collect() }); let timeout_ms: u64 = match matches.value_of("timeout") { diff --git a/src/main.rs b/src/main.rs index d324740..706933d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,6 @@ use std::time::Duration; use std::sync::atomic::{AtomicBool, Ordering}; use pnet::datalink; -use rand::prelude::*; use crate::args::{ScanOptions, OutputFormat}; use crate::vendor::Vendor; @@ -47,12 +46,14 @@ fn main() { process::exit(1); } - let (selected_interface, ip_network) = network::compute_network_configuration(&interfaces, &scan_options); + let (selected_interface, ip_networks) = network::compute_network_configuration(&interfaces, &scan_options); if scan_options.is_plain_output() { + let network_list = ip_networks.iter().map(|network| format!("{}", network)).collect::>().join(", "); + println!(); - println!("Selected interface {} with IP {}", selected_interface.name, ip_network); + println!("Selected interface {} with IP {}", selected_interface.name, network_list); if let Some(forced_source_ipv4) = scan_options.source_ipv4 { println!("The ARP source IPv4 will be forced to {}", forced_source_ipv4); } @@ -95,7 +96,7 @@ fn main() { let cloned_options = Arc::clone(&scan_options); let arp_responses = thread::spawn(move || network::receive_arp_responses(&mut rx, cloned_options, cloned_timed_out, &mut vendor_list)); - let network_size = utils::compute_network_size(&ip_network); + let network_size = utils::compute_network_size(&ip_networks); if scan_options.is_plain_output() { @@ -128,15 +129,8 @@ fn main() { // network iterator exposed by 'ipnetwork': memory usage. Instead of // using a small memory footprint iterator, we have to store all IP // addresses in memory at once. This can cause problems on large ranges. - let ip_addresses: Vec = match scan_options.randomize_targets { - true => { - let mut rng = rand::thread_rng(); - let mut shuffled_addresses: Vec = ip_network.iter().collect(); - shuffled_addresses.shuffle(&mut rng); - shuffled_addresses - }, - false => ip_network.iter().collect() - }; + let ip_addresses: Vec = network::compute_ip_range(&ip_networks, &scan_options); + let source_ip = network::find_source_ip(selected_interface, scan_options.source_ipv4); for ip_address in ip_addresses { @@ -145,7 +139,7 @@ fn main() { } if let IpAddr::V4(ipv4_address) = ip_address { - network::send_arp_request(&mut tx, selected_interface, &ip_network, ipv4_address, Arc::clone(&scan_options)); + network::send_arp_request(&mut tx, selected_interface, source_ip, ipv4_address, Arc::clone(&scan_options)); thread::sleep(Duration::from_millis(scan_options.interval_ms)); } } diff --git a/src/network.rs b/src/network.rs index 948bf4e..eb6f630 100644 --- a/src/network.rs +++ b/src/network.rs @@ -14,6 +14,7 @@ use pnet::packet::{MutablePacket, Packet}; use pnet::packet::ethernet::{EthernetPacket, MutableEthernetPacket, EtherTypes}; use pnet::packet::arp::{MutableArpPacket, ArpOperations, ArpHardwareTypes, ArpPacket}; use pnet::packet::vlan::{ClassOfService, MutableVlanPacket}; +use rand::prelude::*; use crate::args::ScanOptions; use crate::vendor::Vendor; @@ -65,7 +66,7 @@ pub struct TargetDetails { * interfaces. This configuration will be used in the scan process to target a * specific network on a network interfaces. */ -pub fn compute_network_configuration<'a>(interfaces: &'a [NetworkInterface], scan_options: &'a Arc) -> (&'a NetworkInterface, &'a IpNetwork) { +pub fn compute_network_configuration<'a>(interfaces: &'a [NetworkInterface], scan_options: &'a Arc) -> (&'a NetworkInterface, Vec<&'a IpNetwork>) { let interface_name = match &scan_options.interface_name { Some(name) => String::from(name), @@ -92,27 +93,14 @@ pub fn compute_network_configuration<'a>(interfaces: &'a [NetworkInterface], sca process::exit(1); }); - let ip_network = match &scan_options.network_range { - Some(network_range) => network_range, - None => { - - // If no network range given on the CLI, take the first IPv4 network available - // on the default network interface. - match selected_interface.ips.first() { - Some(ip_network) if ip_network.is_ipv4() => ip_network, - Some(_) => { - eprintln!("Only IPv4 networks supported"); - process::exit(1); - }, - None => { - eprintln!("Expects a valid IP on the interface, none found"); - process::exit(1); - } - } - } + let ip_networks: Vec<&IpNetwork> = match &scan_options.network_range { + Some(network_range) => network_range.iter().collect(), + None => selected_interface.ips.iter() + .filter(|ip_network| ip_network.is_ipv4()) + .collect() }; - (selected_interface, ip_network) + (selected_interface, ip_networks) } /** @@ -153,7 +141,7 @@ pub fn compute_scan_estimation(host_count: u128, options: &Arc) -> * 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, ip_network: &IpNetwork, target_ip: Ipv4Addr, options: Arc) { +pub fn send_arp_request(tx: &mut Box, interface: &NetworkInterface, source_ip: Ipv4Addr, target_ip: Ipv4Addr, options: Arc) { let mut ethernet_buffer = match options.has_vlan() { true => vec![0u8; ETHERNET_VLAN_PACKET_SIZE], @@ -191,7 +179,7 @@ pub fn send_arp_request(tx: &mut Box, interface: &NetworkInt process::exit(1); }); - let source_ipv4 = find_source_ip(ip_network, options.source_ipv4); + // let source_ipv4 = find_source_ip(ip_network, options.source_ipv4); arp_packet.set_hardware_type(options.hw_type.unwrap_or(ArpHardwareTypes::Ethernet)); arp_packet.set_protocol_type(options.proto_type.unwrap_or(EtherTypes::Ipv4)); @@ -199,7 +187,7 @@ pub fn send_arp_request(tx: &mut Box, interface: &NetworkInt arp_packet.set_proto_addr_len(options.proto_addr.unwrap_or(4)); arp_packet.set_operation(options.arp_operation.unwrap_or(ArpOperations::Request)); arp_packet.set_sender_hw_addr(source_mac); - arp_packet.set_sender_proto_addr(source_ipv4); + arp_packet.set_sender_proto_addr(source_ip); arp_packet.set_target_hw_addr(target_mac); arp_packet.set_target_proto_addr(target_ip); @@ -226,21 +214,45 @@ pub fn send_arp_request(tx: &mut Box, interface: &NetworkInt tx.send_to(ðernet_packet.to_immutable().packet(), Some(interface.clone())); } +/** + * Compute the whole IP range that must be scanned. This includes every IP + * address contained in the ip_networks argument. Based on the scan options, + * this list may be randomized to enhance scans. + */ +pub fn compute_ip_range(ip_networks: &[&IpNetwork], scan_options: &Arc) -> Vec { + + ip_networks.iter().fold(vec![], |accumulator, ip_network| { + + let ip_addresses = match scan_options.randomize_targets { + true => { + let mut rng = rand::thread_rng(); + let mut shuffled_addresses: Vec = ip_network.iter().collect(); + shuffled_addresses.shuffle(&mut rng); + shuffled_addresses + }, + false => ip_network.iter().collect() + }; + + [accumulator, ip_addresses].concat() + }) +} + /** * Find the most adequate IPv4 address on a given network interface for sending * ARP requests. If the 'forced_source_ipv4' parameter is set, it will take * the priority over the network interface address. */ -fn find_source_ip(ip_network: &IpNetwork, forced_source_ipv4: Option) -> Ipv4Addr { +pub fn find_source_ip(network_interface: &NetworkInterface, forced_source_ipv4: Option) -> Ipv4Addr { if let Some(forced_ipv4) = forced_source_ipv4 { return forced_ipv4; } - match ip_network.ip() { - IpAddr::V4(ipv4_addr) => ipv4_addr, - IpAddr::V6(_ipv6_addr) => { - eprintln!("Expected IPv4 address on network interface, found IPv6"); + let potential_network = network_interface.ips.iter().find(|network| network.is_ipv4()); + match potential_network.map(|network| network.ip()) { + Some(IpAddr::V4(ipv4_addr)) => ipv4_addr, + _ => { + eprintln!("Expected IPv4 address on network interface"); process::exit(1); } } diff --git a/src/utils.rs b/src/utils.rs index 854d096..ca9d50b 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -85,15 +85,19 @@ pub fn select_default_interface(interfaces: &[NetworkInterface]) -> Option u128 { +pub fn compute_network_size(ip_networks: &[&IpNetwork]) -> u128 { - match ip_network.size() { - NetworkSize::V4(ipv4_network_size) => ipv4_network_size.into(), - NetworkSize::V6(_) => { - eprintln!("IPv6 networks are not supported by the ARP protocol"); - process::exit(1); - } - } + ip_networks.iter().fold(0u128, |total_size, ip_network| { + + let network_size: u128 = match ip_network.size() { + NetworkSize::V4(ipv4_network_size) => ipv4_network_size.into(), + NetworkSize::V6(_) => { + eprintln!("IPv6 networks are not supported by the ARP protocol"); + process::exit(1); + } + }; + total_size + network_size + }) } /** From f0a6f33172ab79347f21042b86b45680d61970f1 Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 8 Nov 2021 07:47:47 +0100 Subject: [PATCH 079/121] Smarter formatting when no results --- src/utils.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index ca9d50b..9bc1b1a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -125,9 +125,11 @@ pub fn display_scan_results(response_summary: ResponseSummary, mut target_detail } } - println!(); - println!("| IPv4 | MAC | {: 0 { + println!(); + println!("| IPv4 | MAC | {: print!("no hosts found"), + 0 => print!("{}", Red.paint("no hosts found")), 1 => print!("1 host found"), _ => print!("{} hosts found", target_count) } From 06364e9e168eb7e39740e48ea83f976922db2fb3 Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 8 Nov 2021 10:09:13 +0100 Subject: [PATCH 080/121] Finish support for file reading --- data/ip-list.txt | 10 ++++++++ src/args.rs | 65 +++++++++++++++++++++++++++++++++++++----------- src/main.rs | 12 +-------- src/utils.rs | 25 ++++++++++++++++++- 4 files changed, 85 insertions(+), 27 deletions(-) create mode 100644 data/ip-list.txt diff --git a/data/ip-list.txt b/data/ip-list.txt new file mode 100644 index 0000000..e292aac --- /dev/null +++ b/data/ip-list.txt @@ -0,0 +1,10 @@ +192.168.1.1 +192.168.1.2 +192.168.2.0/29 +192.168.1.3 +192.168.1.4 +192.168.1.5 +192.168.1.6 +192.168.1.7 +192.168.1.8 +192.168.1.9 \ No newline at end of file diff --git a/src/args.rs b/src/args.rs index 41e8255..2ed3362 100644 --- a/src/args.rs +++ b/src/args.rs @@ -2,6 +2,8 @@ use std::str::FromStr; use std::net::Ipv4Addr; use std::process; use std::sync::Arc; +use std::path::Path; +use std::fs; use clap::{Arg, ArgMatches, App}; use ipnetwork::IpNetwork; @@ -56,6 +58,12 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> { .takes_value(true).value_name("NETWORK_RANGE") .help("Network range to scan") ) + .arg( + Arg::with_name("file").short("f").long("file") + .takes_value(true).value_name("FILE_PATH") + .conflicts_with("network") + .help("Read IPv4 addresses from a file") + ) .arg( Arg::with_name("timeout").short("t").long("timeout") .takes_value(true).value_name("TIMEOUT_DURATION") @@ -180,6 +188,47 @@ pub struct ScanOptions { } impl ScanOptions { + + fn compute_networks(matches: &ArgMatches) -> Option> { + + let network_options = (matches.value_of("file"), matches.value_of("network")); + let ranges: Option> = match network_options { + (Some(file_path), None) => { + + let path = Path::new(file_path); + match fs::read_to_string(path) { + Ok(content) => { + Some(content.lines().map(|line| line.to_string()).collect()) + } + Err(err) => { + eprintln!("Could not open file {}", file_path); + eprintln!("{}", err); + process::exit(1); + } + } + + }, + (None, Some(raw_ranges)) => { + Some(raw_ranges.split(',').map(|line| line.to_string()).collect()) + }, + _ => None + }; + + ranges.map(|range_vec| { + + range_vec.iter().map(|raw_range| { + + match IpNetwork::from_str(raw_range) { + Ok(parsed_network) => parsed_network, + Err(err) => { + eprintln!("Expected valid IPv4 network range ({})", err); + process::exit(1); + } + } + + }).collect() + }) + } /** * Build a new 'ScanOptions' struct that will be used in the whole CLI such @@ -207,21 +256,7 @@ impl ScanOptions { let interface_name = matches.value_of("interface").map(String::from); - let network_range: Option> = matches.value_of("network").map(|raw_ranges: &str| { - - let splitted_ranged = raw_ranges.split(','); - splitted_ranged.map(|raw_range| { - - match IpNetwork::from_str(raw_range) { - Ok(parsed_network) => parsed_network, - Err(err) => { - eprintln!("Expected valid IPv4 network range ({})", err); - process::exit(1); - } - } - - }).collect() - }); + let network_range = ScanOptions::compute_networks(matches); let timeout_ms: u64 = match matches.value_of("timeout") { Some(timeout_text) => parse_to_milliseconds(timeout_text).unwrap_or_else(|err| { diff --git a/src/main.rs b/src/main.rs index 706933d..ab8632b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,17 +49,7 @@ fn main() { let (selected_interface, ip_networks) = network::compute_network_configuration(&interfaces, &scan_options); if scan_options.is_plain_output() { - - let network_list = ip_networks.iter().map(|network| format!("{}", network)).collect::>().join(", "); - - println!(); - println!("Selected interface {} with IP {}", selected_interface.name, network_list); - if let Some(forced_source_ipv4) = scan_options.source_ipv4 { - println!("The ARP source IPv4 will be forced to {}", forced_source_ipv4); - } - if let Some(forced_destination_mac) = scan_options.destination_mac { - println!("The ARP destination MAC will be forced to {}", forced_destination_mac); - } + utils::display_prescan_details(&ip_networks, selected_interface, scan_options.clone()); } // Start ARP scan operation diff --git a/src/utils.rs b/src/utils.rs index 9bc1b1a..a4b0a2f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,5 @@ use std::process; +use std::sync::Arc; use pnet::datalink::NetworkInterface; use ipnetwork::{IpNetwork, NetworkSize}; @@ -85,6 +86,28 @@ pub fn select_default_interface(interfaces: &[NetworkInterface]) -> Option, selected_interface: &NetworkInterface, scan_options: Arc) -> () { + + let mut network_list = ip_networks.iter().take(5).map(|network| network.to_string()).collect::>().join(", "); + if ip_networks.len() > 5 { + let more_text = format!(" ({} more)", ip_networks.len()-5); + network_list.push_str(&more_text); + } + + println!(); + println!("Selected interface {} with IP {}", selected_interface.name, network_list); + if let Some(forced_source_ipv4) = scan_options.source_ipv4 { + println!("The ARP source IPv4 will be forced to {}", forced_source_ipv4); + } + if let Some(forced_destination_mac) = scan_options.destination_mac { + println!("The ARP destination MAC will be forced to {}", forced_destination_mac); + } +} + pub fn compute_network_size(ip_networks: &[&IpNetwork]) -> u128 { ip_networks.iter().fold(0u128, |total_size, ip_network| { @@ -125,7 +148,7 @@ pub fn display_scan_results(response_summary: ResponseSummary, mut target_detail } } - if target_details.len() > 0 { + if !target_details.is_empty() { println!(); println!("| IPv4 | MAC | {: Date: Mon, 8 Nov 2021 10:43:59 +0100 Subject: [PATCH 081/121] CSV exports with existing crate --- README.md | 1 + src/args.rs | 4 +++- src/main.rs | 3 ++- src/utils.rs | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9954d9a..74b56a1 100644 --- a/README.md +++ b/README.md @@ -205,6 +205,7 @@ The features below will be shipped in the next releases of the project. - Adding advanced packet options (padding, LLC, ...) - Enable bandwith control (exclusive with interval) - Stronger profile defaults (chaos & stealth) +- Avoid packet copy in userspace for daster scans (BPF filtering) ## Contributing diff --git a/src/args.rs b/src/args.rs index 2ed3362..e0ef6a9 100644 --- a/src/args.rs +++ b/src/args.rs @@ -155,7 +155,8 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> { pub enum OutputFormat { Plain, Json, - Yaml + Yaml, + Csv } pub enum ProfileType { @@ -364,6 +365,7 @@ impl ScanOptions { "json" => OutputFormat::Json, "yaml" => OutputFormat::Yaml, "plain" | "text" => OutputFormat::Plain, + "csv" => OutputFormat::Csv, _ => { eprintln!("Expected correct output format (json/yaml/plain)"); process::exit(1); diff --git a/src/main.rs b/src/main.rs index ab8632b..11e8ae4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -154,6 +154,7 @@ fn main() { match &scan_options.output { OutputFormat::Plain => utils::display_scan_results(response_summary, target_details, &scan_options), OutputFormat::Json => println!("{}", utils::export_to_json(response_summary, target_details)), - OutputFormat::Yaml => println!("{}", utils::export_to_yaml(response_summary, target_details)) + OutputFormat::Yaml => println!("{}", utils::export_to_yaml(response_summary, target_details)), + OutputFormat::Csv => print!("{}", utils::export_to_csv(response_summary, target_details)) } } diff --git a/src/utils.rs b/src/utils.rs index a4b0a2f..0695909 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -271,3 +271,36 @@ pub fn export_to_yaml(response_summary: ResponseSummary, mut target_details: Vec process::exit(1); }) } + +/** + * Export the scan results as a CSV string with response details (timings, ...) + * and ARP results from the local network. + */ +pub fn export_to_csv(response_summary: ResponseSummary, mut target_details: Vec) -> String { + + target_details.sort_by_key(|item| item.ipv4); + + let global_result = get_serializable_result(response_summary, target_details); + + let mut wtr = csv::Writer::from_writer(vec![]); + + for result in global_result.results { + wtr.serialize(result).unwrap_or_else(|err| { + eprintln!("Could not serialize result to CSV ({})", err); + process::exit(1); + }); + } + wtr.flush().unwrap_or_else(|err| { + eprintln!("Could not flush CSV writer buffer ({})", err); + process::exit(1); + }); + + let convert_writer = wtr.into_inner().unwrap_or_else(|err| { + eprintln!("Could not convert final CSV result ({})", err); + process::exit(1); + }); + String::from_utf8(convert_writer).unwrap_or_else(|err| { + eprintln!("Could not convert final CSV result to text ({})", err); + process::exit(1); + }) +} From 9a39c07dfd641498e939c757ad6dd2f1a96338d7 Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 8 Nov 2021 12:50:17 +0100 Subject: [PATCH 082/121] New bandwidth limit feature --- src/args.rs | 49 ++++++++++++++++++++++++++++++++++++------------- src/main.rs | 8 +++++--- src/network.rs | 32 ++++++++++++++++++++++++++------ src/utils.rs | 2 +- 4 files changed, 68 insertions(+), 23 deletions(-) diff --git a/src/args.rs b/src/args.rs index e0ef6a9..0e24b27 100644 --- a/src/args.rs +++ b/src/args.rs @@ -109,6 +109,12 @@ pub fn build_args<'a, 'b>() -> App<'a, 'b> { .takes_value(true).value_name("INTERVAL_DURATION") .help("Milliseconds between ARP requests") ) + .arg( + Arg::with_name("bandwidth").short("B").long("bandwidth") + .takes_value(true).value_name("BITS") + .conflicts_with("interval") + .help("Limit scan bandwidth (bits/second)") + ) .arg( Arg::with_name("oui-file").long("oui-file") .takes_value(true).value_name("FILE_PATH") @@ -166,6 +172,11 @@ pub enum ProfileType { Chaos } +pub enum ScanTiming { + Interval(u64), + Bandwidth(u64) +} + pub struct ScanOptions { pub profile: ProfileType, pub interface_name: Option, @@ -177,7 +188,7 @@ pub struct ScanOptions { pub destination_mac: Option, pub vlan_id: Option, pub retry_count: usize, - pub interval_ms: u64, + pub scan_timing: ScanTiming, pub randomize_targets: bool, pub output: OutputFormat, pub oui_file: String, @@ -230,6 +241,28 @@ impl ScanOptions { }).collect() }) } + + fn compute_interval(matches: &ArgMatches, profile: &ProfileType) -> ScanTiming { + + match (matches.value_of("bandwidth"), matches.value_of("interval")) { + (Some(bandwidth_text), None) => { + let bits_second: u64 = bandwidth_text.parse().unwrap_or_else(|err| { + eprintln!("Expected positive number, {}", err); + process::exit(1); + }); + ScanTiming::Bandwidth(bits_second) + }, + (None, Some(interval_text)) => parse_to_milliseconds(interval_text).map(ScanTiming::Interval).unwrap_or_else(|err| { + eprintln!("Expected correct interval, {}", err); + process::exit(1); + }), + _ => match profile { + ProfileType::Stealth => ScanTiming::Interval(REQUEST_MS_INTERVAL * 2), + ProfileType::Fast => ScanTiming::Interval(0), + _ => ScanTiming::Interval(REQUEST_MS_INTERVAL) + } + } + } /** * Build a new 'ScanOptions' struct that will be used in the whole CLI such @@ -346,17 +379,7 @@ impl ScanOptions { } }; - let interval_ms: u64 = match matches.value_of("interval") { - Some(interval_text) => parse_to_milliseconds(interval_text).unwrap_or_else(|err| { - eprintln!("Expected correct interval, {}", err); - process::exit(1); - }), - None => match profile { - ProfileType::Stealth => REQUEST_MS_INTERVAL * 2, - ProfileType::Fast => 0, - _ => REQUEST_MS_INTERVAL - } - }; + let scan_timing: ScanTiming = ScanOptions::compute_interval(matches, &profile); let output = match matches.value_of("output") { Some(output_request) => { @@ -463,7 +486,7 @@ impl ScanOptions { source_mac, vlan_id, retry_count, - interval_ms, + scan_timing, randomize_targets, output, oui_file, diff --git a/src/main.rs b/src/main.rs index 11e8ae4..4911ee9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -88,12 +88,14 @@ fn main() { let network_size = utils::compute_network_size(&ip_networks); + let estimations = network::compute_scan_estimation(network_size, &scan_options); + let interval_ms = estimations.interval_ms; + if scan_options.is_plain_output() { - let estimations = network::compute_scan_estimation(network_size, &scan_options); let formatted_ms = time::format_milliseconds(estimations.duration_ms); println!("Estimated scan time {} ({} bytes, {} bytes/s)", formatted_ms, estimations.request_size, estimations.bandwidth); - println!("Sending {} ARP requests (waiting at least {}ms, {}ms request interval)", network_size, scan_options.timeout_ms, scan_options.interval_ms); + println!("Sending {} ARP requests (waiting at least {}ms, {}ms request interval)", network_size, scan_options.timeout_ms, interval_ms); } let finish_sleep = Arc::new(AtomicBool::new(false)); @@ -130,7 +132,7 @@ fn main() { if let IpAddr::V4(ipv4_address) = ip_address { network::send_arp_request(&mut tx, selected_interface, source_ip, ipv4_address, Arc::clone(&scan_options)); - thread::sleep(Duration::from_millis(scan_options.interval_ms)); + thread::sleep(Duration::from_millis(interval_ms)); } } } diff --git a/src/network.rs b/src/network.rs index eb6f630..1656e01 100644 --- a/src/network.rs +++ b/src/network.rs @@ -19,6 +19,7 @@ use rand::prelude::*; use crate::args::ScanOptions; use crate::vendor::Vendor; use crate::utils; +use crate::args::ScanTiming; pub const DATALINK_RCV_TIMEOUT: u64 = 500; @@ -34,6 +35,7 @@ const ETHERNET_VLAN_PACKET_SIZE: usize = 46; * starts and should give insights about the scan. */ pub struct ScanEstimation { + pub interval_ms: u64, pub duration_ms: u128, pub request_size: u128, pub bandwidth: u128 @@ -110,7 +112,6 @@ pub fn compute_network_configuration<'a>(interfaces: &'a [NetworkInterface], sca */ pub fn compute_scan_estimation(host_count: u128, options: &Arc) -> ScanEstimation { - let interval: u128 = options.interval_ms.into(); let timeout: u128 = options.timeout_ms.into(); let packet_size: u128 = match options.has_vlan() { true => ETHERNET_VLAN_PACKET_SIZE.try_into().expect("Internal number conversion failed for VLAN packet size"), @@ -120,16 +121,35 @@ pub fn compute_scan_estimation(host_count: u128, options: &Arc) -> // The values below are averages based on an amount of performed network // scans. This may of course vary based on network configurations. - let avg_arp_request_ms = 3; + let avg_arp_request_ms: u128 = 3; let avg_resolve_ms = 500; - let request_phase_ms = (host_count * (avg_arp_request_ms+ interval)) * retry_count; - let duration_ms = request_phase_ms + timeout + avg_resolve_ms; - let request_size = host_count * packet_size; + let request_size: u128 = host_count * packet_size; + + let (interval_ms, bandwidth, request_phase_ms): (u64, u128, u128) = match options.scan_timing { + ScanTiming::Bandwidth(bandwidth) => { + + let bandwidth_lg: u128 = bandwidth.into(); + let request_phase_ms: u128 = (request_size * 1000) as u128 / bandwidth_lg; + let interval_ms: u128 = (request_phase_ms/retry_count/host_count) - avg_arp_request_ms; + + (interval_ms.try_into().unwrap(), bandwidth_lg, request_phase_ms) + + }, + ScanTiming::Interval(interval) => { - let bandwidth = (request_size * 1000) / request_phase_ms; + let interval_ms_lg: u128 = interval.into(); + let request_phase_ms: u128 = (host_count * (avg_arp_request_ms + interval_ms_lg)) * retry_count; + let bandwidth = (request_size * 1000) / request_phase_ms; + + (interval, bandwidth, request_phase_ms) + } + }; + + let duration_ms = request_phase_ms + timeout + avg_resolve_ms; ScanEstimation { + interval_ms, duration_ms, request_size, bandwidth diff --git a/src/utils.rs b/src/utils.rs index 0695909..1d43381 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -90,7 +90,7 @@ pub fn select_default_interface(interfaces: &[NetworkInterface]) -> Option, selected_interface: &NetworkInterface, scan_options: Arc) -> () { +pub fn display_prescan_details(ip_networks: &[&IpNetwork], selected_interface: &NetworkInterface, scan_options: Arc) { let mut network_list = ip_networks.iter().take(5).map(|network| network.to_string()).collect::>().join(", "); if ip_networks.len() > 5 { From ceae9e600a74b75d558cc65886e664309d194297 Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 8 Nov 2021 12:53:11 +0100 Subject: [PATCH 083/121] Releasing version 0.12.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 16 ++++++++++------ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 941577e..9509120 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,7 +29,7 @@ dependencies = [ [[package]] name = "arp-scan" -version = "0.11.0" +version = "0.12.0" dependencies = [ "ansi_term 0.12.1", "clap", diff --git a/Cargo.toml b/Cargo.toml index bf06c27..7ba355a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "arp-scan" description = "A minimalistic ARP scan tool" license = "AGPL-3.0-or-later" -version = "0.11.0" +version = "0.12.0" authors = ["Saluki"] edition = "2018" readme = "README.md" diff --git a/README.md b/README.md index 74b56a1..7b6762a 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Find all hosts in your local network using this fast ARP scanner. The CLI is wri ✔ MAC vendor search -✔ JSON & YAML exports +✔ JSON, YAML & CSV exports ✔ Pre-defined scan profiles (default, fast, stealth & chaos) @@ -63,7 +63,7 @@ ARP scan finished, 5 hosts found in 1.623 seconds Download the `arp-scan` binary for Linux (Ubuntu, Fedora, Debian, ...). See the [releases page](https://github.com/Saluki/arp-scan-rs/releases) for other binaries. ```bash -wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.11.0/arp-scan-v0.11.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan +wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.12.0/arp-scan-v0.12.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan ``` List all available network interfaces. @@ -121,10 +121,14 @@ By default, the scan process will select the first IPv4 network on the interface Enforce a timeout of at least 15 seconds. This timeout is a minimum value (scans may take a little more time). Default value is `2000ms`. -#### Change ARP request interval `-I 30ms` +#### Change ARP request interval `-I 39ms` By default, a `10ms` gap will be set between ARP requests to avoid an ARP storm on the network. This value can be changed to reduce or increase the milliseconds between each ARP request. +#### Enforce scan bandwidth limit `-B 1000` + +Enforce a bandwidth limit (expressed in bits per second) on ARP scans. The `--bandwidth` option conflicts with `--interval` since these 2 arguments change the same parameter underneath. + #### Numeric mode `--numeric` Switch to numeric mode. This will skip the local hostname resolution process and will only display IP addresses. @@ -179,7 +183,7 @@ Change the ARP protocol address length field, this can cause scan failure. #### Set output format `-o json` -Set the output format to either `plain` (a full-text output with tables), `json` or `yaml`. +Set the output format to either `plain` (a full-text output with tables), `json`, `yaml` or `csv`. #### Show version `--version` @@ -201,9 +205,9 @@ The features below will be shipped in the next releases of the project. - ~~Fine-grained scan timings (interval)~~ - released in 0.8.0 - Wide network range support - ~~Partial results on SIGINT~~ - released in 0.11.0 -- Read network targets from file +- ~~Read network targets from file~~ - released in 0.12.0 - Adding advanced packet options (padding, LLC, ...) -- Enable bandwith control (exclusive with interval) +- ~~Enable bandwith control (exclusive with interval)~~ - released in 0.12.0 - Stronger profile defaults (chaos & stealth) - Avoid packet copy in userspace for daster scans (BPF filtering) From decc5ee0c2e8b7c22d62630f1c0ff4a583e8393e Mon Sep 17 00:00:00 2001 From: Saluki Date: Wed, 10 Nov 2021 08:33:26 +0100 Subject: [PATCH 084/121] Adding comments to network --- src/network.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/network.rs b/src/network.rs index 1656e01..4ae8fb3 100644 --- a/src/network.rs +++ b/src/network.rs @@ -126,6 +126,11 @@ pub fn compute_scan_estimation(host_count: u128, options: &Arc) -> let request_size: u128 = host_count * packet_size; + // Either the user provides an interval (expressed in milliseconds), either + // he provides a bandwidth (in bits per second) or either we are using the + // default interval. The goal of the code below is to compute the interval + // & bandwidth, based on the given inputs. Note that the computations in + // each match arm are therefore linked (but rewritten, based on the inputs). let (interval_ms, bandwidth, request_phase_ms): (u64, u128, u128) = match options.scan_timing { ScanTiming::Bandwidth(bandwidth) => { @@ -199,8 +204,6 @@ pub fn send_arp_request(tx: &mut Box, interface: &NetworkInt process::exit(1); }); - // let source_ipv4 = find_source_ip(ip_network, options.source_ipv4); - arp_packet.set_hardware_type(options.hw_type.unwrap_or(ArpHardwareTypes::Ethernet)); arp_packet.set_protocol_type(options.proto_type.unwrap_or(EtherTypes::Ipv4)); arp_packet.set_hw_addr_len(options.hw_addr.unwrap_or(6)); From 1a2d06939b6bf16a31d93550e7d074070d4815b1 Mon Sep 17 00:00:00 2001 From: Saluki Date: Wed, 10 Nov 2021 08:33:46 +0100 Subject: [PATCH 085/121] Refactor & test network CLI arg --- data/ip-list.txt | 7 -- src/args.rs | 196 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 165 insertions(+), 38 deletions(-) diff --git a/data/ip-list.txt b/data/ip-list.txt index e292aac..4dc0850 100644 --- a/data/ip-list.txt +++ b/data/ip-list.txt @@ -1,10 +1,3 @@ 192.168.1.1 192.168.1.2 192.168.2.0/29 -192.168.1.3 -192.168.1.4 -192.168.1.5 -192.168.1.6 -192.168.1.7 -192.168.1.8 -192.168.1.9 \ No newline at end of file diff --git a/src/args.rs b/src/args.rs index 0e24b27..879eb31 100644 --- a/src/args.rs +++ b/src/args.rs @@ -201,48 +201,61 @@ pub struct ScanOptions { impl ScanOptions { - fn compute_networks(matches: &ArgMatches) -> Option> { - - let network_options = (matches.value_of("file"), matches.value_of("network")); - let ranges: Option> = match network_options { + fn list_required_networks(file_value: Option<&str>, network_value: Option<&str>) -> Result>, String> { + + let network_options = (file_value, network_value); + match network_options { (Some(file_path), None) => { let path = Path::new(file_path); - match fs::read_to_string(path) { - Ok(content) => { - Some(content.lines().map(|line| line.to_string()).collect()) - } - Err(err) => { - eprintln!("Could not open file {}", file_path); - eprintln!("{}", err); - process::exit(1); - } - } + fs::read_to_string(path).map(|content| { + Some(content.lines().map(|line| line.to_string()).collect()) + }).map_err(|err| { + format!("Could not open file {} - {}", file_path, err) + }) }, (None, Some(raw_ranges)) => { - Some(raw_ranges.split(',').map(|line| line.to_string()).collect()) + Ok(Some(raw_ranges.split(',').map(|line| line.to_string()).collect())) }, - _ => None - }; + _ => Ok(None) + } + } - ranges.map(|range_vec| { + /** + * Computes the whole network range requested by the user through CLI + * arguments or files. This method will fail of a failure has been detected + * (either on the IO level or the network syntax parsing) + */ + fn compute_networks(file_value: Option<&str>, network_value: Option<&str>) -> Result>, String> { - range_vec.iter().map(|raw_range| { + let required_networks: Option> = ScanOptions::list_required_networks(file_value, network_value)?; + if let None = required_networks { + return Ok(None); + } - match IpNetwork::from_str(raw_range) { - Ok(parsed_network) => parsed_network, - Err(err) => { - eprintln!("Expected valid IPv4 network range ({})", err); - process::exit(1); - } - } + let mut networks: Vec = vec![]; + for network_text in required_networks.unwrap() { - }).collect() - }) + match IpNetwork::from_str(&network_text) { + Ok(parsed_network) => { + networks.push(parsed_network); + Ok(()) + }, + Err(err) => { + Err(format!("Expected valid IPv4 network range ({})", err)) + } + }?; + } + Ok(Some(networks)) } - fn compute_interval(matches: &ArgMatches, profile: &ProfileType) -> ScanTiming { + /** + * Computes scan timing constraints, as requested by the user through CLI + * arguments. The scan timing constraints will be either expressed in bandwidth + * (bits per second) or interval between ARP requests (in milliseconds). + */ + fn compute_scan_timing(matches: &ArgMatches, profile: &ProfileType) -> ScanTiming { match (matches.value_of("bandwidth"), matches.value_of("interval")) { (Some(bandwidth_text), None) => { @@ -290,7 +303,11 @@ impl ScanOptions { let interface_name = matches.value_of("interface").map(String::from); - let network_range = ScanOptions::compute_networks(matches); + let network_range = ScanOptions::compute_networks(matches.value_of("file"), matches.value_of("network")).unwrap_or_else(|err| { + eprintln!("Could not compute requested network range to scan"); + eprintln!("{}", err); + process::exit(1); + }); let timeout_ms: u64 = match matches.value_of("timeout") { Some(timeout_text) => parse_to_milliseconds(timeout_text).unwrap_or_else(|err| { @@ -379,7 +396,7 @@ impl ScanOptions { } }; - let scan_timing: ScanTiming = ScanOptions::compute_interval(matches, &profile); + let scan_timing: ScanTiming = ScanOptions::compute_scan_timing(matches, &profile); let output = match matches.value_of("output") { Some(output_request) => { @@ -509,3 +526,120 @@ impl ScanOptions { } } + + +#[cfg(test)] +mod tests { + + use super::*; + use ipnetwork::Ipv4Network; + + #[test] + fn should_have_no_network_default() { + + let networks = ScanOptions::compute_networks(None, None); + assert_eq!(networks, Ok(None)); + } + + #[test] + fn should_handle_single_ipv4_arg() { + + let networks = ScanOptions::compute_networks(None, Some("192.168.1.20")); + + let target_network: Vec = vec![ + IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 20), 32).unwrap() + ) + ]; + + assert_eq!(networks, Ok(Some(target_network))); + } + + #[test] + fn should_handle_multiple_ipv4_arg() { + + let networks = ScanOptions::compute_networks(None, Some("192.168.1.20,192.168.1.50")); + + let target_network: Vec = vec![ + IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 20), 32).unwrap() + ), + IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 50), 32).unwrap() + ) + ]; + + assert_eq!(networks, Ok(Some(target_network))); + } + + #[test] + fn should_handle_single_network_arg() { + + let networks = ScanOptions::compute_networks(None, Some("192.168.1.0/24")); + + let target_network: Vec = vec![ + IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 0), 24).unwrap() + ) + ]; + + assert_eq!(networks, Ok(Some(target_network))); + } + + #[test] + fn should_handle_network_mix_arg() { + + let networks = ScanOptions::compute_networks(None, Some("192.168.20.1,192.168.1.0/24,192.168.5.4/28")); + + let target_network: Vec = vec![ + IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 20, 1), 32).unwrap() + ), + IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 0), 24).unwrap() + ), + IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 5, 4), 28).unwrap() + ) + ]; + + assert_eq!(networks, Ok(Some(target_network))); + } + + #[test] + fn should_handle_file_input() { + + let networks = ScanOptions::compute_networks(Some("./data/ip-list.txt"), None); + + let target_network: Vec = vec![ + IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 1), 32).unwrap() + ), + IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 2), 32).unwrap() + ), + IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 2, 0), 29).unwrap() + ) + ]; + + assert_eq!(networks, Ok(Some(target_network))); + } + + #[test] + fn should_fail_incorrect_network() { + + let networks = ScanOptions::compute_networks(None, Some("500.10.10.10/24")); + + assert_eq!(networks, Err("Expected valid IPv4 network range (invalid address: 500.10.10.10/24)".to_string())); + } + + #[test] + fn should_fail_unreadable_network() { + + let networks = ScanOptions::compute_networks(None, Some("no-network")); + + assert_eq!(networks, Err("Expected valid IPv4 network range (invalid address: no-network)".to_string())); + } + +} From 13d2e9939de2a11db6467b1bdda15875b53674f8 Mon Sep 17 00:00:00 2001 From: Saluki Date: Thu, 18 Nov 2021 07:46:52 +0100 Subject: [PATCH 086/121] Quick code base improvements --- README.md | 2 +- src/args.rs | 2 +- src/main.rs | 2 +- src/network.rs | 5 ++++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7b6762a..71da21c 100644 --- a/README.md +++ b/README.md @@ -209,7 +209,7 @@ The features below will be shipped in the next releases of the project. - Adding advanced packet options (padding, LLC, ...) - ~~Enable bandwith control (exclusive with interval)~~ - released in 0.12.0 - Stronger profile defaults (chaos & stealth) -- Avoid packet copy in userspace for daster scans (BPF filtering) +- Avoid packet copy in userspace for faster scans (BPF filtering) ## Contributing diff --git a/src/args.rs b/src/args.rs index 879eb31..f804615 100644 --- a/src/args.rs +++ b/src/args.rs @@ -230,7 +230,7 @@ impl ScanOptions { fn compute_networks(file_value: Option<&str>, network_value: Option<&str>) -> Result>, String> { let required_networks: Option> = ScanOptions::list_required_networks(file_value, network_value)?; - if let None = required_networks { + if required_networks.is_none() { return Ok(None); } diff --git a/src/main.rs b/src/main.rs index 4911ee9..4f4ce07 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,7 +42,7 @@ fn main() { let scan_options = ScanOptions::new(&matches); if !utils::is_root_user() { - eprintln!("Should run this binary as root"); + eprintln!("Should run this binary as root or use --help for options"); process::exit(1); } diff --git a/src/network.rs b/src/network.rs index 4ae8fb3..5c86229 100644 --- a/src/network.rs +++ b/src/network.rs @@ -117,7 +117,10 @@ pub fn compute_scan_estimation(host_count: u128, options: &Arc) -> true => ETHERNET_VLAN_PACKET_SIZE.try_into().expect("Internal number conversion failed for VLAN packet size"), false => ETHERNET_STD_PACKET_SIZE.try_into().expect("Internal number conversion failed for Ethernet packet size") }; - let retry_count: u128 = options.retry_count.try_into().unwrap(); + let retry_count: u128 = options.retry_count.try_into().unwrap_or_else(|err| { + eprintln!("[warn] Could not cast retry count, defaults to 1 - {}", err); + 1 + }); // The values below are averages based on an amount of performed network // scans. This may of course vary based on network configurations. From c1fbbd86ccc71edb9243999eb09c4e1ff4b41fe2 Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 6 Dec 2021 08:58:11 +0100 Subject: [PATCH 087/121] Reduce dependencies --- Cargo.lock | 7 ------- Cargo.toml | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9509120..b7145a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,7 +109,6 @@ dependencies = [ "strsim", "textwrap", "unicode-width", - "vec_map", ] [[package]] @@ -524,12 +523,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 7ba355a..8403e11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ categories = ["command-line-utilities"] [dependencies] # CLI & utilities -clap = "2.33" +clap = { version = "2.33", default-features = false, features = [ "suggestions", "color"] } ansi_term = "0.12" rand = "0.8" ctrlc = "3.2" From 426a040a839e844141f2e3af9b9d6901df4e5bdf Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 6 Dec 2021 08:58:44 +0100 Subject: [PATCH 088/121] Introducing new network iterator --- src/network.rs | 205 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 183 insertions(+), 22 deletions(-) diff --git a/src/network.rs b/src/network.rs index 5c86229..ae2aa0a 100644 --- a/src/network.rs +++ b/src/network.rs @@ -240,27 +240,96 @@ pub fn send_arp_request(tx: &mut Box, interface: &NetworkInt tx.send_to(ðernet_packet.to_immutable().packet(), Some(interface.clone())); } -/** - * Compute the whole IP range that must be scanned. This includes every IP - * address contained in the ip_networks argument. Based on the scan options, - * this list may be randomized to enhance scans. - */ -pub fn compute_ip_range(ip_networks: &[&IpNetwork], scan_options: &Arc) -> Vec { - - ip_networks.iter().fold(vec![], |accumulator, ip_network| { - - let ip_addresses = match scan_options.randomize_targets { - true => { - let mut rng = rand::thread_rng(); - let mut shuffled_addresses: Vec = ip_network.iter().collect(); - shuffled_addresses.shuffle(&mut rng); - shuffled_addresses - }, - false => ip_network.iter().collect() +pub struct NetworkIterator { + current_iterator: Option, + networks: Vec, + is_random: bool, + random_pool: Vec +} + +impl NetworkIterator { + + pub fn new(networks_ref: &[&IpNetwork], is_random: bool) -> NetworkIterator { + + // The IpNetwork struct implements the Clone trait, which means that a simple + // dereference will clone the struct in the new vector + let mut networks: Vec = networks_ref.iter().map(|network| *(*network)).collect(); + + if is_random { + let mut rng = rand::thread_rng(); + networks.shuffle(&mut rng); + } + + NetworkIterator { + current_iterator: None, + networks, + is_random, + random_pool: vec![] + } + } + + fn has_no_items_left(&self) -> bool { + self.current_iterator.is_none() && self.networks.is_empty() && self.random_pool.is_empty() + } + + fn fill_random_pool(&mut self) { + + for _ in 0..1000 { + + let next_ip = self.current_iterator.as_mut().unwrap().next(); + if next_ip.is_none() { + break; + } + + self.random_pool.push(next_ip.unwrap()); + } + + let mut rng = rand::thread_rng(); + self.random_pool.shuffle(&mut rng); + } + + fn select_new_iterator(&mut self) { + + self.current_iterator = Some(self.networks.remove(0).iter()); + } + + fn pop_next_iterator_address(&mut self) -> Option { + + self.current_iterator.as_mut().map(|iterator| iterator.next()).unwrap_or(None) + } + +} + +impl Iterator for NetworkIterator { + + type Item = IpAddr; + + fn next(&mut self) -> Option { + + if self.has_no_items_left() { + return None; + } + + if self.current_iterator.is_none() && !self.networks.is_empty() { + self.select_new_iterator(); + } + + if self.is_random && self.random_pool.is_empty() { + self.fill_random_pool(); + } + + let next_ip = match self.is_random { + true => self.random_pool.pop(), + false => self.pop_next_iterator_address() }; - [accumulator, ip_addresses].concat() - }) + if next_ip.is_none() && !self.networks.is_empty() { + self.select_new_iterator(); + return self.pop_next_iterator_address(); + } + + next_ip + } } /** @@ -406,12 +475,21 @@ mod tests { use super::*; + use ipnetwork::Ipv4Network; + use std::env; + #[test] fn should_resolve_public_ip() { - let ipv4 = Ipv4Addr::new(1,1,1,1); - - assert_eq!(find_hostname(ipv4), Some("one.one.one.one".to_string())); + // Sometimes, we do not have access to public networks in the test + // environment and can pass the OFFLINE environment variable. + if env::var("OFFLINE").is_ok() { + assert_eq!(true, true); + } + else { + let ipv4 = Ipv4Addr::new(1,1,1,1); + assert_eq!(find_hostname(ipv4), Some("one.one.one.one".to_string())); + } } #[test] @@ -430,4 +508,87 @@ mod tests { assert_eq!(find_hostname(ipv4), None); } + #[test] + fn should_iterate_over_empty_networks() { + + let mut iterator = NetworkIterator::new(&vec![], false); + + assert_eq!(iterator.next(), None); + } + + #[test] + fn should_iterate_over_single_address() { + + let network_a = IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 1), 32).unwrap() + ); + let target_network: Vec<&IpNetwork> = vec![ + &network_a + ]; + + let mut iterator = NetworkIterator::new(&target_network, false); + + assert_eq!(iterator.next(), Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)))); + assert_eq!(iterator.next(), None); + } + + #[test] + fn should_iterate_over_multiple_address() { + + let network_a = IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 1), 24).unwrap() + ); + let target_network: Vec<&IpNetwork> = vec![ + &network_a + ]; + + let mut iterator = NetworkIterator::new(&target_network, false); + + assert_eq!(iterator.next(), Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0)))); + assert_eq!(iterator.next(), Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)))); + assert_eq!(iterator.next(), Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2)))); + } + + #[test] + fn should_iterate_over_multiple_networks() { + + let network_a = IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 1), 32).unwrap() + ); + let network_b = IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(10, 10, 20, 20), 32).unwrap() + ); + let target_network: Vec<&IpNetwork> = vec![ + &network_a, + &network_b + ]; + + let mut iterator = NetworkIterator::new(&target_network, false); + + assert_eq!(iterator.next(), Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)))); + assert_eq!(iterator.next(), Some(IpAddr::V4(Ipv4Addr::new(10, 10, 20, 20)))); + assert_eq!(iterator.next(), None); + } + + #[test] + fn should_iterate_with_random() { + + let network_a = IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 1), 32).unwrap() + ); + let network_b = IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(10, 10, 20, 20), 32).unwrap() + ); + let target_network: Vec<&IpNetwork> = vec![ + &network_a, + &network_b + ]; + + let mut iterator = NetworkIterator::new(&target_network, true); + + assert_eq!(iterator.next().is_some(), true); + assert_eq!(iterator.next().is_some(), true); + assert_eq!(iterator.next(), None); + } + } From 50726a7f839575d26fb3357107a7658a4f023f0d Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 6 Dec 2021 08:59:05 +0100 Subject: [PATCH 089/121] Small optimizations --- src/main.rs | 11 ++++------- src/utils.rs | 3 ++- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main.rs b/src/main.rs index 4f4ce07..fd66c52 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use pnet::datalink; use crate::args::{ScanOptions, OutputFormat}; +use crate::network::NetworkIterator; use crate::vendor::Vendor; fn main() { @@ -117,11 +118,7 @@ fn main() { break; } - // The random approach has one major drawback, compared with the native - // network iterator exposed by 'ipnetwork': memory usage. Instead of - // using a small memory footprint iterator, we have to store all IP - // addresses in memory at once. This can cause problems on large ranges. - let ip_addresses: Vec = network::compute_ip_range(&ip_networks, &scan_options); + let ip_addresses = NetworkIterator::new(&ip_networks, scan_options.randomize_targets); let source_ip = network::find_source_ip(selected_interface, scan_options.source_ipv4); for ip_address in ip_addresses { @@ -143,8 +140,8 @@ fn main() { let mut sleep_ms_mount: u64 = 0; while !finish_sleep.load(Ordering::Relaxed) && sleep_ms_mount < scan_options.timeout_ms { - thread::sleep(Duration::from_millis(500)); - sleep_ms_mount += 500; + thread::sleep(Duration::from_millis(100)); + sleep_ms_mount += 100; } timed_out.store(true, Ordering::Relaxed); diff --git a/src/utils.rs b/src/utils.rs index 1d43381..a4c4926 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,3 +1,4 @@ +use std::env; use std::process; use std::sync::Arc; @@ -14,7 +15,7 @@ use crate::args::ScanOptions; * user. This approach only supports Linux-like systems (Ubuntu, Fedore, ...). */ pub fn is_root_user() -> bool { - std::env::var("USER").unwrap_or_else(|_| String::from("")) == *"root" + env::var("USER").unwrap_or_else(|_| String::from("")) == *"root" } /** From 40a445b267607992939cd3453fad1e0797555585 Mon Sep 17 00:00:00 2001 From: Saluki Date: Fri, 10 Dec 2021 07:05:02 +0100 Subject: [PATCH 090/121] OUI fetching documentation --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 71da21c..98362c8 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,12 @@ Download the `arp-scan` binary for Linux (Ubuntu, Fedora, Debian, ...). See the wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.12.0/arp-scan-v0.12.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan ``` +Optionnaly, fetch the IEEE OUI reference file (CSV format) that contains all MAC address vendors. + +```bash +wget -O /usr/share/arp-scan/ieee-oui.csv http://standards-oui.ieee.org/oui/oui.csv +``` + List all available network interfaces. ```bash From 3104c2d56a7872320810d916c2f982e715f25531 Mon Sep 17 00:00:00 2001 From: Saluki Date: Fri, 10 Dec 2021 07:05:26 +0100 Subject: [PATCH 091/121] Moving to 2021 edition with code optimizations --- Cargo.toml | 3 ++- src/network.rs | 16 +++++++++++++--- src/utils.rs | 10 +++++----- src/vendor.rs | 4 ++-- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8403e11..8866f75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,12 +4,13 @@ description = "A minimalistic ARP scan tool" license = "AGPL-3.0-or-later" version = "0.12.0" authors = ["Saluki"] -edition = "2018" +edition = "2021" readme = "README.md" homepage = "https://github.com/Saluki/arp-scan-rs" repository = "https://github.com/Saluki/arp-scan-rs" keywords = ["arp", "scan", "network", "security"] categories = ["command-line-utilities"] +exclude = ["/.semaphore", "/data", "/release.sh", ".*"] [dependencies] diff --git a/src/network.rs b/src/network.rs index ae2aa0a..aa03398 100644 --- a/src/network.rs +++ b/src/network.rs @@ -74,7 +74,7 @@ pub fn compute_network_configuration<'a>(interfaces: &'a [NetworkInterface], sca Some(name) => String::from(name), None => { - let name = utils::select_default_interface(&interfaces).map(|interface| interface.name); + let name = utils::select_default_interface(interfaces).map(|interface| interface.name); match name { Some(name) => name, @@ -237,9 +237,14 @@ pub fn send_arp_request(tx: &mut Box, interface: &NetworkInt ethernet_packet.set_payload(arp_packet.packet_mut()); } - tx.send_to(ðernet_packet.to_immutable().packet(), Some(interface.clone())); + tx.send_to(ethernet_packet.to_immutable().packet(), Some(interface.clone())); } +/** + * A network iterator for iterating over multiple network ranges in with a + * low-memory approach. This iterator was crafted to allow iteration over huge + * network ranges (192.168.0.0/16) without consuming excessive memory. + */ pub struct NetworkIterator { current_iterator: Option, networks: Vec, @@ -268,6 +273,11 @@ impl NetworkIterator { } } + /** + * The functions below are not public and only used by the Iterator trait + * to help keep the next() code clean. + */ + fn has_no_items_left(&self) -> bool { self.current_iterator.is_none() && self.networks.is_empty() && self.random_pool.is_empty() } @@ -391,7 +401,7 @@ pub fn receive_arp_responses(rx: &mut Box, options: Arc packet, None => continue }; diff --git a/src/utils.rs b/src/utils.rs index a4c4926..3cf3124 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -158,13 +158,13 @@ pub fn display_scan_results(response_summary: ResponseSummary, mut target_detail for detail in target_details.iter() { let hostname: &str = match &detail.hostname { - Some(hostname) => &hostname, - None if !options.resolve_hostname => &"(disabled)", - None => &"" + Some(hostname) => hostname, + None if !options.resolve_hostname => "(disabled)", + None => "" }; let vendor: &str = match &detail.vendor { - Some(vendor) => &vendor, - None => &"" + Some(vendor) => vendor, + None => "" }; println!("| {: <15} | {: <18} | {: Date: Fri, 10 Dec 2021 07:11:02 +0100 Subject: [PATCH 092/121] Release 0.13.0 --- Cargo.lock | 4 +++- Cargo.toml | 2 +- README.md | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b7145a9..2ca5221 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "aho-corasick" version = "0.7.18" @@ -29,7 +31,7 @@ dependencies = [ [[package]] name = "arp-scan" -version = "0.12.0" +version = "0.13.0" dependencies = [ "ansi_term 0.12.1", "clap", diff --git a/Cargo.toml b/Cargo.toml index 8866f75..a9296c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "arp-scan" description = "A minimalistic ARP scan tool" license = "AGPL-3.0-or-later" -version = "0.12.0" +version = "0.13.0" authors = ["Saluki"] edition = "2021" readme = "README.md" diff --git a/README.md b/README.md index 98362c8..286028a 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ ARP scan finished, 5 hosts found in 1.623 seconds Download the `arp-scan` binary for Linux (Ubuntu, Fedora, Debian, ...). See the [releases page](https://github.com/Saluki/arp-scan-rs/releases) for other binaries. ```bash -wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.12.0/arp-scan-v0.12.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan +wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.13.0/arp-scan-v0.13.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan ``` Optionnaly, fetch the IEEE OUI reference file (CSV format) that contains all MAC address vendors. @@ -209,7 +209,7 @@ The features below will be shipped in the next releases of the project. - ~~Time estimations & bandwidth~~ - released in 0.10.0 - ~~MAC vendor lookup in the results~~ - released in 0.9.0 - ~~Fine-grained scan timings (interval)~~ - released in 0.8.0 -- Wide network range support +- ~~Wide network range support~~ - released in 0.13.0 - ~~Partial results on SIGINT~~ - released in 0.11.0 - ~~Read network targets from file~~ - released in 0.12.0 - Adding advanced packet options (padding, LLC, ...) From 029dbe4d9011853730a2766df5bb0324997f34c3 Mon Sep 17 00:00:00 2001 From: Saluki Date: Fri, 10 Dec 2021 07:54:16 +0100 Subject: [PATCH 093/121] Upgrade Semaphore CI --- .semaphore/semaphore.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 59df300..5116962 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -3,16 +3,18 @@ name: Rust agent: machine: type: e1-standard-2 - os_image: ubuntu1804 + os_image: ubuntu2004 containers: + # Rust 1.56 (2021 edition) is currently not supported - name: main - image: 'registry.semaphoreci.com/rust:1.47' + image: 'registry.semaphoreci.com/rust:1.51' blocks: - name: Test release task: jobs: - name: Cargo test commands: + - rustup update - checkout - cargo build --verbose - cargo test --verbose From 86eb4a8e393e787016538647aab8352116b65412 Mon Sep 17 00:00:00 2001 From: Saluki Date: Fri, 10 Dec 2021 12:57:35 +0100 Subject: [PATCH 094/121] Working on Rust update in CI --- .semaphore/semaphore.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 5116962..b286ba7 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -14,7 +14,8 @@ blocks: jobs: - name: Cargo test commands: - - rustup update - checkout + - rustup update stable + - rustc --version - cargo build --verbose - cargo test --verbose From 0206f3bc382fa74b68d7bc0a4b54feaff0e106d5 Mon Sep 17 00:00:00 2001 From: Saluki Date: Fri, 10 Dec 2021 13:02:17 +0100 Subject: [PATCH 095/121] Allowing rustup path --- .semaphore/semaphore.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index b286ba7..c5ff026 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -16,6 +16,7 @@ blocks: commands: - checkout - rustup update stable + - export PATH="$PATH:$HOME/.cargo/bin" - rustc --version - cargo build --verbose - cargo test --verbose From 623b5c10191e097cd9fc558f93f52e40080b56a2 Mon Sep 17 00:00:00 2001 From: Saluki Date: Fri, 10 Dec 2021 13:22:51 +0100 Subject: [PATCH 096/121] Using custom Rust 1.57 image --- .semaphore/semaphore.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index c5ff026..b244e2f 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -5,9 +5,9 @@ agent: type: e1-standard-2 os_image: ubuntu2004 containers: - # Rust 1.56 (2021 edition) is currently not supported + # Rust 1.56 (2021 edition) is currently not supported in Semaphore - name: main - image: 'registry.semaphoreci.com/rust:1.51' + image: 'saluki/rust-ci:1.57' blocks: - name: Test release task: @@ -15,8 +15,6 @@ blocks: - name: Cargo test commands: - checkout - - rustup update stable - - export PATH="$PATH:$HOME/.cargo/bin" - rustc --version - cargo build --verbose - cargo test --verbose From e1c14473992baf2acabe02b9f91291aada8fd209 Mon Sep 17 00:00:00 2001 From: Saluki Date: Sun, 12 Dec 2021 11:19:57 +0100 Subject: [PATCH 097/121] Minor doc improvements --- README.md | 6 ++++++ src/args.rs | 9 ++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 286028a..38730b2 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,7 @@ The features below will be shipped in the next releases of the project. - Make ARP scans faster - with a per-host retry approach + - add a back-off factor for retries - ~~by closing the response thread faster~~ - released in 0.8.0 - ~~Scan profiles (standard, attacker, light, ...)~~ - released in 0.10.0 - Complete VLAN support @@ -213,8 +214,13 @@ The features below will be shipped in the next releases of the project. - ~~Partial results on SIGINT~~ - released in 0.11.0 - ~~Read network targets from file~~ - released in 0.12.0 - Adding advanced packet options (padding, LLC, ...) + - add padding bits after ARP payload + - support RFC 1042 LLC framing with SNAP - ~~Enable bandwith control (exclusive with interval)~~ - released in 0.12.0 - Stronger profile defaults (chaos & stealth) +- Other platforms (Windows, ...) +- Read targets from *stdout* +- Change verbose options (for debug, network details, quiet mode, ...) - Avoid packet copy in userspace for faster scans (BPF filtering) ## Contributing diff --git a/src/args.rs b/src/args.rs index f804615..fd40abe 100644 --- a/src/args.rs +++ b/src/args.rs @@ -23,13 +23,16 @@ const CLI_VERSION: &str = env!("CARGO_PKG_VERSION"); const EXAMPLES_HELP: &str = "EXAMPLES: - List network interfaces + # Launch a default scan with on the first working interface + arp- scann + + # List network interfaces arp-scan -l - Launch a scan on WiFi interface with fake IP and stealth profile + # Launch a scan on WiFi interface with fake IP and stealth profile arp-scan -i wlp1s0 --source-ip 192.168.0.42 --profile stealth - Launch a scan on VLAN 45 with JSON output + # Launch a scan on VLAN 45 with JSON output arp-scan -Q 45 -o json "; From bc1c9b8caf6a3b35f6d7adeca02bc864f5fa5ff5 Mon Sep 17 00:00:00 2001 From: Saluki Date: Sun, 12 Dec 2021 11:20:08 +0100 Subject: [PATCH 098/121] Moving to a 2-stage CI process --- .semaphore/semaphore.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index b244e2f..d86bb1b 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -12,9 +12,11 @@ blocks: - name: Test release task: jobs: - - name: Cargo test + - name: Build commands: - checkout - rustc --version - cargo build --verbose + - name: Test + commands: - cargo test --verbose From fd1513132f8d2c5e4482cb203e599d3392209195 Mon Sep 17 00:00:00 2001 From: Saluki Date: Sun, 12 Dec 2021 11:42:10 +0100 Subject: [PATCH 099/121] Cancel 2-stage CI process --- .semaphore/semaphore.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index d86bb1b..f65c988 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -12,11 +12,9 @@ blocks: - name: Test release task: jobs: - - name: Build + - name: Build & test commands: - checkout - rustc --version - cargo build --verbose - - name: Test - commands: - cargo test --verbose From 823b39238988faafb1fdbbfc71e627c33184cda6 Mon Sep 17 00:00:00 2001 From: Saluki Date: Sun, 12 Dec 2021 11:43:23 +0100 Subject: [PATCH 100/121] Add link to crates.io in README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 38730b2..444846c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Build Status](https://saluki.semaphoreci.com/badges/arp-scan-rs/branches/master.svg?style=shields)](https://saluki.semaphoreci.com/projects/arp-scan-rs) [![dependency status](https://deps.rs/repo/github/Saluki/arp-scan-rs/status.svg)](https://deps.rs/repo/github/Saluki/arp-scan-rs) +![crates.io](https://img.shields.io/crates/v/arp-scan.svg) Find all hosts in your local network using this fast ARP scanner. The CLI is written in Rust and provides a minimal scanner that finds all hosts using the ARP protocol. Inspired by the awesome [arp-scan project](https://github.com/royhills/arp-scan). From 57c7ae9ff1e41a56fd454dd6ff9dedec3f2edae2 Mon Sep 17 00:00:00 2001 From: Saluki Date: Sun, 2 Jan 2022 14:36:57 +0100 Subject: [PATCH 101/121] Moving to clap v3 --- Cargo.lock | 72 ++++++++++++++++++++++++++++++++++++----------------- Cargo.toml | 2 +- src/args.rs | 58 +++++++++++++++++++++--------------------- 3 files changed, 79 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2ca5221..8ab6b8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,15 +11,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "ansi_term" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" -dependencies = [ - "winapi", -] - [[package]] name = "ansi_term" version = "0.12.1" @@ -33,7 +24,7 @@ dependencies = [ name = "arp-scan" version = "0.13.0" dependencies = [ - "ansi_term 0.12.1", + "ansi_term", "clap", "csv", "ctrlc", @@ -101,16 +92,17 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "2.33.3" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +checksum = "d17bf219fcd37199b9a29e00ba65dfb8cd5b2688b7297ec14ff829c40ac50ca9" dependencies = [ - "ansi_term 0.11.0", "atty", "bitflags", + "indexmap", + "os_str_bytes", "strsim", + "termcolor", "textwrap", - "unicode-width", ] [[package]] @@ -180,6 +172,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + [[package]] name = "hermit-abi" version = "0.1.18" @@ -189,6 +187,16 @@ dependencies = [ "libc", ] +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", +] + [[package]] name = "ipnetwork" version = "0.18.0" @@ -250,6 +258,15 @@ dependencies = [ "memoffset", ] +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] + [[package]] name = "pnet" version = "0.28.0" @@ -489,9 +506,9 @@ dependencies = [ [[package]] name = "strsim" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" @@ -505,19 +522,19 @@ dependencies = [ ] [[package]] -name = "textwrap" -version = "0.11.0" +name = "termcolor" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" dependencies = [ - "unicode-width", + "winapi-util", ] [[package]] -name = "unicode-width" -version = "0.1.8" +name = "textwrap" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" [[package]] name = "unicode-xid" @@ -547,6 +564,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index a9296c7..9c77300 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ exclude = ["/.semaphore", "/data", "/release.sh", ".*"] [dependencies] # CLI & utilities -clap = { version = "2.33", default-features = false, features = [ "suggestions", "color"] } +clap = { version = "3", default-features = false, features = ["std", "suggestions", "color"] } ansi_term = "0.12" rand = "0.8" ctrlc = "3.2" diff --git a/src/args.rs b/src/args.rs index fd40abe..439a645 100644 --- a/src/args.rs +++ b/src/args.rs @@ -24,7 +24,7 @@ const CLI_VERSION: &str = env!("CARGO_PKG_VERSION"); const EXAMPLES_HELP: &str = "EXAMPLES: # Launch a default scan with on the first working interface - arp- scann + arp-scan # List network interfaces arp-scan -l @@ -41,120 +41,120 @@ const EXAMPLES_HELP: &str = "EXAMPLES: * This function groups together all exposed CLI arguments to the end-users * with clap. Other CLI details (version, ...) should be grouped there as well. */ -pub fn build_args<'a, 'b>() -> App<'a, 'b> { +pub fn build_args<'a>() -> App<'a> { App::new("arp-scan") .version(CLI_VERSION) .about("A minimalistic ARP scan tool written in Rust") .arg( - Arg::with_name("profile").short("p").long("profile") + Arg::new("profile").short('p').long("profile") .takes_value(true).value_name("PROFILE_NAME") .help("Scan profile") ) .arg( - Arg::with_name("interface").short("i").long("interface") + Arg::new("interface").short('i').long("interface") .takes_value(true).value_name("INTERFACE_NAME") .help("Network interface") ) .arg( - Arg::with_name("network").short("n").long("network") + Arg::new("network").short('n').long("network") .takes_value(true).value_name("NETWORK_RANGE") .help("Network range to scan") ) .arg( - Arg::with_name("file").short("f").long("file") + Arg::new("file").short('f').long("file") .takes_value(true).value_name("FILE_PATH") .conflicts_with("network") .help("Read IPv4 addresses from a file") ) .arg( - Arg::with_name("timeout").short("t").long("timeout") + Arg::new("timeout").short('t').long("timeout") .takes_value(true).value_name("TIMEOUT_DURATION") .help("ARP response timeout") ) .arg( - Arg::with_name("source_ip").short("S").long("source-ip") + Arg::new("source_ip").short('S').long("source-ip") .takes_value(true).value_name("SOURCE_IPV4") .help("Source IPv4 address for requests") ) .arg( - Arg::with_name("destination_mac").short("M").long("dest-mac") + Arg::new("destination_mac").short('M').long("dest-mac") .takes_value(true).value_name("DESTINATION_MAC") .help("Destination MAC address for requests") ) .arg( - Arg::with_name("source_mac").long("source-mac") + Arg::new("source_mac").long("source-mac") .takes_value(true).value_name("SOURCE_MAC") .help("Source MAC address for requests") ) .arg( - Arg::with_name("numeric").long("numeric") + Arg::new("numeric").long("numeric") .takes_value(false) .help("Numeric mode, no hostname resolution") ) .arg( - Arg::with_name("vlan").short("Q").long("vlan") + Arg::new("vlan").short('Q').long("vlan") .takes_value(true).value_name("VLAN_ID") .help("Send using 802.1Q with VLAN ID") ) .arg( - Arg::with_name("retry_count").short("r").long("retry") + Arg::new("retry_count").short('r').long("retry") .takes_value(true).value_name("RETRY_COUNT") .help("Host retry attempt count") ) .arg( - Arg::with_name("random").short("R").long("random") + Arg::new("random").short('R').long("random") .takes_value(false) .help("Randomize the target list") ) .arg( - Arg::with_name("interval").short("I").long("interval") + Arg::new("interval").short('I').long("interval") .takes_value(true).value_name("INTERVAL_DURATION") .help("Milliseconds between ARP requests") ) .arg( - Arg::with_name("bandwidth").short("B").long("bandwidth") + Arg::new("bandwidth").short('B').long("bandwidth") .takes_value(true).value_name("BITS") .conflicts_with("interval") .help("Limit scan bandwidth (bits/second)") ) .arg( - Arg::with_name("oui-file").long("oui-file") + Arg::new("oui-file").long("oui-file") .takes_value(true).value_name("FILE_PATH") .help("Path to custom IEEE OUI CSV file") ) .arg( - Arg::with_name("list").short("l").long("list") + Arg::new("list").short('l').long("list") .takes_value(false) .help("List network interfaces") ) .arg( - Arg::with_name("output").short("o").long("output") + Arg::new("output").short('o').long("output") .takes_value(true).value_name("FORMAT") .help("Define output format") ) .arg( - Arg::with_name("hw_type").long("hw-type") + Arg::new("hw_type").long("hw-type") .takes_value(true).value_name("HW_TYPE") .help("Custom ARP hardware field") ) .arg( - Arg::with_name("hw_addr").long("hw-addr") + Arg::new("hw_addr").long("hw-addr") .takes_value(true).value_name("ADDRESS_LEN") .help("Custom ARP hardware address length") ) .arg( - Arg::with_name("proto_type").long("proto-type") + Arg::new("proto_type").long("proto-type") .takes_value(true).value_name("PROTO_TYPE") .help("Custom ARP proto type") ) .arg( - Arg::with_name("proto_addr").long("proto-addr") + Arg::new("proto_addr").long("proto-addr") .takes_value(true).value_name("ADDRESS_LEN") .help("Custom ARP proto address length") ) .arg( - Arg::with_name("arp_operation").long("arp-op") + Arg::new("arp_operation").long("arp-op") .takes_value(true).value_name("OPERATION_ID") .help("Custom ARP operation ID") ) @@ -425,7 +425,7 @@ impl ScanOptions { None => "/usr/share/arp-scan/ieee-oui.csv".to_string() }; - let hw_type = match matches.value_of("hw-type") { + let hw_type = match matches.value_of("hw_type") { Some(hw_type_text) => { match hw_type_text.parse::() { @@ -439,7 +439,7 @@ impl ScanOptions { None => None }; - let hw_addr = match matches.value_of("hw-addr") { + let hw_addr = match matches.value_of("hw_addr") { Some(hw_addr_text) => { match hw_addr_text.parse::() { @@ -453,7 +453,7 @@ impl ScanOptions { None => None }; - let proto_type = match matches.value_of("proto-type") { + let proto_type = match matches.value_of("proto_type") { Some(proto_type_text) => { match proto_type_text.parse::() { @@ -467,7 +467,7 @@ impl ScanOptions { None => None }; - let proto_addr = match matches.value_of("proto-addr") { + let proto_addr = match matches.value_of("proto_addr") { Some(proto_addr_text) => { match proto_addr_text.parse::() { @@ -481,7 +481,7 @@ impl ScanOptions { None => None }; - let arp_operation = match matches.value_of("arp-op") { + let arp_operation = match matches.value_of("arp_operation") { Some(arp_op_text) => { match arp_op_text.parse::() { From d147e7830d75399b525ca6d05a7e88ee85421a67 Mon Sep 17 00:00:00 2001 From: Saluki Date: Thu, 20 Jan 2022 08:45:24 +0100 Subject: [PATCH 102/121] Small code improvements --- src/main.rs | 15 ++++++++------- src/time.rs | 4 ++++ src/utils.rs | 8 ++++++++ 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index fd66c52..de519f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -99,31 +99,32 @@ fn main() { println!("Sending {} ARP requests (waiting at least {}ms, {}ms request interval)", network_size, scan_options.timeout_ms, interval_ms); } - let finish_sleep = Arc::new(AtomicBool::new(false)); - let cloned_finish_sleep = Arc::clone(&finish_sleep); + let has_reached_timeout = Arc::new(AtomicBool::new(false)); + let cloned_reached_timeout = Arc::clone(&has_reached_timeout); ctrlc::set_handler(move || { eprintln!("[warn] Receiving halt signal, ending scan with partial results"); - cloned_finish_sleep.store(true, Ordering::Relaxed); + cloned_reached_timeout.store(true, Ordering::Relaxed); }).unwrap_or_else(|err| { eprintln!("Could not set CTRL+C handler ({})", err); process::exit(1); }); + let source_ip = network::find_source_ip(selected_interface, scan_options.source_ipv4); + // The retry count does right now use a 'brute-force' strategy without // synchronization process with the already known hosts. for _ in 0..scan_options.retry_count { - if finish_sleep.load(Ordering::Relaxed) { + if has_reached_timeout.load(Ordering::Relaxed) { break; } let ip_addresses = NetworkIterator::new(&ip_networks, scan_options.randomize_targets); - let source_ip = network::find_source_ip(selected_interface, scan_options.source_ipv4); for ip_address in ip_addresses { - if finish_sleep.load(Ordering::Relaxed) { + if has_reached_timeout.load(Ordering::Relaxed) { break; } @@ -138,7 +139,7 @@ fn main() { // (where T is the timeout option). After the sleep phase, the response // thread will receive a stop request through the 'timed_out' mutex. let mut sleep_ms_mount: u64 = 0; - while !finish_sleep.load(Ordering::Relaxed) && sleep_ms_mount < scan_options.timeout_ms { + while !has_reached_timeout.load(Ordering::Relaxed) && sleep_ms_mount < scan_options.timeout_ms { thread::sleep(Duration::from_millis(100)); sleep_ms_mount += 100; diff --git a/src/time.rs b/src/time.rs index 024b9c7..8f7f25c 100644 --- a/src/time.rs +++ b/src/time.rs @@ -45,6 +45,10 @@ pub fn parse_to_milliseconds(time_arg: &str) -> Result { } } +/** + * Format milliseconds to a human-readable string. This will of course give an + * approximation, but will be readable. + */ pub fn format_milliseconds(milliseconds: u128) -> String { if milliseconds < 1000 { diff --git a/src/utils.rs b/src/utils.rs index 3cf3124..9427f90 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -109,6 +109,10 @@ pub fn display_prescan_details(ip_networks: &[&IpNetwork], selected_interface: & } } +/** + * Computes multiple IPv4 networks total size, IPv6 network are not being + * supported by this function. + */ pub fn compute_network_size(ip_networks: &[&IpNetwork]) -> u128 { ip_networks.iter().fold(0u128, |total_size, ip_network| { @@ -209,6 +213,10 @@ struct SerializableGlobalResult { results: Vec } +/** + * Transforms an ARP scan result (including KPI and target details) to a structure + * that can be serialized for export (JSON, YAML, CSV, ...) + */ fn get_serializable_result(response_summary: ResponseSummary, target_details: Vec) -> SerializableGlobalResult { let exportable_results: Vec = target_details.into_iter() From 925da62effa3924229b32d5941c157f76ff4c3f1 Mon Sep 17 00:00:00 2001 From: Saluki Date: Thu, 20 Jan 2022 08:47:07 +0100 Subject: [PATCH 103/121] Upgrading pnet dependency --- Cargo.lock | 32 ++++++++++++++++---------------- Cargo.toml | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ab6b8a..05c1687 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -269,9 +269,9 @@ dependencies = [ [[package]] name = "pnet" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6d2a0409666964722368ef5fb74b9f93fac11c18bef3308693c16c6733f103" +checksum = "8750e073f82219c01e771133c64718d7685aef922da8a0d430a46aed05b6341a" dependencies = [ "ipnetwork", "pnet_base", @@ -283,15 +283,15 @@ dependencies = [ [[package]] name = "pnet_base" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25488cd551a753dcaaa6fffc9f69a7610a412dd8954425bf7ffad5f7d1156fb8" +checksum = "8205fe084bd43a3af79b3155c19feddd62e733640498842e631a2ffe107d1538" [[package]] name = "pnet_datalink" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d1f8ab1ef6c914cf51dc5dfe0be64088ea5f3b08bbf5a31abc70356d271198" +checksum = "6f85aef5e52e22ff06b1b11f2eb6d52959a9e0ecad3cb3f5cc2d78cadc077f0e" dependencies = [ "ipnetwork", "libc", @@ -302,9 +302,9 @@ dependencies = [ [[package]] name = "pnet_macros" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30490e0852e58402b8fae0d39897b08a24f493023a4d6cf56b2e30f31ed57548" +checksum = "98cc3af95fed6dc318dfede3e81320f96ad5e237c6f7c4688108b19c8e67432d" dependencies = [ "proc-macro2", "quote", @@ -314,18 +314,18 @@ dependencies = [ [[package]] name = "pnet_macros_support" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4714e10f30cab023005adce048f2d30dd4ac4f093662abf2220855655ef8f90" +checksum = "feaba58ba96abb218ec584d6caf0d3ff48922df05dbbeb1560553c197091b29e" dependencies = [ "pnet_base", ] [[package]] name = "pnet_packet" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8588067671d03c9f4254b2e66fecb4d8b93b5d3e703195b84f311cd137e32130" +checksum = "f246edaaf1aaf82072d4cd38ee18bcc5dfc0464093f9ca39e4ac5962d68cf9d4" dependencies = [ "glob", "pnet_base", @@ -335,9 +335,9 @@ dependencies = [ [[package]] name = "pnet_sys" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a3f32b0df45515befd19eed04616f6b56a488da92afc61164ef455e955f07f" +checksum = "028c87a5e3a48fc07df099a2025f2ef16add5993712e1494ba69a6707ee7ed06" dependencies = [ "libc", "winapi", @@ -345,9 +345,9 @@ dependencies = [ [[package]] name = "pnet_transport" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "932b2916d693bcc5fa18443dc99142e0a6fd31a6ce75a511868f7174c17e2bce" +checksum = "950f2a7961e19d22e19e84ff0a6e0955013185fe149673499662633d02b41b7a" dependencies = [ "libc", "pnet_base", diff --git a/Cargo.toml b/Cargo.toml index 9c77300..2e8d378 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ rand = "0.8" ctrlc = "3.2" # Network -pnet = "0.28" +pnet = "0.29" ipnetwork = "0.18" dns-lookup = "1.0" From 29416f04f7916e30e49ec460bb3cdea93c3d44f1 Mon Sep 17 00:00:00 2001 From: Saluki Date: Mon, 24 Jan 2022 06:44:15 +0100 Subject: [PATCH 104/121] Adding Debian package builds --- release.sh | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/release.sh b/release.sh index 5fba853..1a82316 100755 --- a/release.sh +++ b/release.sh @@ -4,7 +4,7 @@ # This script helps with the release process on Github (musl & glibc builds for Linux) mkdir -p ./builds -rm ./builds/* +rm -rf ./builds/* CLI_VERSION=$(/usr/bin/cat Cargo.toml | egrep "version = (.*)" | egrep -o --color=never "([0-9]+\.?){3}" | head -n 1) echo "Releasing v$CLI_VERSION for GNU & musl targets" @@ -19,6 +19,17 @@ cargo build --release --target=x86_64-unknown-linux-gnu --locked cp -p ./target/x86_64-unknown-linux-gnu/release/arp-scan ./builds/arp-scan-v$CLI_VERSION-x86_64-unknown-linux-glibc ./builds/arp-scan-v$CLI_VERSION-x86_64-unknown-linux-glibc --version +# Build the deb archive +mkdir -p ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/DEBIAN +echo "Package: arp-scan-rs" > ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/DEBIAN/control +echo "Version: 0.13.0" >> ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/DEBIAN/control +echo "Architecture: all" >> ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/DEBIAN/control +echo "Maintainer: Saluki" >> ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/DEBIAN/control +echo "Description: Minimalist ARP scan written in Rust" >> ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/DEBIAN/control +mkdir -p ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/usr/local/bin +cp ./builds/arp-scan-v$CLI_VERSION-x86_64-unknown-linux-glibc ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/usr/local/bin/arp-scan +(cd ./builds && dpkg-deb --build --root-owner-group arp-scan-rs_0.13.0-1_amd64) + echo "Update the README instructions for v$CLI_VERSION" echo " ✓ Publish on crates.io" -echo " ✓ Release on Github with Git tag v$CLI_VERSION" \ No newline at end of file +echo " ✓ Release on Github with Git tag v$CLI_VERSION" From a29531d30d6255ca67a14d2ae9f70432773bcd50 Mon Sep 17 00:00:00 2001 From: Saluki Date: Thu, 28 Apr 2022 00:37:48 +0200 Subject: [PATCH 105/121] Cleaning dependencies --- Cargo.lock | 22 ++++++++++++++-------- Cargo.toml | 8 ++++---- src/args.rs | 6 +++--- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 05c1687..ec44e9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,19 +92,28 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "3.0.0" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d17bf219fcd37199b9a29e00ba65dfb8cd5b2688b7297ec14ff829c40ac50ca9" +checksum = "7c167e37342afc5f33fd87bbc870cedd020d2a6dffa05d45ccd9241fbdd146db" dependencies = [ "atty", "bitflags", + "clap_lex", "indexmap", - "os_str_bytes", "strsim", "termcolor", "textwrap", ] +[[package]] +name = "clap_lex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "189ddd3b5d32a70b35e7686054371742a937b0d99128e76dde6340210e966669" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "csv" version = "1.1.6" @@ -263,9 +272,6 @@ name = "os_str_bytes" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" -dependencies = [ - "memchr", -] [[package]] name = "pnet" @@ -532,9 +538,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.14.2" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "unicode-xid" diff --git a/Cargo.toml b/Cargo.toml index 2e8d378..93d33d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ exclude = ["/.semaphore", "/data", "/release.sh", ".*"] [dependencies] # CLI & utilities -clap = { version = "3", default-features = false, features = ["std", "suggestions", "color"] } +clap = { version = "3.1", default-features = false, features = ["std", "suggestions", "color"] } ansi_term = "0.12" rand = "0.8" ctrlc = "3.2" @@ -27,6 +27,6 @@ dns-lookup = "1.0" # Parsing & exports csv = "1.1" -serde = { version = "1.0.126", features = ["derive"] } -serde_json = "1.0.64" -serde_yaml = "~0.8.17" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_yaml = "0.8" diff --git a/src/args.rs b/src/args.rs index 439a645..0522a78 100644 --- a/src/args.rs +++ b/src/args.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use std::path::Path; use std::fs; -use clap::{Arg, ArgMatches, App}; +use clap::{Arg, ArgMatches, Command}; use ipnetwork::IpNetwork; use pnet::datalink::MacAddr; use pnet::packet::arp::{ArpHardwareType, ArpOperation}; @@ -41,9 +41,9 @@ const EXAMPLES_HELP: &str = "EXAMPLES: * This function groups together all exposed CLI arguments to the end-users * with clap. Other CLI details (version, ...) should be grouped there as well. */ -pub fn build_args<'a>() -> App<'a> { +pub fn build_args<'a>() -> Command<'a> { - App::new("arp-scan") + Command::new("arp-scan") .version(CLI_VERSION) .about("A minimalistic ARP scan tool written in Rust") .arg( From 7c6e7262050192d3cbfb08598f8515c43179010c Mon Sep 17 00:00:00 2001 From: Saluki Date: Tue, 14 Jun 2022 23:57:17 +0200 Subject: [PATCH 106/121] Upgrading dependencies --- Cargo.lock | 90 +++++++++++++++++++++++++++----------------------- Cargo.toml | 7 ++-- src/args.rs | 65 +++++++++++++++++++----------------- src/main.rs | 14 ++++---- src/network.rs | 2 +- src/utils.rs | 2 +- src/vendor.rs | 2 +- 7 files changed, 96 insertions(+), 86 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec44e9a..2cd5ed0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,6 +31,7 @@ dependencies = [ "dns-lookup", "ipnetwork", "pnet", + "pnet_datalink", "rand", "serde", "serde_json", @@ -92,9 +93,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "3.1.12" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c167e37342afc5f33fd87bbc870cedd020d2a6dffa05d45ccd9241fbdd146db" +checksum = "a836566fa5f52f7ddf909a8a2f9029b9f78ca584cd95cf7e87f8073110f4c5c9" dependencies = [ "atty", "bitflags", @@ -107,9 +108,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.1.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "189ddd3b5d32a70b35e7686054371742a937b0d99128e76dde6340210e966669" +checksum = "5538cd660450ebeb4234cfecf8f2284b844ffc4c50531e66d584ad5b91293613" dependencies = [ "os_str_bytes", ] @@ -208,9 +209,9 @@ dependencies = [ [[package]] name = "ipnetwork" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4088d739b183546b239688ddbc79891831df421773df95e236daf7867866d355" +checksum = "1f84f1612606f3753f205a4e9a2efd6fe5b4c573a6269b2cc6c3003d44a0d127" dependencies = [ "serde", ] @@ -229,9 +230,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.102" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" [[package]] name = "linked-hash-map" @@ -267,6 +268,12 @@ dependencies = [ "memoffset", ] +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + [[package]] name = "os_str_bytes" version = "6.0.0" @@ -275,29 +282,30 @@ checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" [[package]] name = "pnet" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8750e073f82219c01e771133c64718d7685aef922da8a0d430a46aed05b6341a" +checksum = "0caaf5b11fd907ff15cf14a4477bfabca4b37ab9e447a4f8dead969a59cdafad" dependencies = [ - "ipnetwork", "pnet_base", "pnet_datalink", "pnet_packet", - "pnet_sys", "pnet_transport", ] [[package]] name = "pnet_base" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8205fe084bd43a3af79b3155c19feddd62e733640498842e631a2ffe107d1538" +checksum = "f9d3a993d49e5fd5d4d854d6999d4addca1f72d86c65adf224a36757161c02b6" +dependencies = [ + "no-std-net", +] [[package]] name = "pnet_datalink" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f85aef5e52e22ff06b1b11f2eb6d52959a9e0ecad3cb3f5cc2d78cadc077f0e" +checksum = "e466faf03a98ad27f6e15cd27a2b7cc89e73e640a43527742977bc503c37f8aa" dependencies = [ "ipnetwork", "libc", @@ -308,9 +316,9 @@ dependencies = [ [[package]] name = "pnet_macros" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc3af95fed6dc318dfede3e81320f96ad5e237c6f7c4688108b19c8e67432d" +checksum = "48dd52a5211fac27e7acb14cfc9f30ae16ae0e956b7b779c8214c74559cef4c3" dependencies = [ "proc-macro2", "quote", @@ -320,18 +328,18 @@ dependencies = [ [[package]] name = "pnet_macros_support" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feaba58ba96abb218ec584d6caf0d3ff48922df05dbbeb1560553c197091b29e" +checksum = "89de095dc7739349559913aed1ef6a11e73ceade4897dadc77c5e09de6740750" dependencies = [ "pnet_base", ] [[package]] name = "pnet_packet" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f246edaaf1aaf82072d4cd38ee18bcc5dfc0464093f9ca39e4ac5962d68cf9d4" +checksum = "bc3b5111e697c39c8b9795b9fdccbc301ab696699e88b9ea5a4e4628978f495f" dependencies = [ "glob", "pnet_base", @@ -341,9 +349,9 @@ dependencies = [ [[package]] name = "pnet_sys" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028c87a5e3a48fc07df099a2025f2ef16add5993712e1494ba69a6707ee7ed06" +checksum = "328e231f0add6d247d82421bf3790b4b33b39c8930637f428eef24c4c6a90805" dependencies = [ "libc", "winapi", @@ -351,9 +359,9 @@ dependencies = [ [[package]] name = "pnet_transport" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "950f2a7961e19d22e19e84ff0a6e0955013185fe149673499662633d02b41b7a" +checksum = "ff597185e6f1f5671b3122e4dba892a1c73e17c17e723d7669bd9299cbe7f124" dependencies = [ "libc", "pnet_base", @@ -369,18 +377,18 @@ checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" [[package]] name = "proc-macro2" -version = "1.0.27" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.9" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" dependencies = [ "proc-macro2", ] @@ -427,9 +435,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.5.4" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" dependencies = [ "aho-corasick", "memchr", @@ -447,9 +455,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" [[package]] name = "ryu" @@ -518,13 +526,13 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.72" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" +checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] @@ -543,10 +551,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] -name = "unicode-xid" -version = "0.2.2" +name = "unicode-ident" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" [[package]] name = "wasi" diff --git a/Cargo.toml b/Cargo.toml index 93d33d7..67d6c6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,14 +15,15 @@ exclude = ["/.semaphore", "/data", "/release.sh", ".*"] [dependencies] # CLI & utilities -clap = { version = "3.1", default-features = false, features = ["std", "suggestions", "color"] } +clap = { version = "3.2", default-features = false, features = ["std", "suggestions", "color"] } ansi_term = "0.12" rand = "0.8" ctrlc = "3.2" # Network -pnet = "0.29" -ipnetwork = "0.18" +pnet = "0.31" +pnet_datalink = "0.31" +ipnetwork = "0.19" dns-lookup = "1.0" # Parsing & exports diff --git a/src/args.rs b/src/args.rs index 0522a78..12f6829 100644 --- a/src/args.rs +++ b/src/args.rs @@ -7,7 +7,7 @@ use std::fs; use clap::{Arg, ArgMatches, Command}; use ipnetwork::IpNetwork; -use pnet::datalink::MacAddr; +use pnet_datalink::MacAddr; use pnet::packet::arp::{ArpHardwareType, ArpOperation}; use pnet::packet::ethernet::EtherType; @@ -204,7 +204,7 @@ pub struct ScanOptions { impl ScanOptions { - fn list_required_networks(file_value: Option<&str>, network_value: Option<&str>) -> Result>, String> { + fn list_required_networks(file_value: Option<&String>, network_value: Option<&String>) -> Result>, String> { let network_options = (file_value, network_value); match network_options { @@ -230,7 +230,7 @@ impl ScanOptions { * arguments or files. This method will fail of a failure has been detected * (either on the IO level or the network syntax parsing) */ - fn compute_networks(file_value: Option<&str>, network_value: Option<&str>) -> Result>, String> { + fn compute_networks(file_value: Option<&String>, network_value: Option<&String>) -> Result>, String> { let required_networks: Option> = ScanOptions::list_required_networks(file_value, network_value)?; if required_networks.is_none() { @@ -260,7 +260,7 @@ impl ScanOptions { */ fn compute_scan_timing(matches: &ArgMatches, profile: &ProfileType) -> ScanTiming { - match (matches.value_of("bandwidth"), matches.value_of("interval")) { + match (matches.get_one::("bandwidth"), matches.get_one::("interval")) { (Some(bandwidth_text), None) => { let bits_second: u64 = bandwidth_text.parse().unwrap_or_else(|err| { eprintln!("Expected positive number, {}", err); @@ -287,10 +287,10 @@ impl ScanOptions { */ pub fn new(matches: &ArgMatches) -> Arc { - let profile = match matches.value_of("profile") { + let profile = match matches.get_one::("profile") { Some(output_request) => { - match output_request { + match output_request.as_ref() { "default" | "d" => ProfileType::Default, "fast" | "f" => ProfileType::Fast, "stealth" | "s" => ProfileType::Stealth, @@ -304,15 +304,18 @@ impl ScanOptions { None => ProfileType::Default }; - let interface_name = matches.value_of("interface").map(String::from); + let interface_name = matches.get_one::("interface").cloned(); - let network_range = ScanOptions::compute_networks(matches.value_of("file"), matches.value_of("network")).unwrap_or_else(|err| { + let file_option = matches.get_one::("file"); + let network_option = matches.get_one::("network"); + + let network_range = ScanOptions::compute_networks(file_option, network_option).unwrap_or_else(|err| { eprintln!("Could not compute requested network range to scan"); eprintln!("{}", err); process::exit(1); }); - let timeout_ms: u64 = match matches.value_of("timeout") { + let timeout_ms: u64 = match matches.get_one::("timeout") { Some(timeout_text) => parse_to_milliseconds(timeout_text).unwrap_or_else(|err| { eprintln!("Expected correct timeout, {}", err); process::exit(1); @@ -324,9 +327,9 @@ impl ScanOptions { }; // Hostnames will not be resolved in numeric mode or stealth profile - let resolve_hostname = !matches.is_present("numeric") && !matches!(profile, ProfileType::Stealth); + let resolve_hostname = !matches.contains_id("numeric") && !matches!(profile, ProfileType::Stealth); - let source_ipv4: Option = match matches.value_of("source_ip") { + let source_ipv4: Option = match matches.get_one::("source_ip") { Some(source_ip) => { match source_ip.parse::() { @@ -340,7 +343,7 @@ impl ScanOptions { None => None }; - let destination_mac: Option = match matches.value_of("destination_mac") { + let destination_mac: Option = match matches.get_one::("destination_mac") { Some(mac_address) => { match mac_address.parse::() { @@ -354,7 +357,7 @@ impl ScanOptions { None => None }; - let source_mac: Option = match matches.value_of("source_mac") { + let source_mac: Option = match matches.get_one::("source_mac") { Some(mac_address) => { match mac_address.parse::() { @@ -368,7 +371,7 @@ impl ScanOptions { None => None }; - let vlan_id: Option = match matches.value_of("vlan") { + let vlan_id: Option = match matches.get_one::("vlan") { Some(vlan) => { match vlan.parse::() { @@ -382,7 +385,7 @@ impl ScanOptions { None => None }; - let retry_count = match matches.value_of("retry_count") { + let retry_count = match matches.get_one::("retry_count") { Some(retry_count) => { match retry_count.parse::() { @@ -401,10 +404,10 @@ impl ScanOptions { let scan_timing: ScanTiming = ScanOptions::compute_scan_timing(matches, &profile); - let output = match matches.value_of("output") { + let output = match matches.get_one::("output") { Some(output_request) => { - match output_request { + match output_request.as_ref() { "json" => OutputFormat::Json, "yaml" => OutputFormat::Yaml, "plain" | "text" => OutputFormat::Plain, @@ -418,14 +421,14 @@ impl ScanOptions { None => OutputFormat::Plain }; - let randomize_targets = matches.is_present("random") || matches!(profile, ProfileType::Stealth | ProfileType::Chaos); + let randomize_targets = matches.contains_id("random") || matches!(profile, ProfileType::Stealth | ProfileType::Chaos); - let oui_file: String = match matches.value_of("oui-file") { + let oui_file: String = match matches.get_one::("oui-file") { Some(file) => file.to_string(), None => "/usr/share/arp-scan/ieee-oui.csv".to_string() }; - let hw_type = match matches.value_of("hw_type") { + let hw_type = match matches.get_one::("hw_type") { Some(hw_type_text) => { match hw_type_text.parse::() { @@ -439,7 +442,7 @@ impl ScanOptions { None => None }; - let hw_addr = match matches.value_of("hw_addr") { + let hw_addr = match matches.get_one::("hw_addr") { Some(hw_addr_text) => { match hw_addr_text.parse::() { @@ -453,7 +456,7 @@ impl ScanOptions { None => None }; - let proto_type = match matches.value_of("proto_type") { + let proto_type = match matches.get_one::("proto_type") { Some(proto_type_text) => { match proto_type_text.parse::() { @@ -467,7 +470,7 @@ impl ScanOptions { None => None }; - let proto_addr = match matches.value_of("proto_addr") { + let proto_addr = match matches.get_one::("proto_addr") { Some(proto_addr_text) => { match proto_addr_text.parse::() { @@ -481,7 +484,7 @@ impl ScanOptions { None => None }; - let arp_operation = match matches.value_of("arp_operation") { + let arp_operation = match matches.get_one::("arp_operation") { Some(arp_op_text) => { match arp_op_text.parse::() { @@ -547,7 +550,7 @@ mod tests { #[test] fn should_handle_single_ipv4_arg() { - let networks = ScanOptions::compute_networks(None, Some("192.168.1.20")); + let networks = ScanOptions::compute_networks(None, Some(&"192.168.1.20".to_string())); let target_network: Vec = vec![ IpNetwork::V4( @@ -561,7 +564,7 @@ mod tests { #[test] fn should_handle_multiple_ipv4_arg() { - let networks = ScanOptions::compute_networks(None, Some("192.168.1.20,192.168.1.50")); + let networks = ScanOptions::compute_networks(None, Some(&"192.168.1.20,192.168.1.50".to_string())); let target_network: Vec = vec![ IpNetwork::V4( @@ -578,7 +581,7 @@ mod tests { #[test] fn should_handle_single_network_arg() { - let networks = ScanOptions::compute_networks(None, Some("192.168.1.0/24")); + let networks = ScanOptions::compute_networks(None, Some(&"192.168.1.0/24".to_string())); let target_network: Vec = vec![ IpNetwork::V4( @@ -592,7 +595,7 @@ mod tests { #[test] fn should_handle_network_mix_arg() { - let networks = ScanOptions::compute_networks(None, Some("192.168.20.1,192.168.1.0/24,192.168.5.4/28")); + let networks = ScanOptions::compute_networks(None, Some(&"192.168.20.1,192.168.1.0/24,192.168.5.4/28".to_string())); let target_network: Vec = vec![ IpNetwork::V4( @@ -612,7 +615,7 @@ mod tests { #[test] fn should_handle_file_input() { - let networks = ScanOptions::compute_networks(Some("./data/ip-list.txt"), None); + let networks = ScanOptions::compute_networks(Some(&"./data/ip-list.txt".to_string()), None); let target_network: Vec = vec![ IpNetwork::V4( @@ -632,7 +635,7 @@ mod tests { #[test] fn should_fail_incorrect_network() { - let networks = ScanOptions::compute_networks(None, Some("500.10.10.10/24")); + let networks = ScanOptions::compute_networks(None, Some(&"500.10.10.10/24".to_string())); assert_eq!(networks, Err("Expected valid IPv4 network range (invalid address: 500.10.10.10/24)".to_string())); } @@ -640,7 +643,7 @@ mod tests { #[test] fn should_fail_unreadable_network() { - let networks = ScanOptions::compute_networks(None, Some("no-network")); + let networks = ScanOptions::compute_networks(None, Some(&"no-network".to_string())); assert_eq!(networks, Err("Expected valid IPv4 network range (invalid address: no-network)".to_string())); } diff --git a/src/main.rs b/src/main.rs index de519f3..6b219e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,8 +11,6 @@ use std::sync::Arc; use std::time::Duration; use std::sync::atomic::{AtomicBool, Ordering}; -use pnet::datalink; - use crate::args::{ScanOptions, OutputFormat}; use crate::network::NetworkIterator; use crate::vendor::Vendor; @@ -27,9 +25,9 @@ fn main() { // flag has been given in the request. Note that this can be done without // using a root account (this will be verified later). - let interfaces = datalink::interfaces(); + let interfaces = pnet_datalink::interfaces(); - if matches.is_present("list") { + if matches.contains_id("list") { utils::show_interfaces(&interfaces); process::exit(0); } @@ -59,13 +57,13 @@ fn main() { // while the main thread sends a batch of ARP requests for each IP in the // local network. - let channel_config = datalink::Config { + let channel_config = pnet_datalink::Config { read_timeout: Some(Duration::from_millis(network::DATALINK_RCV_TIMEOUT)), - ..datalink::Config::default() + ..pnet_datalink::Config::default() }; - let (mut tx, mut rx) = match datalink::channel(selected_interface, channel_config) { - Ok(datalink::Channel::Ethernet(tx, rx)) => (tx, rx), + let (mut tx, mut rx) = match pnet_datalink::channel(selected_interface, channel_config) { + Ok(pnet_datalink::Channel::Ethernet(tx, rx)) => (tx, rx), Ok(_) => { eprintln!("Expected an Ethernet datalink channel"); process::exit(1); diff --git a/src/network.rs b/src/network.rs index aa03398..7968154 100644 --- a/src/network.rs +++ b/src/network.rs @@ -9,7 +9,7 @@ use std::convert::TryInto; use dns_lookup::lookup_addr; use ipnetwork::IpNetwork; -use pnet::datalink::{MacAddr, NetworkInterface, DataLinkSender, DataLinkReceiver}; +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}; diff --git a/src/utils.rs b/src/utils.rs index 9427f90..870e7f3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,7 +2,7 @@ use std::env; use std::process; use std::sync::Arc; -use pnet::datalink::NetworkInterface; +use pnet_datalink::NetworkInterface; use ipnetwork::{IpNetwork, NetworkSize}; use serde::Serialize; use ansi_term::Color::{Green, Red}; diff --git a/src/vendor.rs b/src/vendor.rs index f1155cb..27c9742 100644 --- a/src/vendor.rs +++ b/src/vendor.rs @@ -1,7 +1,7 @@ use std::fs::File; use std::process; -use pnet::datalink::MacAddr; +use pnet_datalink::MacAddr; use csv::{Position, Reader}; // The Vendor structure performs search operations on a vendor database to find From d594fce6f9058f4a066421f17e5cc7d60b02e287 Mon Sep 17 00:00:00 2001 From: Saluki Date: Wed, 15 Jun 2022 00:01:36 +0200 Subject: [PATCH 107/121] Fixing nix package 0.22.1 vulnerability --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2cd5ed0..e500bb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -257,9 +257,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.22.1" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7555d6c7164cc913be1ce7f95cbecdabda61eb2ccd89008524af306fb7f5031" +checksum = "d3bb9a13fa32bc5aeb64150cd3f32d6cf4c748f8f8a417cce5d2eb976a8370ba" dependencies = [ "bitflags", "cc", From 254135e37b6b278c4db023a4f8db38d6a8db9834 Mon Sep 17 00:00:00 2001 From: Saluki Date: Wed, 15 Jun 2022 00:04:30 +0200 Subject: [PATCH 108/121] Fix serde YAML vulnerability --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 67d6c6e..765a6bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,4 +30,4 @@ dns-lookup = "1.0" csv = "1.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -serde_yaml = "0.8" +serde_yaml = "0.8.4" From 1628f9bedbac9cd34e6bd7b0e5363969f8893554 Mon Sep 17 00:00:00 2001 From: Saluki Date: Wed, 15 Jun 2022 00:32:09 +0200 Subject: [PATCH 109/121] Use Rust 1.61 for CI --- .semaphore/Dockerfile | 7 +++++++ .semaphore/semaphore.yml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .semaphore/Dockerfile diff --git a/.semaphore/Dockerfile b/.semaphore/Dockerfile new file mode 100644 index 0000000..e0f8f25 --- /dev/null +++ b/.semaphore/Dockerfile @@ -0,0 +1,7 @@ +FROM rust:1.61.0-slim + +# Install SemaphoreCI dependencies +# https://docs.semaphoreci.com/ci-cd-environment/custom-ci-cd-environment-with-docker/ +RUN apt-get update && apt-get install -y curl git openssh-client + +CMD ["/bin/bash"] diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index f65c988..34698cc 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -7,7 +7,7 @@ agent: containers: # Rust 1.56 (2021 edition) is currently not supported in Semaphore - name: main - image: 'saluki/rust-ci:1.57' + image: 'saluki/rust-ci:1.61' blocks: - name: Test release task: From d721d6fa59feb02e87ee90f540104b7ffc8d894d Mon Sep 17 00:00:00 2001 From: Saluki Date: Wed, 15 Jun 2022 00:59:54 +0200 Subject: [PATCH 110/121] Publish 0.13.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e500bb4..8c12f20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,7 +22,7 @@ dependencies = [ [[package]] name = "arp-scan" -version = "0.13.0" +version = "0.13.1" dependencies = [ "ansi_term", "clap", diff --git a/Cargo.toml b/Cargo.toml index 765a6bc..533f36e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "arp-scan" description = "A minimalistic ARP scan tool" license = "AGPL-3.0-or-later" -version = "0.13.0" +version = "0.13.1" authors = ["Saluki"] edition = "2021" readme = "README.md" From 5d759405114912266d608ba7cd14e497983df12c Mon Sep 17 00:00:00 2001 From: Saluki Date: Wed, 15 Jun 2022 01:13:29 +0200 Subject: [PATCH 111/121] Fine-tuning deb archive --- release.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/release.sh b/release.sh index 1a82316..a072ab5 100755 --- a/release.sh +++ b/release.sh @@ -22,13 +22,13 @@ cp -p ./target/x86_64-unknown-linux-gnu/release/arp-scan ./builds/arp-scan-v$CLI # Build the deb archive mkdir -p ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/DEBIAN echo "Package: arp-scan-rs" > ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/DEBIAN/control -echo "Version: 0.13.0" >> ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/DEBIAN/control -echo "Architecture: all" >> ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/DEBIAN/control +echo "Version: $CLI_VERSION" >> ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/DEBIAN/control +echo "Architecture: amd64" >> ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/DEBIAN/control echo "Maintainer: Saluki" >> ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/DEBIAN/control echo "Description: Minimalist ARP scan written in Rust" >> ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/DEBIAN/control mkdir -p ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/usr/local/bin -cp ./builds/arp-scan-v$CLI_VERSION-x86_64-unknown-linux-glibc ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/usr/local/bin/arp-scan -(cd ./builds && dpkg-deb --build --root-owner-group arp-scan-rs_0.13.0-1_amd64) +cp ./builds/arp-scan-v$CLI_VERSION-x86_64-unknown-linux-musl ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/usr/local/bin/arp-scan +(cd ./builds && dpkg-deb --build --root-owner-group arp-scan-rs_$CLI_VERSION-1_amd64) echo "Update the README instructions for v$CLI_VERSION" echo " ✓ Publish on crates.io" From ed802f3fecab6f6560b5b9a0bd2c3fe86a2959d4 Mon Sep 17 00:00:00 2001 From: Saluki Date: Wed, 15 Jun 2022 01:18:59 +0200 Subject: [PATCH 112/121] Update 0.13.1 docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 444846c..41412a3 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ ARP scan finished, 5 hosts found in 1.623 seconds Download the `arp-scan` binary for Linux (Ubuntu, Fedora, Debian, ...). See the [releases page](https://github.com/Saluki/arp-scan-rs/releases) for other binaries. ```bash -wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.13.0/arp-scan-v0.13.0-x86_64-unknown-linux-musl && chmod +x ./arp-scan +wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.13.1/arp-scan-v0.13.1-x86_64-unknown-linux-musl && chmod +x ./arp-scan ``` Optionnaly, fetch the IEEE OUI reference file (CSV format) that contains all MAC address vendors. From 55e7a6722e87e9cd70ff8778b043ca8adff71f7f Mon Sep 17 00:00:00 2001 From: Saluki Date: Fri, 23 Sep 2022 08:25:21 +0200 Subject: [PATCH 113/121] Performing minor upgrades --- Cargo.lock | 64 ++++++++++++++++++++++---------------------------- Cargo.toml | 2 +- src/network.rs | 6 ++--- 3 files changed, 31 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c12f20..c0e2660 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,7 +123,7 @@ checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" dependencies = [ "bstr", "csv-core", - "itoa", + "itoa 0.4.7", "ryu", "serde", ] @@ -159,12 +159,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "dtoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" - [[package]] name = "getrandom" version = "0.2.3" @@ -184,9 +178,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hermit-abi" @@ -199,9 +193,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.7.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", "hashbrown", @@ -222,6 +216,12 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +[[package]] +name = "itoa" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" + [[package]] name = "lazy_static" version = "1.4.0" @@ -234,12 +234,6 @@ version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" -[[package]] -name = "linked-hash-map" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" - [[package]] name = "memchr" version = "2.4.0" @@ -467,18 +461,18 @@ checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" [[package]] name = "serde" -version = "1.0.126" +version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +checksum = "e590c437916fb6b221e1d00df6e3294f3fccd70ca7e92541c475d6ed6ef5fee2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.126" +version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +checksum = "34b5b8d809babe02f538c2cfec6f2c1ed10804c0e5a6a041a049a4f5588ccc2e" dependencies = [ "proc-macro2", "quote", @@ -491,21 +485,22 @@ version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" dependencies = [ - "itoa", + "itoa 0.4.7", "ryu", "serde", ] [[package]] name = "serde_yaml" -version = "0.8.17" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23" +checksum = "79b7c9017c64a49806c6e8df8ef99b92446d09c92457f85f91835b01a8064ae0" dependencies = [ - "dtoa", - "linked-hash-map", + "indexmap", + "itoa 1.0.3", + "ryu", "serde", - "yaml-rust", + "unsafe-libyaml", ] [[package]] @@ -556,6 +551,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" +[[package]] +name = "unsafe-libyaml" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "931179334a56395bcf64ba5e0ff56781381c1a5832178280c7d7f91d1679aeb0" + [[package]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" @@ -592,12 +593,3 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] diff --git a/Cargo.toml b/Cargo.toml index 533f36e..8591f96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,4 +30,4 @@ dns-lookup = "1.0" csv = "1.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -serde_yaml = "0.8.4" +serde_yaml = "0.9" diff --git a/src/network.rs b/src/network.rs index 7968154..70ffa4b 100644 --- a/src/network.rs +++ b/src/network.rs @@ -95,11 +95,9 @@ pub fn compute_network_configuration<'a>(interfaces: &'a [NetworkInterface], sca process::exit(1); }); - let ip_networks: Vec<&IpNetwork> = match &scan_options.network_range { + let ip_networks: Vec<&ipnetwork::IpNetwork> = match &scan_options.network_range { Some(network_range) => network_range.iter().collect(), - None => selected_interface.ips.iter() - .filter(|ip_network| ip_network.is_ipv4()) - .collect() + None => selected_interface.ips.iter().filter(|ip_network| ip_network.is_ipv4()).collect() }; (selected_interface, ip_networks) From 86dc7a529455a4b532c2f1b84440d7b366588d01 Mon Sep 17 00:00:00 2001 From: Saluki Date: Fri, 23 Sep 2022 08:25:59 +0200 Subject: [PATCH 114/121] Add --packet-help option --- src/args.rs | 22 +++++++++++++++++++--- src/main.rs | 5 +++++ src/utils.rs | 25 +++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/args.rs b/src/args.rs index 12f6829..e324cdd 100644 --- a/src/args.rs +++ b/src/args.rs @@ -29,8 +29,11 @@ const EXAMPLES_HELP: &str = "EXAMPLES: # List network interfaces arp-scan -l + # Launch a scan on a specific range + arp-scan -i eth0 -n 10.37.3.1,10.37.4.55/24 + # Launch a scan on WiFi interface with fake IP and stealth profile - arp-scan -i wlp1s0 --source-ip 192.168.0.42 --profile stealth + arp-scan -i eth0 --source-ip 192.168.0.42 --profile stealth # Launch a scan on VLAN 45 with JSON output arp-scan -Q 45 -o json @@ -158,6 +161,11 @@ pub fn build_args<'a>() -> Command<'a> { .takes_value(true).value_name("OPERATION_ID") .help("Custom ARP operation ID") ) + .arg( + Arg::new("packet_help").long("packet-help") + .takes_value(false) + .help("Print details about an ARP packet") + ) .after_help(EXAMPLES_HELP) } @@ -199,7 +207,8 @@ pub struct ScanOptions { pub hw_addr: Option, pub proto_type: Option, pub proto_addr: Option, - pub arp_operation: Option + pub arp_operation: Option, + pub packet_help: bool } impl ScanOptions { @@ -497,6 +506,8 @@ impl ScanOptions { }, None => None }; + + let packet_help = matches.contains_id("packet_help"); Arc::new(ScanOptions { profile, @@ -517,7 +528,8 @@ impl ScanOptions { hw_addr, proto_type, proto_addr, - arp_operation + arp_operation, + packet_help }) } @@ -531,6 +543,10 @@ impl ScanOptions { matches!(&self.vlan_id, Some(_)) } + pub fn request_protocol_print(&self) -> bool { + self.packet_help + } + } diff --git a/src/main.rs b/src/main.rs index 6b219e1..5b40a2b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,6 +39,11 @@ fn main() { // with an IPv4 address and root permissions (for crafting ARP packets). let scan_options = ScanOptions::new(&matches); + + if scan_options.request_protocol_print() { + utils::print_ascii_packet(); + process::exit(0); + } if !utils::is_root_user() { eprintln!("Should run this binary as root or use --help for options"); diff --git a/src/utils.rs b/src/utils.rs index 870e7f3..4aefc8b 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -60,6 +60,31 @@ pub fn show_interfaces(interfaces: &[NetworkInterface]) { println!(); } +pub fn print_ascii_packet() { + + println!(); + println!(" 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 "); + println!("+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+"); + println!("| Hardware type | Protocol type |"); + println!("+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-|"); + println!("| Hlen | Plen | Operation |"); + println!("+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+"); + println!("| Sender HA |"); + println!("+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+"); + println!("| Sender HA | Sender IP |"); + println!("+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-|"); + println!("| Sender IP | Target HA |"); + println!("+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-|"); + println!("| Target HA |"); + println!("+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+"); + println!("| Target IP |"); + println!("+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+"); + println!(); + println!(" - Hardware type (2 bytes), use --hw-type option to change"); + println!(" - Protocol type (2 bytes), use --proto-type option to change"); + println!(); +} + /** * Find a default network interface for scans, based on the operating system * priority and some interface technical details. From 7eaf3762ab59022bf689aeceee9d7ee8d6c84bd4 Mon Sep 17 00:00:00 2001 From: Saluki Date: Thu, 20 Apr 2023 12:54:05 +0200 Subject: [PATCH 115/121] Dependency upgrades --- Cargo.lock | 311 ++++++++++++++++++++++++++++------------------------- 1 file changed, 164 insertions(+), 147 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c0e2660..22efbb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] @@ -51,39 +51,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bitflags" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" - -[[package]] -name = "bstr" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279" -dependencies = [ - "lazy_static", - "memchr", - "regex-automata", - "serde", -] - -[[package]] -name = "byteorder" -version = "1.4.3" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - -[[package]] -name = "cc" -version = "1.0.70" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "cfg-if" @@ -93,9 +69,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "3.2.1" +version = "3.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a836566fa5f52f7ddf909a8a2f9029b9f78ca584cd95cf7e87f8073110f4c5c9" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ "atty", "bitflags", @@ -108,22 +84,21 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5538cd660450ebeb4234cfecf8f2284b844ffc4c50531e66d584ad5b91293613" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" dependencies = [ "os_str_bytes", ] [[package]] name = "csv" -version = "1.1.6" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" +checksum = "0b015497079b9a9d69c02ad25de6c0a6edef051ea6360a327d0bd05802ef64ad" dependencies = [ - "bstr", "csv-core", - "itoa 0.4.7", + "itoa", "ryu", "serde", ] @@ -139,19 +114,19 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.2.0" +version = "3.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "377c9b002a72a0b2c1a18c62e2f3864bdfea4a015e3683a96e24aa45dd6c02d1" +checksum = "bbcf33c2a618cbe41ee43ae6e9f2e48368cd9f9db2896f10167d8d762679f639" dependencies = [ "nix", - "winapi", + "windows-sys", ] [[package]] name = "dns-lookup" -version = "1.0.6" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb4c5ce3a7034c5eb66720bb16e9ac820e01b29032ddc06dd0fe47072acf7454" +checksum = "53ecafc952c4528d9b51a458d1a8904b81783feff9fde08ab6ed2545ff396872" dependencies = [ "cfg-if", "libc", @@ -161,9 +136,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.3" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ "cfg-if", "libc", @@ -172,9 +147,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "hashbrown" @@ -184,18 +159,18 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hermit-abi" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "indexmap" -version = "1.9.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown", @@ -212,54 +187,32 @@ dependencies = [ [[package]] name = "itoa" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" - -[[package]] -name = "itoa" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" - -[[package]] -name = "lazy_static" -version = "1.4.0" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "libc" -version = "0.2.126" +version = "0.2.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" [[package]] name = "memchr" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" - -[[package]] -name = "memoffset" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" -dependencies = [ - "autocfg", -] +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "nix" -version = "0.22.2" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3bb9a13fa32bc5aeb64150cd3f32d6cf4c748f8f8a417cce5d2eb976a8370ba" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" dependencies = [ "bitflags", - "cc", "cfg-if", "libc", - "memoffset", + "static_assertions", ] [[package]] @@ -270,9 +223,9 @@ checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" [[package]] name = "os_str_bytes" -version = "6.0.0" +version = "6.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" [[package]] name = "pnet" @@ -317,7 +270,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn", + "syn 1.0.109", ] [[package]] @@ -365,45 +318,44 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.10" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.39" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.18" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ "proc-macro2", ] [[package]] name = "rand" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", - "rand_hc", ] [[package]] name = "rand_chacha" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", @@ -411,93 +363,75 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] -[[package]] -name = "rand_hc" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" -dependencies = [ - "rand_core", -] - [[package]] name = "regex" -version = "1.5.6" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" +checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] -[[package]] -name = "regex-automata" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" -dependencies = [ - "byteorder", -] - [[package]] name = "regex-syntax" -version = "0.6.26" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "ryu" -version = "1.0.5" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "serde" -version = "1.0.142" +version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590c437916fb6b221e1d00df6e3294f3fccd70ca7e92541c475d6ed6ef5fee2" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.142" +version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b5b8d809babe02f538c2cfec6f2c1ed10804c0e5a6a041a049a4f5588ccc2e" +checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] name = "serde_json" -version = "1.0.64" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ - "itoa 0.4.7", + "itoa", "ryu", "serde", ] [[package]] name = "serde_yaml" -version = "0.9.4" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b7c9017c64a49806c6e8df8ef99b92446d09c92457f85f91835b01a8064ae0" +checksum = "d9d684e3ec7de3bf5466b32bd75303ac16f0736426e5a4e0d6e489559ce1249c" dependencies = [ "indexmap", - "itoa 1.0.3", + "itoa", "ryu", "serde", "unsafe-libyaml", @@ -505,14 +439,20 @@ dependencies = [ [[package]] name = "socket2" -version = "0.4.0" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.10.0" @@ -521,9 +461,20 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.96" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" dependencies = [ "proc-macro2", "quote", @@ -532,36 +483,36 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.1.2" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" dependencies = [ "winapi-util", ] [[package]] name = "textwrap" -version = "0.15.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "unicode-ident" -version = "1.0.1" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" [[package]] name = "unsafe-libyaml" -version = "0.2.2" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "931179334a56395bcf64ba5e0ff56781381c1a5832178280c7d7f91d1679aeb0" +checksum = "1865806a559042e51ab5414598446a5871b561d21b6764f2eabb0dd481d880a6" [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "winapi" @@ -593,3 +544,69 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" From 68628514eb57d1f7b64b6ef1bbf35bb03a44ed12 Mon Sep 17 00:00:00 2001 From: Saluki Date: Thu, 20 Apr 2023 13:55:41 +0200 Subject: [PATCH 116/121] MSRV 1.64 and Clap 4 --- Cargo.lock | 327 ++++++++++++++++++++++++++++++++++++------------- Cargo.toml | 9 +- src/args.rs | 62 +++++----- src/main.rs | 2 +- src/network.rs | 4 +- src/time.rs | 2 +- 6 files changed, 284 insertions(+), 122 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 22efbb6..3f3fd2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,55 @@ dependencies = [ "winapi", ] +[[package]] +name = "anstream" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e579a7752471abc2a8268df8b20005e3eadd975f585398f17efcfd8d4927371" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" + +[[package]] +name = "anstyle-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcd8291a340dd8ac70e18878bc4501dd7b4ff970cfa21c207d36ece51ea88fd" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + [[package]] name = "arp-scan" version = "0.13.1" @@ -38,17 +87,6 @@ dependencies = [ "serde_yaml", ] -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -61,6 +99,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + [[package]] name = "cfg-if" version = "1.0.0" @@ -69,27 +113,37 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "3.2.23" +version = "4.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956ac1f6381d8d82ab4684768f89c0ea3afe66925ceadb4eeb3fc452ffc55d62" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +checksum = "84080e799e54cff944f4b4a4b0e71630b0e0443b25b985175c7dddc1a859b749" dependencies = [ - "atty", + "anstream", + "anstyle", "bitflags", "clap_lex", - "indexmap", "strsim", - "termcolor", - "textwrap", ] [[package]] name = "clap_lex" -version = "0.2.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] +checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "csv" @@ -119,7 +173,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbcf33c2a618cbe41ee43ae6e9f2e48368cd9f9db2896f10167d8d762679f639" dependencies = [ "nix", - "windows-sys", + "windows-sys 0.45.0", ] [[package]] @@ -134,6 +188,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "getrandom" version = "0.2.9" @@ -159,12 +234,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" [[package]] name = "indexmap" @@ -176,15 +248,38 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "io-lifetimes" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "ipnetwork" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f84f1612606f3753f205a4e9a2efd6fe5b4c573a6269b2cc6c3003d44a0d127" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" dependencies = [ "serde", ] +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi", + "io-lifetimes", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "itoa" version = "1.0.6" @@ -197,6 +292,12 @@ version = "0.2.141" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" +[[package]] +name = "linux-raw-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b085a4f2cde5781fc4b1717f2e86c62f5cda49de7ba99a7c2eae02b61c9064c" + [[package]] name = "memchr" version = "2.5.0" @@ -221,38 +322,34 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" -[[package]] -name = "os_str_bytes" -version = "6.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" - [[package]] name = "pnet" -version = "0.31.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0caaf5b11fd907ff15cf14a4477bfabca4b37ab9e447a4f8dead969a59cdafad" +checksum = "cd959a8268165518e2bf5546ba84c7b3222744435616381df3c456fe8d983576" dependencies = [ + "ipnetwork", "pnet_base", "pnet_datalink", "pnet_packet", + "pnet_sys", "pnet_transport", ] [[package]] name = "pnet_base" -version = "0.31.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d3a993d49e5fd5d4d854d6999d4addca1f72d86c65adf224a36757161c02b6" +checksum = "872e46346144ebf35219ccaa64b1dffacd9c6f188cd7d012bd6977a2a838f42e" dependencies = [ "no-std-net", ] [[package]] name = "pnet_datalink" -version = "0.31.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e466faf03a98ad27f6e15cd27a2b7cc89e73e640a43527742977bc503c37f8aa" +checksum = "c302da22118d2793c312a35fb3da6846cb0fab6c3ad53fd67e37809b06cdafce" dependencies = [ "ipnetwork", "libc", @@ -263,9 +360,9 @@ dependencies = [ [[package]] name = "pnet_macros" -version = "0.31.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dd52a5211fac27e7acb14cfc9f30ae16ae0e956b7b779c8214c74559cef4c3" +checksum = "2a780e80005c2e463ec25a6e9f928630049a10b43945fea83207207d4a7606f4" dependencies = [ "proc-macro2", "quote", @@ -275,18 +372,18 @@ dependencies = [ [[package]] name = "pnet_macros_support" -version = "0.31.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89de095dc7739349559913aed1ef6a11e73ceade4897dadc77c5e09de6740750" +checksum = "e6d932134f32efd7834eb8b16d42418dac87086347d1bc7d142370ef078582bc" dependencies = [ "pnet_base", ] [[package]] name = "pnet_packet" -version = "0.31.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc3b5111e697c39c8b9795b9fdccbc301ab696699e88b9ea5a4e4628978f495f" +checksum = "8bde678bbd85cb1c2d99dc9fc596e57f03aa725f84f3168b0eaf33eeccb41706" dependencies = [ "glob", "pnet_base", @@ -296,9 +393,9 @@ dependencies = [ [[package]] name = "pnet_sys" -version = "0.31.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "328e231f0add6d247d82421bf3790b4b33b39c8930637f428eef24c4c6a90805" +checksum = "faf7a58b2803d818a374be9278a1fe8f88fce14b936afbe225000cfcd9c73f16" dependencies = [ "libc", "winapi", @@ -306,9 +403,9 @@ dependencies = [ [[package]] name = "pnet_transport" -version = "0.31.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff597185e6f1f5671b3122e4dba892a1c73e17c17e723d7669bd9299cbe7f124" +checksum = "813d1c0e4defbe7ee22f6fe1755f122b77bfb5abe77145b1b5baaf463cab9249" dependencies = [ "libc", "pnet_base", @@ -387,6 +484,20 @@ version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +[[package]] +name = "rustix" +version = "0.37.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79bef90eb6d984c72722595b5b1348ab39275a5e5123faca6863bf07d75a4e0" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + [[package]] name = "ryu" version = "1.0.13" @@ -481,21 +592,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "termcolor" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "textwrap" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" - [[package]] name = "unicode-ident" version = "1.0.8" @@ -508,6 +604,12 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1865806a559042e51ab5414598446a5871b561d21b6764f2eabb0dd481d880a6" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -530,15 +632,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -551,7 +644,16 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets", + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", ] [[package]] @@ -560,13 +662,28 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", ] [[package]] @@ -575,38 +692,80 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" diff --git a/Cargo.toml b/Cargo.toml index 8591f96..51da0f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,19 +11,20 @@ repository = "https://github.com/Saluki/arp-scan-rs" keywords = ["arp", "scan", "network", "security"] categories = ["command-line-utilities"] exclude = ["/.semaphore", "/data", "/release.sh", ".*"] +rust-version = "1.64" [dependencies] # CLI & utilities -clap = { version = "3.2", default-features = false, features = ["std", "suggestions", "color"] } +clap = { version = "4.2", default-features = false, features = ["std", "suggestions", "color", "help"] } ansi_term = "0.12" rand = "0.8" ctrlc = "3.2" # Network -pnet = "0.31" -pnet_datalink = "0.31" -ipnetwork = "0.19" +pnet = "0.33" +pnet_datalink = "0.33" +ipnetwork = "0.20" dns-lookup = "1.0" # Parsing & exports diff --git a/src/args.rs b/src/args.rs index e324cdd..8f27aa3 100644 --- a/src/args.rs +++ b/src/args.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use std::path::Path; use std::fs; -use clap::{Arg, ArgMatches, Command}; +use clap::{Arg, ArgMatches, Command, ArgAction}; use ipnetwork::IpNetwork; use pnet_datalink::MacAddr; use pnet::packet::arp::{ArpHardwareType, ArpOperation}; @@ -44,126 +44,128 @@ const EXAMPLES_HELP: &str = "EXAMPLES: * This function groups together all exposed CLI arguments to the end-users * with clap. Other CLI details (version, ...) should be grouped there as well. */ -pub fn build_args<'a>() -> Command<'a> { +pub fn build_args() -> Command { Command::new("arp-scan") .version(CLI_VERSION) .about("A minimalistic ARP scan tool written in Rust") .arg( Arg::new("profile").short('p').long("profile") - .takes_value(true).value_name("PROFILE_NAME") + .value_name("PROFILE_NAME") .help("Scan profile") ) .arg( Arg::new("interface").short('i').long("interface") - .takes_value(true).value_name("INTERFACE_NAME") + .value_name("INTERFACE_NAME") .help("Network interface") ) .arg( Arg::new("network").short('n').long("network") - .takes_value(true).value_name("NETWORK_RANGE") + .value_name("NETWORK_RANGE") .help("Network range to scan") ) .arg( Arg::new("file").short('f').long("file") - .takes_value(true).value_name("FILE_PATH") + .value_name("FILE_PATH") .conflicts_with("network") .help("Read IPv4 addresses from a file") ) .arg( Arg::new("timeout").short('t').long("timeout") - .takes_value(true).value_name("TIMEOUT_DURATION") + .value_name("TIMEOUT_DURATION") .help("ARP response timeout") ) .arg( Arg::new("source_ip").short('S').long("source-ip") - .takes_value(true).value_name("SOURCE_IPV4") + .value_name("SOURCE_IPV4") .help("Source IPv4 address for requests") ) .arg( Arg::new("destination_mac").short('M').long("dest-mac") - .takes_value(true).value_name("DESTINATION_MAC") + .value_name("DESTINATION_MAC") .help("Destination MAC address for requests") ) .arg( Arg::new("source_mac").long("source-mac") - .takes_value(true).value_name("SOURCE_MAC") + .value_name("SOURCE_MAC") .help("Source MAC address for requests") ) .arg( Arg::new("numeric").long("numeric") - .takes_value(false) + .action(ArgAction::SetTrue) .help("Numeric mode, no hostname resolution") ) .arg( Arg::new("vlan").short('Q').long("vlan") - .takes_value(true).value_name("VLAN_ID") + .value_name("VLAN_ID") .help("Send using 802.1Q with VLAN ID") ) .arg( Arg::new("retry_count").short('r').long("retry") - .takes_value(true).value_name("RETRY_COUNT") + .value_name("RETRY_COUNT") .help("Host retry attempt count") ) .arg( Arg::new("random").short('R').long("random") - .takes_value(false) + .action(ArgAction::SetTrue) .help("Randomize the target list") ) .arg( Arg::new("interval").short('I').long("interval") - .takes_value(true).value_name("INTERVAL_DURATION") + .value_name("INTERVAL_DURATION") .help("Milliseconds between ARP requests") ) .arg( Arg::new("bandwidth").short('B').long("bandwidth") - .takes_value(true).value_name("BITS") + .value_name("BITS") .conflicts_with("interval") .help("Limit scan bandwidth (bits/second)") ) .arg( Arg::new("oui-file").long("oui-file") - .takes_value(true).value_name("FILE_PATH") + .value_name("FILE_PATH") .help("Path to custom IEEE OUI CSV file") ) .arg( Arg::new("list").short('l').long("list") - .takes_value(false) + .action(ArgAction::SetTrue) + .exclusive(true) .help("List network interfaces") ) .arg( Arg::new("output").short('o').long("output") - .takes_value(true).value_name("FORMAT") + .value_name("FORMAT") .help("Define output format") ) .arg( Arg::new("hw_type").long("hw-type") - .takes_value(true).value_name("HW_TYPE") + .value_name("HW_TYPE") .help("Custom ARP hardware field") ) .arg( Arg::new("hw_addr").long("hw-addr") - .takes_value(true).value_name("ADDRESS_LEN") + .value_name("ADDRESS_LEN") .help("Custom ARP hardware address length") ) .arg( Arg::new("proto_type").long("proto-type") - .takes_value(true).value_name("PROTO_TYPE") + .value_name("PROTO_TYPE") .help("Custom ARP proto type") ) .arg( Arg::new("proto_addr").long("proto-addr") - .takes_value(true).value_name("ADDRESS_LEN") + .value_name("ADDRESS_LEN") .help("Custom ARP proto address length") ) .arg( Arg::new("arp_operation").long("arp-op") - .takes_value(true).value_name("OPERATION_ID") + .value_name("OPERATION_ID") .help("Custom ARP operation ID") ) .arg( Arg::new("packet_help").long("packet-help") - .takes_value(false) + .action(ArgAction::SetTrue) + .exclusive(true) .help("Print details about an ARP packet") ) .after_help(EXAMPLES_HELP) @@ -208,7 +210,7 @@ pub struct ScanOptions { pub proto_type: Option, pub proto_addr: Option, pub arp_operation: Option, - pub packet_help: bool + pub packet_help: bool, } impl ScanOptions { @@ -336,7 +338,7 @@ impl ScanOptions { }; // Hostnames will not be resolved in numeric mode or stealth profile - let resolve_hostname = !matches.contains_id("numeric") && !matches!(profile, ProfileType::Stealth); + let resolve_hostname = !matches.get_flag("numeric") && !matches!(profile, ProfileType::Stealth); let source_ipv4: Option = match matches.get_one::("source_ip") { Some(source_ip) => { @@ -430,7 +432,7 @@ impl ScanOptions { None => OutputFormat::Plain }; - let randomize_targets = matches.contains_id("random") || matches!(profile, ProfileType::Stealth | ProfileType::Chaos); + let randomize_targets = matches.get_flag("random") || matches!(profile, ProfileType::Stealth | ProfileType::Chaos); let oui_file: String = match matches.get_one::("oui-file") { Some(file) => file.to_string(), @@ -507,7 +509,7 @@ impl ScanOptions { None => None }; - let packet_help = matches.contains_id("packet_help"); + let packet_help = matches.get_flag("packet_help"); Arc::new(ScanOptions { profile, @@ -529,7 +531,7 @@ impl ScanOptions { proto_type, proto_addr, arp_operation, - packet_help + packet_help, }) } diff --git a/src/main.rs b/src/main.rs index 5b40a2b..3d3a1f0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,7 +27,7 @@ fn main() { let interfaces = pnet_datalink::interfaces(); - if matches.contains_id("list") { + if matches.get_flag("list") { utils::show_interfaces(&interfaces); process::exit(0); } diff --git a/src/network.rs b/src/network.rs index 70ffa4b..0b2edda 100644 --- a/src/network.rs +++ b/src/network.rs @@ -136,7 +136,7 @@ pub fn compute_scan_estimation(host_count: u128, options: &Arc) -> ScanTiming::Bandwidth(bandwidth) => { let bandwidth_lg: u128 = bandwidth.into(); - let request_phase_ms: u128 = (request_size * 1000) as u128 / bandwidth_lg; + let request_phase_ms: u128 = (request_size * 1000) / bandwidth_lg; let interval_ms: u128 = (request_phase_ms/retry_count/host_count) - avg_arp_request_ms; (interval_ms.try_into().unwrap(), bandwidth_lg, request_phase_ms) @@ -432,7 +432,7 @@ pub fn receive_arp_responses(rx: &mut Box, options: Arc String { } let hours: u128 = milliseconds / 1000 / 60 / 60; - return format!("{}h", hours); + format!("{}h", hours) } #[cfg(test)] From 894dc6dbafd40230cf190fa310f6453989abd542 Mon Sep 17 00:00:00 2001 From: Saluki Date: Tue, 2 May 2023 15:00:54 +0200 Subject: [PATCH 117/121] Moving CI to Rust 1.64 --- .semaphore/Dockerfile | 2 +- .semaphore/semaphore.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.semaphore/Dockerfile b/.semaphore/Dockerfile index e0f8f25..4dbc30d 100644 --- a/.semaphore/Dockerfile +++ b/.semaphore/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.61.0-slim +FROM rust:1.64.0-slim # Install SemaphoreCI dependencies # https://docs.semaphoreci.com/ci-cd-environment/custom-ci-cd-environment-with-docker/ diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 34698cc..79b3c74 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -5,9 +5,9 @@ agent: type: e1-standard-2 os_image: ubuntu2004 containers: - # Rust 1.56 (2021 edition) is currently not supported in Semaphore + # Rust 1.64 (2021 edition) is currently not supported in Semaphore - name: main - image: 'saluki/rust-ci:1.61' + image: 'saluki/rust-ci:1.64' blocks: - name: Test release task: From 4139715035a453c2ecbea24be05b3ec8b8298ea2 Mon Sep 17 00:00:00 2001 From: umizato Date: Tue, 2 May 2023 14:05:38 -0700 Subject: [PATCH 118/121] Added crate sudo in order to provide an easier user experience by giving them an opportunity to escalate their privileges to root when running the app. This is in contrast to having the app exit and the user having the run the app again as sudo. --- Cargo.lock | 20 ++++++++++++++++++++ Cargo.toml | 1 + src/main.rs | 5 +++++ 3 files changed, 26 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 3f3fd2a..5617d4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,6 +85,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "sudo", ] [[package]] @@ -298,6 +299,15 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b085a4f2cde5781fc4b1717f2e86c62f5cda49de7ba99a7c2eae02b61c9064c" +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + [[package]] name = "memchr" version = "2.5.0" @@ -570,6 +580,16 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "sudo" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bd84d4c082e18e37fef52c0088e4407dabcef19d23a607fb4b5ee03b7d5b83" +dependencies = [ + "libc", + "log", +] + [[package]] name = "syn" version = "1.0.109" diff --git a/Cargo.toml b/Cargo.toml index 51da0f0..d348e29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,3 +32,4 @@ csv = "1.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.9" +sudo = "0.6.0" diff --git a/src/main.rs b/src/main.rs index 3d3a1f0..84b9d55 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,11 @@ use crate::network::NetworkIterator; use crate::vendor::Vendor; fn main() { + // Upgrade user privileges when needed + // ---------------------------------------- + // Providing a prompt for the user when + // the app is run and user is not root + sudo::escalate_if_needed().expect("You need root permissions to run this app. Unable to escalate to sudo"); let matches = args::build_args().get_matches(); From 316638cbb3d241567c5b6cfe04be316b932eadc7 Mon Sep 17 00:00:00 2001 From: umizato Date: Wed, 3 May 2023 13:57:06 -0700 Subject: [PATCH 119/121] Removed function to check sudo status in main.rs as this is not needed with sudo crate which automatically upgrades user privileges on app startup. --- src/main.rs | 5 ----- src/utils.rs | 9 --------- 2 files changed, 14 deletions(-) diff --git a/src/main.rs b/src/main.rs index 84b9d55..a6ecde6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,11 +50,6 @@ fn main() { process::exit(0); } - if !utils::is_root_user() { - eprintln!("Should run this binary as root or use --help for options"); - process::exit(1); - } - let (selected_interface, ip_networks) = network::compute_network_configuration(&interfaces, &scan_options); if scan_options.is_plain_output() { diff --git a/src/utils.rs b/src/utils.rs index 4aefc8b..0189b7f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,3 @@ -use std::env; use std::process; use std::sync::Arc; @@ -10,14 +9,6 @@ use ansi_term::Color::{Green, Red}; use crate::network::{ResponseSummary, TargetDetails}; use crate::args::ScanOptions; -/** - * Based on the current UNIX environment, find if the process is run as root - * user. This approach only supports Linux-like systems (Ubuntu, Fedore, ...). - */ -pub fn is_root_user() -> bool { - env::var("USER").unwrap_or_else(|_| String::from("")) == *"root" -} - /** * Prints on stdout a list of all available network interfaces with some * technical details. The goal is to present the most useful technical details From aa58f71a970d37780e1e6af0585781708938ce0f Mon Sep 17 00:00:00 2001 From: umizato Date: Wed, 3 May 2023 14:06:47 -0700 Subject: [PATCH 120/121] Removed patch number in dependency for sudo crate to align with the rest of the Cargo dependencies. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index d348e29..337325f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,4 +32,4 @@ csv = "1.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.9" -sudo = "0.6.0" +sudo = "0.6" From 0e56929d9000520c07b45de3bddc614ea6989c7a Mon Sep 17 00:00:00 2001 From: gsuyemoto Date: Fri, 5 May 2023 11:26:44 -0700 Subject: [PATCH 121/121] Moved the sudo escalate call to after scan_options.request_protocol_print() to allow users to use app for duties that do not require root access (e.g. view protocol ASCII print and list available interfaces). --- src/main.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index a6ecde6..d85cc26 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,12 +16,6 @@ use crate::network::NetworkIterator; use crate::vendor::Vendor; fn main() { - // Upgrade user privileges when needed - // ---------------------------------------- - // Providing a prompt for the user when - // the app is run and user is not root - sudo::escalate_if_needed().expect("You need root permissions to run this app. Unable to escalate to sudo"); - let matches = args::build_args().get_matches(); // Find interfaces & list them if requested @@ -50,6 +44,16 @@ fn main() { process::exit(0); } + // Upgrade user privileges when needed + // ---------------------------------------- + // Providing a prompt for the user when + // the app is run and user is not root + sudo::escalate_if_needed().expect("You need root permissions to run this app. Unable to escalate to sudo"); + + // Get network configuration + // ------------------------- + // See args.rs and in particular the struct ScanOptions + // for a full list of options let (selected_interface, ip_networks) = network::compute_network_configuration(&interfaces, &scan_options); if scan_options.is_plain_output() { @@ -61,7 +65,6 @@ fn main() { // ARP responses on the interface will be collected in a separate thread, // while the main thread sends a batch of ARP requests for each IP in the // local network. - let channel_config = pnet_datalink::Config { read_timeout: Some(Duration::from_millis(network::DATALINK_RCV_TIMEOUT)), ..pnet_datalink::Config::default()