Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 99 additions & 1 deletion src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ pub struct Opts {
#[arg(long, default_value = "1")]
pub tries: u8,

/// Automatically ups the ULIMIT with the value you provided.
/// Automatically increases the Unix file-descriptor limit.
#[cfg_attr(not(unix), arg(hide = true))]
#[arg(short, long)]
pub ulimit: Option<usize>,

Expand Down Expand Up @@ -178,6 +179,27 @@ impl Opts {
opts
}

/// Validates options whose availability or semantics depend on the
/// operating system.
///
/// # Errors
///
/// Returns an error when an option is unsupported on the current platform.
pub fn validate_platform(&self) -> Result<(), String> {
#[cfg(not(unix))]
{
if self.ulimit.is_some() {
return Err(
"--ulimit is only supported on Unix-like operating systems. \
On Windows, use --batch-size (-b) to control scan concurrency."
.to_owned(),
);
}
}

Ok(())
}

/// Reads the command line arguments into an Opts struct and merge
/// values found within the user configuration file.
pub fn merge(&mut self, config: &Config) {
Expand Down Expand Up @@ -395,6 +417,82 @@ mod tests {
assert_eq!(command, opts.command);
}

#[test]
fn parses_explicit_batch_size() {
let opts = Opts::parse_from(["rustscan", "-a", "127.0.0.1", "-b", "1234"]);

assert_eq!(opts.batch_size, 1234);
}

#[test]
fn parses_explicit_long_batch_size() {
let opts = Opts::parse_from(["rustscan", "-a", "127.0.0.1", "--batch-size", "4321"]);

assert_eq!(opts.batch_size, 4321);
}

#[test]
#[cfg(windows)]
fn windows_platform_validation_accepts_batch_size() {
let opts = Opts::parse_from(["rustscan", "-a", "127.0.0.1", "--batch-size", "500"]);

assert!(opts.validate_platform().is_ok());
assert_eq!(opts.batch_size, 500);
}

#[test]
#[cfg(windows)]
fn windows_rejects_ulimit() {
let opts = Opts::parse_from(["rustscan", "-a", "127.0.0.1", "--ulimit", "5000"]);

let error = opts
.validate_platform()
.expect_err("Windows must reject --ulimit");

assert!(error.contains("--ulimit"));
assert!(error.contains("Unix"));
assert!(error.contains("--batch-size"));
}

#[test]
#[cfg(windows)]
fn windows_hides_ulimit_from_help() {
let help = Opts::command().render_long_help().to_string();

assert!(
!help.contains("--ulimit"),
"--ulimit should not be advertised on Windows"
);
}

#[test]
#[cfg(windows)]
fn windows_rejects_ulimit_from_config_merge() {
let mut opts = Opts {
no_config: false,
..Default::default()
};
let mut config = Config::default();
config.ulimit = Some(5_000);

opts.merge(&config);

let error = opts
.validate_platform()
.expect_err("Windows must reject --ulimit supplied by configuration");

assert!(error.contains("--ulimit"));
}

#[test]
#[cfg(unix)]
fn unix_platform_validation_accepts_ulimit() {
let opts = Opts::parse_from(["rustscan", "-a", "127.0.0.1", "--ulimit", "5000"]);

assert!(opts.validate_platform().is_ok());
assert_eq!(opts.ulimit, Some(5_000));
}

#[test]
fn opts_no_merge_when_config_is_ignored() {
let mut opts = Opts::default();
Expand Down
70 changes: 60 additions & 10 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ extern crate dirs;
#[cfg(unix)]
const DEFAULT_FILE_DESCRIPTORS_LIMIT: usize = 8000;
// Safest batch size based on experimentation
#[cfg(unix)]
const AVERAGE_BATCH_SIZE: usize = 3000;

#[macro_use]
Expand All @@ -46,6 +47,11 @@ fn main() {
let config = Config::read(opts.config_path.clone());
opts.merge(&config);

if let Err(message) = opts.validate_platform() {
eprintln!("error: {message}");
std::process::exit(2);
}

debug!("Main() `opts` arguments are {opts:?}");

let scripts_to_run: Vec<ScriptFile> = match init_scripts(&opts.scripts) {
Expand Down Expand Up @@ -77,11 +83,8 @@ fn main() {
std::process::exit(1);
}

#[cfg(unix)]
let batch_size: usize = infer_batch_size(&opts, adjust_ulimit_size(&opts));

#[cfg(not(unix))]
let batch_size: usize = AVERAGE_BATCH_SIZE;
let batch_size = effective_batch_size(&opts);
debug!("Effective batch size: {batch_size}");

let scanner = Scanner::new(
&ips,
Expand Down Expand Up @@ -122,7 +125,7 @@ fn main() {
\n*I used {} batch size, consider lowering it with {} or a comfortable number for your system.
\n Alternatively, increase the timeout if your ping is high. Rustscan -t 2000 for 2000 milliseconds (2s) timeout.\n",
ip,
opts.batch_size,
batch_size,
"'rustscan -b <batch_size> -a <ip address>'");
warning!(x, opts.greppable, opts.accessible);
}
Expand Down Expand Up @@ -191,6 +194,23 @@ fn main() {
info!("{}", benchmarks.summary());
}

/// Determines the actual batch size used by the scanner.
///
/// Unix systems may reduce the requested batch size according to the process
/// file-descriptor limit. Other platforms, including Windows, use the batch
/// size explicitly requested by the user.
fn effective_batch_size(opts: &Opts) -> usize {
#[cfg(unix)]
{
infer_batch_size(opts, adjust_ulimit_size(opts))
}

#[cfg(not(unix))]
{
opts.batch_size
}
}

/// Prints the opening title of RustScan
#[allow(clippy::items_after_statements, clippy::needless_raw_string_hashes)]
fn print_opening(opts: &Opts) {
Expand Down Expand Up @@ -299,7 +319,7 @@ fn infer_batch_size(opts: &Opts, ulimit: usize) -> usize {
mod tests {
#[cfg(unix)]
use super::{adjust_ulimit_size, infer_batch_size};
use super::{print_opening, Opts};
use super::{effective_batch_size, print_opening, Opts};

#[test]
#[cfg(unix)]
Expand Down Expand Up @@ -367,11 +387,41 @@ mod tests {

#[test]
fn test_print_opening_no_panic() {
let opts = Opts::default();
// print opening should not panic
print_opening(&opts);
}

#[test]
#[cfg(windows)]
fn windows_batch_size_uses_requested_value() {
let opts = Opts {
ulimit: Some(2_000),
batch_size: 50,
..Default::default()
};
// print opening should not panic
print_opening(&opts);

assert_eq!(effective_batch_size(&opts), 50);
}

#[test]
#[cfg(windows)]
fn windows_batch_size_preserves_large_requested_value() {
let opts = Opts {
batch_size: 12_345,
..Default::default()
};

assert_eq!(effective_batch_size(&opts), 12_345);
}

#[test]
#[cfg(windows)]
fn windows_batch_size_preserves_single_connection() {
let opts = Opts {
batch_size: 1,
..Default::default()
};

assert_eq!(effective_batch_size(&opts), 1);
}
}
Loading