diff --git a/Cargo.lock b/Cargo.lock index cdd2d8fe2..20cba61f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -927,7 +927,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2", + "socket2 0.5.7", "widestring", "windows-sys 0.48.0", "winreg", @@ -1082,6 +1082,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.48.0", ] @@ -1201,6 +1202,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1219,6 +1226,19 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "ping-rs" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d873f038f84371f9c7fa13f6afea4d5f1fbcd5070ba8eb7af2a6d41c768eff8b" +dependencies = [ + "futures", + "mio", + "paste", + "socket2 0.4.10", + "windows", +] + [[package]] name = "piper" version = "0.2.3" @@ -1508,6 +1528,7 @@ dependencies = [ "log", "once_cell", "parameterized", + "ping-rs", "rand", "rlimit", "serde", @@ -1603,6 +1624,16 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "socket2" version = "0.5.7" @@ -1734,7 +1765,7 @@ dependencies = [ "mio", "num_cpus", "pin-project-lite", - "socket2", + "socket2 0.5.7", "windows-sys 0.48.0", ] @@ -2007,6 +2038,21 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" +dependencies = [ + "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-sys" version = "0.48.0" @@ -2065,6 +2111,12 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[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_gnullvm" version = "0.48.5" @@ -2077,6 +2129,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[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.5" @@ -2089,6 +2147,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[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.5" @@ -2107,6 +2171,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[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.5" @@ -2119,6 +2189,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[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.5" @@ -2131,6 +2207,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[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.5" @@ -2143,6 +2225,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[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.5" diff --git a/Cargo.toml b/Cargo.toml index df1698be5..a1b6dda2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ hickory-resolver = { version = "0.24.2", features = ["dns-over-rustls"] } anyhow = "1.0.40" text_placeholder = { version = "0.5", features = ["struct_context"] } once_cell = "1.20.2" +ping-rs = "0.1.2" [dev-dependencies] parameterized = "2.0.0" diff --git a/benches/benchmark_portscan.rs b/benches/benchmark_portscan.rs index 1206fa08e..e9cac5a59 100644 --- a/benches/benchmark_portscan.rs +++ b/benches/benchmark_portscan.rs @@ -45,6 +45,8 @@ fn criterion_benchmark(c: &mut Criterion) { true, vec![], false, + &addrs, + None, ); c.bench_function("portscan tcp", |b| { @@ -61,6 +63,8 @@ fn criterion_benchmark(c: &mut Criterion) { true, vec![], true, + &addrs, + None, ); let mut udp_group = c.benchmark_group("portscan udp"); diff --git a/src/address.rs b/src/address.rs index b9e5c359e..280ca9920 100644 --- a/src/address.rs +++ b/src/address.rs @@ -2,7 +2,7 @@ use std::collections::BTreeSet; use std::fs::{self, File}; use std::io::{prelude::*, BufReader}; -use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr, ToSocketAddrs}; use std::path::Path; use std::str::FromStr; @@ -84,6 +84,82 @@ pub fn parse_addresses(input: &Opts) -> Vec { .collect() } +/// Parses the string(s) into IP addresses for the deadman switch. +/// +/// Goes through all possible IP inputs (files or via argparsing). +/// +/// Unline parse_addresses() IP in the excluded_ips field will not be removed from the list. +/// +/// ```rust +/// # use rustscan::input::Opts; +/// # use rustscan::address::parse_deadman_addresses; +/// let mut opts = Opts::default(); +/// opts.deadman_addresses = Some(vec!["192.168.0.0/30".to_owned()]); +/// +/// let ips = parse_deadman_addresses(&opts); +/// ``` +/// +/// Finally, any duplicates are removed to avoid excessive scans. + +// Duplicaded parse_addresses to use for the deadman switch. +// Removed the excluded_ips check. +pub fn parse_deadman_addresses(input: &Opts) -> Vec { + let mut ips: Vec = Vec::new(); + let mut unresolved_addresses: Vec<&str> = Vec::new(); + let backup_resolver = get_resolver(&input.resolver); + + // returning default deadman addresses if no deadman addresses set + let addresses = match &input.deadman_addresses { + Some(e) => e.clone(), + None => { + return [ + IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), + IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), + ] + .to_vec() + } + }; + + for address in &addresses { + let parsed_ips = parse_address(address, &backup_resolver); + if !parsed_ips.is_empty() { + ips.extend(parsed_ips); + } else { + unresolved_addresses.push(address); + } + } + + // If we got to this point this can only be a file path or the wrong input. + for file_path in unresolved_addresses { + let file_path = Path::new(file_path); + + if !file_path.is_file() { + warning!( + format!("Host {file_path:?} could not be resolved."), + input.greppable, + input.accessible + ); + + continue; + } + + if let Ok(x) = read_ips_from_file(file_path, &backup_resolver) { + ips.extend(x); + } else { + warning!( + format!("Host {file_path:?} could not be resolved."), + input.greppable, + input.accessible + ); + } + } + + ips.into_iter() + .collect::>() + .into_iter() + .collect() +} + /// Given a string, parse it as a host, IP address, or CIDR. /// /// This allows us to pass files as hosts or cidr or IPs easily @@ -197,7 +273,7 @@ fn read_ips_from_file( #[cfg(test)] mod tests { - use super::{get_resolver, parse_addresses, Opts}; + use super::{get_resolver, parse_addresses, parse_deadman_addresses, Opts}; use std::net::Ipv4Addr; #[test] @@ -384,4 +460,116 @@ mod tests { assert!(lookup.iter().next().is_some()); } + + // duplicating tests for parse_deadman_addresses + #[test] + fn deadman_parse_correct_addresses() { + let opts = Opts { + deadman_addresses: Some(vec!["127.0.0.1".to_owned(), "192.168.0.0/30".to_owned()]), + ..Default::default() + }; + + let ips = parse_deadman_addresses(&opts); + + assert_eq!( + ips, + [ + Ipv4Addr::new(127, 0, 0, 1), + Ipv4Addr::new(192, 168, 0, 0), + Ipv4Addr::new(192, 168, 0, 1), + Ipv4Addr::new(192, 168, 0, 2), + Ipv4Addr::new(192, 168, 0, 3) + ] + ); + } + + #[test] + fn deadman_parse_correct_host_addresses() { + let opts = Opts { + deadman_addresses: Some(vec!["google.com".to_owned()]), + ..Default::default() + }; + + let ips = parse_deadman_addresses(&opts); + + assert_eq!(ips.len(), 1); + } + + #[test] + fn deadman_parse_correct_and_incorrect_addresses() { + let opts = Opts { + deadman_addresses: Some(vec!["127.0.0.1".to_owned(), "im_wrong".to_owned()]), + ..Default::default() + }; + + let ips = parse_deadman_addresses(&opts); + + assert_eq!(ips, [Ipv4Addr::new(127, 0, 0, 1),]); + } + + #[test] + fn deadman_parse_incorrect_addresses() { + let opts = Opts { + deadman_addresses: Some(vec!["im_wrong".to_owned(), "300.10.1.1".to_owned()]), + ..Default::default() + }; + + let ips = parse_deadman_addresses(&opts); + + assert!(ips.is_empty()); + } + + #[test] + fn deadman_parse_hosts_file_and_incorrect_hosts() { + // Host file contains IP, Hosts, incorrect IPs, incorrect hosts + let opts = Opts { + deadman_addresses: Some(vec!["fixtures/hosts.txt".to_owned()]), + ..Default::default() + }; + + let ips = parse_deadman_addresses(&opts); + + assert_eq!(ips.len(), 3); + } + + #[test] + fn deadman_parse_empty_hosts_file() { + // Host file contains IP, Hosts, incorrect IPs, incorrect hosts + let opts = Opts { + deadman_addresses: Some(vec!["fixtures/empty_hosts.txt".to_owned()]), + ..Default::default() + }; + + let ips = parse_deadman_addresses(&opts); + + assert_eq!(ips.len(), 0); + } + + #[test] + fn deadman_parse_naughty_host_file() { + // Host file contains IP, Hosts, incorrect IPs, incorrect hosts + let opts = Opts { + deadman_addresses: Some(vec!["fixtures/naughty_string.txt".to_owned()]), + ..Default::default() + }; + + let ips = parse_deadman_addresses(&opts); + + assert_eq!(ips.len(), 0); + } + + #[test] + fn deadman_parse_duplicate_cidrs() { + let opts = Opts { + deadman_addresses: Some(vec![ + "79.98.104.0/21".to_owned(), + "79.98.104.0/24".to_owned(), + ]), + ..Default::default() + }; + + let ips = parse_deadman_addresses(&opts); + + assert_eq!(ips.len(), 2_048); + } } diff --git a/src/input.rs b/src/input.rs index 2536e8b36..d50525121 100644 --- a/src/input.rs +++ b/src/input.rs @@ -162,6 +162,21 @@ pub struct Opts { /// UDP scanning mode, finds UDP ports that send back responses #[arg(long)] pub udp: bool, + + /// A comma-delimited list or newline-delimited file of separated CIDRs, IPs, or hosts to be checked for the deadman switch. + #[arg(long, value_delimiter = ',')] + pub deadman_addresses: Option>, + + /// Number of seconds for ping timeout when using the deadman switch. + /// Default value is 5 seconds. + #[arg(long, default_value = "5")] + pub deadman_timeout: u32, + + /// Enables a deadman switch. + /// A list of IP addresses will be pinged every 10 seconds, if any fail to ping the scan will stop. + /// Default IP's are 8.8.8.8 and 1.1.1.1.1, change this with the --deadman-address argument. + #[arg(long)] + pub deadman_switch: bool, } #[cfg(not(tarpaulin_include))] @@ -252,6 +267,9 @@ impl Default for Opts { exclude_ports: None, exclude_addresses: None, udp: false, + deadman_addresses: None, + deadman_switch: false, + deadman_timeout: 5, } } } @@ -278,6 +296,9 @@ pub struct Config { exclude_ports: Option>, exclude_addresses: Option>, udp: Option, + deadman_addresses: Option>, + deadman_switch: Option, + deadman_timeout: Option, } #[cfg(not(tarpaulin_include))] @@ -353,6 +374,9 @@ mod tests { exclude_ports: None, exclude_addresses: None, udp: Some(false), + deadman_addresses: None, + deadman_switch: Some(false), + deadman_timeout: Some(5), } } } diff --git a/src/lib.rs b/src/lib.rs index e335de58a..07ecad3f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,8 @@ //! start: 1, //! end: 1_000, //! }; +//! let deadman_addrs = vec!["127.0.0.1".parse::().unwrap()]; +//! //! let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random); // can be serial, random or manual https://github.com/RustScan/RustScan/blob/master/src/port_strategy/mod.rs //! let scanner = Scanner::new( //! &addrs, // the addresses to scan @@ -32,6 +34,8 @@ //! true, // accessible, should the output be A11Y compliant? //! vec![9000], // What ports should RustScan exclude? //! false, // is this a UDP scan? +//! &deadman_addrs, // addresses for the deadman switch +//! Some(Duration::from_secs(5)), // ping timout for deadman //! ); //! //! let scan_result = block_on(scanner.run()); diff --git a/src/main.rs b/src/main.rs index 52a998d74..60ff22f8e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ use std::net::IpAddr; use std::string::ToString; use std::time::Duration; -use rustscan::address::parse_addresses; +use rustscan::address::{parse_addresses, parse_deadman_addresses}; extern crate colorful; extern crate dirs; @@ -67,6 +67,7 @@ fn main() { } let ips: Vec = parse_addresses(&opts); + let deadman_ips: Vec = parse_deadman_addresses(&opts); if ips.is_empty() { warning!( @@ -77,6 +78,8 @@ fn main() { std::process::exit(1); } + let deadman_timeout = Duration::from_secs(opts.deadman_timeout as u64); + #[cfg(unix)] let batch_size: u16 = infer_batch_size(&opts, adjust_ulimit_size(&opts)); @@ -93,9 +96,20 @@ fn main() { opts.accessible, opts.exclude_ports.unwrap_or_default(), opts.udp, + &deadman_ips, + Some(deadman_timeout), ); debug!("Scanner finished building: {:?}", scanner); + // performing deadman fuctions if --deadman-switch is enabled + if opts.deadman_switch { + if scanner.deadman_preflight().is_err() { + println!("{}", "Deadmant preflight failed, terminating scan.".red()); + return; + } + scanner.start_deadman_loop(); + } + let mut portscan_bench = NamedTimer::start("Portscan"); let scan_result = block_on(scanner.run()); portscan_bench.end(); diff --git a/src/scanner/mod.rs b/src/scanner/mod.rs index 47418a108..562795b36 100644 --- a/src/scanner/mod.rs +++ b/src/scanner/mod.rs @@ -11,11 +11,13 @@ use async_std::prelude::*; use async_std::{io, net::UdpSocket}; use colored::Colorize; use futures::stream::FuturesUnordered; +use ping_rs::send_ping; use std::collections::BTreeMap; use std::{ collections::HashSet, net::{IpAddr, Shutdown, SocketAddr}, num::NonZeroU8, + thread, time::Duration, }; @@ -37,6 +39,8 @@ pub struct Scanner { accessible: bool, exclude_ports: Vec, udp: bool, + deadman_addresses: Vec, + deadman_timeout: Duration, } // Allowing too many arguments for clippy. @@ -52,6 +56,8 @@ impl Scanner { accessible: bool, exclude_ports: Vec, udp: bool, + deadman_addresses: &[IpAddr], + deadman_timeout: Option, ) -> Self { Self { batch_size, @@ -63,6 +69,11 @@ impl Scanner { accessible, exclude_ports, udp, + deadman_addresses: deadman_addresses.to_vec(), + deadman_timeout: match deadman_timeout { + Some(e) => e, + None => Duration::from_secs(5), + }, } } @@ -302,6 +313,60 @@ impl Scanner { } } } + + /// Loop that will run the deadman test and forcefully exit the program with std::process::exit(0)in case of falure. + /// Will spawn a new thread to run the loop. + /// + pub fn start_deadman_loop(&self) { + let addresses = self.deadman_addresses.clone(); + let timeout = self.deadman_timeout.clone(); + let sleep_time = Duration::from_secs(10); + + thread::spawn(move || loop { + thread::sleep(sleep_time); + + println!( + "{}", + "Deadman switch is testing ip addresses are still accessible.".blue() + ); + + for address in &addresses { + match send_ping(address, timeout, &[0; 8], None) { + Ok(_) => println!("Ping to {} was successful.", address.to_string()), + Err(_) => { + let print_string = + format!("Ping to {} was unsuccessful.", address.to_string()); + println!("{}", print_string.red().bold()); + println!("{}", "Terminating Scan!!!".red().bold()); + std::process::exit(1); + } + }; + } + println!( + "{}", + "Deadman test has succeeded, will test again in 10 seconds.".blue() + ); + }); + } + + /// Preflight test to make sure all addresses are online. + /// Should be run before start_deadman_loop. + pub fn deadman_preflight(&self) -> Result<(), ()> { + println!("{}", "Testing Deadman addresses are acesable.".blue()); + + for address in &self.deadman_addresses { + match send_ping(address, self.deadman_timeout, &[0; 8], None) { + Ok(_) => println!("Ping to {} was successful.", address.to_string()), + Err(_) => { + let print_string = format!("Ping to {} was unsuccessful.", address.to_string()); + println!("{}", print_string.red().bold()); + return Err(()); + } + }; + } + + return Ok(()); + } } #[cfg(test)] @@ -330,6 +395,8 @@ mod tests { true, vec![9000], false, + &vec![], + None, ); block_on(scanner.run()); // if the scan fails, it wouldn't be able to assert_eq! as it panicked! @@ -354,6 +421,8 @@ mod tests { true, vec![9000], false, + &vec![], + None, ); block_on(scanner.run()); // if the scan fails, it wouldn't be able to assert_eq! as it panicked! @@ -377,6 +446,8 @@ mod tests { true, vec![9000], false, + &vec![], + None, ); block_on(scanner.run()); assert_eq!(1, 1); @@ -399,6 +470,8 @@ mod tests { true, vec![9000], false, + &vec![], + None, ); block_on(scanner.run()); assert_eq!(1, 1); @@ -424,6 +497,8 @@ mod tests { true, vec![9000], false, + &vec![], + None, ); block_on(scanner.run()); assert_eq!(1, 1); @@ -448,6 +523,8 @@ mod tests { true, vec![9000], true, + &vec![], + None, ); block_on(scanner.run()); // if the scan fails, it wouldn't be able to assert_eq! as it panicked! @@ -472,6 +549,8 @@ mod tests { true, vec![9000], true, + &vec![], + None, ); block_on(scanner.run()); // if the scan fails, it wouldn't be able to assert_eq! as it panicked! @@ -495,6 +574,8 @@ mod tests { true, vec![9000], true, + &vec![], + None, ); block_on(scanner.run()); assert_eq!(1, 1); @@ -517,8 +598,35 @@ mod tests { true, vec![9000], true, + &vec![], + None, ); block_on(scanner.run()); assert_eq!(1, 1); } + + #[test] + fn can_ping() { + let addrs = vec!["127.0.0.1".parse::().unwrap()]; + let range = PortRange { + start: 1, + end: 1_000, + }; + let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random); + let scanner = Scanner::new( + &addrs, + 10, + Duration::from_millis(100), + 1, + true, + strategy, + true, + vec![9000], + true, + &addrs, + None, + ); + + assert_eq!(scanner.deadman_preflight(), Ok(())); + } }