mirror of
https://github.com/bee-san/RustScan.git
synced 2026-11-07 17:02:04 -10:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3abce7ce9 | ||
|
|
a05592a8a8 | ||
|
|
b0dedb6700 | ||
|
|
bd5571f5d8 | ||
|
|
e04f9941d3 |
No files matched your search
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"metadata": {
|
||||
"base": "67c08a33100362c54b56fde784b0fd234f70fa11",
|
||||
"candidate": "a05592a8a88b77ad85d961137943dfadfe98b612",
|
||||
"environment": "Linux-7.2.6-1-cachyos-x86_64-with-glibc2.44",
|
||||
"rust": "rustc 1.98.1 (48a229cea 2026-09-01) (Arch Linux rust 1:1.98.1-1.1)",
|
||||
"hosts_bytes": 2760051,
|
||||
"profile": "release with LTO",
|
||||
"network_namespace": "unshare --user --map-root-user --net",
|
||||
"common_args": [
|
||||
"--no-config",
|
||||
"--config-path",
|
||||
"/dev/null",
|
||||
"--no-banner",
|
||||
"--scripts",
|
||||
"none",
|
||||
"--greppable",
|
||||
"--ports",
|
||||
"80",
|
||||
"--exclude-ports",
|
||||
"80"
|
||||
],
|
||||
"workloads": {
|
||||
"literal-ipv4": [
|
||||
"-a",
|
||||
"127.0.0.1"
|
||||
],
|
||||
"mixed-cidrs": [
|
||||
"-a",
|
||||
"127.0.0.1,192.0.2.0/30,2001:db8::/126",
|
||||
"--exclude-addresses",
|
||||
"192.0.2.1,2001:db8::1"
|
||||
]
|
||||
},
|
||||
"warmups_each": 2,
|
||||
"samples_each": 20,
|
||||
"binary_cpus": "0-3",
|
||||
"harness_cpus": "8-9",
|
||||
"binary_sha256": {
|
||||
"base": "4d6b090218ff36249ed95f14ec1a82626e8ea33854f3823f19a6de247b1524b8",
|
||||
"dns": "a9d6bf949f56c53d18cc85bc9b53b21bd85a63bcdd04ad78992d392c675c77ac"
|
||||
}
|
||||
},
|
||||
"summary": {
|
||||
"literal-ipv4": {
|
||||
"base_ms": 168.23068849999999,
|
||||
"dns_ms": 2.248762,
|
||||
"samples_each": 20
|
||||
},
|
||||
"mixed-cidrs": {
|
||||
"base_ms": 178.0855925,
|
||||
"dns_ms": 2.4539600000000004,
|
||||
"samples_each": 20
|
||||
}
|
||||
},
|
||||
"samples": {
|
||||
"literal-ipv4": {
|
||||
"base": [
|
||||
143.391226,
|
||||
146.562804,
|
||||
149.615525,
|
||||
151.188597,
|
||||
152.269193,
|
||||
159.499601,
|
||||
164.926958,
|
||||
164.347529,
|
||||
167.208931,
|
||||
165.433184,
|
||||
172.246939,
|
||||
169.252446,
|
||||
172.777899,
|
||||
170.729249,
|
||||
170.092681,
|
||||
171.863255,
|
||||
169.612842,
|
||||
170.857396,
|
||||
173.117403,
|
||||
175.206345
|
||||
],
|
||||
"dns": [
|
||||
2.241793,
|
||||
1.817341,
|
||||
2.39956,
|
||||
2.048986,
|
||||
3.123401,
|
||||
1.820017,
|
||||
3.615094,
|
||||
2.165312,
|
||||
2.675535,
|
||||
2.103261,
|
||||
2.531392,
|
||||
2.215141,
|
||||
2.572939,
|
||||
2.063803,
|
||||
2.671431,
|
||||
2.059846,
|
||||
2.598858,
|
||||
2.230916,
|
||||
2.678259,
|
||||
2.255731
|
||||
]
|
||||
},
|
||||
"mixed-cidrs": {
|
||||
"base": [
|
||||
175.504767,
|
||||
178.797842,
|
||||
186.183728,
|
||||
176.885958,
|
||||
177.989816,
|
||||
180.028568,
|
||||
177.66299,
|
||||
174.757221,
|
||||
175.468485,
|
||||
175.328023,
|
||||
177.639639,
|
||||
175.637593,
|
||||
177.922435,
|
||||
185.394396,
|
||||
182.587204,
|
||||
180.58311,
|
||||
178.961137,
|
||||
178.293186,
|
||||
180.482419,
|
||||
178.181369
|
||||
],
|
||||
"dns": [
|
||||
3.275193,
|
||||
2.114074,
|
||||
2.700771,
|
||||
2.183317,
|
||||
2.606538,
|
||||
2.368046,
|
||||
2.599702,
|
||||
2.238105,
|
||||
2.683892,
|
||||
2.292249,
|
||||
2.528366,
|
||||
2.114478,
|
||||
2.668921,
|
||||
2.103271,
|
||||
3.275514,
|
||||
2.27974,
|
||||
3.103311,
|
||||
2.265298,
|
||||
2.580743,
|
||||
2.379554
|
||||
]
|
||||
}
|
||||
},
|
||||
"invocations": 88
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
# Lazy DNS initialization benchmark
|
||||
|
||||
Compared current Tokio master `67c08a33100362c54b56fde784b0fd234f70fa11` with the lazy-resolver implementation
|
||||
at `a05592a8a88b77ad85d961137943dfadfe98b612` on 2026-10-02.
|
||||
|
||||
## Method
|
||||
|
||||
Both binaries are release builds with LTO, using Rust 1.98.1 on an Intel Core
|
||||
Ultra 7 165U running Linux 7.2.6-1-cachyos. Each workload used twenty measured
|
||||
runs and two warmups per binary, alternating A/B and B/A order. Binaries used
|
||||
CPUs 0-3; the harness used CPUs 8-9. No builds ran during measurement.
|
||||
|
||||
The harness ran in a private network namespace. Every invocation excluded
|
||||
its only requested port, so no scan traffic was sent. Configuration and
|
||||
scripts were disabled. Timing spans process launch through exit, including
|
||||
the same `taskset` affinity launcher for both builds.
|
||||
|
||||
Common arguments:
|
||||
|
||||
```sh
|
||||
--no-config --config-path /dev/null --no-banner --scripts none --greppable \
|
||||
--ports 80 --exclude-ports 80
|
||||
```
|
||||
|
||||
The literal case adds `-a 127.0.0.1`. The mixed case adds
|
||||
`-a 127.0.0.1,192.0.2.0/30,2001:db8::/126` and
|
||||
`--exclude-addresses 192.0.2.1,2001:db8::1`. Both use `RUST_LOG=rustscan=info`.
|
||||
|
||||
## Results
|
||||
|
||||
| Workload | Master median | Lazy DNS median | Time saved |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Literal IPv4 | 168.23 ms | 2.25 ms | 165.98 ms (98.66%) |
|
||||
| IPv4/IPv6 CIDRs and exclusions | 178.09 ms | 2.45 ms | 175.63 ms (98.62%) |
|
||||
|
||||
All 88 invocations exited successfully with the expected empty scan output.
|
||||
A separate syscall trace of the optimized literal-IP invocation recorded no
|
||||
reads of `/etc/hosts` or `/etc/resolv.conf` and no network connects or sends.
|
||||
|
||||
This host has a 2,760,051-byte hosts file. Eager Hickory initialization parses
|
||||
that file even for numeric targets. Systems with smaller hosts files will
|
||||
save less; this measures CLI startup, not socket throughput. Hostnames that
|
||||
need fallback DNS still pay resolver initialization once, and parsing order,
|
||||
file handling, deduplication and exclusions keep their existing behavior.
|
||||
|
||||
[Raw samples, setup and binary hashes](lazy-dns-benchmark-results.json) are
|
||||
included. Network-free regression tests verify that literal IPv4/IPv6 targets,
|
||||
CIDRs, exclusions, duplicates and empty inputs do not construct the resolver.
|
||||
+82
-11
@@ -1,4 +1,5 @@
|
||||
//! Provides functions to parse input IP addresses, CIDRs or files.
|
||||
use std::cell::LazyCell;
|
||||
use std::collections::BTreeSet;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{prelude::*, BufReader};
|
||||
@@ -39,12 +40,23 @@ use crate::warning;
|
||||
/// start your runtime (as the `rustscan` binary does) or from
|
||||
/// `tokio::task::spawn_blocking`, not directly from async code.
|
||||
pub fn parse_addresses(input: &Opts) -> Vec<IpAddr> {
|
||||
parse_addresses_with_resolver(input, || get_resolver(&input.resolver))
|
||||
}
|
||||
|
||||
fn parse_addresses_with_resolver(
|
||||
input: &Opts,
|
||||
create_resolver: impl FnOnce() -> Resolver,
|
||||
) -> Vec<IpAddr> {
|
||||
let mut ips: Vec<IpAddr> = Vec::new();
|
||||
let mut unresolved_addresses: Vec<&str> = Vec::new();
|
||||
let backup_resolver = get_resolver(&input.resolver);
|
||||
// Most scans use literal IPs or CIDRs. Defer the resolver's runtime and
|
||||
// hosts-file loading until a DNS fallback actually needs them, then reuse
|
||||
// that resolver for the rest of the targets and exclusions.
|
||||
let backup_resolver = LazyCell::new(create_resolver);
|
||||
let resolver = || &*backup_resolver;
|
||||
|
||||
for address in &input.addresses {
|
||||
let parsed_ips = parse_address(address, &backup_resolver);
|
||||
let parsed_ips = parse_address_with_resolver(address, &resolver);
|
||||
if !parsed_ips.is_empty() {
|
||||
ips.extend(parsed_ips);
|
||||
} else {
|
||||
@@ -66,7 +78,7 @@ pub fn parse_addresses(input: &Opts) -> Vec<IpAddr> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(x) = read_ips_from_file(file_path, &backup_resolver) {
|
||||
if let Ok(x) = read_ips_from_file(file_path, &resolver) {
|
||||
ips.extend(x);
|
||||
} else {
|
||||
warning!(
|
||||
@@ -77,7 +89,7 @@ pub fn parse_addresses(input: &Opts) -> Vec<IpAddr> {
|
||||
}
|
||||
}
|
||||
|
||||
let excluded_cidrs = parse_excluded_networks(&input.exclude_addresses, &backup_resolver);
|
||||
let excluded_cidrs = parse_excluded_networks_with_resolver(&input.exclude_addresses, &resolver);
|
||||
|
||||
// Remove duplicated/excluded IPs.
|
||||
let mut seen = BTreeSet::new();
|
||||
@@ -100,6 +112,13 @@ pub fn parse_addresses(input: &Opts) -> Vec<IpAddr> {
|
||||
/// let ips = parse_address("127.0.0.1", &Resolver::default().unwrap());
|
||||
/// ```
|
||||
pub fn parse_address(address: &str, resolver: &Resolver) -> Vec<IpAddr> {
|
||||
parse_address_with_resolver(address, &|| resolver)
|
||||
}
|
||||
|
||||
fn parse_address_with_resolver<'a>(
|
||||
address: &str,
|
||||
resolver: &impl Fn() -> &'a Resolver,
|
||||
) -> Vec<IpAddr> {
|
||||
if let Ok(addr) = IpAddr::from_str(address) {
|
||||
// `address` is an IP string
|
||||
vec![addr]
|
||||
@@ -118,14 +137,17 @@ pub fn parse_address(address: &str, resolver: &Resolver) -> Vec<IpAddr> {
|
||||
}
|
||||
|
||||
/// Uses DNS to get the IPS associated with host
|
||||
fn resolve_ips_from_host(source: &str, backup_resolver: &Resolver) -> Vec<IpAddr> {
|
||||
fn resolve_ips_from_host<'a>(
|
||||
source: &str,
|
||||
backup_resolver: &impl Fn() -> &'a Resolver,
|
||||
) -> Vec<IpAddr> {
|
||||
let mut ips: Vec<IpAddr> = Vec::new();
|
||||
|
||||
if let Ok(addrs) = source.to_socket_addrs() {
|
||||
for ip in addrs {
|
||||
ips.push(ip.ip());
|
||||
}
|
||||
} else if let Ok(addrs) = backup_resolver.lookup_ip(source) {
|
||||
} else if let Ok(addrs) = backup_resolver().lookup_ip(source) {
|
||||
ips.extend(addrs.iter());
|
||||
}
|
||||
|
||||
@@ -148,6 +170,13 @@ fn resolve_ips_from_host(source: &str, backup_resolver: &Resolver) -> Vec<IpAddr
|
||||
pub fn parse_excluded_networks(
|
||||
exclude_addresses: &Option<Vec<String>>,
|
||||
resolver: &Resolver,
|
||||
) -> Vec<IpCidr> {
|
||||
parse_excluded_networks_with_resolver(exclude_addresses, &|| resolver)
|
||||
}
|
||||
|
||||
fn parse_excluded_networks_with_resolver<'a>(
|
||||
exclude_addresses: &Option<Vec<String>>,
|
||||
resolver: &impl Fn() -> &'a Resolver,
|
||||
) -> Vec<IpCidr> {
|
||||
exclude_addresses
|
||||
.iter()
|
||||
@@ -157,7 +186,10 @@ pub fn parse_excluded_networks(
|
||||
}
|
||||
|
||||
/// Parses a single address into an IpCidr, handling CIDR notation, IP addresses, and hostnames.
|
||||
fn parse_single_excluded_address(addr: &str, resolver: &Resolver) -> Vec<IpCidr> {
|
||||
fn parse_single_excluded_address<'a>(
|
||||
addr: &str,
|
||||
resolver: &impl Fn() -> &'a Resolver,
|
||||
) -> Vec<IpCidr> {
|
||||
if let Ok(cidr) = IpCidr::from_str(addr) {
|
||||
return vec![cidr];
|
||||
}
|
||||
@@ -261,9 +293,9 @@ fn read_resolver_from_file(path: &str) -> Result<Vec<IpAddr>, std::io::Error> {
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
/// Parses an input file of IPs and uses those
|
||||
fn read_ips_from_file(
|
||||
fn read_ips_from_file<'a>(
|
||||
ips: &std::path::Path,
|
||||
backup_resolver: &Resolver,
|
||||
backup_resolver: &impl Fn() -> &'a Resolver,
|
||||
) -> Result<Vec<IpAddr>, std::io::Error> {
|
||||
let file = File::open(ips)?;
|
||||
let reader = BufReader::new(file);
|
||||
@@ -272,7 +304,7 @@ fn read_ips_from_file(
|
||||
|
||||
for address_line in reader.lines() {
|
||||
if let Ok(address) = address_line {
|
||||
ips.extend(parse_address(&address, backup_resolver));
|
||||
ips.extend(parse_address_with_resolver(&address, backup_resolver));
|
||||
} else {
|
||||
debug!("Line in file is not valid");
|
||||
}
|
||||
@@ -283,9 +315,48 @@ fn read_ips_from_file(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse_addresses, Opts};
|
||||
use super::{parse_addresses, parse_addresses_with_resolver, Opts};
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
#[test]
|
||||
fn literal_targets_and_exclusions_do_not_initialize_a_resolver() {
|
||||
let opts = Opts {
|
||||
addresses: vec![
|
||||
"192.0.2.0/30".to_owned(),
|
||||
"192.0.2.2".to_owned(),
|
||||
"2001:db8::/126".to_owned(),
|
||||
"2001:db8::3".to_owned(),
|
||||
],
|
||||
exclude_addresses: Some(vec![
|
||||
"192.0.2.0/31".to_owned(),
|
||||
"192.0.2.3".to_owned(),
|
||||
"2001:db8::/127".to_owned(),
|
||||
"2001:db8::3".to_owned(),
|
||||
]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let ips = parse_addresses_with_resolver(&opts, || {
|
||||
panic!("literal targets and exclusions must not initialize DNS")
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
ips,
|
||||
[
|
||||
"192.0.2.2".parse::<IpAddr>().unwrap(),
|
||||
"2001:db8::2".parse::<IpAddr>().unwrap(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_targets_do_not_initialize_a_resolver() {
|
||||
assert!(parse_addresses_with_resolver(&Opts::default(), || {
|
||||
panic!("empty targets must not initialize DNS")
|
||||
})
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_correct_addresses() {
|
||||
let opts = Opts {
|
||||
|
||||
Reference in new issue
Block a user