Compare commits

..
14 changed files with 213 additions and 896 deletions

No files matched your search

+3 -3
View File
@@ -3,15 +3,15 @@ updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
interval: "daily"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "monthly"
interval: "daily"
- package-ecosystem: cargo
directory: "/"
schedule:
interval: monthly
interval: daily
open-pull-requests-limit: 10
ignore:
- dependency-name: anyhow
-7
View File
@@ -196,7 +196,6 @@ def build_scenarios(
scenarios = [
Scenario("tcp-1-port", "TCP, 1 open port", lo, ports=(TCP_PORTS[0],), repeat=3),
Scenario("tcp-open", "TCP, open listeners only", lo, ports=TCP_PORTS, repeat=3),
Scenario("tcp-sweep", f"TCP, {ports(sweep)}, default batch", lo, sweep),
Scenario(
"tcp-sweep-excluded",
@@ -237,12 +236,6 @@ def build_scenarios(
scenarios.append(
Scenario("udp-sweep", f"UDP, {ports(sweep)}", lo, sweep, timeout_ms=500, udp=True)
)
scenarios.append(
Scenario(
"udp-open", "UDP, echo responders only", lo, ports=UDP_PORTS,
timeout_ms=500, udp=True, repeat=9,
)
)
if delayed:
scenarios.append(
Scenario(
+1 -1
View File
@@ -75,7 +75,7 @@ jobs:
run: python -m py_compile .github/scan-benchmark/scan_bench.py
- name: Setup Rust
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2.0.0
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
with:
toolchain: stable
# build.rs formats the generated payload table with rustfmt.
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@v7
- name: Setup Rust
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2.0.0
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
with:
toolchain: ${{ matrix.rust }}
components: clippy, rustfmt
Generated
+155 -41
View File
@@ -2,6 +2,21 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "ahash"
version = "0.8.11"
@@ -120,6 +135,21 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "backtrace"
version = "0.3.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets 0.52.6",
]
[[package]]
name = "base64"
version = "0.21.7"
@@ -279,7 +309,7 @@ version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -387,7 +417,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -455,7 +485,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -569,6 +599,12 @@ dependencies = [
"wasip3",
]
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "half"
version = "2.4.1"
@@ -849,7 +885,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
dependencies = [
"socket2 0.5.8",
"socket2",
"widestring",
"windows-sys 0.48.0",
"winreg",
@@ -933,9 +969,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.191"
version = "0.2.181"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5"
[[package]]
name = "libredox"
@@ -977,9 +1013,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.35"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "lru-cache"
@@ -1002,6 +1038,15 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "miniz_oxide"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
dependencies = [
"adler2",
]
[[package]]
name = "mio"
version = "1.0.3"
@@ -1041,6 +1086,15 @@ dependencies = [
"autocfg",
]
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.21.4"
@@ -1372,6 +1426,12 @@ dependencies = [
"libc",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustix"
version = "1.1.3"
@@ -1382,7 +1442,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -1434,7 +1494,6 @@ dependencies = [
"gcd",
"hickory-resolver",
"itertools 0.15.0",
"libc",
"log",
"once_cell",
"parameterized",
@@ -1445,7 +1504,6 @@ dependencies = [
"text_placeholder",
"tokio",
"toml",
"windows-sys 0.61.2",
]
[[package]]
@@ -1535,9 +1593,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "1.1.2"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c"
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [
"serde_core",
]
@@ -1573,16 +1631,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "socket2"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
@@ -1635,7 +1683,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
dependencies = [
"rustix",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -1726,16 +1774,17 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.50.0"
version = "1.44.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"pin-project-lite",
"socket2 0.6.5",
"windows-sys 0.61.2",
"socket2",
"windows-sys 0.52.0",
]
[[package]]
@@ -1750,9 +1799,9 @@ dependencies = [
[[package]]
name = "toml"
version = "1.1.8+spec-1.1.0"
version = "1.1.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d"
checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a"
dependencies = [
"indexmap 2.13.0",
"serde_core",
@@ -1765,27 +1814,27 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "1.1.2+spec-1.1.0"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053"
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_parser"
version = "1.1.5+spec-1.1.0"
version = "1.1.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c"
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.1.3+spec-1.1.0"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770"
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
[[package]]
name = "tracing"
@@ -2035,7 +2084,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -2079,11 +2128,11 @@ dependencies = [
[[package]]
name = "windows-sys"
version = "0.61.2"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-link",
"windows-targets 0.53.5",
]
[[package]]
@@ -2110,13 +2159,30 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
"windows_i686_gnullvm 0.53.1",
"windows_i686_msvc 0.53.1",
"windows_x86_64_gnu 0.53.1",
"windows_x86_64_gnullvm 0.53.1",
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
@@ -2129,6 +2195,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
@@ -2141,6 +2213,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
@@ -2153,12 +2231,24 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
@@ -2171,6 +2261,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
@@ -2183,6 +2279,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
@@ -2195,6 +2297,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
@@ -2207,6 +2315,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "1.0.0"
-6
View File
@@ -43,12 +43,6 @@ anyhow = "1.0.40"
text_placeholder = { version = "0.5", features = ["struct_context"] }
once_cell = "1.21.4"
[target.'cfg(unix)'.dependencies]
libc = "0.2"
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61", features = ["Win32_Networking_WinSock"] }
[dev-dependencies]
parameterized = "2.0.0"
criterion = { version = "0.8", features = ["html_reports"] }
-151
View File
@@ -1,151 +0,0 @@
{
"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
}
-48
View File
@@ -1,48 +0,0 @@
# 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.
+11 -82
View File
@@ -1,5 +1,4 @@
//! 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};
@@ -40,23 +39,12 @@ 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();
// 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;
let backup_resolver = get_resolver(&input.resolver);
for address in &input.addresses {
let parsed_ips = parse_address_with_resolver(address, &resolver);
let parsed_ips = parse_address(address, &backup_resolver);
if !parsed_ips.is_empty() {
ips.extend(parsed_ips);
} else {
@@ -78,7 +66,7 @@ fn parse_addresses_with_resolver(
continue;
}
if let Ok(x) = read_ips_from_file(file_path, &resolver) {
if let Ok(x) = read_ips_from_file(file_path, &backup_resolver) {
ips.extend(x);
} else {
warning!(
@@ -89,7 +77,7 @@ fn parse_addresses_with_resolver(
}
}
let excluded_cidrs = parse_excluded_networks_with_resolver(&input.exclude_addresses, &resolver);
let excluded_cidrs = parse_excluded_networks(&input.exclude_addresses, &backup_resolver);
// Remove duplicated/excluded IPs.
let mut seen = BTreeSet::new();
@@ -112,13 +100,6 @@ fn parse_addresses_with_resolver(
/// 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]
@@ -137,17 +118,14 @@ fn parse_address_with_resolver<'a>(
}
/// Uses DNS to get the IPS associated with host
fn resolve_ips_from_host<'a>(
source: &str,
backup_resolver: &impl Fn() -> &'a Resolver,
) -> Vec<IpAddr> {
fn resolve_ips_from_host(source: &str, backup_resolver: &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());
}
@@ -170,13 +148,6 @@ fn resolve_ips_from_host<'a>(
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()
@@ -186,10 +157,7 @@ fn parse_excluded_networks_with_resolver<'a>(
}
/// Parses a single address into an IpCidr, handling CIDR notation, IP addresses, and hostnames.
fn parse_single_excluded_address<'a>(
addr: &str,
resolver: &impl Fn() -> &'a Resolver,
) -> Vec<IpCidr> {
fn parse_single_excluded_address(addr: &str, resolver: &Resolver) -> Vec<IpCidr> {
if let Ok(cidr) = IpCidr::from_str(addr) {
return vec![cidr];
}
@@ -293,9 +261,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<'a>(
fn read_ips_from_file(
ips: &std::path::Path,
backup_resolver: &impl Fn() -> &'a Resolver,
backup_resolver: &Resolver,
) -> Result<Vec<IpAddr>, std::io::Error> {
let file = File::open(ips)?;
let reader = BufReader::new(file);
@@ -304,7 +272,7 @@ fn read_ips_from_file<'a>(
for address_line in reader.lines() {
if let Ok(address) = address_line {
ips.extend(parse_address_with_resolver(&address, backup_resolver));
ips.extend(parse_address(&address, backup_resolver));
} else {
debug!("Line in file is not valid");
}
@@ -315,48 +283,9 @@ fn read_ips_from_file<'a>(
#[cfg(test)]
mod tests {
use super::{parse_addresses, parse_addresses_with_resolver, Opts};
use super::{parse_addresses, 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 {
+6 -181
View File
@@ -1,9 +1,6 @@
//! Provides a means to read, parse and hold configuration options for scans.
use clap::parser::ValueSource;
use clap::{CommandFactory, FromArgMatches, Parser, ValueEnum};
use clap::{Parser, ValueEnum};
use serde_derive::Deserialize;
use std::collections::HashSet;
use std::ffi::OsString;
use std::fs;
use std::path::PathBuf;
@@ -25,11 +22,8 @@ pub enum ScanOrder {
/// - custom will read the ScriptConfig file and the available scripts in the predefined folders
#[derive(Deserialize, Debug, ValueEnum, Clone, PartialEq, Eq, Copy)]
pub enum ScriptsRequired {
#[serde(alias = "none")]
None,
#[serde(alias = "default")]
Default,
#[serde(alias = "custom")]
Custom,
}
@@ -237,48 +231,18 @@ pub struct Opts {
/// scanning the next one, for slow, low-noise scans. 0 disables the delay.
#[arg(long, default_value = "0", value_name = "MS")]
pub interval: u64,
/// Ids of the options given explicitly on the command line. The
/// configuration file never overrides these (see [`Opts::merge`]).
#[arg(skip)]
pub cli_args: HashSet<String>,
}
#[cfg(not(tarpaulin_include))]
impl Opts {
pub fn read() -> Self {
Self::try_read_from(std::env::args_os()).unwrap_or_else(|e| e.exit())
}
/// Parses `args` like [`Opts::read`] does, returning an error instead of
/// exiting.
fn try_read_from<I, T>(args: I) -> Result<Self, clap::Error>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let mut command = Self::command();
let mut matches = command.try_get_matches_from_mut(args)?;
// clap fills in default values, so the parsed struct alone cannot
// tell `-b 4500` apart from no `-b` at all. Record what the user
// actually typed so that `merge` lets it win over the config file.
let cli_args = command
.get_arguments()
.map(|arg| arg.get_id().as_str())
.filter(|id| matches.value_source(id) == Some(ValueSource::CommandLine))
.map(str::to_owned)
.collect();
let mut opts =
Self::from_arg_matches_mut(&mut matches).map_err(|e| e.format(&mut command))?;
opts.cli_args = cli_args;
let mut opts = Opts::parse();
if opts.ports.is_none() && opts.range.is_none() {
opts.range = Some(PortRanges(vec![(LOWEST_PORT_NUMBER, TOP_PORT_NUMBER)]));
}
Ok(opts)
opts
}
/// Validates options whose availability or semantics depend on the
@@ -303,8 +267,7 @@ impl Opts {
}
/// Reads the command line arguments into an Opts struct and merge
/// values found within the user configuration file. Options given on the
/// command line take precedence over the configuration file.
/// values found within the user configuration file.
pub fn merge(&mut self, config: &Config) {
if !self.no_config {
self.merge_required(config);
@@ -317,9 +280,7 @@ impl Opts {
($($field: ident),+) => {
$(
if let Some(e) = &config.$field {
if !self.cli_args.contains(stringify!($field)) {
self.$field = e.clone();
}
self.$field = e.clone();
}
)+
}
@@ -335,7 +296,7 @@ impl Opts {
macro_rules! merge_optional {
($($field: ident),+) => {
$(
if config.$field.is_some() && !self.cli_args.contains(stringify!($field)) {
if config.$field.is_some() {
self.$field = config.$field.clone();
}
)+
@@ -376,7 +337,6 @@ impl Default for Opts {
udp: false,
closed: false,
interval: 0,
cli_args: HashSet::new(),
}
}
}
@@ -499,7 +459,6 @@ pub fn old_default_config_path() -> PathBuf {
mod tests {
use clap::{CommandFactory, Parser};
use parameterized::parameterized;
use std::collections::HashSet;
use super::{Config, Opts, PortRanges, ScanOrder, ScriptsRequired};
@@ -529,55 +488,6 @@ mod tests {
}
}
fn assert_config_script_mode(value: &str, expected: ScriptsRequired) {
let config: Config = toml::from_str(&format!("scripts = \"{value}\""))
.expect("documented script mode must deserialize");
assert_eq!(config.scripts, Some(expected));
}
#[test]
fn config_scripts_lowercase_none() {
assert_config_script_mode("none", ScriptsRequired::None);
}
#[test]
fn config_scripts_lowercase_default() {
assert_config_script_mode("default", ScriptsRequired::Default);
}
#[test]
fn config_scripts_lowercase_custom() {
assert_config_script_mode("custom", ScriptsRequired::Custom);
}
#[test]
fn config_scripts_legacy_and_unknown_controls() {
assert_config_script_mode("None", ScriptsRequired::None);
assert_config_script_mode("Default", ScriptsRequired::Default);
assert_config_script_mode("Custom", ScriptsRequired::Custom);
for invalid in ["disabled", "unknown", ""] {
assert!(toml::from_str::<Config>(&format!("scripts = \"{invalid}\"")).is_err());
}
}
#[test]
fn config_read_accepts_documented_lowercase_script_modes() {
for (value, expected) in [
("none", ScriptsRequired::None),
("default", ScriptsRequired::Default),
("custom", ScriptsRequired::Custom),
] {
let fixture = std::env::temp_dir().join(format!(
"rustscan-config-scripts-{}-{value}.toml",
std::process::id()
));
std::fs::write(&fixture, format!("scripts = \"{value}\"\n")).unwrap();
let config = Config::read(Some(fixture.clone()));
std::fs::remove_file(fixture).unwrap();
assert_eq!(config.scripts, Some(expected));
}
}
#[test]
fn verify_cli() {
Opts::command().debug_assert();
@@ -725,91 +635,6 @@ mod tests {
assert_eq!(opts.resolver, config.resolver);
}
#[test]
fn cli_arguments_override_config() {
// Regression test for #722: the config file used to silently replace
// options given on the command line, e.g. `-r` with its `range`.
let mut opts = Opts::try_read_from([
"rustscan",
"-a",
"192.168.0.1",
"-r",
"8600-8650",
"-b",
"100",
// Passing clap's default value explicitly must still win.
"-t",
"1500",
"--tries",
"3",
"--scan-order",
"serial",
"--scripts",
"none",
"-g",
"--",
"-sV",
])
.unwrap();
let config: Config = toml::from_str(
r#"
addresses = ["127.0.0.1"]
range = { start = 1, end = 100 }
batch_size = 25000
timeout = 1000
tries = 1
scan_order = "Random"
scripts = "Custom"
greppable = false
command = ["-A"]
"#,
)
.unwrap();
opts.merge(&config);
assert_eq!(opts.addresses, vec!["192.168.0.1".to_owned()]);
assert_eq!(opts.range, Some(PortRanges(vec![(8_600, 8_650)])));
assert_eq!(opts.batch_size, 100);
assert_eq!(opts.timeout, 1_500);
assert_eq!(opts.tries, 3);
assert_eq!(opts.scan_order, ScanOrder::Serial);
assert_eq!(opts.scripts, ScriptsRequired::None);
assert!(opts.greppable);
assert_eq!(opts.command, vec!["-sV".to_owned()]);
}
#[test]
fn config_fills_arguments_not_given_on_cli() {
let mut opts = Opts::try_read_from(["rustscan", "-a", "192.168.0.1"]).unwrap();
let config: Config = toml::from_str(
r#"
addresses = ["127.0.0.1"]
range = { start = 1, end = 100 }
batch_size = 25000
timeout = 1000
tries = 3
scan_order = "Random"
greppable = true
command = ["-A"]
"#,
)
.unwrap();
opts.merge(&config);
// Only `-a` was typed; clap defaults such as `-b 4500` must not count.
assert_eq!(opts.cli_args, HashSet::from(["addresses".to_owned()]));
assert_eq!(opts.addresses, vec!["192.168.0.1".to_owned()]);
assert_eq!(opts.range, Some(PortRanges(vec![(1, 100)])));
assert_eq!(opts.batch_size, 25_000);
assert_eq!(opts.timeout, 1_000);
assert_eq!(opts.tries, 3);
assert_eq!(opts.scan_order, ScanOrder::Random);
assert!(opts.greppable);
assert_eq!(opts.command, vec!["-A".to_owned()]);
}
#[test]
fn parses_comma_separated_ranges() {
let opts = Opts::parse_from([
+15 -72
View File
@@ -128,7 +128,20 @@ fn main() {
map.entry(socket.ip()).or_default().push(socket.port());
}
if let Some(x) = no_open_ports_warning(&ips, &ports_per_ip, &closed_ports_per_ip, batch_size) {
for ip in ips {
if ports_per_ip.contains_key(&ip) {
continue;
}
// If we got here it means the IP was not found within the HashMap, this
// means the scan couldn't find any open ports for it.
let x = format!("Looks like I didn't find any open ports for {:?}. This is usually caused by a high batch size.
\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,
batch_size,
"'rustscan -b <batch_size> -a <ip address>'");
warning!(x, opts.greppable, opts.accessible);
}
@@ -211,35 +224,6 @@ fn main() {
info!("{}", benchmarks.summary());
}
/// Builds the hint printed when the scan found no open ports.
///
/// A host without open ports is normal when another host answered (a subnet
/// sweep for one port): replies got through at this batch size and timeout,
/// so batch size and timeout are unlikely to be the cause. The hint is printed
/// once, and only when no host answered with an open or, under `--closed`, a
/// closed port.
fn no_open_ports_warning(
ips: &[IpAddr],
ports_per_ip: &HashMap<IpAddr, Vec<u16>>,
closed_ports_per_ip: &HashMap<IpAddr, Vec<u16>>,
batch_size: usize,
) -> Option<String> {
if !ports_per_ip.is_empty() || !closed_ports_per_ip.is_empty() {
return None;
}
let target = match ips {
[ip] => format!("for {ip:?}"),
_ => format!("on any of the {} addresses", ips.len()),
};
Some(format!("Looks like I didn't find any open ports {}. This is usually caused by a high batch size.
\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",
target,
batch_size,
"'rustscan -b <batch_size> -a <ip address>'"))
}
/// Determines the actual batch size used by the scanner.
///
/// Unix systems may reduce the requested batch size according to the process
@@ -389,48 +373,7 @@ mod tests {
use super::effective_batch_size;
#[cfg(unix)]
use super::{adjust_ulimit_size, infer_batch_size};
use super::{no_open_ports_warning, print_opening, Opts};
use std::collections::HashMap;
use std::net::IpAddr;
fn hosts(n: u8) -> Vec<IpAddr> {
(1..=n).map(|i| IpAddr::from([192, 0, 2, i])).collect()
}
#[test]
fn no_open_ports_silent_when_another_host_answered() {
// Sweeping a subnet for one port: one host has it open, the rest do not.
let ips = hosts(254);
let ports_per_ip = HashMap::from([(ips[0], vec![22])]);
assert!(no_open_ports_warning(&ips, &ports_per_ip, &HashMap::new(), 4500).is_none());
}
#[test]
fn no_open_ports_silent_when_a_host_answered_closed() {
// With --closed, a refused connection also proves replies get through.
let ips = hosts(254);
let closed_ports_per_ip = HashMap::from([(ips[0], vec![443])]);
assert!(no_open_ports_warning(&ips, &HashMap::new(), &closed_ports_per_ip, 4500).is_none());
}
#[test]
fn no_open_ports_warns_once_when_nothing_found() {
let warning = no_open_ports_warning(&hosts(254), &HashMap::new(), &HashMap::new(), 4500);
let warning = warning.expect("one hint when nothing was found");
assert!(warning.contains("on any of the 254 addresses."));
assert!(warning.contains("4500 batch size"));
}
#[test]
fn no_open_ports_names_the_single_host() {
let warning = no_open_ports_warning(&hosts(1), &HashMap::new(), &HashMap::new(), 4500);
let warning = warning.expect("one hint for the single host");
assert!(warning.contains("for 192.0.2.1."));
}
use super::{print_opening, Opts};
#[test]
#[cfg(unix)]
-163
View File
@@ -1,163 +0,0 @@
use std::{collections::HashSet, fmt, io, net::IpAddr};
/// Keep the OS error and its target intact until diagnostics need formatting.
#[derive(Debug)]
struct ScanError {
ip: IpAddr,
error: io::Error,
}
impl fmt::Display for ScanError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.error, self.ip)
}
}
impl std::error::Error for ScanError {}
/// Preserve the normal I/O result layout; only diagnostics need target context.
pub(super) fn diagnostic_error(error: io::Error, ip: IpAddr, enabled: bool) -> io::Error {
if enabled {
io::Error::other(ScanError { ip, error })
} else {
error
}
}
pub(super) struct ScanErrors {
enabled: bool,
limit: usize,
messages: HashSet<String>,
}
impl ScanErrors {
pub(super) fn new(enabled: bool, limit: usize) -> Self {
Self {
enabled,
limit,
messages: HashSet::new(),
}
}
pub(super) fn record(&mut self, failure: io::Error) {
if self.enabled && self.messages.len() < self.limit {
self.messages.insert(failure.to_string());
}
}
pub(super) fn messages(&self) -> &HashSet<String> {
&self.messages
}
}
/// Detect descriptor exhaustion without allocating or interpreting OS messages.
#[cfg(unix)]
pub(super) fn is_descriptor_exhaustion(error: &io::Error) -> bool {
matches!(error.raw_os_error(), Some(libc::EMFILE | libc::ENFILE))
}
#[cfg(windows)]
pub(super) fn is_descriptor_exhaustion(error: &io::Error) -> bool {
error.raw_os_error() == Some(windows_sys::Win32::Networking::WinSock::WSAEMFILE)
}
// Retain the previous fallback for targets without Unix or Winsock error codes.
#[cfg(not(any(unix, windows)))]
pub(super) fn is_descriptor_exhaustion(error: &io::Error) -> bool {
error
.to_string()
.to_lowercase()
.contains("too many open files")
}
#[cfg(test)]
mod tests {
use super::*;
use std::{fmt, net::SocketAddr};
#[derive(Debug)]
struct MustNotFormat;
impl fmt::Display for MustNotFormat {
fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result {
panic!("discarded diagnostics must not format errors")
}
}
impl std::error::Error for MustNotFormat {}
fn failure(error: io::Error) -> io::Error {
diagnostic_error(error, "127.0.0.1".parse().unwrap(), true)
}
#[test]
fn disabled_diagnostics_do_not_format_errors() {
let mut errors = ScanErrors::new(false, 1000);
errors.record(diagnostic_error(
io::Error::other(MustNotFormat),
"127.0.0.1".parse().unwrap(),
false,
));
assert!(errors.messages().is_empty());
}
#[test]
fn disabled_diagnostics_preserve_the_original_os_error() {
let original = io::Error::from_raw_os_error(123);
let error = diagnostic_error(original, "127.0.0.1".parse().unwrap(), false);
assert_eq!(error.raw_os_error(), Some(123));
}
#[test]
fn full_diagnostics_do_not_format_more_errors() {
let mut errors = ScanErrors::new(true, 1);
errors.record(failure(io::ErrorKind::ConnectionRefused.into()));
errors.record(failure(io::Error::other(MustNotFormat)));
assert_eq!(errors.messages().len(), 1);
}
#[test]
fn enabled_diagnostics_keep_error_details_and_deduplicate_per_ip() {
let mut errors = ScanErrors::new(true, 1000);
for port in [80, 443] {
errors.record(diagnostic_error(
io::Error::other("test socket error"),
SocketAddr::from(([127, 0, 0, 1], port)).ip(),
true,
));
}
assert_eq!(errors.messages().len(), 1);
assert!(errors.messages().contains("test socket error 127.0.0.1"));
}
#[cfg(unix)]
#[test]
fn detects_unix_descriptor_limits_by_code() {
for code in [libc::EMFILE, libc::ENFILE] {
assert!(is_descriptor_exhaustion(&io::Error::from_raw_os_error(
code
)));
}
assert!(!is_descriptor_exhaustion(&io::Error::from_raw_os_error(
libc::ECONNREFUSED
)));
}
#[cfg(windows)]
#[test]
fn detects_winsock_descriptor_limits_by_code() {
use windows_sys::Win32::Networking::WinSock::{WSAECONNREFUSED, WSAEMFILE};
assert!(is_descriptor_exhaustion(&io::Error::from_raw_os_error(
WSAEMFILE
)));
assert!(!is_descriptor_exhaustion(&io::Error::from_raw_os_error(
WSAECONNREFUSED
)));
}
#[cfg(any(unix, windows))]
#[test]
fn descriptor_check_never_formats_an_error() {
assert!(!is_descriptor_exhaustion(&io::Error::other(MustNotFormat)));
}
}
+19 -19
View File
@@ -7,16 +7,13 @@ use log::debug;
mod socket_iterator;
use socket_iterator::SocketIterator;
mod errors;
use errors::{diagnostic_error, is_descriptor_exhaustion, ScanErrors};
use colored::Colorize;
use futures::stream::{FuturesUnordered, StreamExt};
use std::collections::BTreeMap;
use std::future::poll_fn;
use std::task::Poll;
use std::{
collections::HashMap,
collections::{HashMap, HashSet},
io,
net::{IpAddr, Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr},
num::NonZeroU8,
@@ -218,8 +215,7 @@ impl Scanner {
async fn scan(&self) -> Vec<PortStatus> {
let ports = filter_excluded_ports(self.port_strategy.order(), &self.exclude_ports);
let mut found_sockets: Vec<PortStatus> = Vec::new();
let mut errors =
ScanErrors::new(log::log_enabled!(log::Level::Debug), self.ips.len() * 1000);
let mut errors: HashSet<String> = HashSet::new();
// Build UDP payload lookup once (only if we are scanning UDP).
// This avoids cloning a big map into every spawned future and turns
@@ -254,7 +250,7 @@ impl Scanner {
}
}
debug!("Typical socket connection errors {:?}", errors.messages());
debug!("Typical socket connection errors {errors:?}");
debug!("Sockets found: {:?}", found_sockets);
found_sockets
}
@@ -275,7 +271,7 @@ impl Scanner {
mut sockets: SocketIterator<'_>,
udp_payloads: &Option<Arc<UdpPayloadLookup>>,
found_sockets: &mut Vec<PortStatus>,
errors: &mut ScanErrors,
errors: &mut HashSet<String>,
) {
let mut ftrs = FuturesUnordered::new();
@@ -323,19 +319,24 @@ impl Scanner {
&self,
result: io::Result<PortStatus>,
found_sockets: &mut Vec<PortStatus>,
errors: &mut ScanErrors,
errors: &mut HashSet<String>,
) {
match result {
Ok(status) => found_sockets.push(status),
Err(error) => errors.record(error),
Err(e) => {
let error_string = e.to_string();
if errors.len() < self.ips.len() * 1000 {
errors.insert(error_string);
}
}
}
}
/// Given a socket, scan it self.tries times.
/// Turns the address into a SocketAddr
/// Deals with the `<result>` type
/// Panics if the OS reports descriptor exhaustion.
/// Other failures retain their I/O error, with target context for debug diagnostics.
/// If it experiences error ErrorKind::Other then too many files are open and it Panics!
/// Else any other error, it returns the error in Result as a string
/// If no errors occur, it returns the port number in Result to signify the port is open.
/// This function mainly deals with the logic of Results handling.
/// # Example
@@ -375,14 +376,14 @@ impl Scanner {
return Ok(PortStatus::Closed(socket));
}
assert!(!is_descriptor_exhaustion(&e), "Too many open files. Please reduce batch size. The default is 5000. Try -b 2500.");
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.");
if nr_try == tries {
return Err(diagnostic_error(
e,
socket.ip(),
log::log_enabled!(log::Level::Debug),
));
error_string.push(' ');
error_string.push_str(&socket.ip().to_string());
return Err(io::Error::other(error_string));
}
}
};
@@ -605,7 +606,6 @@ mod tests {
use super::*;
use crate::input::{PortRanges, ScanOrder};
use std::collections::HashSet;
// These tests never open sockets: they only build a `Scanner` (and its
// futures), run a scan that has no socket to scan, or inspect the payload
+2 -121
View File
@@ -143,78 +143,9 @@ pub fn init_scripts(scripts: &ScriptsRequired) -> Result<Vec<ScriptFile>> {
}
}
verify_script_programs(&scripts_to_run)?;
Ok(scripts_to_run)
}
/// Whether `path/program` is something the shell could run.
#[cfg(not(windows))]
fn program_exists_in(dir: &std::path::Path, program: &str) -> bool {
dir.join(program).is_file()
}
/// Whether `path/program` is something `cmd.exe` could run, which appends
/// entries from `PATHEXT` to a bare name.
#[cfg(windows)]
fn program_exists_in(dir: &std::path::Path, program: &str) -> bool {
if dir.join(program).is_file() {
return true;
}
let extensions = std::env::var("PATHEXT").unwrap_or_default();
extensions
.split(';')
.filter(|extension| !extension.is_empty())
.any(|extension| dir.join(format!("{program}{extension}")).is_file())
}
fn program_is_available(program: &str) -> bool {
match std::env::var_os("PATH") {
Some(path) => std::env::split_paths(&path).any(|dir| program_exists_in(&dir, program)),
None => false,
}
}
/// Check that the program each selected script starts with is available.
///
/// Scripts run through `sh -c`, so a missing binary surfaces only as a bare
/// "Exit code = 127" once the scan has already finished. Report it up front
/// instead, before a long scan has been wasted.
fn verify_script_programs(scripts_to_run: &[ScriptFile]) -> Result<()> {
for script in scripts_to_run {
let call_format = match &script.call_format {
Some(call_format) => call_format,
None => continue,
};
let program = match call_format.split_whitespace().next() {
Some(program) => program,
None => continue,
};
// A leading assignment (`FOO=bar nmap ...`) or an absolute path is not
// a bare program name, and `{{...}}` placeholders are only filled in
// when the script actually runs, so leave those formats alone.
if program.contains('=')
|| program.contains('/')
|| program.contains('$')
|| program.contains("{{")
{
continue;
}
if !program_is_available(program) {
return Err(anyhow!(
"The script `{}` runs `{}`, which is not installed or not in PATH. \
Install it, or skip scripts with `--scripts none`.",
script
.path
.as_ref()
.map_or_else(|| "default".to_string(), |path| path.display().to_string()),
program
));
}
}
Ok(())
}
pub fn parse_scripts(scripts: Vec<PathBuf>) -> Vec<ScriptFile> {
let mut parsed_scripts: Vec<ScriptFile> = Vec::with_capacity(scripts.len());
for script in scripts {
@@ -417,7 +348,8 @@ impl ScriptFile {
if let Ok(file) = File::open(script) {
for mut line in io::BufReader::new(file).lines().skip(1).flatten() {
if line.starts_with('#') {
line = line[1..].trim().to_string();
line.retain(|c| c != '#');
line = line.trim().to_string();
line.push('\n');
lines_buf.push_str(&line);
} else {
@@ -471,21 +403,6 @@ impl ScriptConfig {
mod tests {
use super::*;
#[test]
fn script_headers_preserve_literal_hash_characters() {
let fixture =
std::env::temp_dir().join(format!("rustscan-literal-hash-{}.txt", std::process::id()));
fs::write(&fixture, "#!/usr/bin/env python3\n#tags = [\"C#\"]\n#call_format = \"fixture {{ip}} {{port}} https://example.invalid/#section\"\n\n").unwrap();
let parsed = ScriptFile::new(fixture.clone());
fs::remove_file(fixture).unwrap();
let parsed = parsed.expect("valid TOML metadata");
assert_eq!(parsed.tags, Some(vec!["C#".to_owned()]));
assert_eq!(
parsed.call_format.as_deref(),
Some("fixture {{ip}} {{port}} https://example.invalid/#section")
);
}
#[test]
fn find_and_parse_scripts() {
let scripts = find_scripts("fixtures/.rustscan_scripts".into()).unwrap();
@@ -578,40 +495,4 @@ mod tests {
assert_eq!(script_dir_base, dirs::home_dir().unwrap());
}
fn script_with_call_format(call_format: &str) -> ScriptFile {
ScriptFile {
path: Some(PathBuf::from("/tmp/script.txt")),
tags: None,
developer: None,
port: None,
ports_separator: None,
call_format: Some(call_format.to_string()),
}
}
#[test]
fn missing_script_program_is_reported() {
let scripts = vec![script_with_call_format("definitely-not-a-real-binary {{ip}}")];
let err = verify_script_programs(&scripts).unwrap_err();
assert!(err.to_string().contains("definitely-not-a-real-binary"));
}
#[test]
fn available_script_program_is_accepted() {
// `sh` is what the scripts are actually run through, so it is present
// on every platform that can execute them.
let scripts = vec![script_with_call_format("sh -c true {{ip}}")];
assert!(verify_script_programs(&scripts).is_ok());
}
#[test]
fn placeholder_and_assignment_prefixes_are_skipped() {
let scripts = vec![
script_with_call_format("{{script}} {{ip}} {{port}}"),
script_with_call_format("FOO=bar definitely-not-a-real-binary"),
script_with_call_format("/opt/custom/nmap {{ip}}"),
];
assert!(verify_script_programs(&scripts).is_ok());
}
}