Skip to content
Merged
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
11 changes: 10 additions & 1 deletion src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,11 @@ pub struct Opts {
/// UDP scanning mode, finds UDP ports that send back responses
#[arg(long)]
pub udp: bool,

/// Also list TCP ports that actively refused the connection (closed).
/// Closed ports are only printed; scripts are never run against them.
#[arg(long)]
pub closed: bool,
}

#[cfg(not(tarpaulin_include))]
Expand Down Expand Up @@ -278,7 +283,7 @@ impl Opts {

merge_required!(
addresses, greppable, accessible, batch_size, timeout, tries, scan_order, scripts,
command, udp, no_banner
command, udp, no_banner, closed
);
}

Expand Down Expand Up @@ -325,6 +330,7 @@ impl Default for Opts {
exclude_ports: None,
exclude_addresses: None,
udp: false,
closed: false,
}
}
}
Expand Down Expand Up @@ -352,6 +358,7 @@ pub struct Config {
exclude_addresses: Option<Vec<String>>,
udp: Option<bool>,
no_banner: Option<bool>,
closed: Option<bool>,
}

#[cfg(not(tarpaulin_include))]
Expand All @@ -369,6 +376,7 @@ impl Config {
/// scan_order = "Serial"
/// exclude_ports = [8080, 9090, 80]
/// udp = false
/// closed = false
///
pub fn read(custom_config_path: Option<PathBuf>) -> Self {
let mut content = String::new();
Expand Down Expand Up @@ -466,6 +474,7 @@ mod tests {
exclude_addresses: None,
udp: Some(false),
no_banner: None,
closed: Some(false),
}
}
}
Expand Down
32 changes: 26 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use rustscan::benchmark::{Benchmark, NamedTimer};
use rustscan::input::{self, Config, Opts, ScriptsRequired};
use rustscan::port_strategy::PortStrategy;
use rustscan::scanner::Scanner;
use rustscan::scanner::{PortStatus, Scanner};
use rustscan::scripts::{init_scripts, Script, ScriptFile};
use rustscan::tui::println_safe;
use rustscan::{detail, funny_opening, output, warning};
Expand Down Expand Up @@ -87,7 +87,7 @@ fn main() {
let batch_size = effective_batch_size(&opts);
debug!("Effective batch size: {batch_size}");

let scanner = Scanner::new(
let mut scanner = Scanner::new(
&ips,
batch_size,
Duration::from_millis(opts.timeout.into()),
Expand All @@ -99,18 +99,25 @@ fn main() {
opts.udp,
)
.with_open_port_output();
if opts.closed {
scanner = scanner.with_closed_ports();
}
debug!("Scanner finished building: {scanner:?}");

let mut portscan_bench = NamedTimer::start("Portscan");
let scan_result = block_on(scanner.run());
let scan_result = block_on(scanner.run_with_status());
portscan_bench.end();
benchmarks.push(portscan_bench);

let mut ports_per_ip = HashMap::new();
let mut closed_ports_per_ip: HashMap<IpAddr, Vec<u16>> = HashMap::new();

for socket in scan_result {
ports_per_ip
.entry(socket.ip())
for status in scan_result {
let (map, socket) = match status {
PortStatus::Open(socket) => (&mut ports_per_ip, socket),
PortStatus::Closed(socket) => (&mut closed_ports_per_ip, socket),
};
map.entry(socket.ip())
.or_insert_with(Vec::new)
.push(socket.port());
}
Expand Down Expand Up @@ -187,6 +194,19 @@ fn main() {
}
}

// Closed ports are only listed; scripts are never run against them.
if opts.closed {
println_safe(format_args!("closed ports:"));
for (ip, ports) in &closed_ports_per_ip {
let ports_str = ports
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",");
println_safe(format_args!("{ip} -> [{ports_str}]"));
}
}

// To use the runtime benchmark, run the process as: RUST_LOG=info ./rustscan
script_bench.end();
benchmarks.push(script_bench);
Expand Down
83 changes: 74 additions & 9 deletions src/scanner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ pub struct Scanner {
exclude_ports: Vec<u16>,
udp: bool,
print_open_ports: bool,
report_closed: bool,
}

/// The outcome for a single socket, as returned by [`Scanner::run_with_status`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PortStatus {
/// The TCP connection succeeded, or the UDP target answered.
Open(SocketAddr),
/// The target actively refused the TCP connection (for example with a
/// RST). Only reported when [`Scanner::with_closed_ports`] is enabled.
Closed(SocketAddr),
}

// Allowing too many arguments for clippy.
Expand Down Expand Up @@ -89,6 +100,7 @@ impl Scanner {
exclude_ports,
udp,
print_open_ports: false,
report_closed: false,
}
}

Expand All @@ -101,10 +113,35 @@ impl Scanner {
self
}

/// Also reports TCP ports that actively refuse the connection, as
/// [`PortStatus::Closed`] from [`Self::run_with_status`].
///
/// Ports that time out are still treated as filtered and are not reported.
/// UDP scans never report closed ports.
#[must_use]
pub fn with_closed_ports(mut self) -> Self {
self.report_closed = true;
self
}

/// Runs scan_range with chunk sizes
/// If you want to run RustScan normally, this is the entry point used
/// Returns all open ports as `Vec<u16>`
/// Returns all open sockets.
pub async fn run(&self) -> Vec<SocketAddr> {
self.run_with_status()
.await
.into_iter()
.filter_map(|status| match status {
PortStatus::Open(socket) => Some(socket),
PortStatus::Closed(_) => None,
})
.collect()
}

/// Like [`Self::run`], but returns the status of every socket that gave a
/// definitive answer: open sockets, plus closed sockets when
/// [`Self::with_closed_ports`] is enabled.
pub async fn run_with_status(&self) -> Vec<PortStatus> {
let ports: Vec<u16> = self
.port_strategy
.order()
Expand All @@ -113,7 +150,7 @@ impl Scanner {
.copied()
.collect();
let mut socket_iterator: SocketIterator = SocketIterator::new(&self.ips, &ports);
let mut open_sockets: Vec<SocketAddr> = Vec::new();
let mut found_sockets: Vec<PortStatus> = Vec::new();
let mut ftrs = FuturesUnordered::new();
let mut errors: HashSet<String> = HashSet::new();

Expand Down Expand Up @@ -146,7 +183,7 @@ impl Scanner {
}

match result {
Ok(socket) => open_sockets.push(socket),
Ok(status) => found_sockets.push(status),
Err(e) => {
let error_string = e.to_string();
if errors.len() < self.ips.len() * 1000 {
Expand All @@ -156,8 +193,8 @@ impl Scanner {
}
}
debug!("Typical socket connection errors {errors:?}");
debug!("Open Sockets found: {:?}", open_sockets);
open_sockets
debug!("Sockets found: {:?}", found_sockets);
found_sockets
}

/// Given a socket, scan it self.tries times.
Expand All @@ -178,7 +215,7 @@ impl Scanner {
&self,
socket: SocketAddr,
udp_payloads: Option<Arc<UdpPayloadLookup>>,
) -> io::Result<SocketAddr> {
) -> io::Result<PortStatus> {
if self.udp {
return self.scan_udp_socket(socket, udp_payloads).await;
}
Expand All @@ -194,9 +231,16 @@ impl Scanner {
self.fmt_ports(socket);

debug!("Return Ok after {nr_try} tries");
return Ok(socket);
return Ok(PortStatus::Open(socket));
}
Err(e) => {
// A refused connection is a definitive answer, so there is
// no point in retrying it.
if self.report_closed && e.kind() == io::ErrorKind::ConnectionRefused {
self.fmt_closed_port(socket);
return Ok(PortStatus::Closed(socket));
}

let mut error_string = e.to_string();

assert!(!error_string.to_lowercase().contains("too many open files"), "Too many open files. Please reduce batch size. The default is 5000. Try -b 2500.");
Expand All @@ -216,7 +260,7 @@ impl Scanner {
&self,
socket: SocketAddr,
udp_payloads: Option<Arc<UdpPayloadLookup>>,
) -> io::Result<SocketAddr> {
) -> io::Result<PortStatus> {
let payload: &[u8] = udp_payloads
.as_ref()
.and_then(|m| m.get(&socket.port()).copied())
Expand All @@ -225,7 +269,7 @@ impl Scanner {
let tries = self.tries.get();
for _ in 1..=tries {
match self.udp_scan(socket, payload, self.timeout).await {
Ok(true) => return Ok(socket),
Ok(true) => return Ok(PortStatus::Open(socket)),
Ok(false) => continue,
Err(e) => return Err(e),
}
Expand Down Expand Up @@ -342,6 +386,17 @@ impl Scanner {
}
}
}

/// Prints a closed port (CLI output only, never in greppable mode).
fn fmt_closed_port(&self, socket: SocketAddr) {
if self.print_open_ports && !self.greppable {
if self.accessible {
println_safe(format_args!("Closed {socket}"));
} else {
println_safe(format_args!("Closed {}", socket.to_string().red()));
}
}
}
}

#[cfg(test)]
Expand Down Expand Up @@ -383,6 +438,16 @@ mod tests {
assert!(scanner.print_open_ports);
}

#[test]
fn closed_ports_are_not_reported_by_default() {
assert!(!test_scanner().report_closed);
}

#[test]
fn closed_port_reporting_is_opt_in() {
assert!(test_scanner().with_closed_ports().report_closed);
}

/// Regression test for https://github.com/bee-san/RustScan/issues/933:
/// the SNMP public-walk probe must be the exact 33-byte BER packet, with
/// the literal `public` community string intact. The old hexdigits-only
Expand Down
Loading