Compare commits

...
220 Commits
Author SHA1 Message Date
bee eae9fce582 Make the baseline benchmark adapter whitespace clean 2026-10-01 18:43:57 +01:00
bee 74e1e056ff Document final Tokio performance comparison and remaining overhead 2026-10-01 18:42:55 +01:00
bee 42427e88c1 Poll I/O directly before initializing timeout state 2026-10-01 18:33:26 +01:00
bee 81a55fcf6b Keep timeout state small for immediately ready operations 2026-10-01 18:30:40 +01:00
bee e83641fca8 Avoid precise timer registration for early socket replies 2026-10-01 18:23:15 +01:00
bee b5fedf363a Retain the existing timer backend for short scan deadlines 2026-10-01 18:15:05 +01:00
bee 62ed937768 Preserve timeout precision and avoid unnecessary timer allocation 2026-10-01 18:08:36 +01:00
bee 397526df8a Document real TCP and UDP runtime benchmarks 2026-10-01 17:59:36 +01:00
bee 2e57686260 Migrate scanner from async-std to Tokio 2026-10-01 17:38:30 +01:00
Syed Ghufran HassanandAutumn Skerritt cfb8645901 Refactor SocketIterator for allocation-free performance (#945)
* Refactor SocketIterator for allocation-free performance

Refactor SocketIterator to eliminate boxing and improve memory efficiency. Update documentation and tests to reflect changes.

* fix(socket_iterator): fix clone test off-by-one and skip private doc example

- clone_resumes_independently expected None after 3 of 4 sockets; assert
  the exact remaining sequence for both the original and the clone.
- The struct doc example referenced a placeholder crate and a private
  module, so it could never compile as a doctest; mark it ignore.
- Add a test for empty IP/port inputs covering size_hint and the
  FusedIterator promise.

---------

Co-authored-by: Autumn Skerritt <[email protected]>
2026-10-01 22:03:40 +09:00
ReacherandAutumn Skerritt d7cdad29c6 refactor(main): avoid per-IP script list clones and safer ulimit fallback (#905)
Small cleanups in main.rs:

- Iterate over the parsed scripts by reference and clone only the script being run, instead of cloning the whole list for every IP.
- Use `or_default()` for the per-IP port map.
- If the soft file-descriptor limit does not fit in a `usize` (32-bit targets), fall back to `DEFAULT_FILE_DESCRIPTORS_LIMIT` instead of `usize::MAX`, which bypassed all batch-size adjustments.
- Fix a typo.

The effective-batch-size warning fix originally proposed here landed via #921.

Co-authored-by: Autumn Skerritt <[email protected]>
2026-10-01 20:24:16 +09:00
dependabot[bot] ea9e6f0736 build(deps): bump toml from 1.1.5+spec-1.1.0 to 1.1.6+spec-1.1.0 (#931)
Bumps [toml](https://github.com/toml-rs/toml) from 1.1.5+spec-1.1.0 to 1.1.6+spec-1.1.0.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v1.1.5...toml-v1.1.6)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 1.1.6+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-01 20:23:25 +09:00
dependabot[bot] 2c854eb551 build(deps): bump clap from 4.6.2 to 4.6.7 (#934)
Bumps [clap](https://github.com/clap-rs/clap) from 4.6.2 to 4.6.7.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/main/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.2...clap_complete-v4.6.7)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.6.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-01 20:23:10 +09:00
dependabot[bot] 1c3fd8c378 build(deps): bump rand from 0.10.2 to 0.10.3 (#936)
Bumps [rand](https://github.com/rust-random/rand) from 0.10.2 to 0.10.3.
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.10.2...0.10.3)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.10.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-01 20:22:54 +09:00
onsaliandAutumn Skerritt 4e64b5f00a feat: optional interval between scanned ports (#713)
New opt-in `--interval <MS>` option (also `interval` in the config file): after a port has been scanned on every address, wait that many milliseconds before scanning the next port, for slow, low-noise scans. Within a port `--batch-size` still limits concurrency; with the default of 0 scanning behaviour is unchanged.

Library: `Scanner::with_interval(Duration)` is added; `Scanner::new()` is unchanged.

Co-authored-by: Autumn Skerritt <[email protected]>
2026-10-01 20:19:49 +09:00
fabianbeesandAutumn Skerritt faa21b7d14 feat: option to also list closed ports (#736)
New `--closed` option (also `closed = true` in the config file) that additionally lists TCP ports that actively refused the connection: printed live as `Closed ip:port` and summarised under `closed ports:` at the end (also in greppable mode). Scripts are never run against closed ports, and without `--closed` scanning behaviour is unchanged.

Library: `Scanner::with_closed_ports()` and `Scanner::run_with_status() -> Vec<PortStatus>` are added; `Scanner::new()` and `run()` are unchanged.

Co-authored-by: Autumn Skerritt <[email protected]>
2026-10-01 20:14:48 +09:00
satyanand choudharyandAutumn Skerritt bf74950995 feat: support comma-separated multi-range port input (#852)
`-r/--range` now accepts several comma-separated ranges, e.g. `-r 1-100,200-300,5000-5100`. Overlapping/adjacent ranges are merged; random order uses the existing LCG walk extended over the merged ranges (every port visited exactly once), serial order follows the input order and skips duplicates. Reversed ranges (`300-200`) are now rejected instead of underflowing.

`range` in the config file keeps accepting the old `{ start = 1, end = 1000 }` table and additionally accepts `"1-500,1000-2500"` or `[[1, 500], [1000, 2500]]`.

Library API: `input::PortRange { start, end }` is replaced by `input::PortRanges(Vec<(u16, u16)>)`; `PortStrategy::pick` keeps its signature otherwise.

Fixes #835.

Co-authored-by: Autumn Skerritt <[email protected]>
2026-10-01 20:09:16 +09:00
Niklas Saari c027ebc7da test(address): cover CIDR notation with host bits set (#899)
Add regression tests for CIDR inputs whose host bits are set (e.g. `192.168.1.13/29` -> 192.168.1.8–15, `192.168.1.5/23`, `10.0.0.7/30`). The parsing itself was already fixed on master by 3bdac9c (IpInet accepts host bits), so the proposed normalize_cidr helper was dropped as unreachable.
2026-10-01 19:59:17 +09:00
DanDrewManand0xxreacher 183eafd22e fix: honor configured batch size on Windows and reject unsupported --ulimit (#921)
On native Windows, use the configured `-b/--batch-size` instead of a hard-coded 3000, and report the effective batch size (not the raw CLI value) in the "no open ports" hint on all platforms. `--ulimit` is hidden from Windows help and rejected on Windows (exit code 2), including when it comes from the config file. Unix batch-size/ulimit inference is unchanged.

Effective-batch-size reporting fix originally identified by @0xxreacher in #905.

Co-authored-by: 0xxreacher <[email protected]>
2026-10-01 19:57:28 +09:00
ChrisJr404 e497adf530 fix(config): use canonical XDG path for default config (#904)
Look for the config file at the XDG-style `$XDG_CONFIG_HOME/rustscan/config.toml` first (via `dirs::config_dir()`, so the platform equivalent on macOS/Windows), then fall back to the previous `$XDG_CONFIG_HOME/.rustscan.toml` and `~/.rustscan.toml` locations so existing configs keep working.

Fixes #741.
2026-10-01 19:55:08 +09:00
Chase Dustin 9ef667caed perf(scanner): precompute UDP payload lookup (#886)
Build a port -> payload lookup once per UDP scan instead of cloning the whole nmap payload BTreeMap into every scanned socket and scanning it linearly. Payload selection keeps the previous "last match wins" semantics.

Adds a socket-free micro-benchmark (benches/benchmark_helpers.rs) and lookup tests (tests/udp_payload_lookup.rs).
2026-10-01 19:54:38 +09:00
Andrew Barnes 002f736073 fix: keep library scans quiet by default (#916)
`Scanner` no longer prints each open socket to stdout by default, so library users can embed it and use the returned sockets without unsolicited output. The CLI opts in via the new `Scanner::with_open_port_output()` builder. The UDP socket bind error is now logged at debug level instead of printed.

Fixes #706.
2026-10-01 19:52:04 +09:00
kyd 1c2a982320 fix(build): decode literal text in UDP payload probes (#938)
Rewrite the nmap-payloads decoder in build.rs to follow Nmap's C-style escape format: `\xNN` and single-character escapes are decoded, all other characters are kept literally, and multi-line quoted segments are concatenated. Literal protocol text (e.g. the SNMP `public` community string, NetBIOS names, SSDP headers) was previously dropped, producing malformed probes. Also strip CR line endings when reading nmap-payloads.

Adds regression tests pinning the SNMP (161) and SSDP (1900) probe bytes.

Fixes #933.
2026-10-01 19:48:04 +09:00
jk 9644b3a2cd fix: exit quietly on closed stdout and survive missing SystemRoot (#941)
Route all user-facing stdout writes through tui::println_safe, which exits 0 on BrokenPipe (e.g. `rustscan -g ... | head`) instead of panicking (fatal under panic = "abort"). On Windows, skip hickory-resolver's system configuration and hosts file when SystemRoot is unset, falling back to the existing cloudflare_tls resolver.

Fixes #939, fixes #940.
2026-10-01 19:43:53 +09:00
herbenderblerandAutumn Skerritt a21bbb31bb ci: run the test suite on arm64 Linux runners (#868)
Add the GitHub-hosted ubuntu-24.04-arm runner to the test matrix so the suite runs natively on arm64 Linux (macos-latest already covers arm64 macOS; build.yml already cross-builds armv7/aarch64 Linux binaries), and document ARM support in docs/arm-support.md.

Fixes #683.

Co-authored-by: Autumn Skerritt <[email protected]>
2026-10-01 19:42:49 +09:00
Brad CowieandAutumn Skerritt 8b143f8ad7 ci: upload build artifacts to GitHub releases (#858)
Build release archives with houseabsolute/actions-rust-release and publish them (plus the .deb and checksums) to the GitHub release when a tag is pushed. CD now runs on pushes to master and tags only.

Also: read-only default workflow token (publish job gets contents: write), third-party actions pinned to release SHAs, fail on unmatched release assets, and the disabled trigger-less dependabot auto-merge workflow is removed.

Fixes #700, fixes #801.

Co-authored-by: Autumn Skerritt <[email protected]>
2026-10-01 19:41:56 +09:00
Sujyot KambleandAutumn Skerritt 111316f446 ci: replace deprecated actions-rs toolchain in test workflow (#853)
Replace the deprecated actions-rs/toolchain action with actions-rust-lang/setup-rust-toolchain (pinned to v1.17.0, clippy + rustfmt components, built-in cache and default RUSTFLAGS disabled) and restrict the workflow token to contents: read.

Co-authored-by: Autumn Skerritt <[email protected]>
2026-10-01 19:32:38 +09:00
Autumn (Bee) 8be97b841d test: clean obsolete executable fixtures and CI hooks (#943) 2026-10-01 18:05:37 +09:00
Autumn (Bee) 7446a42e56 test: remove network scans and external script execution (#942) 2026-10-01 17:44:10 +09:00
Autumn (Bee) e9ba3fa547 Merge pull request #935 from rezamqds/master
Fixes a typo in the Scriptfile parser comment (Sriptfile → Scriptfile) and improves the wording for clarity.
2026-09-21 19:26:33 +09:00
rezamqds 9a45d26e99 Fix typo in Scriptfile comment
Fix typo in comment about Scriptfile parser
2026-09-20 17:28:35 +03:30
rezamqds 6cb99747e1 Fix typo in Scriptfile comment
Clarify comment on scriptfile parser behavior
2026-09-20 17:28:31 +03:30
rezamqds bc80fbbad4 Fix typo in Scriptfile comment
Fix comment for clarity in test_script.py
2026-09-20 17:27:10 +03:30
rezamqds 3168580b86 Fix typo in Scriptfile comment
Fix comment for clarity in test_script.pl
2026-09-20 17:24:58 +03:30
Autumn (Bee) 2e78c22644 Merge pull request #930 from bee-san/fix/dependabot-clippy
chore: merge Dependabot updates and restore stable Clippy checks
2026-09-16 15:31:58 +09:00
Autumn Skerritt 996f818b5e Merge remote-tracking branch 'origin/dependabot/cargo/log-0.4.34' into fix/dependabot-clippy 2026-09-16 06:13:58 +00:00
Autumn Skerritt 90c5c1766b Merge commit '55c9f2a1c87b63f23363f6626954b871bbf4ab8e' into fix/dependabot-clippy 2026-09-16 06:13:37 +00:00
Autumn Skerritt f570d9ff8e Merge commit 'ce880ebe8c83be476d217f96ca26c2f90e6261c3' into fix/dependabot-clippy 2026-09-16 06:13:36 +00:00
Autumn Skerritt 42a3c87c83 Merge commit '64f60a97eddd7a0445be732175f272960ae1bd08' into fix/dependabot-clippy
# Conflicts:
#	Cargo.toml
2026-09-16 06:13:36 +00:00
Autumn Skerritt 1e93f44d8b Merge commit 'a084c1b3b9d6831074ff1929316a14a510e32430' into fix/dependabot-clippy 2026-09-16 06:12:26 +00:00
Autumn Skerritt bf290d7c7f Merge commit 'c668636a619ca96906be7d0b2aaf4ed0356560e3' into fix/dependabot-clippy 2026-09-16 06:12:26 +00:00
Autumn Skerritt 160dc7a3a2 Merge commit '730f709fd631955582993bc03d2aab5f580aa63c' into fix/dependabot-clippy 2026-09-16 06:12:26 +00:00
Autumn Skerritt 2d52f239ca Merge commit '827acd9cb00255662954cc36b5a90f6dbe5ba54f' into fix/dependabot-clippy 2026-09-16 06:12:26 +00:00
Autumn Skerritt 5900974fda Merge commit 'f9bc5abf212d85154d91b973f4761ac62950db11' into fix/dependabot-clippy 2026-09-16 06:12:26 +00:00
Autumn Skerritt d0d388c813 Merge commit '82c1a9373adfce5345c1745f90c9984656fbadc3' into fix/dependabot-clippy 2026-09-16 06:12:26 +00:00
Autumn Skerritt 5492dd729c fix: satisfy current stable Clippy without suppressing checks 2026-09-16 06:12:25 +00:00
Autumn Skerritt b84f1fa046 Merge remote-tracking branch 'origin/master' into fix/dependabot-clippy 2026-09-16 06:05:38 +00:00
dependabot[bot] 3d4db2b6a1 build(deps): bump log from 0.4.32 to 0.4.34
Bumps [log](https://github.com/rust-lang/log) from 0.4.32 to 0.4.34.
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.32...0.4.34)

---
updated-dependencies:
- dependency-name: log
  dependency-version: 0.4.34
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-09-16 06:05:03 +00:00
dependabot[bot] 1967598d96 build(deps): bump dirs from 6.0.0 to 7.0.0 (#924)
Bumps dirs from 6.0.0 to 7.0.0.

---
updated-dependencies:
- dependency-name: dirs
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-16 15:02:40 +09:00
dependabot[bot] 94be5711d9 build(deps): bump toml from 1.1.2+spec-1.1.0 to 1.1.5+spec-1.1.0
Bumps [toml](https://github.com/toml-rs/toml) from 1.1.2+spec-1.1.0 to 1.1.5+spec-1.1.0.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v1.1.2...toml-v1.1.5)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 1.1.5+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-09-07 09:03:00 +00:00
dependabot[bot] 55c9f2a1c8 build(deps): bump clap from 4.6.1 to 4.6.2
Bumps [clap](https://github.com/clap-rs/clap) from 4.6.1 to 4.6.2.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.1...clap_complete-v4.6.2)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-07-16 09:03:00 +00:00
dependabot[bot] ce880ebe8c build(deps): bump rand from 0.10.1 to 0.10.2
Bumps [rand](https://github.com/rust-random/rand) from 0.10.1 to 0.10.2.
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.10.1...0.10.2)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.10.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-07-02 09:02:49 +00:00
dependabot[bot] 64f60a97ed build(deps): bump cidr-utils from 0.6.2 to 0.7.1
Bumps [cidr-utils](https://github.com/magiclen/cidr-utils) from 0.6.2 to 0.7.1.
- [Commits](https://github.com/magiclen/cidr-utils/compare/v0.6.2...v0.7.1)

---
updated-dependencies:
- dependency-name: cidr-utils
  dependency-version: 0.7.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-06-29 09:03:10 +00:00
dependabot[bot] a084c1b3b9 build(deps): bump env_logger from 0.11.10 to 0.11.11
Bumps [env_logger](https://github.com/rust-cli/env_logger) from 0.11.10 to 0.11.11.
- [Release notes](https://github.com/rust-cli/env_logger/releases)
- [Changelog](https://github.com/rust-cli/env_logger/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-cli/env_logger/compare/v0.11.10...v0.11.11)

---
updated-dependencies:
- dependency-name: env_logger
  dependency-version: 0.11.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-06-26 09:02:48 +00:00
dependabot[bot] c668636a61 build(deps): bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-06-19 09:03:11 +00:00
dependabot[bot] 730f709fd6 build(deps): bump itertools from 0.14.0 to 0.15.0
Bumps [itertools](https://github.com/rust-itertools/itertools) from 0.14.0 to 0.15.0.
- [Changelog](https://github.com/rust-itertools/itertools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-itertools/itertools/compare/v0.14.0...v0.15.0)

---
updated-dependencies:
- dependency-name: itertools
  dependency-version: 0.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-06-17 09:03:14 +00:00
Roger G. Coram e9dadb4a30 Merge pull request #902 from bee-san/dependabot/github_actions/actions/checkout-6
build(deps): bump actions/checkout from 4 to 6
2026-06-10 11:33:31 +01:00
Roger G. Coram 036297fbb9 Merge pull request #898 from bee-san/dependabot/cargo/clap-4.6.1
build(deps): bump clap from 4.6.0 to 4.6.1
2026-06-10 11:32:43 +01:00
Roger G. Coram e7db81a478 Merge pull request #908 from bee-san/dependabot/cargo/log-0.4.32
build(deps): bump log from 0.4.29 to 0.4.32
2026-06-10 11:32:09 +01:00
Roger G. Coram 7c7018530b Merge pull request #896 from bee-san/dependabot/cargo/toml-1.1.2spec-1.1.0
build(deps): bump toml from 1.1.0+spec-1.1.0 to 1.1.2+spec-1.1.0
2026-06-10 11:31:47 +01:00
Roger G. Coram 3abd1abe68 Merge pull request #897 from bee-san/dependabot/cargo/rand-0.10.1
build(deps): bump rand from 0.10.0 to 0.10.1
2026-06-10 11:31:13 +01:00
dependabot[bot] a4099a4ec2 build(deps): bump log from 0.4.29 to 0.4.32
Bumps [log](https://github.com/rust-lang/log) from 0.4.29 to 0.4.32.
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.29...0.4.32)

---
updated-dependencies:
- dependency-name: log
  dependency-version: 0.4.32
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-06-04 09:02:56 +00:00
dependabot[bot] 827acd9cb0 build(deps): bump actions/upload-artifact from 4 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-04-30 09:06:04 +00:00
dependabot[bot] e724d1953c build(deps): bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-04-30 09:06:00 +00:00
dependabot[bot] f9bc5abf21 build(deps): bump houseabsolute/actions-rust-cross from 0 to 1
Bumps [houseabsolute/actions-rust-cross](https://github.com/houseabsolute/actions-rust-cross) from 0 to 1.
- [Release notes](https://github.com/houseabsolute/actions-rust-cross/releases)
- [Changelog](https://github.com/houseabsolute/actions-rust-cross/blob/v1/Changes.md)
- [Commits](https://github.com/houseabsolute/actions-rust-cross/compare/v0...v1)

---
updated-dependencies:
- dependency-name: houseabsolute/actions-rust-cross
  dependency-version: '1'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-04-30 09:05:56 +00:00
dependabot[bot] 82c1a9373a build(deps): bump extractions/setup-just from 2 to 4
Bumps [extractions/setup-just](https://github.com/extractions/setup-just) from 2 to 4.
- [Release notes](https://github.com/extractions/setup-just/releases)
- [Commits](https://github.com/extractions/setup-just/compare/v2...v4)

---
updated-dependencies:
- dependency-name: extractions/setup-just
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-04-30 09:05:52 +00:00
dependabot[bot] 45ece51c63 build(deps): bump clap from 4.6.0 to 4.6.1
Bumps [clap](https://github.com/clap-rs/clap) from 4.6.0 to 4.6.1.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.0...clap_complete-v4.6.1)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-04-21 09:04:00 +00:00
dependabot[bot] e234127874 build(deps): bump rand from 0.10.0 to 0.10.1
Bumps [rand](https://github.com/rust-random/rand) from 0.10.0 to 0.10.1.
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.10.0...0.10.1)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.10.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-04-13 09:13:48 +00:00
dependabot[bot] 053a3eb0eb build(deps): bump toml from 1.1.0+spec-1.1.0 to 1.1.2+spec-1.1.0
Bumps [toml](https://github.com/toml-rs/toml) from 1.1.0+spec-1.1.0 to 1.1.2+spec-1.1.0.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v1.1.0...toml-v1.1.2)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 1.1.2+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-04-02 09:03:34 +00:00
Roger G. Coram b03379890e Merge pull request #893 from bee-san/dependabot/cargo/env_logger-0.11.10
build(deps): bump env_logger from 0.11.9 to 0.11.10
2026-03-26 07:47:59 +00:00
Roger G. Coram 52ba449b70 Merge pull request #894 from bee-san/dependabot/cargo/toml-1.1.0spec-1.1.0
build(deps): bump toml from 1.0.6+spec-1.1.0 to 1.1.0+spec-1.1.0
2026-03-26 07:47:33 +00:00
dependabot[bot] 79768931e3 build(deps): bump toml from 1.0.6+spec-1.1.0 to 1.1.0+spec-1.1.0
Bumps [toml](https://github.com/toml-rs/toml) from 1.0.6+spec-1.1.0 to 1.1.0+spec-1.1.0.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v1.0.6...toml-v1.1.0)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 1.1.0+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-03-24 09:03:49 +00:00
dependabot[bot] 3c3a69b06b build(deps): bump env_logger from 0.11.9 to 0.11.10
Bumps [env_logger](https://github.com/rust-cli/env_logger) from 0.11.9 to 0.11.10.
- [Release notes](https://github.com/rust-cli/env_logger/releases)
- [Changelog](https://github.com/rust-cli/env_logger/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-cli/env_logger/compare/v0.11.9...v0.11.10)

---
updated-dependencies:
- dependency-name: env_logger
  dependency-version: 0.11.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-03-24 09:03:39 +00:00
Roger G. Coram 487acbe382 Merge pull request #888 from bee-san/dependabot/cargo/toml-1.0.6spec-1.1.0
build(deps): bump toml from 1.0.4+spec-1.1.0 to 1.0.6+spec-1.1.0
2026-03-14 10:17:00 +00:00
Roger G. Coram 8115b2d62e Merge pull request #889 from bee-san/dependabot/cargo/clap-4.6.0
build(deps): bump clap from 4.5.60 to 4.6.0
2026-03-14 10:16:24 +00:00
Roger G. Coram 99c3c1a650 Merge pull request #890 from bee-san/dependabot/cargo/once_cell-1.21.4
build(deps): bump once_cell from 1.21.3 to 1.21.4
2026-03-14 10:15:47 +00:00
dependabot[bot] f366ce741d build(deps): bump once_cell from 1.21.3 to 1.21.4
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.21.3 to 1.21.4.
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.21.3...v1.21.4)

---
updated-dependencies:
- dependency-name: once_cell
  dependency-version: 1.21.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-03-13 09:03:56 +00:00
dependabot[bot] 6f388ae152 build(deps): bump clap from 4.5.60 to 4.6.0
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.60 to 4.6.0.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.60...clap_complete-v4.6.0)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-03-13 09:03:46 +00:00
dependabot[bot] a11b624644 build(deps): bump toml from 1.0.4+spec-1.1.0 to 1.0.6+spec-1.1.0
Bumps [toml](https://github.com/toml-rs/toml) from 1.0.4+spec-1.1.0 to 1.0.6+spec-1.1.0.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v1.0.4...toml-v1.0.6)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 1.0.6+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-03-09 09:05:12 +00:00
Roger G. Coram 85c080ee56 Merge pull request #887 from bee-san/dependabot/cargo/toml-1.0.4spec-1.1.0
build(deps): bump toml from 1.0.0+spec-1.1.0 to 1.0.4+spec-1.1.0
2026-03-07 11:00:36 +00:00
Roger G. Coram b367fbe48e Merge pull request #885 from bee-san/dependabot/cargo/clap-4.5.60
build(deps): bump clap from 4.5.58 to 4.5.60
2026-03-07 10:59:44 +00:00
dependabot[bot] 50aed26d29 build(deps): bump toml from 1.0.0+spec-1.1.0 to 1.0.4+spec-1.1.0
Bumps [toml](https://github.com/toml-rs/toml) from 1.0.0+spec-1.1.0 to 1.0.4+spec-1.1.0.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v1.0.0...toml-v1.0.4)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 1.0.4+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-03-05 09:03:36 +00:00
dependabot[bot] 1ceb758acf build(deps): bump clap from 4.5.58 to 4.5.60
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.58 to 4.5.60.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.58...clap_complete-v4.5.60)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.60
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-02-20 09:03:53 +00:00
Roger G. Coram 07aa04a94d Merge pull request #880 from bee-san/dependabot/cargo/clap-4.5.58
build(deps): bump clap from 4.5.57 to 4.5.58
2026-02-13 08:34:08 +00:00
dependabot[bot] 42d2f6128e build(deps): bump clap from 4.5.57 to 4.5.58
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.57 to 4.5.58.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.57...clap_complete-v4.5.58)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.58
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-02-12 16:40:40 +00:00
Roger G. Coram f71ed28ff7 Merge pull request #879 from bee-san/dependabot/cargo/anstream-1.0.0
build(deps): bump anstream from 0.6.20 to 1.0.0
2026-02-12 16:39:41 +00:00
dependabot[bot] bb8aa7fa80 build(deps): bump anstream from 0.6.20 to 1.0.0
Bumps [anstream](https://github.com/rust-cli/anstyle) from 0.6.20 to 1.0.0.
- [Commits](https://github.com/rust-cli/anstyle/compare/anstream-v0.6.20...anstream-v1.0.0)

---
updated-dependencies:
- dependency-name: anstream
  dependency-version: 1.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-02-12 16:30:24 +00:00
Roger G. Coram cd9852ae9d Merge pull request #878 from bee-san/dependabot/cargo/toml-1.0.0spec-1.1.0
build(deps): bump toml from 0.9.12+spec-1.1.0 to 1.0.0+spec-1.1.0
2026-02-12 16:28:44 +00:00
Roger G. Coram 6cd10727ee Merge pull request #877 from bee-san/dependabot/cargo/env_logger-0.11.9
build(deps): bump env_logger from 0.11.8 to 0.11.9
2026-02-12 16:27:39 +00:00
dependabot[bot] 034cbccf7e build(deps): bump toml from 0.9.12+spec-1.1.0 to 1.0.0+spec-1.1.0
Bumps [toml](https://github.com/toml-rs/toml) from 0.9.12+spec-1.1.0 to 1.0.0+spec-1.1.0.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.12...toml-v1.0.0)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 1.0.0+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-02-12 09:03:59 +00:00
dependabot[bot] 6bae2a1aa2 build(deps): bump env_logger from 0.11.8 to 0.11.9
Bumps [env_logger](https://github.com/rust-cli/env_logger) from 0.11.8 to 0.11.9.
- [Release notes](https://github.com/rust-cli/env_logger/releases)
- [Changelog](https://github.com/rust-cli/env_logger/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-cli/env_logger/compare/v0.11.8...v0.11.9)

---
updated-dependencies:
- dependency-name: env_logger
  dependency-version: 0.11.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-02-12 09:03:47 +00:00
Roger G. Coram 36e303e6ad Merge pull request #876 from bee-san/dependabot/cargo/toml-0.9.12spec-1.1.0
build(deps): bump toml from 0.9.7 to 0.9.12+spec-1.1.0
2026-02-12 08:22:13 +00:00
dependabot[bot] 3c2c869ba9 build(deps): bump toml from 0.9.7 to 0.9.12+spec-1.1.0
Bumps [toml](https://github.com/toml-rs/toml) from 0.9.7 to 0.9.12+spec-1.1.0.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.7...toml-v0.9.12)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 0.9.12+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-02-11 09:03:49 +00:00
Roger G. Coramanddependabot[bot] 7f2fc87aa3 build(deps): bump rand from 0.9.2 to 0.10.0 (#875)
* build(deps): bump rand from 0.9.2 to 0.10.0

Bumps [rand](https://github.com/rust-random/rand) from 0.9.2 to 0.10.0.
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/rand_core-0.9.2...0.10.0)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>

* fix: change rand import

as per recent changelog.

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 10:58:40 +00:00
Roger G. Coram 6440aed8a7 Merge pull request #872 from bee-san/dependabot/cargo/criterion-0.8.2
build(deps): bump criterion from 0.7.0 to 0.8.2
2026-02-10 10:16:05 +00:00
dependabot[bot] 51efbca7aa build(deps): bump criterion from 0.7.0 to 0.8.2
Bumps [criterion](https://github.com/criterion-rs/criterion.rs) from 0.7.0 to 0.8.2.
- [Release notes](https://github.com/criterion-rs/criterion.rs/releases)
- [Changelog](https://github.com/criterion-rs/criterion.rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/criterion-rs/criterion.rs/compare/criterion-plot-v0.7.0...criterion-v0.8.2)

---
updated-dependencies:
- dependency-name: criterion
  dependency-version: 0.8.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-02-10 10:01:16 +00:00
Roger G. Coram c78975fa87 Merge pull request #871 from bee-san/dependabot/cargo/clap-4.5.57
build(deps): bump clap from 4.5.48 to 4.5.57
2026-02-10 09:59:42 +00:00
dependabot[bot] 3a7230127f build(deps): bump clap from 4.5.48 to 4.5.57
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.48 to 4.5.57.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.48...clap_complete-v4.5.57)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.57
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-02-10 09:53:36 +00:00
Roger G. Coram 6dbf9f51cb Merge pull request #870 from bee-san/dependabot/cargo/bytes-1.11.1
build(deps): bump bytes from 1.10.0 to 1.11.1
2026-02-10 09:46:24 +00:00
dependabot[bot] c7a265e270 build(deps): bump bytes from 1.10.0 to 1.11.1
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.10.0 to 1.11.1.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.10.0...v1.11.1)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.11.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-02-10 09:40:51 +00:00
Roger G. Coram e3d31b3957 Merge pull request #869 from bee-san/dependabot/cargo/rlimit-0.11.0
build(deps): bump rlimit from 0.10.2 to 0.11.0
2026-02-10 09:39:22 +00:00
dependabot[bot] b0965dc946 build(deps): bump rlimit from 0.10.2 to 0.11.0
Bumps [rlimit](https://github.com/Nugine/rlimit) from 0.10.2 to 0.11.0.
- [Changelog](https://github.com/Nugine/rlimit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Nugine/rlimit/compare/v0.10.2...v0.11.0)

---
updated-dependencies:
- dependency-name: rlimit
  dependency-version: 0.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-02-10 09:22:27 +00:00
Roger G. Coram 2c66b4203a Merge pull request #865 from bee-san/dependabot/cargo/colored-3.1.1
build(deps): bump colored from 3.0.0 to 3.1.1
2026-02-10 09:21:26 +00:00
dependabot[bot] c8596c7679 build(deps): bump colored from 3.0.0 to 3.1.1
Bumps [colored](https://github.com/mackwic/colored) from 3.0.0 to 3.1.1.
- [Release notes](https://github.com/mackwic/colored/releases)
- [Changelog](https://github.com/colored-rs/colored/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mackwic/colored/compare/v3.0.0...v3.1.1)

---
updated-dependencies:
- dependency-name: colored
  dependency-version: 3.1.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-02-10 09:16:32 +00:00
Roger G. Coram aeef44c6e0 test: correct Clippy warnings (#874)
[`unnecessary_unwrap`](https://rust-lang.github.io/rust-clippy/rust-1.93.0/index.html#unnecessary_unwrap)
2026-02-10 08:37:35 +00:00
Roger G. Coram b228abfaa9 Merge pull request #857 from bee-san/dependabot/cargo/cidr-utils-0.6.2
build(deps): bump cidr-utils from 0.6.1 to 0.6.2
2026-02-10 08:18:19 +00:00
Roger G. Coram 7fbd96be7c Merge pull request #850 from bee-san/dependabot/cargo/log-0.4.29
build(deps): bump log from 0.4.28 to 0.4.29
2026-02-10 08:14:51 +00:00
dependabot[bot] 4a7ca7c00b build(deps): bump cidr-utils from 0.6.1 to 0.6.2
Bumps [cidr-utils](https://github.com/magiclen/cidr-utils) from 0.6.1 to 0.6.2.
- [Commits](https://github.com/magiclen/cidr-utils/compare/v0.6.1...v0.6.2)

---
updated-dependencies:
- dependency-name: cidr-utils
  dependency-version: 0.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-12-26 09:03:16 +00:00
dependabot[bot] 60a5ae0102 build(deps): bump log from 0.4.28 to 0.4.29
Bumps [log](https://github.com/rust-lang/log) from 0.4.28 to 0.4.29.
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.28...0.4.29)

---
updated-dependencies:
- dependency-name: log
  dependency-version: 0.4.29
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-12-03 09:05:12 +00:00
bee 1b74455a35 fix(ci) 2025-11-27 11:11:39 +09:00
Jakub Kołodziej 00d1003afa Change batch_size and ulimit type (#811)
* Change batch_size and ulimit type from u16 and u64 to usize

* add error handling for overflow

* remove unnecessary conversions `clippy`

* fix missed

* chore: fix CI (unrelated)
2025-11-27 11:04:53 +09:00
4c056a9039 [Enhancement] Read config file from platform-specific config folder (#839)
* read config file from platform-specific config folder

* Update src/input.rs

Co-authored-by: Copilot <[email protected]>

---------

Co-authored-by: Autumn (Bee) <[email protected]>
Co-authored-by: Copilot <[email protected]>
2025-11-27 10:50:14 +09:00
defigli a40c4ed17f feat: implement config file support for no_banner (fixes #845) (#847) 2025-11-27 10:49:23 +09:00
bee 59c6f10fad fix ci 2025-11-27 10:48:45 +09:00
bee ec2d714e39 fix tests 2025-11-27 10:46:39 +09:00
Ben Woods c19726c74f README.md: fix arch install, install guide link (#816)
The example installation for Arch Linux previously showed installation via yay, which is an unofficial package manager only required if installing from the AUR. Since rustscan is in the official extra package repository, change the instructions to use pacman.

While here, also fix link to full install guide.
2025-11-24 22:46:01 +09:00
Autumn (Bee) 2600036621 break dependabot 2025-09-24 20:02:10 +09:00
github-actions[bot] 9596712a19 Merge pull request #833 from bee-san/dependabot/cargo/clap-4.5.48
build(deps): bump clap from 4.5.47 to 4.5.48
2025-09-22 09:16:08 +00:00
dependabot[bot] fe98a3b6ef build(deps): bump clap from 4.5.47 to 4.5.48
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.47 to 4.5.48.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.47...clap_complete-v4.5.48)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.48
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-09-22 09:15:55 +00:00
github-actions[bot] f4411e551a Merge pull request #832 from bee-san/dependabot/cargo/toml-0.9.7
build(deps): bump toml from 0.9.6 to 0.9.7
2025-09-19 09:04:20 +00:00
dependabot[bot] 0640d94f53 build(deps): bump toml from 0.9.6 to 0.9.7
Bumps [toml](https://github.com/toml-rs/toml) from 0.9.6 to 0.9.7.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.6...toml-v0.9.7)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 0.9.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-09-19 09:04:09 +00:00
github-actions[bot] d3508f8192 Merge pull request #830 from bee-san/dependabot/cargo/toml-0.9.6
build(deps): bump toml from 0.9.5 to 0.9.6
2025-09-16 09:05:32 +00:00
dependabot[bot] aacde90cf1 build(deps): bump toml from 0.9.5 to 0.9.6
Bumps [toml](https://github.com/toml-rs/toml) from 0.9.5 to 0.9.6.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.5...toml-v0.9.6)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 0.9.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-09-16 09:05:17 +00:00
github-actions[bot] 81a367e40b Merge pull request #828 from bee-san/dependabot/cargo/log-0.4.28
build(deps): bump log from 0.4.27 to 0.4.28
2025-09-04 12:59:57 +00:00
dependabot[bot] cc418f08b4 build(deps): bump log from 0.4.27 to 0.4.28
Bumps [log](https://github.com/rust-lang/log) from 0.4.27 to 0.4.28.
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.27...0.4.28)

---
updated-dependencies:
- dependency-name: log
  dependency-version: 0.4.28
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-09-04 12:59:46 +00:00
github-actions[bot] 6781108db1 Merge pull request #827 from bee-san/dependabot/cargo/clap-4.5.47
build(deps): bump clap from 4.5.46 to 4.5.47
2025-09-03 23:58:30 +00:00
dependabot[bot] ea8015747f build(deps): bump clap from 4.5.46 to 4.5.47
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.46 to 4.5.47.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.46...clap_complete-v4.5.47)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.47
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-09-03 23:58:18 +00:00
github-actions[bot] c09fb7bcfd Merge pull request #826 from bee-san/dependabot/cargo/clap-4.5.46
build(deps): bump clap from 4.5.45 to 4.5.46
2025-08-27 18:22:09 +00:00
dependabot[bot] 6b96e84878 build(deps): bump clap from 4.5.45 to 4.5.46
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.45 to 4.5.46.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.45...clap_complete-v4.5.46)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.46
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-08-27 18:21:55 +00:00
github-actions[bot] 6f8b7f42f4 Merge pull request #823 from bee-san/dependabot/cargo/async-std-1.13.2
build(deps): bump async-std from 1.13.1 to 1.13.2
2025-08-15 09:19:32 +00:00
dependabot[bot] 24ff49622c build(deps): bump async-std from 1.13.1 to 1.13.2
Bumps [async-std](https://github.com/async-rs/async-std) from 1.13.1 to 1.13.2.
- [Release notes](https://github.com/async-rs/async-std/releases)
- [Changelog](https://github.com/async-rs/async-std/blob/main/CHANGELOG.md)
- [Commits](https://github.com/async-rs/async-std/compare/v1.13.1...v1.13.2)

---
updated-dependencies:
- dependency-name: async-std
  dependency-version: 1.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-08-15 09:19:22 +00:00
github-actions[bot] 87399a1583 Merge pull request #821 from bee-san/dependabot/cargo/clap-4.5.45
build(deps): bump clap from 4.5.44 to 4.5.45
2025-08-13 09:50:37 +00:00
dependabot[bot] 578457f6bd build(deps): bump clap from 4.5.44 to 4.5.45
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.44 to 4.5.45.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.44...clap_complete-v4.5.45)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.45
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-08-13 09:50:26 +00:00
github-actions[bot] e315f82d03 Merge pull request #820 from bee-san/dependabot/cargo/clap-4.5.44
build(deps): bump clap from 4.5.43 to 4.5.44
2025-08-12 15:16:50 +00:00
dependabot[bot] 63c9fdc05a build(deps): bump clap from 4.5.43 to 4.5.44
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.43 to 4.5.44.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.43...clap_complete-v4.5.44)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.44
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-08-12 15:16:40 +00:00
github-actions[bot] 6a1dd069b1 Merge pull request #819 from bee-san/dependabot/cargo/clap-4.5.43
build(deps): bump clap from 4.5.41 to 4.5.43
2025-08-07 09:07:30 +00:00
dependabot[bot] c89fad6c58 build(deps): bump clap from 4.5.41 to 4.5.43
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.41 to 4.5.43.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.41...clap_complete-v4.5.43)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.43
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-08-07 09:07:21 +00:00
github-actions[bot] ac83fe7da0 Merge pull request #818 from bee-san/dependabot/cargo/anstream-0.6.20
build(deps): bump anstream from 0.6.19 to 0.6.20
2025-08-06 09:28:03 +00:00
dependabot[bot] 3a06c22b43 build(deps): bump anstream from 0.6.19 to 0.6.20
Bumps [anstream](https://github.com/rust-cli/anstyle) from 0.6.19 to 0.6.20.
- [Commits](https://github.com/rust-cli/anstyle/compare/anstream-v0.6.19...anstream-v0.6.20)

---
updated-dependencies:
- dependency-name: anstream
  dependency-version: 0.6.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-08-06 09:27:54 +00:00
github-actions[bot] 9524b3503d Merge pull request #817 from bee-san/dependabot/cargo/toml-0.9.5
build(deps): bump toml from 0.9.4 to 0.9.5
2025-08-05 16:19:40 +00:00
dependabot[bot] eb11342a66 build(deps): bump toml from 0.9.4 to 0.9.5
Bumps [toml](https://github.com/toml-rs/toml) from 0.9.4 to 0.9.5.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.4...toml-v0.9.5)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 0.9.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-08-05 16:19:26 +00:00
github-actions[bot] 7e8230f762 Merge pull request #814 from bee-san/dependabot/cargo/toml-0.9.4
build(deps): bump toml from 0.9.3 to 0.9.4
2025-07-30 09:37:43 +00:00
dependabot[bot] d51fbb7411 build(deps): bump toml from 0.9.3 to 0.9.4
Bumps [toml](https://github.com/toml-rs/toml) from 0.9.3 to 0.9.4.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.3...toml-v0.9.4)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 0.9.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-07-30 09:37:25 +00:00
github-actions[bot] 56a234b41f Merge pull request #813 from bee-san/dependabot/cargo/criterion-0.7.0
build(deps): bump criterion from 0.6.0 to 0.7.0
2025-07-29 09:09:25 +00:00
dependabot[bot] 59b645aa0e build(deps): bump criterion from 0.6.0 to 0.7.0
Bumps [criterion](https://github.com/bheisler/criterion.rs) from 0.6.0 to 0.7.0.
- [Changelog](https://github.com/bheisler/criterion.rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bheisler/criterion.rs/compare/0.6.0...0.7.0)

---
updated-dependencies:
- dependency-name: criterion
  dependency-version: 0.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-07-29 09:09:12 +00:00
github-actions[bot] f5cbfef10f Merge pull request #812 from bee-san/dependabot/cargo/toml-0.9.3
build(deps): bump toml from 0.9.2 to 0.9.3
2025-07-29 09:09:02 +00:00
dependabot[bot] 0772c4948d build(deps): bump toml from 0.9.2 to 0.9.3
Bumps [toml](https://github.com/toml-rs/toml) from 0.9.2 to 0.9.3.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.2...toml-v0.9.3)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 0.9.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-07-29 09:08:52 +00:00
github-actions[bot] 06e37f114e Merge pull request #808 from bee-san/dependabot/cargo/rand-0.9.2
build(deps): bump rand from 0.9.1 to 0.9.2
2025-07-21 10:22:19 +00:00
dependabot[bot] 18a71f2625 build(deps): bump rand from 0.9.1 to 0.9.2
Bumps [rand](https://github.com/rust-random/rand) from 0.9.1 to 0.9.2.
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/rand_core-0.9.1...rand_core-0.9.2)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.9.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-07-21 10:22:10 +00:00
github-actions[bot] 48f826e545 Merge pull request #806 from bee-san/dependabot/cargo/toml-0.9.2
build(deps): bump toml from 0.9.1 to 0.9.2
2025-07-14 11:40:37 +00:00
dependabot[bot] f1006e4b3c build(deps): bump toml from 0.9.1 to 0.9.2
Bumps [toml](https://github.com/toml-rs/toml) from 0.9.1 to 0.9.2.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.1...toml-v0.9.2)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 0.9.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-07-14 11:40:26 +00:00
github-actions[bot] a7257acbbb Merge pull request #804 from bee-san/dependabot/cargo/toml-0.9.1
build(deps): bump toml from 0.9.0 to 0.9.1
2025-07-11 09:41:19 +00:00
dependabot[bot] 23724292e4 build(deps): bump toml from 0.9.0 to 0.9.1
Bumps [toml](https://github.com/toml-rs/toml) from 0.9.0 to 0.9.1.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.0...toml-v0.9.1)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 0.9.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-07-11 09:41:08 +00:00
Melroy dsilva e1e5e7a020 (BUG)Typo in the tui.rs file and change in the contributing.Dockerfile (#797)
* change in the contributing.Docker File

* typo in the tui.rs file !
2025-07-11 15:55:57 +09:00
ggFROOK 2e85a6e31d change from HashMap to Vec (#792) 2025-07-11 15:54:25 +09:00
github-actions[bot] 9b43d1c6ce Merge pull request #803 from bee-san/dependabot/cargo/clap-4.5.41
build(deps): bump clap from 4.5.40 to 4.5.41
2025-07-10 09:30:17 +00:00
dependabot[bot] 7f7de101dc build(deps): bump clap from 4.5.40 to 4.5.41
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.40 to 4.5.41.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.40...clap_complete-v4.5.41)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.41
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-07-10 09:30:06 +00:00
github-actions[bot] c353cf5fd8 Merge pull request #802 from bee-san/dependabot/cargo/toml-0.9.0
build(deps): bump toml from 0.8.23 to 0.9.0
2025-07-09 09:50:32 +00:00
dependabot[bot] 94b9c0e30a build(deps): bump toml from 0.8.23 to 0.9.0
Bumps [toml](https://github.com/toml-rs/toml) from 0.8.23 to 0.9.0.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.23...toml-v0.9.0)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 0.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-07-09 09:50:20 +00:00
github-actions[bot] 042875afec Merge pull request #794 from bee-san/dependabot/cargo/clap-4.5.40
build(deps): bump clap from 4.5.39 to 4.5.40
2025-06-10 09:31:15 +00:00
dependabot[bot] edc90a4db3 build(deps): bump clap from 4.5.39 to 4.5.40
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.39 to 4.5.40.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.39...clap_complete-v4.5.40)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.40
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-06-10 09:31:05 +00:00
github-actions[bot] 028554c18f Merge pull request #793 from bee-san/dependabot/cargo/toml-0.8.23
build(deps): bump toml from 0.8.22 to 0.8.23
2025-06-09 09:44:13 +00:00
dependabot[bot] f979926275 build(deps): bump toml from 0.8.22 to 0.8.23
Bumps [toml](https://github.com/toml-rs/toml) from 0.8.22 to 0.8.23.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.22...toml-v0.8.23)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 0.8.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-06-09 09:44:03 +00:00
github-actions[bot] cf1eaf8e5c Merge pull request #790 from bee-san/dependabot/cargo/anstream-0.6.19
build(deps): bump anstream from 0.6.18 to 0.6.19
2025-06-05 09:33:59 +00:00
dependabot[bot] 618b8aa9a9 build(deps): bump anstream from 0.6.18 to 0.6.19
Bumps [anstream](https://github.com/rust-cli/anstyle) from 0.6.18 to 0.6.19.
- [Commits](https://github.com/rust-cli/anstyle/compare/anstream-v0.6.18...anstream-v0.6.19)

---
updated-dependencies:
- dependency-name: anstream
  dependency-version: 0.6.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-06-05 09:33:46 +00:00
Roger G. Coram 0f23a671a5 Merge pull request #789 from zombiepigdragon/permissive-cidr
Allow CIDR addresses with non-zero host bits
2025-06-02 06:34:28 -07:00
Alex Rawson 3bdac9c9c7 allow CIDR addresses with non-zero host bits
fixes #777
2025-05-31 13:44:45 -05:00
Roger G. Coram 1085a723c4 Merge pull request #787 from mikhail5555/improve-excluded_nets-filter-performance
Improve excluded nets filter performance dractically
2025-05-31 09:37:48 -07:00
Mikhail Epifanov e1c8c254a1 fix all clippy errors 2025-05-31 15:07:13 +02:00
github-actions[bot] 05e5c50f49 Merge pull request #788 from bee-san/dependabot/cargo/clap-4.5.39
build(deps): bump clap from 4.5.38 to 4.5.39
2025-05-28 09:50:31 +00:00
dependabot[bot] f42f887da3 build(deps): bump clap from 4.5.38 to 4.5.39
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.38 to 4.5.39.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.38...clap_complete-v4.5.39)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.39
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-05-28 09:50:20 +00:00
Mikhail Epifanov d5b7f4a024 use and a flat_map for the addresses 2025-05-23 14:04:53 +02:00
Mikhail Epifanov c5d54880fc switch to IpCidr library 2025-05-22 15:30:13 +02:00
Mikhail Epifanov 179d7fdfb8 add more realistic exclude_addresses 2025-05-22 15:18:59 +02:00
Mikhail Epifanov 2176262f22 use IpNets for filtering instead of individual IPs 2025-05-22 15:03:31 +02:00
github-actions[bot] 447283cf23 Merge pull request #786 from bee-san/dependabot/cargo/criterion-0.6.0
build(deps): bump criterion from 0.5.1 to 0.6.0
2025-05-19 09:15:54 +00:00
dependabot[bot] 8b317cec1f build(deps): bump criterion from 0.5.1 to 0.6.0
Bumps [criterion](https://github.com/bheisler/criterion.rs) from 0.5.1 to 0.6.0.
- [Changelog](https://github.com/bheisler/criterion.rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bheisler/criterion.rs/compare/0.5.1...0.6.0)

---
updated-dependencies:
- dependency-name: criterion
  dependency-version: 0.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-05-19 09:15:41 +00:00
github-actions[bot] 08ccf624c1 Merge pull request #785 from bee-san/dependabot/cargo/clap-4.5.38
build(deps): bump clap from 4.5.37 to 4.5.38
2025-05-12 09:41:37 +00:00
dependabot[bot] 16e45cea2b build(deps): bump clap from 4.5.37 to 4.5.38
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.37 to 4.5.38.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.37...clap_complete-v4.5.38)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.38
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-05-12 09:41:26 +00:00
github-actions[bot] a5c36c3f52 Merge pull request #784 from bee-san/dependabot/cargo/toml-0.8.22
build(deps): bump toml from 0.8.20 to 0.8.22
2025-04-29 09:06:33 +00:00
dependabot[bot] 1124c46f37 build(deps): bump toml from 0.8.20 to 0.8.22
Bumps [toml](https://github.com/toml-rs/toml) from 0.8.20 to 0.8.22.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.20...toml-v0.8.22)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 0.8.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-04-29 09:06:23 +00:00
github-actions[bot] 8330e4363d Merge pull request #782 from bee-san/dependabot/cargo/clap-4.5.37
build(deps): bump clap from 4.5.36 to 4.5.37
2025-04-21 11:27:15 +00:00
dependabot[bot] 08ee05ff2d build(deps): bump clap from 4.5.36 to 4.5.37
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.36 to 4.5.37.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.36...clap_complete-v4.5.37)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.37
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-04-21 11:27:04 +00:00
github-actions[bot] 34c285af75 Merge pull request #781 from bee-san/dependabot/cargo/rand-0.9.1
build(deps): bump rand from 0.9.0 to 0.9.1
2025-04-18 10:00:21 +00:00
dependabot[bot] e4eeeac6ef build(deps): bump rand from 0.9.0 to 0.9.1
Bumps [rand](https://github.com/rust-random/rand) from 0.9.0 to 0.9.1.
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.9.0...rand_core-0.9.1)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.9.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-04-18 10:00:10 +00:00
github-actions[bot] a70bd8b374 Merge pull request #779 from bee-san/dependabot/cargo/clap-4.5.36
build(deps): bump clap from 4.5.35 to 4.5.36
2025-04-14 09:17:23 +00:00
dependabot[bot] 2884a448be build(deps): bump clap from 4.5.35 to 4.5.36
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.35 to 4.5.36.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.35...clap_complete-v4.5.36)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.5.36
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-04-14 09:17:09 +00:00
github-actions[bot] 6ec01bb154 Merge pull request #776 from bee-san/dependabot/cargo/tokio-1.44.2
build(deps): bump tokio from 1.43.0 to 1.44.2
2025-04-08 02:14:53 +00:00
dependabot[bot] c09baef4f8 build(deps): bump tokio from 1.43.0 to 1.44.2
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.43.0 to 1.44.2.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.43.0...tokio-1.44.2)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.44.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-04-08 02:14:42 +00:00
github-actions[bot] cf426427ed Merge pull request #774 from bee-san/dependabot/cargo/env_logger-0.11.8
build(deps): bump env_logger from 0.11.7 to 0.11.8
2025-04-02 09:49:49 +00:00
github-actions[bot] 293056ef19 Merge pull request #773 from bee-san/dependabot/cargo/clap-4.5.35
build(deps): bump clap from 4.5.34 to 4.5.35
2025-04-02 09:49:45 +00:00
dependabot[bot] 2888336657 build(deps): bump env_logger from 0.11.7 to 0.11.8
Bumps [env_logger](https://github.com/rust-cli/env_logger) from 0.11.7 to 0.11.8.
- [Release notes](https://github.com/rust-cli/env_logger/releases)
- [Changelog](https://github.com/rust-cli/env_logger/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-cli/env_logger/compare/v0.11.7...v0.11.8)

---
updated-dependencies:
- dependency-name: env_logger
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-04-02 09:49:38 +00:00
dependabot[bot] 800cb3ba39 build(deps): bump clap from 4.5.34 to 4.5.35
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.34 to 4.5.35.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.34...clap_complete-v4.5.35)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-04-02 09:49:31 +00:00
github-actions[bot] 096ebbf73d Merge pull request #772 from bee-san/dependabot/cargo/once_cell-1.21.3
build(deps): bump once_cell from 1.21.2 to 1.21.3
2025-03-31 10:00:28 +00:00
dependabot[bot] 5e6cc410e7 build(deps): bump once_cell from 1.21.2 to 1.21.3
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.21.2 to 1.21.3.
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.21.2...v1.21.3)

---
updated-dependencies:
- dependency-name: once_cell
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-03-31 10:00:16 +00:00
github-actions[bot] 5739c6175e Merge pull request #771 from bee-san/dependabot/cargo/once_cell-1.21.2
build(deps): bump once_cell from 1.21.1 to 1.21.2
2025-03-28 09:43:12 +00:00
dependabot[bot] c134747788 build(deps): bump once_cell from 1.21.1 to 1.21.2
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.21.1 to 1.21.2.
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.21.1...v1.21.2)

---
updated-dependencies:
- dependency-name: once_cell
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-03-28 09:42:57 +00:00
Roger G. Coram 2c2a55723a Merge pull request #769 from druil0or/master
Update README.md
2025-03-28 08:36:08 +00:00
dunno c5fb1b006c Update README.md
Edited Wiki link in links-2
2025-03-27 13:45:46 +01:00
github-actions[bot] 083a20269f Merge pull request #768 from bee-san/dependabot/cargo/clap-4.5.34
build(deps): bump clap from 4.5.32 to 4.5.34
2025-03-27 09:40:56 +00:00
dependabot[bot] 815b4b5e05 build(deps): bump clap from 4.5.32 to 4.5.34
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.32 to 4.5.34.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.32...clap_complete-v4.5.34)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-03-27 09:40:43 +00:00
github-actions[bot] b7734da3a7 Merge pull request #767 from bee-san/dependabot/cargo/log-0.4.27
build(deps): bump log from 0.4.26 to 0.4.27
2025-03-24 09:47:55 +00:00
dependabot[bot] 36d71894b9 build(deps): bump log from 0.4.26 to 0.4.27
Bumps [log](https://github.com/rust-lang/log) from 0.4.26 to 0.4.27.
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.26...0.4.27)

---
updated-dependencies:
- dependency-name: log
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-03-24 09:47:43 +00:00
github-actions[bot] 8a6334dfe8 Merge pull request #766 from bee-san/dependabot/cargo/async-std-1.13.1
build(deps): bump async-std from 1.13.0 to 1.13.1
2025-03-17 09:39:31 +00:00
dependabot[bot] e7c4205ec5 build(deps): bump async-std from 1.13.0 to 1.13.1
Bumps [async-std](https://github.com/async-rs/async-std) from 1.13.0 to 1.13.1.
- [Release notes](https://github.com/async-rs/async-std/releases)
- [Changelog](https://github.com/async-rs/async-std/blob/main/CHANGELOG.md)
- [Commits](https://github.com/async-rs/async-std/compare/v1.13.0...v1.13.1)

---
updated-dependencies:
- dependency-name: async-std
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-03-17 09:39:20 +00:00
Roger G. Coram 1553e58cf7 Merge pull request #764 from bee-san/dependabot/cargo/env_logger-0.11.7
build(deps): bump env_logger from 0.11.6 to 0.11.7
2025-03-16 09:30:12 +00:00
github-actions[bot] 2e5e5fd71e Merge pull request #765 from bee-san/dependabot/cargo/once_cell-1.21.1
build(deps): bump once_cell from 1.21.0 to 1.21.1
2025-03-14 09:20:53 +00:00
dependabot[bot] ea2e8f6fbe build(deps): bump once_cell from 1.21.0 to 1.21.1
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.21.0 to 1.21.1.
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.21.0...v1.21.1)

---
updated-dependencies:
- dependency-name: once_cell
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-03-14 09:20:41 +00:00
github-actions[bot] d6bc7e7219 Merge pull request #763 from bee-san/dependabot/cargo/once_cell-1.21.0
build(deps): bump once_cell from 1.20.3 to 1.21.0
2025-03-11 09:33:47 +00:00
dependabot[bot] 5d66529231 build(deps): bump env_logger from 0.11.6 to 0.11.7
Bumps [env_logger](https://github.com/rust-cli/env_logger) from 0.11.6 to 0.11.7.
- [Release notes](https://github.com/rust-cli/env_logger/releases)
- [Changelog](https://github.com/rust-cli/env_logger/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-cli/env_logger/compare/v0.11.6...v0.11.7)

---
updated-dependencies:
- dependency-name: env_logger
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-03-11 09:33:38 +00:00
github-actions[bot] 9e3c547615 Merge pull request #762 from bee-san/dependabot/cargo/clap-4.5.32
build(deps): bump clap from 4.5.31 to 4.5.32
2025-03-11 09:33:38 +00:00
dependabot[bot] a8c723fc68 build(deps): bump once_cell from 1.20.3 to 1.21.0
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.20.3 to 1.21.0.
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.20.3...v1.21.0)

---
updated-dependencies:
- dependency-name: once_cell
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-03-11 09:33:31 +00:00
dependabot[bot] bbb8237d19 build(deps): bump clap from 4.5.31 to 4.5.32
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.31 to 4.5.32.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/v4.5.31...clap_complete-v4.5.32)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-03-11 09:33:24 +00:00
G. 5f8e015686 Add funny quotes (#759) 2025-03-09 10:02:24 +00:00
github-actions[bot] 9d6bca1119 Merge pull request #760 from bee-san/dependabot/cargo/ring-0.17.13
build(deps): bump ring from 0.17.11 to 0.17.13
2025-03-07 17:05:19 +00:00
dependabot[bot] 4168bf612e build(deps): bump ring from 0.17.11 to 0.17.13
Bumps [ring](https://github.com/briansmith/ring) from 0.17.11 to 0.17.13.
- [Changelog](https://github.com/briansmith/ring/blob/main/RELEASES.md)
- [Commits](https://github.com/briansmith/ring/commits)

---
updated-dependencies:
- dependency-name: ring
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-03-07 17:00:06 +00:00
Autumn (Bee)andbee 6a1b49e7a3 Add custom script directory (#758)
* Add custom script directory

* run just

---------

Co-authored-by: bee <[email protected]>
2025-03-05 09:28:57 +00:00
github-actions[bot] c123457734 Merge pull request #756 from RustScan/dependabot/cargo/clap-4.5.31
build(deps): bump clap from 4.5.30 to 4.5.31
2025-02-25 09:57:21 +00:00
dependabot[bot] 727a64d934 build(deps): bump clap from 4.5.30 to 4.5.31
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.30 to 4.5.31.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.30...v4.5.31)

---
updated-dependencies:
- dependency-name: clap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-02-25 09:57:05 +00:00
bee b1f008a9c9 2.4.1 2025-02-23 15:03:57 +00:00
41 changed files with 3285 additions and 1840 deletions

No files matched your search

+83 -61
View File
@@ -1,13 +1,22 @@
name: CD Pipeline
on: [push]
on:
push:
branches:
- "master"
tags:
- "*"
# Build jobs only need to read the repository. The publish job below is the
# only job that gets write access, and it only runs for tag pushes.
permissions:
contents: read
jobs:
build-nix:
env:
IN_PIPELINE: true
runs-on: ${{ matrix.os }}
if: github.ref == 'refs/heads/master'
strategy:
matrix:
type: [ubuntu-x64, ubuntu-x86, armv7, aarch64]
@@ -37,40 +46,35 @@ jobs:
path: target/aarch64-unknown-linux-gnu/release/rustscan
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Cache cargo & target directories
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Build binary
uses: houseabsolute/actions-rust-cross@v0
uses: houseabsolute/actions-rust-cross@21b0f18dc621b25bfae556ff2791fca4173121e8 # v1.0.8
with:
command: build
target: ${{ matrix.target }}
args: "--locked --release"
strip: true
toolchain: stable
- name: Build tar.gz for homebrew installs
if: matrix.type == 'ubuntu-x64'
run: |
tar czf ${{ matrix.name }}.tar.gz -C target/x86_64-unknown-linux-musl/release rustscan
- uses: actions/upload-artifact@v4
- name: Package and upload artifacts
uses: houseabsolute/actions-rust-release@9e126fd5fc18f4cecf358b3909f3991ad59663dc # v1.0.1
with:
name: ${{ matrix.name }}
path: ${{ matrix.path }}
- uses: actions/upload-artifact@v4
if: matrix.type == 'ubuntu-x64'
with:
name: ${{ matrix.name }}.tar.gz
path: ${{ matrix.name }}.tar.gz
executable-name: rustscan
target: ${{ matrix.target }}
archive-name: ${{ matrix.name }}
changes-file: ""
build-deb:
needs: [build-nix]
runs-on: ubuntu-latest
env:
IN_PIPELINE: true
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Install cargo-deb
run: cargo install -f cargo-deb
- uses: awalsh128/cache-apt-pkgs-action@v1
- uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: musl-tools # provides musl-gcc
version: 1.0
@@ -79,7 +83,7 @@ jobs:
- name: Deb Build
run: cargo deb --target=x86_64-unknown-linux-musl
- name: Upload Deb Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: rustscan.deb
path: ./target/x86_64-unknown-linux-musl/debian/*
@@ -88,65 +92,54 @@ jobs:
env:
IN_PIPELINE: true
runs-on: macos-latest
if: github.ref == 'refs/heads/master'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Cache cargo & target directories
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Build binary
uses: houseabsolute/actions-rust-cross@v0
uses: houseabsolute/actions-rust-cross@21b0f18dc621b25bfae556ff2791fca4173121e8 # v1.0.8
with:
command: build
target: x86_64-apple-darwin
args: "--locked --release"
strip: true
toolchain: stable
- name: Build tar.gz for homebrew installs
run: |
tar czf x86_64-macos-rustscan.tar.gz -C target/x86_64-apple-darwin/release rustscan
- uses: actions/upload-artifact@v4
- name: Package and upload artifacts
uses: houseabsolute/actions-rust-release@9e126fd5fc18f4cecf358b3909f3991ad59663dc # v1.0.1
with:
name: x86_64-macos-rustscan
path: target/x86_64-apple-darwin/release/rustscan
- uses: actions/upload-artifact@v4
with:
name: x86_64-macos-rustscan.tar.gz
path: x86_64-macos-rustscan.tar.gz
executable-name: rustscan
target: x86_64-apple-darwin
archive-name: x86_64-macos-rustscan
changes-file: ""
build-macos-aarch64:
env:
IN_PIPELINE: true
runs-on: macos-latest
if: github.ref == 'refs/heads/master'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Cache cargo & target directories
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Build binary
uses: houseabsolute/actions-rust-cross@v0
uses: houseabsolute/actions-rust-cross@21b0f18dc621b25bfae556ff2791fca4173121e8 # v1.0.8
with:
command: build
target: aarch64-apple-darwin
args: "--locked --release"
strip: true
toolchain: stable
- name: Build tar.gz for homebrew installs
run: |
tar czf aarch64-macos-rustscan.tar.gz -C target/aarch64-apple-darwin/release rustscan
- uses: actions/upload-artifact@v4
- name: Package and upload artifacts
uses: houseabsolute/actions-rust-release@9e126fd5fc18f4cecf358b3909f3991ad59663dc # v1.0.1
with:
name: aarch64-macos-rustscan
path: target/aarch64-apple-darwin/release/rustscan
- uses: actions/upload-artifact@v4
with:
name: aarch64-macos-rustscan.tar.gz
path: aarch64-macos-rustscan.tar.gz
executable-name: rustscan
target: aarch64-apple-darwin
archive-name: aarch64-macos-rustscan
changes-file: ""
build-windows:
env:
IN_PIPELINE: true
runs-on: ${{ matrix.os }}
if: github.ref == 'refs/heads/master'
strategy:
matrix:
type: [windows-x64, windows-x86]
@@ -154,26 +147,55 @@ jobs:
- type: windows-x64
os: windows-latest
target: x86_64-pc-windows-msvc
name: x86_64-windows-rustscan.exe
path: target\x86_64-pc-windows-msvc\release\rustscan.exe
name: x86_64-windows-rustscan
- type: windows-x86
os: windows-latest
target: i686-pc-windows-msvc
name: x86-windows-rustscan.exe
path: target\i686-pc-windows-msvc\release\rustscan.exe
name: x86-windows-rustscan
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Cache cargo & target directories
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Build binary
uses: houseabsolute/actions-rust-cross@v0
uses: houseabsolute/actions-rust-cross@21b0f18dc621b25bfae556ff2791fca4173121e8 # v1.0.8
with:
command: build
target: ${{ matrix.target }}
target: ${{ matrix.target }}
args: "--locked --release"
strip: true
toolchain: stable
- uses: actions/upload-artifact@v4
- name: Package and upload artifacts
uses: houseabsolute/actions-rust-release@9e126fd5fc18f4cecf358b3909f3991ad59663dc # v1.0.1
with:
name: ${{ matrix.name }}
path: ${{ matrix.path }}
executable-name: rustscan
target: ${{ matrix.target }}
archive-name: ${{ matrix.name }}
changes-file: ""
# Runs only for tag pushes (tags can only be created by maintainers), and
# only after every build job succeeded, so a release never ships partially.
publish:
if: github.ref_type == 'tag'
needs:
- build-nix
- build-deb
- build-macos
- build-macos-aarch64
- build-windows
runs-on: ubuntu-latest
permissions:
contents: write # create the release / upload release assets
steps:
- name: Download build artifacts
uses: actions/download-artifact@v8
with:
merge-multiple: true
- name: Publish artifacts to GitHub release
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
with:
fail_on_unmatched_files: true
files: |
*.deb
*.sha256
*.tar.gz
*.zip
-19
View File
@@ -1,19 +0,0 @@
name: Dependabot Auto-Merge
on: pull_request
permissions:
contents: write
pull-requests: write
jobs:
dependabot:
runs-on: ubuntu-latest
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Enable auto-merge for Dependabot PRs
run: |
gh pr review --approve "$PR_URL"
gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-21
View File
@@ -1,21 +0,0 @@
name: Check Markdown links
on:
push:
branches:
- master
paths-ignore:
- '.github/**'
schedule:
# Run everyday at 9:00 AM (See https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07)
- cron: "0 9 * * *"
jobs:
markdown-link-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: gaurav-nelson/github-action-markdown-link-check@v1
with:
use-quiet-mode: 'yes'
config-file: './.github/workflows/mlc_config.json'
+19 -8
View File
@@ -1,34 +1,45 @@
name: Test
on:
pull_request:
branches: [master]
push:
branches:
- master
permissions:
contents: read
jobs:
test:
name: Test Suite
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
# ubuntu-24.04-arm is the GitHub-hosted arm64 Linux runner;
# macos-latest already runs on Apple Silicon (arm64).
os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest]
rust: [stable]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
- name: Setup Rust
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
with:
profile: minimal
toolchain: ${{ matrix.rust }}
override: true
components: clippy, rustfmt
# Caching is handled by Swatinem/rust-cache below.
cache: false
# Keep RUSTFLAGS unset; `just test` already denies Clippy warnings.
rustflags: ""
- uses: taiki-e/install-action@nextest
- uses: Swatinem/rust-cache@v2
- name: Install Just
uses: extractions/setup-just@v2
uses: extractions/setup-just@v4
- name: Run just
- name: Run tests
run: just test
-5
View File
@@ -17,12 +17,7 @@ jobs:
script:
- cargo build
- cargo test
- "ulimit -n 5000; cargo test timelimits:: -- --ignored --test-threads=1 --show-output"
- cargo fmt -- --check
after_failure:
- wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh
- chmod +x send.sh
- "./send.sh failure $WEBHOOK_URL"
after_success: |
if [ -n "${GITHUB_TOKEN}" ]; then
[ "${TRAVIS_BRANCH} " = master ] &&
Generated
+701 -669
View File
File diff suppressed because it is too large. Load diff
+18 -17
View File
@@ -1,6 +1,6 @@
[package]
name = "rustscan"
version = "2.3.0"
version = "2.4.1"
authors = ["Autumn <[email protected]>"]
edition = "2018"
description = "Faster Nmap Scanning with Rust"
@@ -17,33 +17,34 @@ exclude = [
]
[dependencies]
clap = { version = "4.5.13", features = ["derive", "wrap_help"] }
colored = "3.0.0"
async-std = "1.13.0"
clap = { version = "4.6.1", features = ["derive", "wrap_help"] }
colored = "3.1.1"
tokio = { version = "1.53.1", features = ["rt", "net", "time"] }
futures = "0.3"
rlimit = "0.10.2"
log = "0.4.25"
env_logger = "0.11.6"
anstream = "=0.6.18"
dirs = "6.0.0"
async-io = "2.4.0"
rlimit = "0.11.0"
log = "0.4.32"
env_logger = "0.11.10"
anstream = "=1.0.0"
dirs = "7.0.0"
gcd = "2.0.1"
rand = "0.9.0"
rand = "0.10.1"
colorful = "0.3.2"
ansi_term = "0.12.1"
toml = "0.8.20"
toml = "1.1.2"
serde = "1.0.124"
serde_derive = "1.0.116"
cidr-utils = "0.6.1"
itertools = "0.14.0"
cidr-utils = "0.7.1"
itertools = "0.15.0"
hickory-resolver = { version = "0.24.3", features = ["dns-over-rustls"] }
anyhow = "1.0.40"
text_placeholder = { version = "0.5", features = ["struct_context"] }
once_cell = "1.20.3"
once_cell = "1.21.4"
[dev-dependencies]
tokio = { version = "1.53.1", features = ["test-util"] }
parameterized = "2.0.0"
wait-timeout = "0.2"
criterion = { version = "0.5", features = ["html_reports"] }
criterion = { version = "0.8", features = ["html_reports"] }
[package.metadata.deb]
depends = "$auto, nmap"
@@ -63,5 +64,5 @@ path = "src/main.rs"
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(tarpaulin_include)"] }
[[bench]]
name = "benchmark_portscan"
name = "benchmark_helpers"
harness = false
+5 -3
View File
@@ -40,7 +40,7 @@ MacOS:
Arch:
```
yay rustscan
pacman -S rustscan
```
# ✨ Features
@@ -55,12 +55,14 @@ Arch:
| <!--Installation Guide--> | <!--Documentation--> | <!--Discord--> |
| :----------------------------------------: | :------------------------------------: | :-------------------------: |
| :book: [Installation Guide][links-table-1] | :books: [Documentation][links-table-2] | :parrot: [Discord][discord] |
| :book: [Installation Guide][toc-install] | :books: [Documentation][links-table-2] | :parrot: [Discord][discord] |
| | :penguin: [ARM Support](docs/arm-support.md) | |
## 🙋 Table of Contents
- 📖 [Installation Guide][toc-install]
- 🐋 [Docker Usage][toc-docker-usage]
- 🐧 [ARM Support](docs/arm-support.md)
- 🦜 [Discord][discord]
- 🤸 [Usage][usage-1]
@@ -209,7 +211,7 @@ This project follows the [all-contributors](https://github.com/all-contributors/
[speed-3]: https://github.com/RustScan/RustScan/wiki/Increasing-Speed-&-Accuracy "Increasing Speed & Accuracy"
[toc-community]: https://github.com/RustScan/RustScan#-community "Community"
[links-table-1]: https://github.com/RustScan/RustScan#-full-installation-guide "Full installation guide"
[links-table-2]: https://rustscan.github.io/RustScan/ "Rustscan"
[links-table-2]: https://github.com/bee-san/RustScan/wiki "Rustscan"
[discord]: http://discord.skerritt.blog "Discord blog"
[toc-install]: https://github.com/RustScan/RustScan/wiki/Installation-Guide "Installation Guide Wiki"
[toc-docker-usage]: https://github.com/RustScan/RustScan/wiki/Installation-Guide#docker- "Docker Installation Guide Wiki"
+140
View File
@@ -0,0 +1,140 @@
use criterion::{criterion_group, criterion_main, Criterion};
use futures::{stream::FuturesUnordered, StreamExt};
use rustscan::generated::get_parsed_data;
use rustscan::input::{Opts, PortRanges, ScanOrder};
use rustscan::port_strategy::PortStrategy;
use rustscan::scanner::build_udp_payload_lookup;
use std::collections::BTreeMap;
use std::hint::black_box;
use std::io;
use std::net::IpAddr;
use std::time::Duration;
// Benchmark the production timeout path with simulated I/O operations.
#[path = "../src/scanner/timeout.rs"]
mod timeout;
fn bench_address() {
let _addrs = ["127.0.0.1".parse::<IpAddr>().unwrap()];
}
fn bench_port_strategy() {
let range = PortRanges(vec![(1, 1_000)]);
let _strategy = PortStrategy::pick(&Some(range.clone()), None, ScanOrder::Serial);
}
fn bench_address_parsing() {
let opts = Opts {
addresses: vec![
"127.0.0.1".to_owned(),
"10.2.0.1".to_owned(),
"192.168.0.0/24".to_owned(),
],
exclude_addresses: Some(vec![
"10.0.0.0/8".to_owned(),
"172.16.0.0/12".to_owned(),
"192.168.0.0/16".to_owned(),
"172.16.0.1".to_owned(),
]),
..Default::default()
};
let _ips = rustscan::address::parse_addresses(&opts);
}
// Replicates the old UDP payload selection behavior:
// scan the whole UDP payload map and find the last payload whose port list contains `port`.
fn old_payload_for_port(udp_map: &'static BTreeMap<Vec<u16>, Vec<u8>>, port: u16) -> &'static [u8] {
let mut payload: &'static [u8] = b"";
for (ports, value) in udp_map.iter() {
if ports.contains(&port) {
payload = value.as_slice();
}
}
payload
}
// Exercise the scanner's bounded scheduling pattern without opening sockets.
// Ready operations model immediate replies; pending operations expire instead.
async fn simulated_operation(expires: bool) -> io::Result<()> {
timeout::io_timeout(Duration::from_millis(10), async {
if expires {
std::future::pending::<()>().await;
}
Ok(())
})
.await
}
async fn simulated_batch(expires: bool) {
const OPERATIONS: usize = 4096;
const BATCH_SIZE: usize = 256;
let mut pending = FuturesUnordered::new();
for _ in 0..BATCH_SIZE {
pending.push(simulated_operation(expires));
}
let mut remaining = OPERATIONS - BATCH_SIZE;
while let Some(result) = pending.next().await {
let _ = black_box(result);
if remaining > 0 {
pending.push(simulated_operation(expires));
remaining -= 1;
}
}
}
fn criterion_benchmark(c: &mut Criterion) {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let mut runtime_group = c.benchmark_group("runtime scheduling");
runtime_group.sample_size(20);
runtime_group.warm_up_time(Duration::from_secs(1));
runtime_group.measurement_time(Duration::from_secs(5));
runtime_group.bench_function("ready operations", |b| {
b.iter(|| runtime.block_on(simulated_batch(false)));
});
runtime_group.bench_function("expired operations", |b| {
b.iter(|| runtime.block_on(simulated_batch(true)));
});
runtime_group.finish();
// Benching helper functions
c.bench_function("parse address", |b| b.iter(bench_address));
c.bench_function("port strategy", |b| b.iter(bench_port_strategy));
let mut address_group = c.benchmark_group("address parsing");
address_group.measurement_time(Duration::from_secs(10));
address_group.bench_function("parse addresses with exclusions", |b| {
b.iter(bench_address_parsing)
});
address_group.finish();
// UDP payload lookup micro-benchmark: compares the old linear scan of the
// payload map with the precomputed port -> payload lookup. No sockets.
let udp_map = get_parsed_data();
let lookup = build_udp_payload_lookup(udp_map);
let ports: Vec<u16> = (1..=4096).collect();
c.bench_function("udp payload lookup/old scan map 1..4096", |b| {
b.iter(|| {
for &p in ports.iter() {
let payload = old_payload_for_port(black_box(udp_map), black_box(p));
black_box(payload);
}
})
});
c.bench_function("udp payload lookup/new hashmap 1..4096", |b| {
b.iter(|| {
for &p in ports.iter() {
let payload = lookup.get(&p).copied().unwrap_or(b"");
black_box(payload);
}
})
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
-80
View File
@@ -1,80 +0,0 @@
use async_std::task::block_on;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use rustscan::input::{PortRange, ScanOrder};
use rustscan::port_strategy::PortStrategy;
use rustscan::scanner::Scanner;
use std::net::IpAddr;
use std::time::Duration;
fn portscan_tcp(scanner: &Scanner) {
let _scan_result = block_on(scanner.run());
}
fn portscan_udp(scanner: &Scanner) {
let _scan_result = block_on(scanner.run());
}
fn bench_address() {
let _addrs = ["127.0.0.1".parse::<IpAddr>().unwrap()];
}
fn bench_port_strategy() {
let range = PortRange {
start: 1,
end: 1_000,
};
let _strategy = PortStrategy::pick(&Some(range.clone()), None, ScanOrder::Serial);
}
fn criterion_benchmark(c: &mut Criterion) {
let addrs = vec!["127.0.0.1".parse::<IpAddr>().unwrap()];
let range = PortRange {
start: 1,
end: 1_000,
};
let strategy_tcp = PortStrategy::pick(&Some(range.clone()), None, ScanOrder::Serial);
let strategy_udp = PortStrategy::pick(&Some(range.clone()), None, ScanOrder::Serial);
let scanner_tcp = Scanner::new(
&addrs,
10,
Duration::from_millis(10),
1,
false,
strategy_tcp,
true,
vec![],
false,
);
c.bench_function("portscan tcp", |b| {
b.iter(|| portscan_tcp(black_box(&scanner_tcp)))
});
let scanner_udp = Scanner::new(
&addrs,
10,
Duration::from_millis(10),
1,
false,
strategy_udp,
true,
vec![],
true,
);
let mut udp_group = c.benchmark_group("portscan udp");
udp_group.measurement_time(Duration::from_secs(20));
udp_group.bench_function("portscan udp", |b| {
b.iter(|| portscan_udp(black_box(&scanner_udp)))
});
udp_group.finish();
// Benching helper functions
c.bench_function("parse address", |b| b.iter(bench_address));
c.bench_function("port strategy", |b| b.iter(bench_port_strategy));
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
+91 -19
View File
@@ -25,6 +25,10 @@ pub fn main() {
let mut curr = String::new();
for line in data.trim().split('\n') {
// Strip a trailing carriage return so CRLF checkouts parse
// identically to LF ones (otherwise `\r` pollutes port tokens and
// blank lines stop looking blank).
let line = line.strip_suffix('\r').unwrap_or(line);
if line.contains('#') || line.is_empty() {
continue;
}
@@ -141,7 +145,7 @@ fn ports_v(fp_map: &BTreeMap<i32, String>) -> BTreeMap<i32, Vec<u16>> {
} else if !segment.is_empty() {
match segment.parse::<u16>() {
Ok(port) => port_list.push(port),
Err(_) => println!("Error parsing port: {}", segment),
Err(_) => println!("Error parsing port: {segment}"),
}
}
}
@@ -167,38 +171,43 @@ fn payloads_v(fp_map: &BTreeMap<i32, String>) -> BTreeMap<i32, Vec<u8>> {
let mut payb_linenr: BTreeMap<i32, Vec<u8>> = BTreeMap::new();
for (&line_nr, data) in fp_map {
if data.contains('\"') {
let start = data.find('\"').expect("payload opening \" not found");
let payloads = &data[start + 1..];
payb_linenr.insert(line_nr, parser(payloads.trim()));
if data.contains('"') {
let start = data.find('"').expect("payload opening \" not found");
// Pass from the opening quote: the decoder pairs quotes itself
// to split multi-line segments.
payb_linenr.insert(line_nr, parser(data[start..].trim()));
}
}
payb_linenr
}
/// Converts a hexadecimal string to a Vec<u8>
/// Converts a quoted Nmap payload block to bytes.
///
/// # Arguments
///
/// * `payload` - A string slice containing the hexadecimal payload
/// * `payload` - The raw block text starting at its first opening quote:
/// one or more `"..."` segments separated by whitespace
///
/// # Returns
///
/// A vector of bytes representing the decoded payload
/// The decoded probe bytes: segments are decoded with [`decode_segment`]
/// and concatenated with no separators, so multi-line probes reassemble
/// exactly.
fn parser(payload: &str) -> Vec<u8> {
let payload = payload.trim_matches('"');
let mut tmp_str = String::new();
let mut bytes: Vec<u8> = Vec::new();
for (idx, char) in payload.chars().enumerate() {
if char == '\\' && payload.chars().nth(idx + 1) == Some('x') {
continue;
} else if char.is_ascii_hexdigit() {
tmp_str.push(char);
if tmp_str.len() == 2 {
bytes.push(u8::from_str_radix(&tmp_str, 16).unwrap());
tmp_str.clear();
let mut rest = payload.trim();
while let Some(open) = rest.find('"') {
let after_open = &rest[open + 1..];
match split_segment(after_open) {
Some((segment, remainder)) => {
decode_segment(segment, &mut bytes);
rest = remainder;
}
None => {
// Unterminated trailing quote: decode what's left literally.
decode_segment(after_open, &mut bytes);
break;
}
}
}
@@ -206,6 +215,69 @@ fn parser(payload: &str) -> Vec<u8> {
bytes
}
/// Splits off the first `"..."` segment: `text` must start just after an
/// opening quote. Escaped quotes (`\"`) do not terminate the segment.
///
/// Returns the segment body and the remainder after the closing quote.
fn split_segment(text: &str) -> Option<(&str, &str)> {
let mut escaped = false;
for (i, c) in text.char_indices() {
if escaped {
escaped = false;
continue;
}
if c == '\\' {
escaped = true;
continue;
}
if c == '"' {
return Some((&text[..i], &text[i + 1..]));
}
}
None
}
/// Decodes one quoted Nmap payload segment in C-style escape format:
/// `\xNN` becomes a byte, common single-char escapes (`\\`, `\"`,
/// `\0`, `\n`, `\r`, `\t`) decode to their value, and every other
/// character contributes its literal bytes.
///
/// Preserving literal text matters: several probes embed plain-text
/// protocol words (the SNMP `public` community string, NetBIOS names,
/// LDAP `objectClass`, SSDP headers). The previous hexdigits-only
/// decoding mangled them into wrong bytes, so agents never answered.
fn decode_segment(segment: &str, bytes: &mut Vec<u8>) {
let mut chars = segment.chars();
while let Some(c) = chars.next() {
if c != '\\' {
let mut buf = [0; 4];
bytes.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
continue;
}
match chars.next() {
Some('x') => {
let hi = chars.next().unwrap_or('0');
let lo = chars.next().unwrap_or('0');
let hex: String = [hi, lo].iter().collect();
bytes.push(u8::from_str_radix(&hex, 16).unwrap_or(0));
}
Some('0') => bytes.push(0),
Some('n') => bytes.push(b'\n'),
Some('r') => bytes.push(b'\r'),
Some('t') => bytes.push(b'\t'),
Some('\\') => bytes.push(b'\\'),
Some('"') => bytes.push(b'"'),
// Unknown escape: keep the character literally, matching Nmap,
// which passes unrecognized escapes through untouched.
Some(other) => {
let mut buf = [0; 4];
bytes.extend_from_slice(other.encode_utf8(&mut buf).as_bytes());
}
None => bytes.push(b'\\'),
}
}
}
/// Combines the ports BTreeMap and the Payloads BTreeMap
///
/// # Arguments
+4 -4
View File
@@ -1,5 +1,5 @@
# Dockerfile for RustScan development environment
# Provides a containerized setup with Rust and development tools
FROM rust
# Install nmap first.
RUN apt-get update -qy && apt-get install -qy nmap
# Then install rustfmt and clippy for cargo.
RUN rustup component add rustfmt clippy
# Install rustfmt for code formatting and clippy for linting.
RUN rustup component add rustfmt clippy
+7 -2
View File
@@ -54,10 +54,10 @@ You can now modify RustScan files with your favorite editor, once you want to co
root@container:/rustscan# cargo build
```
You are now ready to use RustScan:
You can check the command-line interface without starting a scan:
```bash
root@container:/rustscan# cargo run -- -b 2000 -t 5000 -a 127.0.0.1
root@container:/rustscan# cargo run -- --help
```
You can also format, lint with `clippy` and test the code with the following commands:
@@ -67,3 +67,8 @@ root@container:/rustscan# cargo fmt
root@container:/rustscan# cargo clippy
root@container:/rustscan# cargo test
```
Automated tests and benchmarks must run without network traffic or external script
execution. Keep coverage focused on parsing, configuration, port ordering, socket
address iteration, and timing helpers. Do not add tests that run TCP/UDP scans
(including localhost), resolve hostnames, or launch Nmap or other scripts.
+38
View File
@@ -0,0 +1,38 @@
# ARM support
This page covers which ARM targets RustScan's CI builds and tests, and how to build ARM binaries yourself.
## What CI covers
| Target | Built by | Tested by |
| --- | --- | --- |
| `aarch64-unknown-linux-gnu` (64-bit ARM Linux) | `build-nix` job in [build.yml](../.github/workflows/build.yml) | `Test Suite (ubuntu-24.04-arm)` in [test.yml](../.github/workflows/test.yml) |
| `armv7-unknown-linux-gnueabihf` (32-bit ARM Linux) | `build-nix` job in [build.yml](../.github/workflows/build.yml) | not tested in CI |
| `aarch64-apple-darwin` (Apple Silicon macOS) | `build-macos-aarch64` job in [build.yml](../.github/workflows/build.yml) | `Test Suite (macos-latest)` in [test.yml](../.github/workflows/test.yml) |
- **Builds** use [`houseabsolute/actions-rust-cross`](https://github.com/houseabsolute/actions-rust-cross). It cross-compiles the Linux ARM targets with [`cross`](https://github.com/cross-rs/cross) on x86_64 runners.
- **Tests** run natively on GitHub-hosted arm64 runners: `ubuntu-24.04-arm` for Linux, and `macos-latest`, which runs on Apple Silicon. They run the same `just test` recipe as every other platform.
## Building ARM binaries locally
The simplest option is `cross`, which runs the build in a container that already has the right toolchain and linker. It needs Docker or Podman.
```sh
cargo install cross
cross build --locked --release --target aarch64-unknown-linux-gnu
# or, for 32-bit ARM:
cross build --locked --release --target armv7-unknown-linux-gnueabihf
```
The binary ends up at `target/<target>/release/rustscan`.
On an ARM machine (for example a Raspberry Pi running a 64-bit OS, or an Apple Silicon Mac) you don't need `cross`; a normal native build works:
```sh
cargo build --locked --release
```
## Troubleshooting
- **`error[E0463]: can't find crate for std`, or linker errors:** you're cross-compiling with plain `cargo`. Either use `cross`, or install the target (`rustup target add <target>`) along with a matching cross linker.
- **`cross` can't pull its image:** check that Docker or Podman is running and that your user can reach it.
+49
View File
@@ -0,0 +1,49 @@
--- a/benches/benchmark_helpers.rs
+++ b/benches/benchmark_helpers.rs
@@ -1,0 +2 @@
+use futures::{stream::FuturesUnordered, StreamExt};
@@ -7,0 +9 @@
+use std::io;
@@ -49,0 +52,29 @@
+// Exercise the scanner's bounded scheduling pattern without opening sockets.
+// Ready operations model immediate replies; pending operations expire instead.
+async fn simulated_operation(expires: bool) -> io::Result<()> {
+ async_std::io::timeout(Duration::from_millis(10), async {
+ if expires {
+ std::future::pending::<()>().await;
+ }
+ Ok(())
+ })
+ .await
+}
+
+async fn simulated_batch(expires: bool) {
+ const OPERATIONS: usize = 4096;
+ const BATCH_SIZE: usize = 256;
+ let mut pending = FuturesUnordered::new();
+ for _ in 0..BATCH_SIZE {
+ pending.push(simulated_operation(expires));
+ }
+ let mut remaining = OPERATIONS - BATCH_SIZE;
+ while let Some(result) = pending.next().await {
+ let _ = black_box(result);
+ if remaining > 0 {
+ pending.push(simulated_operation(expires));
+ remaining -= 1;
+ }
+ }
+}
+
@@ -50,0 +82,12 @@
+ let mut runtime_group = c.benchmark_group("runtime scheduling");
+ runtime_group.sample_size(20);
+ runtime_group.warm_up_time(Duration::from_secs(1));
+ runtime_group.measurement_time(Duration::from_secs(5));
+ runtime_group.bench_function("ready operations", |b| {
+ b.iter(|| futures::executor::block_on(simulated_batch(false)));
+ });
+ runtime_group.bench_function("expired operations", |b| {
+ b.iter(|| futures::executor::block_on(simulated_batch(true)));
+ });
+ runtime_group.finish();
+
+25
View File
@@ -0,0 +1,25 @@
{"metadata": {"base": "cfb864590161d1c8a003811dcf4846ceb8c8284f", "head": "42427e88c1a6587d0a8b198a73bac4553bb4ad72", "kernel": "7.2.6-1-cachyos", "scanner_cpus": "0-3", "server_cpus": [4, 5, 6, 7], "trials": 12, "warmups": 2, "binary_sha256": {"async-std": "00c7f0d91bab260ac622540e86083175afacfd1677647d2b8e96e56ba4f60732", "tokio": "669627834d8a32f60e2167e78a9518dcede7562831adb85fb61a9d94871494b8"}, "date": "2026-10-01", "cpu": "Intel Core Ultra 7 165U", "rustc": "1.98.1", "rlimit_nofile_soft": 65536, "server_observed_counts": {"tcp": 58702, "udp_echo": 14336, "udp_silent": 17920}, "firewall_dropped_tcp_packets": 21504, "sample_order": "First two entries per series are warmups; remaining twelve are measured runs. Runtime execution order alternates for each pair.", "build_profile": "release with LTO", "network": "private namespace; loopback only"},
"cases": [
{"case": "tcp_closed_65535_b256", "arguments": ["--addresses", "127.0.0.2", "--range", "1-65535", "--batch-size", "256", "--timeout", "1500", "--tries", "1"], "expected_open_count": 0, "expected_closed_count": 0, "summary": {"case": "tcp_closed_65535_b256", "async-std": {"n": 12, "scan_median_ms": 565.61545, "scan_min_ms": 542.8159, "scan_max_ms": 597.2202, "wall_median_ms": 678.4058859998368}, "tokio": {"n": 12, "scan_median_ms": 377.77217499999995, "scan_min_ms": 365.76328, "scan_max_ms": 391.70304, "wall_median_ms": 490.91583049948895}, "paired_ratio_median": 0.6691842486934787, "paired_ratio_bootstrap_95_percent": [0.651478199869526, 0.680316018080831]}, "samples": {"async-std": {"scan_ms": [547.71084, 546.87524, 573.7815, 542.8159, 557.4494, 551.8315, 552.46323, 547.43415, 579.65297, 576.02394, 577.4498, 548.5023, 580.08724, 597.2202], "wall_ms": [663.019741, 658.9046, 687.255539, 653.559305, 669.556233, 660.832612, 664.688112, 665.16539, 692.104642, 692.894896, 699.378062, 663.848877, 693.821868, 715.319005]}, "tokio": {"scan_ms": [386.10637, 366.50577, 388.7874, 388.9165, 367.81788, 391.70304, 375.97436, 365.76328, 379.11865, 386.067, 374.6031, 373.03102, 376.4257, 384.8664], "wall_ms": [498.365617, 480.96794, 508.885514, 507.263527, 481.161202, 502.921307, 490.034017, 479.758393, 491.797644, 497.79691, 489.335471, 487.71927, 489.31152, 502.222538]}}},
{"case": "tcp_closed_65535_b4500", "arguments": ["--addresses", "127.0.0.2", "--range", "1-65535", "--batch-size", "4500", "--timeout", "1500", "--tries", "1"], "expected_open_count": 0, "expected_closed_count": 0, "summary": {"case": "tcp_closed_65535_b4500", "async-std": {"n": 12, "scan_median_ms": 769.2585799999999, "scan_min_ms": 751.53786, "scan_max_ms": 794.6415, "wall_median_ms": 886.4860560001944}, "tokio": {"n": 12, "scan_median_ms": 571.338355, "scan_min_ms": 554.6383000000001, "scan_max_ms": 612.74534, "wall_median_ms": 686.1564379996707}, "paired_ratio_median": 0.7392118097843652, "paired_ratio_bootstrap_95_percent": [0.7270143421681834, 0.7653997249648308]}, "samples": {"async-std": {"scan_ms": [775.69896, 781.10725, 767.2349, 769.74416, 755.10293, 775.59453, 783.22995, 767.6211, 776.608, 751.53786, 752.1119, 779.8275, 768.773, 794.6415], "wall_ms": [900.142351, 901.448905, 882.951503, 885.785811, 871.963026, 891.82683, 895.949848, 883.562564, 891.482204, 863.927796, 866.721748, 894.248947, 887.186301, 909.792057]}, "tokio": {"scan_ms": [579.2819, 566.00755, 582.7605, 559.8762, 566.8316, 554.6383, 576.05165, 563.19356, 612.74534, 586.5669, 580.0588, 566.6812, 571.15334, 571.52337], "wall_ms": [692.862157, 680.198058, 698.352173, 671.65832, 680.533892, 668.544743, 688.044384, 674.552564, 735.085916, 699.754035, 695.691597, 680.953486, 687.596672, 684.716204]}}},
{"case": "tcp_mixed_4096_b256", "arguments": ["--addresses", "127.0.0.1", "--range", "20000-24095", "--batch-size", "256", "--timeout", "1500", "--tries", "1"], "expected_open_count": 512, "expected_closed_count": 0, "summary": {"case": "tcp_mixed_4096_b256", "async-std": {"n": 12, "scan_median_ms": 68.141467, "scan_min_ms": 55.01162, "scan_max_ms": 74.22786, "wall_median_ms": 187.83275400073762}, "tokio": {"n": 12, "scan_median_ms": 27.07619, "scan_min_ms": 26.30396, "scan_max_ms": 29.205604, "wall_median_ms": 141.96964999973716}, "paired_ratio_median": 0.3978116368627497, "paired_ratio_bootstrap_95_percent": [0.37823381704207826, 0.43222201838556734]}, "samples": {"async-std": {"scan_ms": [63.719936, 66.61555, 55.01162, 70.96551, 73.14818, 73.35321, 63.210845, 67.237474, 63.8246, 74.22786, 72.827265, 69.04546, 66.88673, 56.79349], "wall_ms": [180.925745, 189.033784, 169.724796, 182.401319, 193.146419, 198.574615, 187.32776, 187.759146, 189.552171, 193.552151, 187.906362, 186.211485, 188.919732, 170.810584]}, "tokio": {"scan_ms": [26.11342, 28.618133, 26.30396, 27.60356, 26.624788, 26.957022, 27.126616, 27.040524, 27.782789, 29.205604, 26.756745, 27.10681, 27.859924, 27.04557], "wall_ms": [142.457495, 146.887664, 141.370764, 145.212753, 139.031326, 141.839596, 142.860691, 141.604121, 142.410272, 144.175928, 141.592621, 140.95038, 142.099704, 142.563949]}}},
{"case": "tcp_mixed_4096_b4500", "arguments": ["--addresses", "127.0.0.1", "--range", "20000-24095", "--batch-size", "4500", "--timeout", "1500", "--tries", "1"], "expected_open_count": 512, "expected_closed_count": 0, "summary": {"case": "tcp_mixed_4096_b4500", "async-std": {"n": 12, "scan_median_ms": 105.183213, "scan_min_ms": 92.38623, "scan_max_ms": 118.32085000000001, "wall_median_ms": 223.5699919997387}, "tokio": {"n": 12, "scan_median_ms": 40.7245485, "scan_min_ms": 40.203160000000004, "scan_max_ms": 43.439656, "wall_median_ms": 156.73000700007833}, "paired_ratio_median": 0.4002557620046163, "paired_ratio_bootstrap_95_percent": [0.37097988370736024, 0.41671607494471485]}, "samples": {"async-std": {"scan_ms": [115.14703, 114.446916, 92.38623, 108.52898, 107.40388, 102.962546, 110.30818, 101.29879, 110.973485, 99.98305, 96.58376, 93.3442, 118.32085, 109.31534], "wall_ms": [237.680642, 228.93913, 214.413981, 229.134308, 220.8739, 224.930336, 225.133944, 224.350373, 224.012733, 218.937123, 211.773245, 213.402572, 230.26942, 223.127251]}, "tokio": {"scan_ms": [42.504027, 40.87808, 42.628516, 40.42182, 43.439656, 41.02149, 40.5159, 41.792054, 41.588504, 40.20316, 40.6492, 40.36123, 40.799897, 40.392883], "wall_ms": [164.520424, 153.958011, 168.511547, 151.627343, 157.813913, 158.826988, 155.48564, 159.408084, 159.302398, 157.162484, 155.533542, 153.505071, 156.29753, 155.263059]}}},
{"case": "tcp_open_512_b4500", "arguments": ["--addresses", "127.0.0.1", "--range", "20000-20511", "--batch-size", "4500", "--timeout", "1500", "--tries", "1"], "expected_open_count": 512, "expected_closed_count": 0, "summary": {"case": "tcp_open_512_b4500", "async-std": {"n": 12, "scan_median_ms": 47.70304, "scan_min_ms": 36.67692, "scan_max_ms": 68.96059, "wall_median_ms": 160.65413699971032}, "tokio": {"n": 12, "scan_median_ms": 6.261838, "scan_min_ms": 6.104236, "scan_max_ms": 6.608183, "wall_median_ms": 124.33755050005857}, "paired_ratio_median": 0.13084120838120605, "paired_ratio_bootstrap_95_percent": [0.11244047830149623, 0.1483106271556775]}, "samples": {"async-std": {"scan_ms": [46.98071, 49.201056, 46.035964, 58.204245, 68.96059, 45.698512, 38.846433, 56.73356, 46.20831, 36.67692, 41.455615, 49.29091, 49.19777, 57.64334], "wall_ms": [160.558191, 160.740311, 158.833574, 170.663018, 181.746459, 157.622693, 152.247554, 167.369295, 158.456413, 148.534519, 155.633953, 162.4747, 167.594306, 173.535561]}, "tokio": {"scan_ms": [6.28506, 6.0532, 6.104236, 6.147001, 6.206233, 6.270633, 6.42198, 6.306521, 6.194992, 6.253043, 6.608183, 6.158266, 6.350709, 6.555237], "wall_ms": [125.899195, 127.460307, 125.087796, 116.874444, 118.344468, 117.786915, 125.684343, 129.993995, 123.587305, 127.177718, 128.525335, 130.741019, 116.031822, 120.737224]}}},
{"case": "tcp_ipv6_mixed_4096_b4500", "arguments": ["--addresses", "::1", "--range", "20000-24095", "--batch-size", "4500", "--timeout", "1500", "--tries", "1"], "expected_open_count": 512, "expected_closed_count": 0, "summary": {"case": "tcp_ipv6_mixed_4096_b4500", "async-std": {"n": 12, "scan_median_ms": 106.25599, "scan_min_ms": 92.231296, "scan_max_ms": 119.37266000000001, "wall_median_ms": 225.6745664999471}, "tokio": {"n": 12, "scan_median_ms": 40.983215, "scan_min_ms": 40.500764000000004, "scan_max_ms": 43.351088000000004, "wall_median_ms": 154.43425250032305}, "paired_ratio_median": 0.38505220824311115, "paired_ratio_bootstrap_95_percent": [0.36158756791142943, 0.39815014470090115]}, "samples": {"async-std": {"scan_ms": [103.4242, 101.70391, 113.68274, 102.45315, 105.20947, 100.802965, 105.56823, 113.60494, 116.37136, 106.94375, 119.37266, 112.66027, 104.448915, 92.231296], "wall_ms": [227.238681, 213.929011, 228.832603, 223.209492, 218.101656, 211.377824, 221.963913, 228.139641, 230.733897, 221.140289, 232.238324, 230.332511, 228.886917, 212.248323]}, "tokio": {"scan_ms": [41.84887, 42.669304, 43.351088, 41.374706, 40.902335, 42.38061, 41.326944, 40.948737, 40.565256, 40.500764, 41.106615, 40.864874, 40.992025, 40.974405], "wall_ms": [154.656851, 162.111319, 164.86059, 152.724654, 155.535032, 161.052929, 153.618886, 154.69096, 152.754894, 154.585249, 154.283256, 153.658405, 153.15487, 155.942636]}}},
{"case": "udp_echo_256_b256", "arguments": ["--addresses", "127.0.0.1", "--range", "30000-30255", "--batch-size", "256", "--timeout", "1500", "--tries", "1", "--udp"], "expected_open_count": 256, "expected_closed_count": 0, "summary": {"case": "udp_echo_256_b256", "async-std": {"n": 12, "scan_median_ms": 26.09995, "scan_min_ms": 16.339796, "scan_max_ms": 51.66373, "wall_median_ms": 144.53015349999987}, "tokio": {"n": 12, "scan_median_ms": 3.550261, "scan_min_ms": 3.4090409999999998, "scan_max_ms": 3.760615, "wall_median_ms": 117.10653199997978}, "paired_ratio_median": 0.13508511965414755, "paired_ratio_bootstrap_95_percent": [0.10720311129055016, 0.16944309058040086]}, "samples": {"async-std": {"scan_ms": [21.703886, 37.568197, 16.339796, 19.275581, 25.42451, 26.131544, 26.231969, 26.068356, 40.599234, 21.879036, 51.66373, 29.525625, 37.230626, 20.666992], "wall_ms": [132.136823, 160.093568, 127.916215, 136.567585, 139.767169, 145.03025, 147.152265, 144.030057, 152.23601, 140.657323, 174.678649, 151.764345, 157.714272, 138.801602]}, "tokio": {"scan_ms": [3.58793, 3.647557, 3.512101, 3.733033, 3.409041, 3.46458, 3.760615, 3.547523, 3.589418, 3.47494, 3.5728, 3.552999, 3.502289, 3.721321], "wall_ms": [116.999981, 123.901236, 115.435224, 118.95129, 116.437523, 116.181437, 113.989477, 117.653641, 119.460373, 116.987976, 119.387785, 118.771634, 112.816753, 117.225088]}}},
{"case": "udp_ipv6_echo_256_b256", "arguments": ["--addresses", "::1", "--range", "30000-30255", "--batch-size", "256", "--timeout", "1500", "--tries", "1", "--udp"], "expected_open_count": 256, "expected_closed_count": 0, "summary": {"case": "udp_ipv6_echo_256_b256", "async-std": {"n": 12, "scan_median_ms": 27.625454499999996, "scan_min_ms": 19.875253999999998, "scan_max_ms": 38.867235, "wall_median_ms": 145.4195784995136}, "tokio": {"n": 12, "scan_median_ms": 3.799017, "scan_min_ms": 3.6955, "scan_max_ms": 4.067423, "wall_median_ms": 119.61354899995058}, "paired_ratio_median": 0.13852945122327148, "paired_ratio_bootstrap_95_percent": [0.11635290629059791, 0.15096773740372016]}, "samples": {"async-std": {"scan_ms": [40.33954, 21.779982, 25.549388, 33.147827, 25.456525, 26.251681, 28.999228, 38.867235, 36.46853, 19.875254, 32.311674, 25.559943, 29.844549, 24.700988], "wall_ms": [163.080692, 139.558642, 147.557955, 145.782568, 145.197701, 140.07066, 148.222519, 152.220419, 156.648518, 130.942647, 145.126391, 137.323428, 141.182265, 145.641456]}, "tokio": {"scan_ms": [3.874475, 3.758481, 3.860633, 3.877691, 3.839627, 3.6955, 3.873471, 3.699186, 3.83033, 3.767704, 3.739238, 3.763777, 4.067423, 3.736082], "wall_ms": [116.249398, 116.498595, 115.169205, 119.516884, 123.086973, 115.31348, 123.611914, 113.79081, 114.133146, 120.625814, 119.710214, 126.404842, 121.332105, 115.011208]}}},
{"case": "tcp_filtered_128_t20_retry2_b64", "arguments": ["--addresses", "127.0.0.3", "--range", "40000-40127", "--batch-size", "64", "--timeout", "20", "--tries", "2"], "expected_open_count": 0, "expected_closed_count": 0, "summary": {"case": "tcp_filtered_128_t20_retry2_b64", "async-std": {"n": 12, "scan_median_ms": 87.6446925, "scan_min_ms": 83.705105, "scan_max_ms": 95.73294, "wall_median_ms": 203.2048140004008}, "tokio": {"n": 12, "scan_median_ms": 90.200395, "scan_min_ms": 84.00111, "scan_max_ms": 99.7434, "wall_median_ms": 203.24993450003603}, "paired_ratio_median": 1.0119900466846206, "paired_ratio_bootstrap_95_percent": [0.9963617580190984, 1.0629572999350771]}, "samples": {"async-std": {"scan_ms": [87.98372, 91.602646, 86.6592, 92.56119, 83.82087, 83.89964, 91.62219, 83.705105, 92.42998, 88.630185, 85.04144, 95.73294, 90.631686, 86.57442], "wall_ms": [209.289809, 203.418129, 204.967734, 208.14526, 201.807086, 200.596062, 209.949818, 200.093062, 210.158309, 204.013388, 200.23668, 204.49939, 202.39624, 197.121998]}, "tokio": {"scan_ms": [83.77635, 87.813005, 90.50424, 93.56978, 89.589715, 93.88123, 84.00111, 84.11684, 93.56413, 89.66802, 89.89655, 93.36173, 89.5264, 99.7434], "wall_ms": [194.068953, 202.098137, 203.901289, 205.50657, 199.834834, 204.891561, 197.104592, 195.919982, 202.59858, 200.310774, 199.606024, 210.950254, 205.862549, 208.864636]}}},
{"case": "tcp_filtered_128_t1500_b4500", "arguments": ["--addresses", "127.0.0.3", "--range", "40000-40127", "--batch-size", "4500", "--timeout", "1500", "--tries", "1"], "expected_open_count": 0, "expected_closed_count": 0, "summary": {"case": "tcp_filtered_128_t1500_b4500", "async-std": {"n": 12, "scan_median_ms": 1519.9909000000002, "scan_min_ms": 1514.0051, "scan_max_ms": 1527.4003, "wall_median_ms": 1625.5414565002866}, "tokio": {"n": 12, "scan_median_ms": 1500.6792500000001, "scan_min_ms": 1500.6533, "scan_max_ms": 1500.7889, "wall_median_ms": 1607.9048350006815}, "paired_ratio_median": 0.9872864815881686, "paired_ratio_bootstrap_95_percent": [0.9860211048523806, 0.9893007737644279]}, "samples": {"async-std": {"scan_ms": [1520.2672, 1516.4726, 1514.8814, 1521.4541, 1526.0279, 1521.2376, 1516.8859, 1514.0051, 1517.0343, 1517.0065, 1527.4003, 1521.6393, 1522.2969, 1518.7442], "wall_ms": [1629.725436, 1624.468006, 1623.793379, 1628.174016, 1633.755173, 1633.153861, 1623.867637, 1620.035128, 1623.419386, 1623.095493, 1633.97451, 1627.215276, 1628.552662, 1623.647248]}, "tokio": {"scan_ms": [1500.7088, 1500.7062, 1500.6834, 1500.7012, 1500.6533, 1500.6555, 1500.6707, 1500.6621, 1500.7889, 1500.6878, 1500.6776, 1500.6809, 1500.7043, 1500.6754], "wall_ms": [1610.089424, 1608.901673, 1607.865974, 1607.1767, 1609.037891, 1613.202192, 1607.943696, 1605.800055, 1608.702953, 1610.989485, 1606.903005, 1606.369366, 1607.172129, 1610.257635]}}},
{"case": "udp_silent_128_t20_retry2_b64", "arguments": ["--addresses", "127.0.0.1", "--range", "31000-31127", "--batch-size", "64", "--timeout", "20", "--tries", "2", "--udp"], "expected_open_count": 0, "expected_closed_count": 0, "summary": {"case": "udp_silent_128_t20_retry2_b64", "async-std": {"n": 12, "scan_median_ms": 91.0113, "scan_min_ms": 85.47477, "scan_max_ms": 101.52072500000001, "wall_median_ms": 198.82166349998442}, "tokio": {"n": 12, "scan_median_ms": 82.374075, "scan_min_ms": 82.15854999999999, "scan_max_ms": 82.71911999999999, "wall_median_ms": 193.47493999975995}, "paired_ratio_median": 0.9056145986510931, "paired_ratio_bootstrap_95_percent": [0.8684654796153525, 0.9399090634650837]}, "samples": {"async-std": {"scan_ms": [99.2002, 87.0283, 92.82178, 93.114525, 96.1305, 89.20082, 87.134354, 86.520605, 88.210866, 85.47477, 101.520725, 93.399614, 97.3816, 88.98857], "wall_ms": [206.826011, 195.030786, 201.546836, 202.165958, 203.125042, 195.259398, 194.754565, 194.000578, 195.239916, 193.140741, 209.96905, 200.410742, 205.804614, 197.232585]}, "tokio": {"scan_ms": [82.729965, 82.53965, 82.33637, 82.18716, 82.41128, 82.5791, 82.33687, 82.54546, 82.466274, 82.26455, 82.71912, 82.15855, 82.562424, 82.242526], "wall_ms": [192.803999, 192.725638, 193.896498, 192.528066, 194.065959, 195.141017, 191.801711, 191.701865, 192.781206, 194.752557, 195.949918, 194.693378, 193.053382, 189.984702]}}},
{"case": "udp_silent_128_t1500_b4500", "arguments": ["--addresses", "127.0.0.1", "--range", "31000-31127", "--batch-size", "4500", "--timeout", "1500", "--tries", "1", "--udp"], "expected_open_count": 0, "expected_closed_count": 0, "summary": {"case": "udp_silent_128_t1500_b4500", "async-std": {"n": 12, "scan_median_ms": 1518.52745, "scan_min_ms": 1514.0991, "scan_max_ms": 1528.4443, "wall_median_ms": 1626.3375505000113}, "tokio": {"n": 12, "scan_median_ms": 1501.6571000000001, "scan_min_ms": 1501.6054000000001, "scan_max_ms": 1501.781, "wall_median_ms": 1608.1746704999205}, "paired_ratio_median": 0.9888991810266266, "paired_ratio_bootstrap_95_percent": [0.9865208648810749, 0.9898526070883118]}, "samples": {"async-std": {"scan_ms": [1518.4212, 1517.0245, 1518.4689, 1516.9675, 1520.9479, 1523.4239, 1518.586, 1514.0991, 1515.2898, 1517.1113, 1526.8329, 1520.123, 1517.5409, 1528.4443], "wall_ms": [1625.755639, 1625.149282, 1623.676844, 1629.639593, 1629.579045, 1629.622137, 1625.341947, 1622.234258, 1621.903718, 1622.809251, 1632.650784, 1627.333154, 1623.842187, 1634.457294]}, "tokio": {"scan_ms": [1501.6277, 1501.7376, 1501.656, 1501.6828, 1501.6761, 1501.6582, 1501.6851, 1501.6054, 1501.781, 1501.608, 1501.6263, 1501.6633, 1501.641, 1501.6465], "wall_ms": [1609.273123, 1608.924992, 1609.919092, 1607.439902, 1608.738686, 1609.290304, 1607.610655, 1606.815587, 1606.94348, 1606.421318, 1610.999882, 1608.945001, 1608.793034, 1607.310885]}}},
{"case": "tcp_open_16_interval5", "arguments": ["--addresses", "127.0.0.1", "--range", "20000-20015", "--batch-size", "256", "--timeout", "1500", "--tries", "1", "--interval", "5"], "expected_open_count": 16, "expected_closed_count": 0, "summary": {"case": "tcp_open_16_interval5", "async-std": {"n": 12, "scan_median_ms": 76.526898, "scan_min_ms": 76.28906, "scan_max_ms": 76.98857000000001, "wall_median_ms": 184.3007565003063}, "tokio": {"n": 12, "scan_median_ms": 76.198385, "scan_min_ms": 75.96929, "scan_max_ms": 76.34483, "wall_median_ms": 182.87123099980818}, "paired_ratio_median": 0.995816455071374, "paired_ratio_bootstrap_95_percent": [0.9940940322939229, 0.997153398744211]}, "samples": {"async-std": {"scan_ms": [76.22546, 76.634064, 76.59302, 76.39457, 76.645374, 76.3332, 76.62176, 76.28906, 76.41959, 76.36926, 76.4935, 76.560296, 76.98857, 76.76206], "wall_ms": [184.669709, 182.866488, 185.538649, 183.313847, 184.925599, 183.675914, 183.637934, 185.093184, 187.063112, 183.638293, 182.9653, 182.843469, 186.901708, 185.251247]}, "tokio": {"scan_ms": [76.195486, 75.97554, 76.34483, 76.11899, 76.28056, 76.30077, 76.1682, 76.29438, 75.96929, 76.17687, 76.25071, 76.16523, 76.15225, 76.2199], "wall_ms": [184.722406, 182.572365, 187.34923, 182.041604, 183.36494, 182.523404, 183.084177, 182.781318, 182.961144, 182.379863, 182.473341, 182.171679, 187.598076, 184.172442]}}},
{"case": "tcp_filtered_128_t1_b1", "arguments": ["--addresses", "127.0.0.3", "--range", "40000-40127", "--batch-size", "1", "--timeout", "1", "--tries", "1"], "expected_open_count": 0, "expected_closed_count": 0, "summary": {"case": "tcp_filtered_128_t1_b1", "async-std": {"n": 12, "scan_median_ms": 130.93322, "scan_min_ms": 130.62957, "scan_max_ms": 131.24298000000002, "wall_median_ms": 239.21898499975214}, "tokio": {"n": 12, "scan_median_ms": 131.0992, "scan_min_ms": 130.88854, "scan_max_ms": 131.27832, "wall_median_ms": 239.95067800024117}, "paired_ratio_median": 1.0005112769246107, "paired_ratio_bootstrap_95_percent": [0.9999663972630607, 1.0021629287391751]}, "samples": {"async-std": {"scan_ms": [130.78079, 131.10988, 130.9561, 130.94509, 130.83921, 131.1865, 130.62957, 131.02207, 130.70644, 130.90175, 130.75319, 131.24298, 130.92135, 131.06887], "wall_ms": [239.781417, 238.586222, 239.454156, 237.931582, 238.284514, 238.520237, 241.34305, 238.878925, 240.314313, 240.056949, 238.664271, 241.762701, 238.983814, 241.307165]}, "tokio": {"scan_ms": [130.6635, 130.95422, 130.92089, 130.88854, 130.99842, 131.1627, 131.0357, 131.17874, 131.2091, 130.93698, 131.27832, 131.25797, 130.9423, 131.16762], "wall_ms": [241.110604, 238.237406, 241.265624, 236.701241, 243.135758, 242.763681, 240.139445, 237.299031, 243.166426, 239.761911, 238.337466, 242.007046, 237.278855, 238.046974]}}},
{"case": "tcp_filtered_128_t5_b1", "arguments": ["--addresses", "127.0.0.3", "--range", "40000-40127", "--batch-size", "1", "--timeout", "5", "--tries", "1"], "expected_open_count": 0, "expected_closed_count": 0, "summary": {"case": "tcp_filtered_128_t5_b1", "async-std": {"n": 12, "scan_median_ms": 644.5146500000001, "scan_min_ms": 644.1285, "scan_max_ms": 645.423, "wall_median_ms": 753.1218705003084}, "tokio": {"n": 12, "scan_median_ms": 644.9479249999999, "scan_min_ms": 644.26196, "scan_max_ms": 645.4041000000001, "wall_median_ms": 753.6899540000377}, "paired_ratio_median": 1.0003472826276902, "paired_ratio_bootstrap_95_percent": [0.9997139996239722, 1.0010076863466117]}, "samples": {"async-std": {"scan_ms": [644.68694, 644.6084, 644.4459, 644.1285, 644.51116, 644.7121, 644.44476, 644.48434, 644.4745, 645.2324, 644.8094, 645.423, 644.8345, 644.51814], "wall_ms": [752.617209, 752.072663, 751.723478, 753.035024, 754.164624, 753.567375, 754.457316, 751.579595, 751.879974, 753.208717, 756.413493, 755.272146, 751.549782, 750.185216]}, "tokio": {"scan_ms": [644.9864, 644.3036, 644.2817, 644.75507, 644.70977, 644.51116, 644.26196, 645.1563, 645.34944, 645.04635, 645.124, 644.8495, 645.08367, 645.4041], "wall_ms": [750.909824, 751.864968, 752.487215, 754.880392, 752.804057, 752.172361, 751.845615, 752.622455, 754.163878, 759.625524, 753.21603, 761.649699, 759.674361, 759.360811]}}},
{"case": "udp_silent_128_t1_b1", "arguments": ["--addresses", "127.0.0.1", "--range", "31000-31127", "--batch-size", "1", "--timeout", "1", "--tries", "1", "--udp"], "expected_open_count": 0, "expected_closed_count": 0, "summary": {"case": "udp_silent_128_t1_b1", "async-std": {"n": 12, "scan_median_ms": 133.377255, "scan_min_ms": 132.9543, "scan_max_ms": 134.05539, "wall_median_ms": 241.22244349973698}, "tokio": {"n": 12, "scan_median_ms": 133.435925, "scan_min_ms": 132.90279999999998, "scan_max_ms": 133.90249, "wall_median_ms": 241.50215099962224}, "paired_ratio_median": 1.000629542926133, "paired_ratio_bootstrap_95_percent": [0.9983003180595498, 1.0009995969657295]}, "samples": {"async-std": {"scan_ms": [133.02739, 133.06117, 133.60235, 133.16965, 133.40722, 133.34133, 133.79982, 133.50019, 133.34729, 132.9543, 132.97513, 133.86926, 134.05539, 133.24223], "wall_ms": [239.632582, 239.299313, 240.69326, 241.236812, 245.210151, 240.884955, 240.974041, 241.96585, 239.067496, 241.208075, 254.893531, 245.278615, 248.441437, 240.973139]}, "tokio": {"scan_ms": [133.238, 133.49098, 133.4732, 132.9028, 133.5228, 133.4069, 133.90249, 133.85785, 133.1612, 133.06777, 133.46495, 133.47353, 133.05344, 133.39317], "wall_ms": [252.542821, 243.438741, 246.292204, 241.467916, 246.338636, 241.292069, 242.210823, 241.536386, 240.60291, 240.217646, 241.608773, 243.789372, 240.538868, 241.191347]}}},
{"case": "udp_silent_128_t5_b1", "arguments": ["--addresses", "127.0.0.1", "--range", "31000-31127", "--batch-size", "1", "--timeout", "5", "--tries", "1", "--udp"], "expected_open_count": 0, "expected_closed_count": 0, "summary": {"case": "udp_silent_128_t5_b1", "async-std": {"n": 12, "scan_median_ms": 647.45235, "scan_min_ms": 647.13037, "scan_max_ms": 647.9096, "wall_median_ms": 754.5120764998501}, "tokio": {"n": 12, "scan_median_ms": 647.5434499999999, "scan_min_ms": 646.6989, "scan_max_ms": 649.0831999999999, "wall_median_ms": 756.2571695002589}, "paired_ratio_median": 1.0001673335253631, "paired_ratio_bootstrap_95_percent": [0.9995790714638624, 1.0005779645783957]}, "samples": {"async-std": {"scan_ms": [647.3635, 647.20917, 647.3771, 647.33446, 647.413, 647.9096, 647.4971, 647.56674, 647.6953, 647.18795, 647.13037, 647.24445, 647.6764, 647.4917], "wall_ms": [758.592425, 755.975749, 758.079069, 754.578821, 754.257856, 754.087964, 754.511489, 754.512664, 754.525435, 757.162554, 757.284223, 753.779497, 753.794215, 754.096046]}, "tokio": {"scan_ms": [647.42637, 647.2082, 647.5038, 649.0832, 646.6989, 647.32325, 647.8038, 647.9199, 647.7853, 647.5831, 647.7122, 647.13156, 647.30984, 647.31306], "wall_ms": [756.138619, 753.755517, 757.528466, 753.601939, 757.316129, 757.89494, 753.950169, 756.446763, 754.649347, 756.228975, 753.679032, 754.926627, 756.285364, 758.323076]}}},
{"case": "tcp_open_16_interval1", "arguments": ["--addresses", "127.0.0.1", "--range", "20000-20015", "--batch-size", "256", "--timeout", "1500", "--tries", "1", "--interval", "1"], "expected_open_count": 16, "expected_closed_count": 0, "summary": {"case": "tcp_open_16_interval1", "async-std": {"n": 12, "scan_median_ms": 16.380881000000002, "scan_min_ms": 16.150762999999998, "scan_max_ms": 16.9349, "wall_median_ms": 127.33355999989726}, "tokio": {"n": 12, "scan_median_ms": 15.968566, "scan_min_ms": 15.782170000000002, "scan_max_ms": 16.192745000000002, "wall_median_ms": 123.29525399991326}, "paired_ratio_median": 0.9745702232857189, "paired_ratio_bootstrap_95_percent": [0.9638471208044207, 0.9843992590941175]}, "samples": {"async-std": {"scan_ms": [16.742926, 16.30988, 16.436962, 16.418753, 16.9349, 16.31153, 16.568368, 16.311154, 16.377501, 16.749995, 16.150763, 16.382523, 16.379239, 16.34811], "wall_ms": [123.728541, 124.449409, 127.732305, 129.316049, 129.626865, 127.054303, 128.552302, 126.115599, 127.535378, 127.131742, 127.636723, 123.361358, 125.967128, 126.564565]}, "tokio": {"scan_ms": [15.898872, 16.012024, 16.004669, 16.192745, 15.848257, 15.910927, 15.932463, 16.0934, 15.86936, 16.149458, 15.913244, 16.077776, 15.78217, 16.078446], "wall_ms": [123.129233, 124.075349, 123.24542, 125.239692, 121.153835, 122.962107, 123.345088, 123.750072, 123.947659, 123.936385, 123.59239, 122.694261, 122.874058, 122.894589]}}},
{"case": "tcp_open_16_interval20", "arguments": ["--addresses", "127.0.0.1", "--range", "20000-20015", "--batch-size", "256", "--timeout", "1500", "--tries", "1", "--interval", "20"], "expected_open_count": 16, "expected_closed_count": 0, "summary": {"case": "tcp_open_16_interval20", "async-std": {"n": 12, "scan_median_ms": 302.22183499999994, "scan_min_ms": 301.97242, "scan_max_ms": 302.5761, "wall_median_ms": 412.45387100025255}, "tokio": {"n": 12, "scan_median_ms": 301.93652499999996, "scan_min_ms": 301.76678000000004, "scan_max_ms": 302.2357, "wall_median_ms": 411.5444945005038}, "paired_ratio_median": 0.9989881724371461, "paired_ratio_bootstrap_95_percent": [0.9987076082418354, 0.9995525033875622]}, "samples": {"async-std": {"scan_ms": [302.18267, 302.13732, 301.9773, 302.5761, 302.25044, 301.97242, 302.143, 302.19373, 302.2255, 302.00857, 302.36325, 302.21817, 302.42342, 302.41114], "wall_ms": [413.653618, 410.964384, 412.493781, 411.729865, 412.494401, 411.493839, 418.091404, 412.413961, 414.137723, 415.698233, 410.759472, 412.03479, 412.157087, 413.086966]}, "tokio": {"scan_ms": [301.75188, 301.73197, 302.215, 301.85777, 301.83905, 301.76678, 302.06013, 301.8798, 301.8705, 302.0028, 301.99325, 301.77155, 302.2357, 302.11332], "wall_ms": [411.204909, 409.04968, 409.769905, 407.712868, 413.807731, 413.013744, 409.228791, 410.831539, 412.670222, 410.527218, 413.87037, 411.52089, 411.798223, 411.568099]}}}
],
"filtered_tcp_followup": {"purpose": "More samples for the noisy filtered TCP 20 ms result; separate from the 19-case run.", "trials": 40, "warmups": 2, "summary": {"case": "tcp_filtered_128_t20_retry2_b64", "async-std": {"n": 40, "scan_median_ms": 90.806345, "scan_min_ms": 83.67984, "scan_max_ms": 98.48335399999999, "wall_median_ms": 199.7979699995085}, "tokio": {"n": 40, "scan_median_ms": 89.66677, "scan_min_ms": 83.70783, "scan_max_ms": 98.51789, "wall_median_ms": 199.89575449972108}, "paired_ratio_median": 0.9904734730864315, "paired_ratio_bootstrap_95_percent": [0.9584064276305695, 1.0173184206016161]}, "samples": {"async-std": {"scan_ms": [88.323325, 92.4213, 88.936195, 92.37154, 96.92283, 84.850445, 89.15503, 92.10235, 83.80518, 92.24769, 92.32318, 84.8584, 92.456356, 88.23032, 92.04123, 92.18477, 86.79756, 98.483354, 89.1963, 91.340594, 83.67984, 93.583435, 92.54462, 89.31536, 91.27254, 88.60093, 84.68702, 93.62957, 92.30719, 88.04115, 84.974065, 94.37316, 89.16571, 95.54624, 93.07317, 84.02605, 90.34015, 96.3984, 90.19143, 90.06581, 96.61417, 85.11115], "wall_ms": [195.0308, 198.595023, 205.550084, 201.45625, 204.755249, 190.154735, 195.168845, 200.063899, 193.003685, 197.985884, 210.720138, 198.077596, 210.647758, 194.687149, 199.664278, 204.498946, 195.373439, 207.872652, 196.887853, 198.249704, 190.054223, 199.931662, 199.328127, 197.193407, 199.537635, 195.275976, 192.193237, 206.635061, 200.028809, 198.330338, 196.621195, 215.452312, 198.812027, 208.724402, 207.834586, 202.403001, 203.333875, 206.879636, 203.607022, 208.38006, 204.950703, 197.268808]}, "tokio": {"scan_ms": [92.301026, 93.38117, 93.45285, 94.5628, 88.58762, 84.90488, 89.12393, 91.01911, 96.5046, 88.37657, 98.51789, 88.854186, 84.06957, 91.783285, 87.75008, 94.83653, 90.99176, 89.60787, 90.4417, 92.33754, 92.54058, 89.72567, 83.76211, 93.127385, 87.82867, 83.70783, 84.0695, 87.62123, 90.449825, 89.82943, 92.07562, 88.124685, 90.16974, 83.91896, 87.90877, 89.39017, 84.15518, 93.53352, 86.91823, 85.3855, 89.75218, 98.28125], "wall_ms": [198.423459, 203.341894, 201.703401, 202.09388, 195.800999, 193.598472, 195.551461, 200.580659, 206.981415, 194.671571, 206.959222, 195.364517, 193.512965, 206.094437, 199.177046, 200.599854, 201.202883, 199.020153, 201.921695, 199.143086, 201.97124, 201.214532, 192.511818, 205.472348, 197.018051, 190.893376, 206.579472, 194.892288, 199.580795, 198.181881, 206.1589, 195.554125, 199.940732, 203.003288, 197.682916, 201.536251, 205.527831, 212.436077, 194.466474, 196.772168, 199.850777, 205.605438]}}},
"synthetic": {"operations": 4096, "batch_size": 256, "timeout_ms": 10, "samples_per_run": 20, "warmup_seconds": 1, "measurement_seconds": 5, "runtime_construction": "excluded", "ready_affinity": "CPU 0", "ready_order": ["async-std-1", "tokio-1", "tokio-2", "async-std-2"], "expired_affinity": "CPUs 0-3", "binary_sha256": {"async-std": "60e3fc23233d914f36a3d1913202a099440e926febcc36d37d6fcd3b79e7b867", "tokio": "64adfd7f5194bfc9ce7ad7c7875560f43727299b1b714c013a340a7353d57214"}, "ready": {"async-std": {"median_ms": 0.29120075443342, "runs": [{"median_ms": 0.283435356372549, "sample_ms": [0.2825720470588235, 0.28204494705882355, 0.28254624705882353, 0.28159535294117644, 0.2827166776470589, 0.28334777843137254, 0.2822799512605042, 0.28262099705882354, 0.28297270196078433, 0.28314464705882353, 0.2836823540106952, 0.2835229343137255, 0.28407015746606334, 0.2842827201680672, 0.2849620729411765, 0.2847762551470588, 0.285210384083045, 0.2895305209150327, 0.2874756, 0.2877200064705882]}, {"median_ms": 0.29475756493621547, "sample_ms": [0.2928709879518072, 0.2937114819277108, 0.2933189156626506, 0.29493332831325303, 0.2935973734939759, 0.2949348473895582, 0.29569418416523235, 0.295620109939759, 0.29519459571619816, 0.29707792530120486, 0.29418250054764516, 0.294691171686747, 0.2942722882298424, 0.29563811101549053, 0.29616976465863454, 0.2951352620481928, 0.2948239581856839, 0.2945176673360107, 0.29444605960684844, 0.2944720572289157]}]}, "tokio": {"median_ms": 0.302037698125, "runs": [{"median_ms": 0.30038876319444446, "sample_ms": [0.298729525, 0.29950843125000004, 0.3017063583333333, 0.30070625, 0.299885025, 0.3007033645833333, 0.3001008196428571, 0.2997647140625, 0.30044873055555554, 0.3001521925, 0.30016312272727275, 0.30431824270833335, 0.30060469615384616, 0.3011879410714286, 0.3012789341666666, 0.30614084375, 0.30109486838235294, 0.30032879583333333, 0.29896177302631577, 0.299311944375]}, {"median_ms": 0.3030938016369047, "sample_ms": [0.3012269375, 0.30389388125, 0.30436450416666666, 0.30480048125000003, 0.3020644975, 0.3028290625, 0.30420522678571427, 0.302707453125, 0.30225569861111107, 0.30201089875, 0.30262227954545456, 0.30319728541666663, 0.30556625480769234, 0.3029903178571428, 0.30277277, 0.30272753671875, 0.3081278419117647, 0.30388027500000003, 0.3033398302631579, 0.303353878125]}]}}, "expired": {"async-std": {"median_ms": 160.8818555, "sample_ms": [160.938174, 160.915957, 160.831742, 160.9983525, 160.883492, 160.906152, 160.858202, 160.7991255, 160.880219, 160.786231, 160.763265, 160.9403975, 160.8187525, 160.847539, 160.7793675, 160.8122135, 160.891816, 160.9187445, 160.945858, 161.08323]}, "tokio": {"median_ms": 160.96872275, "sample_ms": [161.0623005, 161.067578, 160.9593655, 160.9637835, 161.007304, 160.938726, 160.86018, 160.973662, 160.9531165, 160.9530565, 161.099919, 160.872232, 160.845637, 161.0776695, 160.8679585, 161.043725, 160.911652, 161.115825, 161.00697, 161.0155235]}}}
}
+229
View File
@@ -0,0 +1,229 @@
# Tokio scan benchmark
The runtime migration was compared with its async-std base using real TCP and UDP
sockets on 2026-10-01. This was a one-off, explicitly requested measurement in
an isolated Linux network namespace. No network benchmark was added to the
automated test or Criterion suites.
## Builds and environment
- Baseline: `cfb864590161d1c8a003811dcf4846ceb8c8284f`, async-std 1.13.2.
- Migration: `42427e88c1a6587d0a8b198a73bac4553bb4ad72`, Tokio 1.53.1.
- Both are optimized release binaries with the repository's LTO settings,
built using Rust 1.98.1.
- Intel Core Ultra 7 165U, Linux 7.2.6-1-cachyos, x86_64.
- Scanner processes were restricted to CPUs 0-3 (two physical performance
cores); the responder process used CPUs 4-7. Both runtimes used the same
affinity. The harness used CPUs 8-9.
- File descriptor soft limit: 65,536, so the requested batches of 256 and 4,500
were not reduced by RustScan's limit checks.
- The private namespace contained only loopback, with no external interface.
Targets used literal IPv4/IPv6 addresses; scripts and configuration loading
were disabled. The normal synchronous resolver initialization still occurred.
Binary SHA-256 hashes:
```text
async-std 00c7f0d91bab260ac622540e86083175afacfd1677647d2b8e96e56ba4f60732
tokio 669627834d8a32f60e2167e78a9518dcede7562831adb85fb61a9d94871494b8
```
## Method
Each workload used two warmup runs and twelve measured runs per binary. The
order alternated between async-std/Tokio and Tokio/async-std for each pair.
Each run started a fresh CLI process, matching ordinary command-line use.
The primary measurement is RustScan's `Portscan` timer, read using
`RUST_LOG=info`. It includes scan runtime initialization and cleanup and excludes
address parsing and final output. The wall time measures the entire process,
including startup, address parsing and output capture. Runtime startup was
excluded from the synthetic Criterion measurements below, so those numbers
measure a different workload.
The responder bound 512 TCP listeners, 256 UDP echo sockets and 128 silent UDP
sockets on each of `127.0.0.1` and `::1`. Empty UDP requests received empty replies.
TCP connections were accepted and closed immediately. Silent UDP sockets received
datagrams without replying. A firewall rule inside the private namespace dropped
TCP packets to `127.0.0.3:40000-40127`, producing actual connection timeouts.
No listeners were bound to `127.0.0.2`, used for the all-closed TCP scans.
Every run checked the exact set of reported open ports and rejected duplicates.
A separate check verified closed-port reporting and port exclusions for both
binaries. Each invocation used this common prefix:
```sh
RUST_LOG=info taskset -c 0-3 /path/to/rustscan \
--no-config --config-path /dev/null --no-banner --scripts none \
--greppable --scan-order serial
```
Append the arguments for the workload:
| Workload | Arguments |
| --- | --- |
| TCP closed, 65,535 ports | `-a 127.0.0.2 -r 1-65535 -b 256` or `-b 4500` |
| TCP mixed, 512 open / 4,096 ports | `-a 127.0.0.1 -r 20000-24095 -b 256` or `-b 4500` |
| TCP open, 512 ports | `-a 127.0.0.1 -r 20000-20511 -b 4500` |
| TCP IPv6 mixed, 512 open / 4,096 ports | `-a ::1 -r 20000-24095 -b 4500` |
| UDP echo, 256 ports | `-a 127.0.0.1 -r 30000-30255 -b 256 --udp` |
| UDP IPv6 echo, 256 ports | `-a ::1 -r 30000-30255 -b 256 --udp` |
| TCP filtered, short timeout and retries | `-a 127.0.0.3 -r 40000-40127 -b 64 -t 20 --tries 2` |
| TCP filtered, default timeout | `-a 127.0.0.3 -r 40000-40127 -b 4500 -t 1500` |
| UDP silent, short timeout and retries | `-a 127.0.0.1 -r 31000-31127 -b 64 -t 20 --tries 2 --udp` |
| UDP silent, default timeout | `-a 127.0.0.1 -r 31000-31127 -b 4500 -t 1500 --udp` |
| TCP open, 16 ports with intervals | `-a 127.0.0.1 -r 20000-20015 -b 256 --interval 1` (also 5 and 20 ms) |
| TCP filtered, short serial timeouts | `-a 127.0.0.3 -r 40000-40127 -b 1 -t 1` (also 5 ms) |
| UDP silent, short serial timeouts | `-a 127.0.0.1 -r 31000-31127 -b 1 -t 1 --udp` (also 5 ms) |
All unspecified timeouts use 1,500 ms and one try. The mixed TCP workloads have
listeners on ports 20000-20511 and no listeners on the remaining ports.
## Results
These results cover the revised timeout implementation. The initial migration
added 20.1% to the 5 ms interval workload (76.45 to 91.79 ms). Port intervals and
short deadlines now retain the existing `async_io::Timer`; long deadlines use
Tokio until 32 ms before expiry, then finish with the precise timer. Both phases
preserve the original deadline. This avoids timer rounding on short waits and
avoids registering a second timer for normal early socket replies.
All values below are medians of twelve measured runs. Negative changes mean
less elapsed time. The final column is a bootstrap 95% interval for the median
of paired Tokio/async-std scan-time ratios, expressed as a percentage change.
It uses 5,000 resamples of twelve pairs and does not account for every source
of system noise. It differs from the ratio of the two medians in the Change
column. [Per-run timings and summaries](tokio-benchmark-results.json) include
the warmups and full precision.
| Workload | async-std scan (ms) | Tokio scan (ms) | Change | Paired change, 95% interval |
| --- | ---: | ---: | ---: | ---: |
| TCP closed, 65,535 ports, batch 256 | 565.62 | 377.77 | -33.21% | -34.85% to -31.97% |
| TCP closed, 65,535 ports, batch 4,500 | 769.26 | 571.34 | -25.73% | -27.30% to -23.46% |
| TCP mixed, 4,096 ports, batch 256 | 68.14 | 27.08 | -60.26% | -62.18% to -56.78% |
| TCP mixed, 4,096 ports, batch 4,500 | 105.18 | 40.72 | -61.28% | -62.90% to -58.33% |
| TCP open, 512 ports, batch 4,500 | 47.70 | 6.26 | -86.87% | -88.76% to -85.17% |
| TCP IPv6 mixed, 4,096 ports, batch 4,500 | 106.26 | 40.98 | -61.43% | -63.84% to -60.18% |
| UDP echo, 256 ports, batch 256 | 26.10 | 3.55 | -86.40% | -89.28% to -83.06% |
| UDP IPv6 echo, 256 ports, batch 256 | 27.63 | 3.80 | -86.25% | -88.36% to -84.90% |
| TCP filtered, 20 ms, two tries, batch 64 | 87.64 | 90.20 | +2.92% | -0.36% to +6.30% |
| TCP filtered, 1,500 ms, batch 4,500 | 1519.99 | 1500.68 | -1.27% | -1.40% to -1.07% |
| UDP silent, 20 ms, two tries, batch 64 | 91.01 | 82.37 | -9.49% | -13.15% to -6.01% |
| UDP silent, 1,500 ms, batch 4,500 | 1518.53 | 1501.66 | -1.11% | -1.35% to -1.01% |
| TCP open, 16 ports, 5 ms interval | 76.53 | 76.20 | -0.43% | -0.59% to -0.28% |
| TCP filtered, 1 ms, batch 1 | 130.93 | 131.10 | +0.13% | -0.00% to +0.22% |
| TCP filtered, 5 ms, batch 1 | 644.51 | 644.95 | +0.07% | -0.03% to +0.10% |
| UDP silent, 1 ms, batch 1 | 133.38 | 133.44 | +0.04% | -0.17% to +0.10% |
| UDP silent, 5 ms, batch 1 | 647.45 | 647.54 | +0.01% | -0.04% to +0.06% |
| TCP open, 16 ports, 1 ms interval | 16.38 | 15.97 | -2.52% | -3.62% to -1.56% |
| TCP open, 16 ports, 20 ms interval | 302.22 | 301.94 | -0.09% | -0.13% to -0.04% |
Bulk TCP scans took 25.7-86.9% less scan time, and UDP echo scans took
86.2-86.4% less. All three interval workloads improved; the former 5 ms
regression is gone. Serial 1 ms and 5 ms timeout medians took 0.01-0.13% more
time. Default 1,500 ms timeout scans took about 1.1-1.3% less time.
These measurements support substantial bulk-scan gains, but **do not establish
literally zero regressions**. The noisy filtered TCP case and the remaining
synthetic overhead are detailed below.
### Follow-up on the noisy filtered TCP case
The 20 ms filtered TCP workload had a 2.92% higher median in the main run, with
substantial variation. A separate repeat used the same binaries, setup and
alternating order, with two warmups and **40 measured runs per binary**:
| Measurement | async-std | Tokio | Change |
| --- | ---: | ---: | ---: |
| Scan median | 90.81 ms | 89.67 ms | -1.25% |
| Entire CLI median | 199.80 ms | 199.90 ms | +0.05% |
The paired scan-ratio bootstrap 95% interval was **-4.16% to +1.73%**. The
repeat does not establish a slowdown, but it cannot rule out a small change
in this noisy case. Its 84 runs all returned the expected results. The original
result remains in the table above; the JSON includes both runs.
### Entire CLI process
Address parsing and startup contribute substantial time on this machine, whose
hosts file is approximately 2.7 MB. Total process changes are consequently
smaller than scan-phase changes. These figures include startup, address parsing,
output capture and process exit:
| Workload | async-std wall (ms) | Tokio wall (ms) | Change |
| --- | ---: | ---: | ---: |
| TCP closed, 65,535 ports, batch 256 | 678.41 | 490.92 | -27.64% |
| TCP closed, 65,535 ports, batch 4,500 | 886.49 | 686.16 | -22.60% |
| TCP mixed, 4,096 ports, batch 256 | 187.83 | 141.97 | -24.42% |
| TCP mixed, 4,096 ports, batch 4,500 | 223.57 | 156.73 | -29.90% |
| TCP open, 512 ports, batch 4,500 | 160.65 | 124.34 | -22.61% |
| TCP IPv6 mixed, 4,096 ports, batch 4,500 | 225.67 | 154.43 | -31.57% |
| UDP echo, 256 ports, batch 256 | 144.53 | 117.11 | -18.97% |
| UDP IPv6 echo, 256 ports, batch 256 | 145.42 | 119.61 | -17.75% |
| TCP filtered, 20 ms, two tries, batch 64 | 203.20 | 203.25 | +0.02% |
| TCP filtered, 1,500 ms, batch 4,500 | 1625.54 | 1607.90 | -1.08% |
| UDP silent, 20 ms, two tries, batch 64 | 198.82 | 193.47 | -2.69% |
| UDP silent, 1,500 ms, batch 4,500 | 1626.34 | 1608.17 | -1.12% |
| TCP open, 16 ports, 5 ms interval | 184.30 | 182.87 | -0.78% |
| TCP filtered, 1 ms, batch 1 | 239.22 | 239.95 | +0.31% |
| TCP filtered, 5 ms, batch 1 | 753.12 | 753.69 | +0.08% |
| UDP silent, 1 ms, batch 1 | 241.22 | 241.50 | +0.12% |
| UDP silent, 5 ms, batch 1 | 754.51 | 756.26 | +0.23% |
| TCP open, 16 ports, 1 ms interval | 127.33 | 123.30 | -3.17% |
| TCP open, 16 ports, 20 ms interval | 412.45 | 411.54 | -0.22% |
### Correctness
All 532 runs (456 measured and 76 warmups) returned exactly the expected open
ports, with no duplicates or missing replies. Both binaries also passed the
separate closed-port and exclusion check. Responder counters matched the expected
58,702 accepted TCP connections, 14,336 echoed UDP datagrams and 17,920 silent UDP
datagrams, including retries. The isolated firewall counted 21,504 dropped TCP
packets during the filtered-port workloads.
## Synthetic scheduling comparison
These use simulated operations without sockets, DNS, or scripts. Each iteration
completes 4,096 operations through a 256-operation window. One workload replies
immediately; the other expires every operation after 10 ms. Both builds use the
same loop; the baseline uses `async_std::io::timeout` and its original
`futures::executor::block_on`, while the migration uses the production timeout
helper and Tokio. Runtime construction is outside the measurement.
Each Criterion run has twenty samples, a one-second warmup and a five-second
measurement target. The ready case was repeated on CPU 0 in A/B/B/A order to
reduce core-migration and run-order effects. Its table value is the median of
the forty per-sample batch times for each binary. The expiration case used one
run per binary on CPUs 0-3. No builds or socket benchmarks ran concurrently.
| Simulated workload | async-std median | Tokio median | Change |
| --- | ---: | ---: | ---: |
| Immediately ready operations | 0.29120 ms | 0.30204 ms | +3.72% |
| Every operation expires after 10 ms | 160.88186 ms | 160.96872 ms | +0.05% |
The ready case still costs about **11 microseconds more per 4,096 operations**.
Its two individual run medians were 0.28344/0.29476 ms for async-std and
0.30039/0.30309 ms for Tokio. This remaining overhead is included rather than
claiming that every workload improved. The original 11% synthetic expiration
regression has been reduced to about 0.05% in this comparison.
Reproduce the migration benchmark with:
```sh
cargo bench --locked --bench benchmark_helpers -- 'runtime scheduling'
```
For the baseline, apply [the benchmark adapter](tokio-benchmark-baseline.patch)
with `git apply --unidiff-zero` to a checkout of
`cfb864590161d1c8a003811dcf4846ceb8c8284f`, then run the same benchmark command.
The JSON includes per-sample times and benchmark binary hashes.
## Scope
These results apply to this machine and its loopback stack. They do not establish
throughput across a LAN or WAN, under packet loss, or on macOS or Windows. The
single Python responder can affect reply timing, and the host was an ordinary
interactive system. Both binaries used the same responder and CPU restrictions,
and every expected reply was checked. A finite benchmark cannot guarantee zero
slowdown for every workload or distinguish every sub-percent difference from
system noise.
+104
View File
@@ -0,0 +1,104 @@
# Tokio runtime migration
This implements the runtime migration discussed in [#856](https://github.com/bee-san/RustScan/pull/856)
and [#944](https://github.com/bee-san/RustScan/issues/944), based on master at
`cfb864590161d1c8a003811dcf4846ceb8c8284f`.
## CLI behavior
TCP connections and UDP sockets now use Tokio. The CLI creates a runtime on the calling thread for the
scan. `FuturesUnordered` still polls at most `batch_size` socket attempts at once;
concurrent sockets do not require a thread per socket or a task per socket.
Address parsing and Hickory's synchronous resolver run before entering the scan
runtime. Script execution runs after the runtime has been dropped. Port ordering,
exclusions, retries, UDP payloads, closed-port reporting and CLI options retain
their existing implementations.
Port intervals and short timeouts retain `async_io::Timer`, the timer underneath
async-std, independently of the Tokio socket runtime. Longer timeouts first use
Tokio's timer until 32 ms before the deadline, then use `async_io::Timer` for the
remaining wait. This avoids registering a second timer for ordinary replies
while leaving room for coarse timer rounding. Both phases use the original
deadline, including time spent setting up the socket.
An expired timer maps to `std::io::ErrorKind::TimedOut`. Socket errors retain
their original kind, so TCP refusal reporting and UDP timeout retries continue
to distinguish replies from failures. Successful TCP streams are converted to
standard streams, shut down in both directions, and dropped before another
attempt is polled.
Using Tokio's timers for short waits added approximately one millisecond per
interval in the first benchmark. Retaining the existing timer avoids that extra
rounding while keeping the async runtime responsive.
Dropping a scan cancels its timers; runtime shutdown never has to wait for a
sleeping blocking task. Immediately ready I/O operations avoid timer allocation,
and the time spent polling a pending operation is charged to its original timeout.
## Library callers
`Scanner::new`, `run`, `run_with_status`, `PortStrategy`, and the address helpers
keep their signatures. However, awaiting a scan now requires a Tokio runtime
with both I/O and time enabled. Calling it from `futures::executor::block_on` or
`async_std::task::block_on` alone is no longer sufficient.
Treat this new runtime requirement as a **breaking library change for the next
major release**. This PR does not publish a release or change the package version.
Add Tokio to the embedding application's dependencies:
```toml
tokio = { version = "1.53.1", features = ["rt", "net", "time"] }
```
For a synchronous application, resolve inputs first, then create the runtime:
```rust,ignore
let ips = rustscan::address::parse_addresses(&opts);
let scanner = Scanner::new(/* existing arguments, including &ips */);
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
let sockets = runtime.block_on(scanner.run());
```
The [crate example](../src/lib.rs) provides a complete, compile-checked example.
Applications already running on Tokio can simply use `scanner.run().await` or
`scanner.run_with_status().await`; do not nest `Runtime::block_on` calls.
Keep the synchronous address helpers outside async runtime execution. If an
existing Tokio application needs to call `parse_addresses`, move owned options
into `tokio::task::spawn_blocking(move || parse_addresses(&opts)).await` and handle
the join result before constructing the scanner. Hickory's synchronous resolver
owns its own runtime and performs blocking lookups.
## Dependencies and validation
The direct Tokio dependency enables only `rt`, `net`, and `time`; `test-util` is
enabled for deterministic tests with a paused clock. Tokio was already present
through Hickory. The existing `futures` dependency continues to provide
`FuturesUnordered`, `StreamExt` and future polling. `async-io` becomes a direct
dependency solely to retain the existing timer implementation and its short-wait
performance; socket I/O uses Tokio. `async-std` and its executor are absent from
the resolved dependency tree.
Regression tests cover successful operations (including zero-byte responses),
preservation of I/O errors, timeout cancellation, immediate results at a zero
deadline, wakeups during the first poll, scan cancellation, and interval timing with
excluded or empty port sets. They use simulated operations and scanners with no
target addresses, following the repository's rule against network tests.
Run the repository checks with `just test`. Run the isolated scheduling
microbenchmarks with:
```sh
cargo bench --locked --bench benchmark_helpers -- 'runtime scheduling'
```
Each benchmark completes 4,096 simulated operations through a 256-operation
window. One case completes immediately; the other expires every operation after
10 ms. Runtime construction is outside the measurement. Criterion uses 20
samples, a one-second warmup and a five-second measurement target. These measure
scheduling and timer costs. The [real TCP/UDP scan comparison](tokio-benchmark.md)
records a separate measurement in an isolated loopback environment, including
full CLI timings and short port intervals. LAN/WAN performance
and network performance on other operating systems remain unmeasured.
+4
View File
@@ -0,0 +1,4 @@
tags = ["core_approved", "example"]
ports = ["80", "443", "8080"]
developer = ["example", "https://example.org"]
directory = "fixtures/.rustscan_scripts"
-23
View File
@@ -1,23 +0,0 @@
#!/usr/bin/perl
#tags = ["core_approved", "example",]
#developer = [ "example", "https://example.org" ]
#ports_separator = ","
#call_format = "perl {{script}} {{ip}} {{port}}"
# Sriptfile parser stops at the first blank line with parsing.
# This script will run itself as an argument with the system installed perl interpreter, ports will be concatenated with "," .
# Unused field: trigger_port = "80"
# get total arg passed to this script
my $total = $#ARGV + 1;
my $counter = 1;
# get script name
my $scriptname = $0;
print "Total args passed to $scriptname : $total\n";
# Use loop to print all args stored in an array called @ARGV
foreach my $a(@ARGV) {
print "Arg # $counter : $a\n";
$counter++;
}
-13
View File
@@ -1,13 +0,0 @@
#!/usr/bin/python3
#tags = ["core_approved", "example",]
#developer = [ "example", "https://example.org" ]
#trigger_port = "80"
#call_format = "python3 {{script}} {{ip}} {{port}}"
# Sriptfile parser stops at the first blank line with parsing.
# This script will run itself as an argument with the system installed python interpreter, only scanning port 80.
# Unused filed: ports_separator = ","
import sys
print('Python script ran with arguments', str(sys.argv))
-12
View File
@@ -1,12 +0,0 @@
#!/bin/bash
#tags = ["core_approved", "example",]
#developer = [ "example", "https://example.org" ]
#ports_separator = ","
#call_format = "bash {{script}} {{ip}} {{port}}"
# Sriptfile parser stops at the first blank line with parsing.
# This script will run itself as an argument with the system installed bash interpreter, scanning all ports concatenated with "," .
# Unused filed: trigger_port = "80"
# print all arguments passed to the script
echo $@
+3 -5
View File
@@ -1,9 +1,7 @@
#!intentional_blank_line
Metadata-only test fixture; not executable.
#tags = ["core_approved", "example"]
#developer = [ "example", "https://example.org" ]
#ports_separator = ","
#call_format = "nmap -vvv -p {{port}} {{ip}}"
#call_format = "fixture {{ip}} {{port}}"
# Scriptfile parser stops at the first blank line with parsing.
# This script will run the system installed nmap, ports will be concatenated with "," .
# Unused field: trigger_port = "80"
The parser reads only the metadata above. This file contains no script body.
@@ -1,7 +1,7 @@
#!intentional_blank_line
Metadata-only test fixture; not executable.
#tags = "core_approved"
#developer = [ "example", "https://example.org" ]
#ports_separator = ","
#call_format = "nmap -vvv -p {{port}} {{ip}}"
#call_format = "fixture {{ip}} {{port}}"
# tags has to be an array, thus this file won't be parsed
# tags has to be an array, thus this file won't be parsed
View File
Whitespace-only changes.
-6
View File
@@ -1,6 +0,0 @@
127.0.0.1
google.com
example.com
66666666666666.666666666666666666.66666666666666666.6666666666
....
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.radiatorFixtures
+1 -1
View File
@@ -1,5 +1,5 @@
test:
cargo nextest run
cargo test
cargo clippy -- --deny warnings
cargo clippy --tests -- --deny warnings
cargo fmt --check
+196 -118
View File
@@ -6,7 +6,7 @@ use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
use std::path::Path;
use std::str::FromStr;
use cidr_utils::cidr::IpCidr;
use cidr_utils::cidr::{IpCidr, IpInet};
use hickory_resolver::{
config::{NameServerConfig, Protocol, ResolverConfig, ResolverOpts},
Resolver,
@@ -69,17 +69,11 @@ pub fn parse_addresses(input: &Opts) -> Vec<IpAddr> {
}
}
// Finally, craft a list of addresses to be excluded from the scan.
let mut excluded_ips: BTreeSet<IpAddr> = BTreeSet::new();
if let Some(exclude_addresses) = &input.exclude_addresses {
for addr in exclude_addresses {
excluded_ips.extend(parse_address(addr, &backup_resolver));
}
}
let excluded_cidrs = parse_excluded_networks(&input.exclude_addresses, &backup_resolver);
// Remove duplicated/excluded IPs.
let mut seen = BTreeSet::new();
ips.retain(|ip| seen.insert(*ip) && !excluded_ips.contains(ip));
ips.retain(|ip| seen.insert(*ip) && !excluded_cidrs.iter().any(|cidr| cidr.contains(ip)));
ips
}
@@ -98,16 +92,21 @@ 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> {
IpCidr::from_str(address)
.map(|cidr| cidr.iter().map(|c| c.address()).collect())
.ok()
.or_else(|| {
format!("{}:80", &address)
.to_socket_addrs()
.ok()
.map(|mut iter| vec![iter.next().unwrap().ip()])
})
.unwrap_or_else(|| resolve_ips_from_host(address, resolver))
if let Ok(addr) = IpAddr::from_str(address) {
// `address` is an IP string
vec![addr]
} else if let Ok(net_addr) = IpInet::from_str(address) {
// `address` is a CIDR string
net_addr.network().into_iter().addresses().collect()
} else {
// `address` is a hostname or DNS name
// attempt default DNS lookup
match format!("{address}:80").to_socket_addrs() {
Ok(mut iter) => vec![iter.next().unwrap().ip()],
// default lookup didn't work, so try again with the dedicated resolver
Err(_) => resolve_ips_from_host(address, resolver),
}
}
}
/// Uses DNS to get the IPS associated with host
@@ -125,6 +124,46 @@ fn resolve_ips_from_host(source: &str, backup_resolver: &Resolver) -> Vec<IpAddr
ips
}
/// Parses excluded networks from a list of addresses.
///
/// This function handles three types of inputs:
/// 1. CIDR notation (e.g. "192.168.0.0/24")
/// 2. Single IP addresses (e.g. "192.168.0.1")
/// 3. Hostnames that need to be resolved (e.g. "example.com")
///
/// ```rust
/// # use rustscan::address::parse_excluded_networks;
/// # use hickory_resolver::Resolver;
/// let resolver = Resolver::default().unwrap();
/// let excluded = parse_excluded_networks(&Some(vec!["192.168.0.0/24".to_owned()]), &resolver);
/// ```
pub fn parse_excluded_networks(
exclude_addresses: &Option<Vec<String>>,
resolver: &Resolver,
) -> Vec<IpCidr> {
exclude_addresses
.iter()
.flatten()
.flat_map(|addr| parse_single_excluded_address(addr, resolver))
.collect()
}
/// 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> {
if let Ok(cidr) = IpCidr::from_str(addr) {
return vec![cidr];
}
if let Ok(ip) = IpAddr::from_str(addr) {
return vec![IpCidr::new_host(ip)];
}
resolve_ips_from_host(addr, resolver)
.into_iter()
.map(IpCidr::new_host)
.collect()
}
/// Derive a DNS resolver.
///
/// 1. if the `resolver` parameter has been set:
@@ -152,17 +191,56 @@ fn get_resolver(resolver: &Option<String>) -> Resolver {
Protocol::Udp,
));
}
Resolver::new(config, ResolverOpts::default()).unwrap()
Resolver::new(config, resolver_opts()).unwrap()
}
None => match Resolver::from_system_conf() {
None => match system_resolver() {
Ok(resolver) => resolver,
Err(_) => {
Resolver::new(ResolverConfig::cloudflare_tls(), ResolverOpts::default()).unwrap()
}
Err(_) => Resolver::new(ResolverConfig::cloudflare_tls(), resolver_opts()).unwrap(),
},
}
}
/// `true` on Windows when the `SystemRoot` environment variable is unset.
///
/// hickory-resolver locates the hosts file via
/// `std::env::var_os("SystemRoot").expect(...)`, which panics when the
/// variable is missing. Processes spawned with a minimal environment
/// (services, scheduled tasks, WMI) can lack `SystemRoot`, and the panic is
/// fatal under this crate's `panic = "abort"` release profile.
fn windows_system_root_missing() -> bool {
cfg!(windows) && std::env::var_os("SystemRoot").is_none()
}
/// Derives a resolver from the system configuration, e.g. `/etc/resolv.conf`
/// on *nix or the registry on Windows.
///
/// Returns an error without touching the system configuration when doing so
/// would panic inside hickory-resolver (see [`windows_system_root_missing`]).
fn system_resolver() -> std::io::Result<Resolver> {
if windows_system_root_missing() {
debug!("SystemRoot is not set; skipping system resolver configuration");
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"SystemRoot environment variable is not set",
));
}
Resolver::from_system_conf()
}
/// Resolver options that are safe to use in the current environment.
///
/// hickory-resolver eagerly loads the hosts file when `use_hosts_file` is set
/// (the default), which hits the same missing-`SystemRoot` panic described in
/// [`windows_system_root_missing`]; disable it in that case.
fn resolver_opts() -> ResolverOpts {
let mut opts = ResolverOpts::default();
if windows_system_root_missing() {
opts.use_hosts_file = false;
}
opts
}
/// Parses and input file of IPs for use in DNS resolution.
fn read_resolver_from_file(path: &str) -> Result<Vec<IpAddr>, std::io::Error> {
let ips = fs::read_to_string(path)?
@@ -197,8 +275,8 @@ fn read_ips_from_file(
#[cfg(test)]
mod tests {
use super::{get_resolver, parse_addresses, Opts};
use std::net::Ipv4Addr;
use super::{parse_addresses, Opts};
use std::net::{IpAddr, Ipv4Addr};
#[test]
fn parse_correct_addresses() {
@@ -279,81 +357,6 @@ mod tests {
);
}
#[test]
fn parse_correct_host_addresses() {
let opts = Opts {
addresses: vec!["google.com".to_owned()],
..Default::default()
};
let ips = parse_addresses(&opts);
assert_eq!(ips.len(), 1);
}
#[test]
fn parse_correct_and_incorrect_addresses() {
let opts = Opts {
addresses: vec!["127.0.0.1".to_owned(), "im_wrong".to_owned()],
..Default::default()
};
let ips = parse_addresses(&opts);
assert_eq!(ips, [Ipv4Addr::new(127, 0, 0, 1),]);
}
#[test]
fn parse_incorrect_addresses() {
let opts = Opts {
addresses: vec!["im_wrong".to_owned(), "300.10.1.1".to_owned()],
..Default::default()
};
let ips = parse_addresses(&opts);
assert!(ips.is_empty());
}
#[test]
fn parse_hosts_file_and_incorrect_hosts() {
// Host file contains IP, Hosts, incorrect IPs, incorrect hosts
let opts = Opts {
addresses: vec!["fixtures/hosts.txt".to_owned()],
..Default::default()
};
let ips = parse_addresses(&opts);
assert_eq!(ips.len(), 3);
}
#[test]
fn parse_empty_hosts_file() {
// Host file contains IP, Hosts, incorrect IPs, incorrect hosts
let opts = Opts {
addresses: vec!["fixtures/empty_hosts.txt".to_owned()],
..Default::default()
};
let ips = parse_addresses(&opts);
assert_eq!(ips.len(), 0);
}
#[test]
fn parse_naughty_host_file() {
// Host file contains IP, Hosts, incorrect IPs, incorrect hosts
let opts = Opts {
addresses: vec!["fixtures/naughty_string.txt".to_owned()],
..Default::default()
};
let ips = parse_addresses(&opts);
assert_eq!(ips.len(), 0);
}
#[test]
fn parse_duplicate_cidrs() {
let opts = Opts {
@@ -367,27 +370,102 @@ mod tests {
}
#[test]
fn resolver_default_cloudflare() {
let opts = Opts::default();
let resolver = get_resolver(&opts.resolver);
let lookup = resolver.lookup_ip("www.example.com.").unwrap();
assert!(opts.resolver.is_none());
assert!(lookup.iter().next().is_some());
}
#[test]
fn resolver_args_google_dns() {
// https://developers.google.com/speed/public-dns
fn parse_overspecific_cidr() {
// a canonical CIDR string has 0 in all host bits, but we want to treat any CIDR-like string as CIDR
let opts = Opts {
resolver: Some("8.8.8.8,8.8.4.4".to_owned()),
addresses: vec!["192.128.1.1/24".to_owned()],
..Default::default()
};
let resolver = get_resolver(&opts.resolver);
let lookup = resolver.lookup_ip("www.example.com.").unwrap();
let ips = parse_addresses(&opts);
assert!(lookup.iter().next().is_some());
assert_eq!(ips.len(), 256);
}
#[test]
fn parse_non_canonical_cidr_mid_block() {
// 192.168.1.13/29: .13 = 0000 1101, mask clears last 3 bits → .8 = 0000 1000
// network is 192.168.1.8/29, spanning .8 through .15
let opts = Opts {
addresses: vec!["192.168.1.13/29".to_owned()],
..Default::default()
};
let ips = parse_addresses(&opts);
assert_eq!(
ips,
[
Ipv4Addr::new(192, 168, 1, 8),
Ipv4Addr::new(192, 168, 1, 9),
Ipv4Addr::new(192, 168, 1, 10),
Ipv4Addr::new(192, 168, 1, 11),
Ipv4Addr::new(192, 168, 1, 12),
Ipv4Addr::new(192, 168, 1, 13),
Ipv4Addr::new(192, 168, 1, 14),
Ipv4Addr::new(192, 168, 1, 15),
]
);
}
#[test]
fn parse_non_canonical_cidr_last_in_block() {
// 192.168.1.15/29: last address in the block, should still resolve to same .8–.15 network
let opts = Opts {
addresses: vec!["192.168.1.15/29".to_owned()],
..Default::default()
};
let ips = parse_addresses(&opts);
assert_eq!(
ips,
[
Ipv4Addr::new(192, 168, 1, 8),
Ipv4Addr::new(192, 168, 1, 9),
Ipv4Addr::new(192, 168, 1, 10),
Ipv4Addr::new(192, 168, 1, 11),
Ipv4Addr::new(192, 168, 1, 12),
Ipv4Addr::new(192, 168, 1, 13),
Ipv4Addr::new(192, 168, 1, 14),
Ipv4Addr::new(192, 168, 1, 15),
]
);
}
#[test]
fn parse_non_canonical_cidr_crosses_third_octet() {
// 192.168.1.5/23: host bits span into the third octet
// .1.5 in 23-bit context → network is 192.168.0.0/23, spanning .0.0 through .1.255 (512 addresses)
let opts = Opts {
addresses: vec!["192.168.1.5/23".to_owned()],
..Default::default()
};
let ips = parse_addresses(&opts);
assert_eq!(
ips.first(),
Some(&IpAddr::V4(Ipv4Addr::new(192, 168, 0, 0)))
);
assert_eq!(
ips.last(),
Some(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 255)))
);
assert_eq!(ips.len(), 512);
}
#[test]
fn parse_non_canonical_cidr_slash30() {
// 10.0.0.7/30: .7 = 0000 0111, mask clears last 2 bits → .4 = 0000 0100
// network is 10.0.0.4/30, spanning .4 through .7
let opts = Opts {
addresses: vec!["10.0.0.7/30".to_owned()],
..Default::default()
};
let ips = parse_addresses(&opts);
assert_eq!(
ips,
[
Ipv4Addr::new(10, 0, 0, 4),
Ipv4Addr::new(10, 0, 0, 5),
Ipv4Addr::new(10, 0, 0, 6),
Ipv4Addr::new(10, 0, 0, 7),
]
);
}
}
+5 -7
View File
@@ -42,13 +42,11 @@ impl Benchmark {
let mut summary = String::from("\nRustScan Benchmark Summary");
for timer in &self.named_timers {
if timer.start.is_some() && timer.end.is_some() {
let runtime_secs = timer
.end
.unwrap()
.saturating_duration_since(timer.start.unwrap())
.as_secs_f32();
summary.push_str(&format!("\n{0: <10} | {1: <10}s", timer.name, runtime_secs));
if let Some(start) = timer.start {
if let Some(end) = timer.end {
let runtime_secs = end.saturating_duration_since(start).as_secs_f32();
summary.push_str(&format!("\n{0: <10} | {1: <10}s", timer.name, runtime_secs));
}
}
}
summary
+326 -52
View File
@@ -1,7 +1,6 @@
//! Provides a means to read, parse and hold configuration options for scans.
use clap::{Parser, ValueEnum};
use serde_derive::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
@@ -28,35 +27,94 @@ pub enum ScriptsRequired {
Custom,
}
/// Represents the range of ports to be scanned.
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct PortRange {
pub start: u16,
pub end: u16,
/// The port ranges to scan, as inclusive `(start, end)` pairs.
///
/// Parsed from `-r/--range` (e.g. `1-500,1000-2500`) and from the `range`
/// key of the configuration file.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PortRanges(pub Vec<(u16, u16)>);
/// Accepted spellings of `range` in the configuration file.
#[derive(Deserialize)]
#[serde(untagged)]
enum PortRangesConfig {
/// `range = { start = 1, end = 1000 }`: the format used before multiple
/// ranges were supported, kept so existing configuration files still work.
Single { start: u16, end: u16 },
/// `range = "1-500,1000-2500"`: the same syntax as `--range`.
Text(String),
/// `range = [[1, 500], [1000, 2500]]`.
Pairs(Vec<(u16, u16)>),
}
impl<'de> serde::Deserialize<'de> for PortRanges {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let pairs = match PortRangesConfig::deserialize(deserializer)? {
PortRangesConfig::Single { start, end } => vec![(start, end)],
PortRangesConfig::Text(text) => {
return parse_ranges(&text).map_err(serde::de::Error::custom)
}
PortRangesConfig::Pairs(pairs) => pairs,
};
if pairs.is_empty() {
return Err(serde::de::Error::custom("expected at least one port range"));
}
if let Some(&(start, end)) = pairs.iter().find(|&&(start, end)| start > end) {
return Err(serde::de::Error::custom(format!(
"invalid port range {start}-{end}: start must not be greater than end"
)));
}
Ok(PortRanges(pairs))
}
}
#[cfg(not(tarpaulin_include))]
fn parse_range(input: &str) -> Result<PortRange, String> {
let range = input
.split('-')
.map(str::parse)
.collect::<Result<Vec<u16>, std::num::ParseIntError>>();
/// Parse a single `start-end` token (e.g. "100-200") into `(start, end)`.
/// Returns `None` when the token is malformed, cannot be parsed as `u16`,
/// or `start > end`.
fn parse_range(input: &str) -> Option<(u16, u16)> {
let mut parts = input.trim().splitn(2, '-').map(str::trim);
let a = parts.next()?;
let b = parts.next()?;
if range.is_err() {
return Err(String::from(
"the range format must be 'start-end'. Example: 1-1000.",
));
let start = a.parse::<u16>().ok()?;
let end = b.parse::<u16>().ok()?;
if start <= end {
Some((start, end))
} else {
None
}
}
#[cfg(not(tarpaulin_include))]
/// Parse a comma-separated list of `start-end` ranges into `PortRanges`.
///
/// Errors with a helpful message identifying the bad token.
fn parse_ranges(input: &str) -> Result<PortRanges, String> {
let s = input.trim();
if s.is_empty() {
return Err("empty input: expected one or more comma-separated 'start-end' pairs".into());
}
match range.unwrap().as_slice() {
[start, end] => Ok(PortRange {
start: *start,
end: *end,
}),
_ => Err(String::from(
"the range format must be 'start-end'. Example: 1-1000.",
)),
}
let ranges_res: Result<Vec<(u16, u16)>, String> = s
.split(',')
.map(|token| {
let t = token.trim();
parse_range(t).ok_or_else(|| {
format!(
"invalid range token `{}` — expected `start-end` with 0 <= start <= end <= 65535",
t
)
})
})
.collect();
ranges_res.map(PortRanges)
}
#[derive(Parser, Debug, Clone)]
@@ -81,9 +139,9 @@ pub struct Opts {
#[arg(short, long, value_delimiter = ',')]
pub ports: Option<Vec<u16>>,
/// A range of ports with format start-end. Example: 1-1000.
#[arg(short, long, conflicts_with = "ports", value_parser = parse_range)]
pub range: Option<PortRange>,
/// Ranges of ports as comma-separated start-end pairs. Example: 1-500,1000-2500,4000-7000
#[arg(short, long, conflicts_with = "ports", value_parser = parse_ranges)]
pub range: Option<PortRanges>,
/// Whether to ignore the configuration file or not.
#[arg(short, long)]
@@ -114,7 +172,7 @@ pub struct Opts {
/// it will do every port at the same time. Although, your OS may not
/// support this.
#[arg(short, long, default_value = "4500")]
pub batch_size: u16,
pub batch_size: usize,
/// The timeout in milliseconds before a port is assumed to be closed.
#[arg(short, long, default_value = "1500")]
@@ -125,9 +183,10 @@ 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<u64>,
pub ulimit: Option<usize>,
/// The order of scanning to be performed. The "serial" option will
/// scan ports in ascending order while the "random" option will scan
@@ -162,6 +221,16 @@ 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,
/// Milliseconds to wait after scanning a port (on every address) before
/// scanning the next one, for slow, low-noise scans. 0 disables the delay.
#[arg(long, default_value = "0", value_name = "MS")]
pub interval: u64,
}
#[cfg(not(tarpaulin_include))]
@@ -170,15 +239,33 @@ impl Opts {
let mut opts = Opts::parse();
if opts.ports.is_none() && opts.range.is_none() {
opts.range = Some(PortRange {
start: LOWEST_PORT_NUMBER,
end: TOP_PORT_NUMBER,
});
opts.range = Some(PortRanges(vec![(LOWEST_PORT_NUMBER, TOP_PORT_NUMBER)]));
}
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) {
@@ -201,7 +288,7 @@ impl Opts {
merge_required!(
addresses, greppable, accessible, batch_size, timeout, tries, scan_order, scripts,
command, udp
command, udp, no_banner, closed, interval
);
}
@@ -218,11 +305,7 @@ impl Opts {
// Only use top ports when the user asks for them
if self.top && config.ports.is_some() {
let mut ports: Vec<u16> = Vec::with_capacity(config.ports.as_ref().unwrap().len());
for entry in config.ports.as_ref().unwrap().keys() {
ports.push(entry.parse().unwrap());
}
self.ports = Some(ports);
self.ports = config.ports.clone();
}
merge_optional!(range, resolver, ulimit, exclude_ports, exclude_addresses);
@@ -252,6 +335,8 @@ impl Default for Opts {
exclude_ports: None,
exclude_addresses: None,
udp: false,
closed: false,
interval: 0,
}
}
}
@@ -263,14 +348,14 @@ impl Default for Opts {
#[derive(Debug, Deserialize)]
pub struct Config {
addresses: Option<Vec<String>>,
ports: Option<HashMap<String, u16>>,
range: Option<PortRange>,
ports: Option<Vec<u16>>,
range: Option<PortRanges>,
greppable: Option<bool>,
accessible: Option<bool>,
batch_size: Option<u16>,
batch_size: Option<usize>,
timeout: Option<u32>,
tries: Option<u8>,
ulimit: Option<u64>,
ulimit: Option<usize>,
resolver: Option<String>,
scan_order: Option<ScanOrder>,
command: Option<Vec<String>>,
@@ -278,6 +363,9 @@ pub struct Config {
exclude_ports: Option<Vec<u16>>,
exclude_addresses: Option<Vec<String>>,
udp: Option<bool>,
no_banner: Option<bool>,
closed: Option<bool>,
interval: Option<u64>,
}
#[cfg(not(tarpaulin_include))]
@@ -295,10 +383,26 @@ impl Config {
/// scan_order = "Serial"
/// exclude_ports = [8080, 9090, 80]
/// udp = false
/// closed = false
/// interval = 0
///
pub fn read(custom_config_path: Option<PathBuf>) -> Self {
let mut content = String::new();
let config_path = custom_config_path.unwrap_or_else(default_config_path);
let config_path = custom_config_path.unwrap_or_else(|| {
// Try the XDG-idiomatic location first, then fall back to legacy
// paths so existing users keep working unchanged.
for path in [
default_config_path(),
legacy_dot_config_path(),
old_default_config_path(),
] {
if path.exists() {
return path;
}
}
default_config_path()
});
if config_path.exists() {
content = match fs::read_to_string(config_path) {
Ok(content) => content,
@@ -309,7 +413,9 @@ impl Config {
let config: Config = match toml::from_str(&content) {
Ok(config) => config,
Err(e) => {
println!("Found {e} in configuration file.\nAborting scan.\n");
crate::tui::println_safe(format_args!(
"Found {e} in configuration file.\nAborting scan.\n"
));
std::process::exit(1);
}
};
@@ -318,8 +424,30 @@ impl Config {
}
}
/// Constructs default path to config toml
/// Returns the preferred config file path: `$XDG_CONFIG_HOME/rustscan/config.toml`
/// on Linux (with the usual `~/.config` fallback when the variable is unset),
/// and the platform-equivalent `dirs::config_dir()` location on macOS / Windows.
pub fn default_config_path() -> PathBuf {
let Some(mut config_path) = dirs::config_dir() else {
panic!("Could not infer config file path.");
};
config_path.push("rustscan");
config_path.push("config.toml");
config_path
}
/// Returns the transitional `$XDG_CONFIG_HOME/.rustscan.toml` path that older
/// builds wrote to. Kept readable for backwards compatibility.
pub fn legacy_dot_config_path() -> PathBuf {
let Some(mut config_path) = dirs::config_dir() else {
panic!("Could not infer config file path.");
};
config_path.push(".rustscan.toml");
config_path
}
/// Returns the deprecated home directory config path used for backwards compatibility.
pub fn old_default_config_path() -> PathBuf {
let Some(mut config_path) = dirs::home_dir() else {
panic!("Could not infer config file path.");
};
@@ -332,7 +460,7 @@ mod tests {
use clap::{CommandFactory, Parser};
use parameterized::parameterized;
use super::{Config, Opts, PortRange, ScanOrder, ScriptsRequired};
use super::{Config, Opts, PortRanges, ScanOrder, ScriptsRequired};
impl Config {
fn default() -> Self {
@@ -353,6 +481,9 @@ mod tests {
exclude_ports: None,
exclude_addresses: None,
udp: Some(false),
no_banner: None,
closed: Some(false),
interval: None,
}
}
}
@@ -382,6 +513,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();
@@ -417,10 +624,7 @@ mod tests {
fn opts_merge_optional_arguments() {
let mut opts = Opts::default();
let mut config = Config::default();
config.range = Some(PortRange {
start: 1,
end: 1_000,
});
config.range = Some(PortRanges(vec![(1, 1_000)]));
config.ulimit = Some(1_000);
config.resolver = Some("1.1.1.1".to_owned());
@@ -430,4 +634,74 @@ mod tests {
assert_eq!(opts.ulimit, config.ulimit);
assert_eq!(opts.resolver, config.resolver);
}
#[test]
fn parses_comma_separated_ranges() {
let opts = Opts::parse_from([
"rustscan",
"-a",
"127.0.0.1",
"-r",
"1-100, 200-300,5000-5100",
]);
assert_eq!(
opts.range,
Some(PortRanges(vec![(1, 100), (200, 300), (5_000, 5_100)]))
);
}
#[test]
fn rejects_malformed_ranges() {
for range in ["", "300-200", "1-100,", "1-2-3", "a-b", "1-70000"] {
assert!(
Opts::try_parse_from(["rustscan", "-a", "127.0.0.1", "-r", range]).is_err(),
"{:?} should be rejected",
range
);
}
}
#[test]
fn config_range_accepts_legacy_table() {
let config: Config = toml::from_str("range = { start = 1, end = 1000 }").unwrap();
assert_eq!(config.range, Some(PortRanges(vec![(1, 1_000)])));
}
#[test]
fn config_range_accepts_string_and_pairs() {
let expected = Some(PortRanges(vec![(1, 100), (200, 300)]));
let config: Config = toml::from_str("range = \"1-100,200-300\"").unwrap();
assert_eq!(config.range, expected);
let config: Config = toml::from_str("range = [[1, 100], [200, 300]]").unwrap();
assert_eq!(config.range, expected);
}
#[test]
fn config_range_rejects_invalid_ranges() {
for range in [
"range = { start = 10, end = 1 }",
"range = [[10, 1]]",
"range = []",
"range = \"10-1\"",
] {
assert!(
toml::from_str::<Config>(range).is_err(),
"{:?} should be rejected",
range
);
}
}
#[test]
fn parses_interval_in_milliseconds() {
let opts = Opts::parse_from(["rustscan", "-a", "127.0.0.1", "--interval", "250"]);
assert_eq!(opts.interval, 250);
let opts = Opts::parse_from(["rustscan", "-a", "127.0.0.1"]);
assert_eq!(opts.interval, 0);
}
}
+14 -8
View File
@@ -7,20 +7,22 @@
//! [`Scanner`](crate::scanner::Scanner) which in turn requires a
//! [`PortStrategy`](crate::port_strategy::PortStrategy):
//!
//! ```rust
//! use async_std::task::block_on;
//! Scans require a Tokio runtime with I/O and time enabled. Synchronous address
//! parsing should run before entering the runtime; see the
//! [runtime migration guide](https://github.com/bee-san/RustScan/blob/master/docs/tokio-migration.md).
//!
//! This example is compiled but never executed by the test suite.
//!
//! ```no_run
//! use std::{net::IpAddr, time::Duration};
//!
//! use rustscan::input::{PortRange, ScanOrder};
//! use rustscan::input::{PortRanges, ScanOrder};
//! use rustscan::port_strategy::PortStrategy;
//! use rustscan::scanner::Scanner;
//!
//! fn main() {
//! let addrs = vec!["127.0.0.1".parse::<IpAddr>().unwrap()];
//! let range = PortRange {
//! start: 1,
//! end: 1_000,
//! };
//! let range = PortRanges(vec![(1, 1_000)]);
//! 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
@@ -34,7 +36,11 @@
//! false, // is this a UDP scan?
//! );
//!
//! let scan_result = block_on(scanner.run());
//! let runtime = tokio::runtime::Builder::new_current_thread()
//! .enable_all()
//! .build()
//! .unwrap();
//! let scan_result = runtime.block_on(scanner.run());
//!
//! println!("{:?}", scan_result);
//! }
+144 -49
View File
@@ -5,12 +5,12 @@
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};
use colorful::{Color, Colorful};
use futures::executor::block_on;
use std::collections::HashMap;
use std::net::IpAddr;
use std::string::ToString;
@@ -23,9 +23,10 @@ extern crate dirs;
// Average value for Ubuntu
#[cfg(unix)]
const DEFAULT_FILE_DESCRIPTORS_LIMIT: u64 = 8000;
const DEFAULT_FILE_DESCRIPTORS_LIMIT: usize = 8000;
// Safest batch size based on experimentation
const AVERAGE_BATCH_SIZE: u16 = 3000;
#[cfg(unix)]
const AVERAGE_BATCH_SIZE: usize = 3000;
#[macro_use]
extern crate log;
@@ -46,7 +47,12 @@ fn main() {
let config = Config::read(opts.config_path.clone());
opts.merge(&config);
debug!("Main() `opts` arguments are {:?}", opts);
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) {
Ok(scripts_to_run) => scripts_to_run,
@@ -60,7 +66,7 @@ fn main() {
}
};
debug!("Scripts initialized {:?}", &scripts_to_run);
debug!("Scripts initialized {scripts_to_run:?}");
if !opts.greppable && !opts.accessible && !opts.no_banner {
print_opening(&opts);
@@ -77,13 +83,10 @@ fn main() {
std::process::exit(1);
}
#[cfg(unix)]
let batch_size: u16 = infer_batch_size(&opts, adjust_ulimit_size(&opts));
let batch_size = effective_batch_size(&opts);
debug!("Effective batch size: {batch_size}");
#[cfg(not(unix))]
let batch_size: u16 = AVERAGE_BATCH_SIZE;
let scanner = Scanner::new(
let mut scanner = Scanner::new(
&ips,
batch_size,
Duration::from_millis(opts.timeout.into()),
@@ -93,21 +96,37 @@ fn main() {
opts.accessible,
opts.exclude_ports.unwrap_or_default(),
opts.udp,
);
debug!("Scanner finished building: {:?}", scanner);
)
.with_open_port_output();
if opts.closed {
scanner = scanner.with_closed_ports();
}
if opts.interval > 0 {
scanner = scanner.with_interval(Duration::from_millis(opts.interval));
}
debug!("Scanner finished building: {scanner:?}");
let mut portscan_bench = NamedTimer::start("Portscan");
let scan_result = block_on(scanner.run());
// Keep synchronous DNS resolution and script execution outside the runtime.
// FuturesUnordered drives the bounded scan concurrently on this thread.
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to create the Tokio runtime");
let scan_result = runtime.block_on(scanner.run_with_status());
drop(runtime);
portscan_bench.end();
benchmarks.push(portscan_bench);
let mut ports_per_ip = HashMap::new();
let mut ports_per_ip: HashMap<IpAddr, Vec<u16>> = 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())
.or_insert_with(Vec::new)
.push(socket.port());
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_default().push(socket.port());
}
for ip in ips {
@@ -122,7 +141,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);
}
@@ -136,17 +155,19 @@ fn main() {
// if option scripts is none, no script will be spawned
if opts.greppable || opts.scripts == ScriptsRequired::None {
println!("{} -> [{}]", &ip, ports_str);
println_safe(format_args!("{ip} -> [{ports_str}]"));
continue;
}
detail!("Starting Script(s)", opts.greppable, opts.accessible);
// Run all the scripts we found and parsed based on the script config file tags field.
for mut script_f in scripts_to_run.clone() {
for script_f in &scripts_to_run {
// Clone only the script being run instead of the whole list per IP.
let mut script_f = script_f.clone();
// This part allows us to add commandline arguments to the Script call_format, appending them to the end of the command.
if !opts.command.is_empty() {
let user_extra_args = &opts.command.join(" ");
debug!("Extra args vec {:?}", user_extra_args);
debug!("Extra args vec {user_extra_args:?}");
if script_f.call_format.is_some() {
let mut call_f = script_f.call_format.unwrap();
call_f.push(' ');
@@ -156,7 +177,7 @@ fn main() {
opts.greppable,
opts.accessible
);
debug!("Call format {}", call_f);
debug!("Call format {call_f}");
script_f.call_format = Some(call_f);
}
}
@@ -173,7 +194,7 @@ fn main() {
);
match script.run() {
Ok(script_result) => {
detail!(script_result.to_string(), opts.greppable, opts.accessible);
detail!(script_result.clone(), opts.greppable, opts.accessible);
}
Err(e) => {
warning!(&format!("Error {e}"), opts.greppable, opts.accessible);
@@ -182,15 +203,45 @@ 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);
rustscan_bench.end();
benchmarks.push(rustscan_bench);
debug!("Benchmarks raw {:?}", benchmarks);
debug!("Benchmarks raw {benchmarks:?}");
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) {
@@ -201,12 +252,12 @@ fn print_opening(opts: &Opts) {
`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'
The Modern Day Port Scanner."#;
println!("{}", s.gradient(Color::Green).bold());
println_safe(format_args!("{}", s.gradient(Color::Green).bold()));
let info = r#"________________________________________
: http://discord.skerritt.blog :
: https://github.com/RustScan/RustScan :
--------------------------------------"#;
println!("{}", info.gradient(Color::Yellow).bold());
println_safe(format_args!("{}", info.gradient(Color::Yellow).bold()));
funny_opening!();
let config_path = opts
@@ -219,13 +270,26 @@ The Modern Day Port Scanner."#;
opts.greppable,
opts.accessible
);
if opts.config_path.is_none() {
let old_config_path = input::old_default_config_path();
detail!(
format!(
"For backwards compatibility, the config file may also be at {old_config_path:?}"
),
opts.greppable,
opts.accessible
);
}
}
#[cfg(unix)]
fn adjust_ulimit_size(opts: &Opts) -> u64 {
fn adjust_ulimit_size(opts: &Opts) -> usize {
use rlimit::Resource;
use std::convert::TryInto;
if let Some(limit) = opts.ulimit {
let limit = limit as u64;
if Resource::NOFILE.set(limit, limit).is_ok() {
detail!(
format!("Automatically increasing ulimit value to {limit}."),
@@ -242,14 +306,15 @@ fn adjust_ulimit_size(opts: &Opts) -> u64 {
}
let (soft, _) = Resource::NOFILE.get().unwrap();
soft
// On 32-bit targets the soft limit (e.g. RLIM_INFINITY) may not fit in a
// usize. Fall back to the conservative default rather than usize::MAX,
// which would skip every batch-size adjustment in infer_batch_size.
soft.try_into().unwrap_or(DEFAULT_FILE_DESCRIPTORS_LIMIT)
}
#[cfg(unix)]
fn infer_batch_size(opts: &Opts, ulimit: u64) -> u16 {
use std::convert::TryInto;
let mut batch_size: u64 = opts.batch_size.into();
fn infer_batch_size(opts: &Opts, ulimit: usize) -> usize {
let mut batch_size = opts.batch_size;
// Adjust the batch size when the ulimit value is lower than the desired batch size
if ulimit < batch_size {
@@ -260,8 +325,8 @@ fn infer_batch_size(opts: &Opts, ulimit: u64) -> u16 {
// When the OS supports high file limits like 8000, but the user
// selected a batch size higher than this we should reduce it to
// a lower number.
if ulimit < AVERAGE_BATCH_SIZE.into() {
// ulimit is smaller than aveage batch size
if ulimit < AVERAGE_BATCH_SIZE {
// ulimit is smaller than average batch size
// user must have very small ulimit
// decrease batch size to half of ulimit
warning!("Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'. ", opts.greppable, opts.accessible);
@@ -269,7 +334,7 @@ fn infer_batch_size(opts: &Opts, ulimit: u64) -> u16 {
batch_size = ulimit / 2;
} else if ulimit > DEFAULT_FILE_DESCRIPTORS_LIMIT {
info!("Batch size is now average batch size");
batch_size = AVERAGE_BATCH_SIZE.into();
batch_size = AVERAGE_BATCH_SIZE;
} else {
batch_size = ulimit - 100;
}
@@ -282,12 +347,12 @@ fn infer_batch_size(opts: &Opts, ulimit: u64) -> u16 {
}
batch_size
.try_into()
.expect("Couldn't fit the batch size into a u16.")
}
#[cfg(test)]
mod tests {
#[cfg(windows)]
use super::effective_batch_size;
#[cfg(unix)]
use super::{adjust_ulimit_size, infer_batch_size};
use super::{print_opening, Opts};
@@ -313,7 +378,7 @@ mod tests {
};
let batch_size = infer_batch_size(&opts, 9_000);
assert!(batch_size == 3_000);
assert_eq!(batch_size, 3_000);
}
#[test]
#[cfg(unix)]
@@ -326,7 +391,7 @@ mod tests {
};
let batch_size = infer_batch_size(&opts, 5_000);
assert!(batch_size == 4_900);
assert_eq!(batch_size, 4_900);
}
#[test]
#[cfg(unix)]
@@ -339,7 +404,7 @@ mod tests {
};
let batch_size = adjust_ulimit_size(&opts);
assert!(batch_size == 2_000);
assert_eq!(batch_size, 2_000);
}
#[test]
@@ -353,16 +418,46 @@ mod tests {
let batch_size = infer_batch_size(&opts, 1_000_000);
assert!(batch_size == opts.batch_size);
assert_eq!(batch_size, opts.batch_size);
}
#[test]
fn test_print_opening_no_panic() {
let opts = Opts {
ulimit: Some(2_000),
..Default::default()
};
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 {
batch_size: 50,
..Default::default()
};
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);
}
}
+37 -27
View File
@@ -1,6 +1,6 @@
//! Provides a means to hold configuration options specifically for port scanning.
mod range_iterator;
use crate::input::{PortRange, ScanOrder};
use crate::input::{PortRanges, ScanOrder};
use rand::rng;
use rand::seq::SliceRandom;
use range_iterator::RangeIterator;
@@ -17,20 +17,18 @@ pub enum PortStrategy {
}
impl PortStrategy {
pub fn pick(range: &Option<PortRange>, ports: Option<Vec<u16>>, order: ScanOrder) -> Self {
pub fn pick(range: &Option<PortRanges>, ports: Option<Vec<u16>>, order: ScanOrder) -> Self {
match order {
ScanOrder::Serial if ports.is_none() => {
let range = range.as_ref().unwrap();
let port_ranges = range.as_ref().unwrap();
PortStrategy::Serial(SerialRange {
start: range.start,
end: range.end,
range: port_ranges.0.clone(),
})
}
ScanOrder::Random if ports.is_none() => {
let range = range.as_ref().unwrap();
let port_ranges = range.as_ref().unwrap();
PortStrategy::Random(RandomRange {
start: range.start,
end: range.end,
range: port_ranges.0.clone(),
})
}
ScanOrder::Serial => PortStrategy::Manual(ports.unwrap()),
@@ -62,13 +60,12 @@ trait RangeOrder {
/// ascending order.
#[derive(Debug)]
pub struct SerialRange {
start: u16,
end: u16,
range: Vec<(u16, u16)>,
}
impl RangeOrder for SerialRange {
fn generate(&self) -> Vec<u16> {
(self.start..=self.end).collect()
RangeIterator::new_serial(&self.range).collect()
}
}
@@ -76,8 +73,7 @@ impl RangeOrder for SerialRange {
/// a random order. This vector is built following the LCG algorithm.
#[derive(Debug)]
pub struct RandomRange {
start: u16,
end: u16,
range: Vec<(u16, u16)>,
}
impl RangeOrder for RandomRange {
@@ -91,33 +87,47 @@ impl RangeOrder for RandomRange {
// port numbers close to each other are pretty slim due to the way the
// algorithm works.
fn generate(&self) -> Vec<u16> {
RangeIterator::new(self.start.into(), self.end.into()).collect()
RangeIterator::new_random(&self.range).collect()
}
}
#[cfg(test)]
mod tests {
use super::PortStrategy;
use crate::input::{PortRange, ScanOrder};
use crate::input::{PortRanges, ScanOrder};
use std::collections::HashSet;
fn expected_ports_from_ranges(input: &[(u16, u16)]) -> Vec<u16> {
let mut s = HashSet::new();
for &(start, end) in input {
for p in start..=end {
s.insert(p);
}
}
let mut v: Vec<u16> = s.into_iter().collect();
v.sort_unstable();
v
}
#[test]
fn serial_strategy_with_range() {
let range = PortRange { start: 1, end: 100 };
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Serial);
let ranges = PortRanges(vec![(1u16, 10u16), (20u16, 30u16), (100u16, 110u16)]);
let strategy = PortStrategy::pick(&Some(ranges.clone()), None, ScanOrder::Serial);
let result = strategy.order();
let expected_range = (1..=100).collect::<Vec<u16>>();
assert_eq!(expected_range, result);
let expected = expected_ports_from_ranges(&ranges.0);
assert_eq!(expected, result);
}
#[test]
fn random_strategy_with_range() {
let range = PortRange { start: 1, end: 100 };
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
let ranges = PortRanges(vec![(1u16, 10u16), (20u16, 30u16), (100u16, 110u16)]);
let strategy = PortStrategy::pick(&Some(ranges.clone()), None, ScanOrder::Random);
let mut result = strategy.order();
let expected_range = (1..=100).collect::<Vec<u16>>();
assert_ne!(expected_range, result);
let expected = expected_ports_from_ranges(&ranges.0);
assert_ne!(expected, result);
result.sort_unstable();
assert_eq!(expected_range, result);
assert_eq!(expected, result);
}
#[test]
@@ -131,10 +141,10 @@ mod tests {
fn random_strategy_with_ports() {
let strategy = PortStrategy::pick(&None, Some((1..10).collect()), ScanOrder::Random);
let mut result = strategy.order();
let expected_range = (1..10).collect::<Vec<u16>>();
assert_ne!(expected_range, result);
let expected = (1..10).collect::<Vec<u16>>();
assert_ne!(expected, result);
result.sort_unstable();
assert_eq!(expected_range, result);
assert_eq!(expected, result);
}
}
+251 -57
View File
@@ -1,71 +1,196 @@
use gcd::Gcd;
use rand::Rng;
use rand::RngExt;
use std::convert::TryInto;
/// Yields ports produced from a collection of (possibly overlapping)
/// inclusive `u16` ranges in two modes:
///
/// **Randomized** — created by `RangeIterator::new_random`:
/// - The algorithm generates a permutation of indices `0..N-1` using the
/// additive congruential step `x_{i+1} = (x_i + step) % N`.
/// - `step` is chosen so `gcd(step, N) == 1` to ensure the sequence is a
/// full-length cycle (visits each index exactly once).
/// - `x_0` (the seed stored as `normalized_first_pick`) is chosen uniformly
/// in `0..N`.
///
/// For more information: <https://en.wikipedia.org/wiki/Linear_congruential_generator>
///
/// **Serial** — `RangeIterator::new_serial`:
/// Iterates the input ranges in the **original input order** and yields
/// each port the first time it is encountered. Duplicate ports (from
/// overlapping ranges) are skipped using a 65_536-entry "seen" table.
pub struct RangeIterator {
active: bool,
normalized_end: u32,
total: u32,
normalized_first_pick: u32,
normalized_pick: u32,
actual_start: u32,
step: u32,
ranges: Vec<(u32, u32)>,
prefix: Vec<u32>,
serial_itr: Option<Box<dyn Iterator<Item = u16>>>,
/// `seen[port]` is true once `port` has been yielded (serial mode only).
serial_seen: Option<Vec<bool>>,
}
/// An iterator that follows the `Linear Congruential Generator` algorithm.
///
/// For more information: <https://en.wikipedia.org/wiki/Linear_congruential_generator>
impl RangeIterator {
/// Receives the start and end of a range and normalize
/// these values before selecting a coprime for the end of the range
/// which will server as the step for the algorithm.
/// Construct a randomized iterator (LCG permutation).
///
/// For example, the range `1000-2500` will be normalized to `0-1500`
/// before going through the algorithm.
pub fn new(start: u32, end: u32) -> Self {
let normalized_end = end - start + 1;
let step = pick_random_coprime(normalized_end);
/// Preconditions:
/// - `input` must contain at least one `(u16,u16)`
/// and each pair must satisfy `start <= end`.
pub fn new_random(input: &[(u16, u16)]) -> Self {
// normalize & merge into (start, len) u32 pairs
// Example: [(10,12),(11,15)] -> merged [(10,6)]
let mut ranges: Vec<(u32, u32)> = input
.iter()
.map(|(s, e)| {
let start = *s as u32;
let end_excl = (*e as u32) + 1; // convert inclusive -> exclusive
(start, end_excl)
})
.collect();
// Randomly choose a number within the range to be the first
// and assign it as a pick.
ranges.sort_unstable_by_key(|&(s, _)| s);
let mut merged: Vec<(u32, u32)> = Vec::with_capacity(ranges.len());
if !ranges.is_empty() {
let mut iter = ranges.into_iter();
let (mut cur_s, mut cur_end) = iter.next().unwrap(); // cur_end exclusive
for (s, end_excl) in iter {
if s <= cur_end {
// overlap/adjacent -> extend
if end_excl > cur_end {
cur_end = end_excl;
}
} else {
// push disjoint segment as (start, len)
merged.push((cur_s, cur_end - cur_s)); // len = excl - start
cur_s = s;
cur_end = end_excl;
}
}
merged.push((cur_s, cur_end - cur_s));
}
// build prefix sums (prefix[0] = 0; prefix.len() == merged.len() + 1)
let prefix = merged.iter().fold(vec![0u32], |mut acc, (_, len)| {
let last = acc.last().unwrap();
acc.push(last.saturating_add(*len));
acc
});
// total is guaranteed > 0 by precondition (input.len() >= 1)
let total = *prefix.last().unwrap();
// pick step and seed
let step = pick_random_coprime(total);
let mut rng = rand::rng();
let normalized_first_pick = rng.random_range(0..normalized_end);
let first = rng.random_range(0..total);
Self {
active: true,
normalized_end,
total,
normalized_first_pick: first,
normalized_pick: first,
step,
normalized_first_pick,
normalized_pick: normalized_first_pick,
actual_start: start,
ranges: merged,
prefix,
serial_itr: None,
serial_seen: None,
}
}
/// Construct a serial iterator that yields ports in original input order,
/// skipping duplicates. The deduplication is done on the fly with a seen-table.
///
/// Preconditions:
/// - `input` must contain at least one `(u16,u16)` and each pair must satisfy `start <= end`.
pub fn new_serial(input: &[(u16, u16)]) -> Self {
// Build a serial iterator that yields ports in *input order* (start..=end).
// We keep the merged ranges/prefix empty here (they are not needed for serial mode).
let input = input.to_vec();
let serial_itr = input.into_iter().flat_map(|(start, end)| start..=end);
let serial_itr_boxed: Box<dyn Iterator<Item = u16>> = Box::new(serial_itr);
// One entry per possible port (0..=65535).
let seen = vec![false; usize::from(u16::MAX) + 1];
Self {
active: true,
total: 0,
normalized_first_pick: 0,
normalized_pick: 0,
step: 0,
ranges: Vec::new(),
prefix: Vec::new(),
serial_itr: Some(serial_itr_boxed),
serial_seen: Some(seen),
}
}
}
impl Iterator for RangeIterator {
type Item = u16;
// The next step is always bound by the formula: N+1 = (N + STEP) % TOP_OF_THE_RANGE
// It will only stop once we generate a number equal to the first generated number.
/// Advance the iterator by one port.
///
/// 1. Read the current normalized index `cur`.
/// 2. Compute `next = (cur + step) % total` and update `normalized_pick`.
/// 3. If `next == normalized_first_pick` mark `active = false` (we completed the cycle).
/// 4. Map the returned index `cur` into the merged ranges via the prefix array:
/// - find the range index `idx` where `prefix[idx] <= cur < prefix[idx+1]`,
/// - offset = `cur - prefix[idx]`,
/// - port = `ranges[idx].0 + offset`.
/// 5. Return `port as u16`.
///
fn next(&mut self) -> Option<Self::Item> {
if !self.active {
return None;
}
let current_pick = self.normalized_pick;
let next_pick = (current_pick + self.step) % self.normalized_end;
// SERIAL iterator fast-path: preserve original input order but skip duplicates.
if let (Some(it), Some(seen)) = (self.serial_itr.as_mut(), self.serial_seen.as_mut()) {
for p in it.by_ref() {
// Yield each port only the first time it is seen.
if !std::mem::replace(&mut seen[usize::from(p)], true) {
return Some(p);
}
// otherwise skip duplicate and continue
}
// serial iterator exhausted: drop it and mark inactive
self.serial_itr = None;
self.serial_seen = None;
self.active = false;
return None;
}
// If the next pick is equal to the first pick this means that
// we have iterated through the entire range.
if next_pick == self.normalized_first_pick {
// RANDOMIZED (LCG) path
let cur = self.normalized_pick;
let next = (cur + self.step) % self.total;
// if next equals the original seed we finished the cycle after returning cur
if next == self.normalized_first_pick {
self.active = false;
}
self.normalized_pick = next_pick;
Some(
(self.actual_start + current_pick)
.try_into()
.expect("Could not convert u32 to u16"),
)
self.normalized_pick = next;
// Map cur -> port using prefix + ranges (binary search)
let mut lo: usize = 0;
let mut hi: usize = self.ranges.len();
while lo < hi {
let mid = (lo + hi) / 2;
if self.prefix[mid + 1] > cur {
hi = mid;
} else {
lo = mid + 1;
}
}
let idx = lo;
let offset = cur - self.prefix[idx];
let (start, _len) = self.ranges[idx];
let port = (start + offset)
.try_into()
.expect("Could not convert u32 to u16");
Some(port)
}
}
@@ -98,36 +223,105 @@ fn pick_random_coprime(end: u32) -> u32 {
#[cfg(test)]
mod tests {
use super::RangeIterator;
use super::*;
use std::collections::HashSet;
// Helper: collect, sort and return ports produced by randomized RangeIterator
fn generate_sorted_from_ranges_random(input: &[(u16, u16)]) -> Vec<u16> {
let mut it = RangeIterator::new_random(input);
let mut v: Vec<u16> = it.by_ref().collect();
v.sort_unstable();
v
}
// Helper: collect, sort and return ports produced by serial RangeIterator
fn generate_sorted_from_ranges_serial(input: &[(u16, u16)]) -> Vec<u16> {
let mut it = RangeIterator::new_serial(input);
let mut v: Vec<u16> = it.by_ref().collect();
v.sort_unstable();
v
}
// Build expected sorted unique ports from input ranges (inclusive)
fn expected_ports_from_ranges(input: &[(u16, u16)]) -> Vec<u16> {
let mut s = HashSet::new();
for &(start, end) in input {
for p in start..=end {
s.insert(p);
}
}
let mut v: Vec<u16> = s.into_iter().collect();
v.sort_unstable();
v
}
#[test]
fn range_iterator_iterates_through_the_entire_range() {
let result = generate_sorted_range(1, 10);
let expected_range = (1..=10).collect::<Vec<u16>>();
assert_eq!(expected_range, result);
fn random_range_iterator_test() {
// small disjoint ranges
let input = &[(1u16, 10u16), (20u16, 30u16), (100u16, 110u16)];
let result = generate_sorted_from_ranges_random(input);
let expected = expected_ports_from_ranges(input);
assert_eq!(expected, result);
let result = generate_sorted_range(1, 100);
let expected_range = (1..=100).collect::<Vec<u16>>();
assert_eq!(expected_range, result);
// larger disjoint ranges
let input = &[(1u16, 100u16), (200u16, 500u16)];
let result = generate_sorted_from_ranges_random(input);
let expected = expected_ports_from_ranges(input);
assert_eq!(expected, result);
let result = generate_sorted_range(1, 1000);
let expected_range = (1..=1000).collect::<Vec<u16>>();
assert_eq!(expected_range, result);
// overlapping and adjacent
let input = &[(10u16, 20u16), (15u16, 25u16), (26u16, 30u16)];
let result = generate_sorted_from_ranges_random(input);
let expected = expected_ports_from_ranges(input);
assert_eq!(expected, result);
let result = generate_sorted_range(1, 65_535);
let expected_range = (1..=65_535).collect::<Vec<u16>>();
assert_eq!(expected_range, result);
// near-full domain (heavy): we only assert lengths & equality
let input = &[(1u16, 65_535u16)];
let result = generate_sorted_from_ranges_random(input);
let expected = expected_ports_from_ranges(input);
assert_eq!(expected.len(), result.len());
assert_eq!(expected, result);
let result = generate_sorted_range(1000, 2000);
let expected_range = (1000..=2000).collect::<Vec<u16>>();
assert_eq!(expected_range, result);
// multiple disjoint ranges - check dedupe & coverage
let input = &[(50u16, 100u16), (1000u16, 2000u16), (30000u16, 30010u16)];
let result = generate_sorted_from_ranges_random(input);
let set_len = result.iter().copied().collect::<HashSet<u16>>().len();
assert_eq!(set_len, result.len());
let expected = expected_ports_from_ranges(input);
assert_eq!(expected, result);
}
fn generate_sorted_range(start: u32, end: u32) -> Vec<u16> {
let range = RangeIterator::new(start, end);
let mut result = range.into_iter().collect::<Vec<u16>>();
result.sort_unstable();
#[test]
fn serial_range_iterator_test() {
// serial should preserve input-order semantics but here we only assert
// coverage (no duplicates) by sorting results and comparing expected set.
result
// small disjoint ranges
let input = &[(1u16, 10u16), (20u16, 30u16), (100u16, 110u16)];
let result = generate_sorted_from_ranges_serial(input);
let expected = expected_ports_from_ranges(input);
assert_eq!(expected, result);
// overlapping and adjacent
let input = &[(10u16, 20u16), (15u16, 25u16), (26u16, 30u16)];
let result = generate_sorted_from_ranges_serial(input);
let expected = expected_ports_from_ranges(input);
assert_eq!(expected, result);
// multiple disjoint ranges
let input = &[(50u16, 100u16), (1000u16, 2000u16), (30000u16, 30010u16)];
let result = generate_sorted_from_ranges_serial(input);
let set_len = result.iter().copied().collect::<HashSet<u16>>().len();
assert_eq!(set_len, result.len());
let expected = expected_ports_from_ranges(input);
assert_eq!(expected, result);
// all possible inputs
let input = &[(u16::MIN, u16::MAX)];
let result = generate_sorted_from_ranges_serial(input);
let set_len = result.iter().copied().collect::<HashSet<u16>>().len();
assert_eq!(set_len, result.len());
let expected = expected_ports_from_ranges(input);
assert_eq!(expected, result);
}
}
+473 -258
View File
@@ -1,23 +1,49 @@
//! Core functionality for actual scanning behaviour.
use crate::generated::get_parsed_data;
use crate::port_strategy::PortStrategy;
use crate::tui::println_safe;
use log::debug;
mod socket_iterator;
mod timeout;
use socket_iterator::SocketIterator;
use timeout::io_timeout;
use async_std::net::TcpStream;
use async_std::prelude::*;
use async_std::{io, net::UdpSocket};
use colored::Colorize;
use futures::stream::FuturesUnordered;
use futures::{stream::FuturesUnordered, StreamExt};
use std::collections::BTreeMap;
use std::{
collections::HashSet,
collections::{HashMap, HashSet},
future::Future,
io,
net::{IpAddr, Shutdown, SocketAddr},
num::NonZeroU8,
sync::Arc,
time::Duration,
};
use tokio::net::{TcpStream, UdpSocket};
/// UDP payload lookup: port -> payload bytes
///
/// `get_parsed_data()` returns a `&'static BTreeMap<...>`, so we can store
/// references to the payload bytes without cloning them.
#[doc(hidden)]
pub type UdpPayloadLookup = HashMap<u16, &'static [u8]>;
#[doc(hidden)]
pub fn build_udp_payload_lookup(udp_map: &'static BTreeMap<Vec<u16>, Vec<u8>>) -> UdpPayloadLookup {
let mut lookup: UdpPayloadLookup = HashMap::new();
for (ports, payload_vec) in udp_map.iter() {
let payload: &'static [u8] = payload_vec.as_slice();
for &port in ports.iter() {
// Preserve existing behavior: if duplicates exist, last insert wins.
lookup.insert(port, payload);
}
}
lookup
}
/// The class for the scanner
/// IP is data type IpAddr and is the IP address
@@ -29,7 +55,7 @@ use std::{
#[derive(Debug)]
pub struct Scanner {
ips: Vec<IpAddr>,
batch_size: u16,
batch_size: usize,
timeout: Duration,
tries: NonZeroU8,
greppable: bool,
@@ -37,6 +63,19 @@ pub struct Scanner {
accessible: bool,
exclude_ports: Vec<u16>,
udp: bool,
print_open_ports: bool,
report_closed: bool,
interval: Duration,
}
/// 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.
@@ -44,7 +83,7 @@ pub struct Scanner {
impl Scanner {
pub fn new(
ips: &[IpAddr],
batch_size: u16,
batch_size: usize,
timeout: Duration,
tries: u8,
greppable: bool,
@@ -63,13 +102,81 @@ impl Scanner {
accessible,
exclude_ports,
udp,
print_open_ports: false,
report_closed: false,
interval: Duration::ZERO,
}
}
/// Enables the CLI's incremental open-port output.
///
/// Library callers are quiet by default and can inspect the sockets returned by [`Self::run`].
#[must_use]
pub fn with_open_port_output(mut self) -> Self {
self.print_open_ports = true;
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
}
/// Waits `interval` after scanning one port (on every address) before
/// scanning the next port, for slow, low-noise scans.
///
/// Within a port, up to `batch_size` addresses are still scanned
/// concurrently. A zero interval (the default) scans all sockets in
/// batches without any delay.
#[must_use]
pub fn with_interval(mut self, interval: Duration) -> Self {
self.interval = interval;
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.
///
/// Must be awaited inside a Tokio runtime with I/O and time enabled.
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.
///
/// Must be awaited inside a Tokio runtime with I/O and time enabled.
pub async fn run_with_status(&self) -> Vec<PortStatus> {
// Tokio's millisecond rounding adds about 1 ms to each short interval.
// This cancellable delay keeps the requested spacing without blocking
// the async runtime or accumulating that rounding on every port.
self.run_with_delay(|interval| async move {
async_io::Timer::after(interval).await;
})
.await
}
/// Supplying the delay lets tests verify spacing with a virtual clock.
async fn run_with_delay<D, F>(&self, delay: D) -> Vec<PortStatus>
where
D: Fn(Duration) -> F,
F: Future<Output = ()>,
{
let ports: Vec<u16> = self
.port_strategy
.order()
@@ -77,33 +184,73 @@ impl Scanner {
.filter(|&port| !self.exclude_ports.contains(port))
.copied()
.collect();
let mut socket_iterator: SocketIterator = SocketIterator::new(&self.ips, &ports);
let mut open_sockets: Vec<SocketAddr> = Vec::new();
let mut ftrs = FuturesUnordered::new();
let mut found_sockets: Vec<PortStatus> = Vec::new();
let mut errors: HashSet<String> = HashSet::new();
let udp_map = get_parsed_data();
// Build UDP payload lookup once (only if we are scanning UDP).
// This avoids cloning a big map into every spawned future and turns
// payload selection from O(n) to O(1).
let udp_payloads: Option<Arc<UdpPayloadLookup>> = if self.udp {
Some(Arc::new(build_udp_payload_lookup(get_parsed_data())))
} else {
None
};
debug!("Start scanning sockets. \nBatch size {}\nNumber of ip-s {}\nNumber of ports {}\nTargets all together {}\nInterval between ports {:?}",
self.batch_size,
self.ips.len(),
ports.len(),
(self.ips.len() * ports.len()),
self.interval);
if self.interval.is_zero() {
let sockets = SocketIterator::new(&self.ips, &ports);
self.scan_sockets(sockets, &udp_payloads, &mut found_sockets, &mut errors)
.await;
} else {
// Scan one port (on every address) at a time and wait `interval`
// before moving on to the next port.
for (i, port) in ports.iter().enumerate() {
if i > 0 {
delay(self.interval).await;
}
let sockets = SocketIterator::new(&self.ips, std::slice::from_ref(port));
self.scan_sockets(sockets, &udp_payloads, &mut found_sockets, &mut errors)
.await;
}
}
debug!("Typical socket connection errors {errors:?}");
debug!("Sockets found: {:?}", found_sockets);
found_sockets
}
/// Scans every socket yielded by `sockets`, keeping at most `batch_size`
/// connection attempts in flight.
async fn scan_sockets(
&self,
mut sockets: SocketIterator<'_>,
udp_payloads: &Option<Arc<UdpPayloadLookup>>,
found_sockets: &mut Vec<PortStatus>,
errors: &mut HashSet<String>,
) {
let mut ftrs = FuturesUnordered::new();
for _ in 0..self.batch_size {
if let Some(socket) = socket_iterator.next() {
ftrs.push(self.scan_socket(socket, udp_map.clone()));
if let Some(socket) = sockets.next() {
ftrs.push(self.scan_socket(socket, udp_payloads.clone()));
} else {
break;
}
}
debug!("Start scanning sockets. \nBatch size {}\nNumber of ip-s {}\nNumber of ports {}\nTargets all together {} ",
self.batch_size,
self.ips.len(),
&ports.len(),
(self.ips.len() * ports.len()));
while let Some(result) = ftrs.next().await {
if let Some(socket) = socket_iterator.next() {
ftrs.push(self.scan_socket(socket, udp_map.clone()));
if let Some(socket) = sockets.next() {
ftrs.push(self.scan_socket(socket, udp_payloads.clone()));
}
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 {
@@ -112,9 +259,6 @@ impl Scanner {
}
}
}
debug!("Typical socket connection errors {:?}", errors);
debug!("Open Sockets found: {:?}", &open_sockets);
open_sockets
}
/// Given a socket, scan it self.tries times.
@@ -134,29 +278,36 @@ impl Scanner {
async fn scan_socket(
&self,
socket: SocketAddr,
udp_map: BTreeMap<Vec<u16>, Vec<u8>>,
) -> io::Result<SocketAddr> {
udp_payloads: Option<Arc<UdpPayloadLookup>>,
) -> io::Result<PortStatus> {
if self.udp {
return self.scan_udp_socket(socket, udp_map).await;
return self.scan_udp_socket(socket, udp_payloads).await;
}
let tries = self.tries.get();
for nr_try in 1..=tries {
match self.connect(socket).await {
Ok(tcp_stream) => {
debug!(
"Connection was successful, shutting down stream {}",
&socket
);
if let Err(e) = tcp_stream.shutdown(Shutdown::Both) {
debug!("Shutdown stream error {}", &e);
debug!("Connection was successful, shutting down stream {}", socket);
if let Err(e) = tcp_stream
.into_std()
.and_then(|stream| stream.shutdown(Shutdown::Both))
{
debug!("Shutdown stream error {}", e);
}
self.fmt_ports(socket);
debug!("Return Ok after {} tries", nr_try);
return Ok(socket);
debug!("Return Ok after {nr_try} tries");
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.");
@@ -164,7 +315,7 @@ impl Scanner {
if nr_try == tries {
error_string.push(' ');
error_string.push_str(&socket.ip().to_string());
return Err(io::Error::new(io::ErrorKind::Other, error_string));
return Err(io::Error::other(error_string));
}
}
};
@@ -175,28 +326,25 @@ impl Scanner {
async fn scan_udp_socket(
&self,
socket: SocketAddr,
udp_map: BTreeMap<Vec<u16>, Vec<u8>>,
) -> io::Result<SocketAddr> {
let mut payload: Vec<u8> = Vec::new();
for (key, value) in udp_map {
if key.contains(&socket.port()) {
payload = value;
}
}
udp_payloads: Option<Arc<UdpPayloadLookup>>,
) -> io::Result<PortStatus> {
let payload: &[u8] = udp_payloads
.as_ref()
.and_then(|m| m.get(&socket.port()).copied())
.unwrap_or(b"");
let tries = self.tries.get();
for _ in 1..=tries {
match self.udp_scan(socket, &payload, self.timeout).await {
Ok(true) => return Ok(socket),
match self.udp_scan(socket, payload, self.timeout).await {
Ok(true) => return Ok(PortStatus::Open(socket)),
Ok(false) => continue,
Err(e) => return Err(e),
}
}
Err(io::Error::new(
io::ErrorKind::Other,
format!("UDP scan timed-out for all tries on socket {}", socket),
))
Err(io::Error::other(format!(
"UDP scan timed-out for all tries on socket {socket}"
)))
}
/// Performs the connection to the socket with timeout
@@ -214,12 +362,7 @@ impl Scanner {
/// ```
///
async fn connect(&self, socket: SocketAddr) -> io::Result<TcpStream> {
let stream = io::timeout(
self.timeout,
async move { TcpStream::connect(socket).await },
)
.await?;
Ok(stream)
io_timeout(self.timeout, TcpStream::connect(socket)).await
}
/// Binds to a UDP socket so we can send and receive packets
@@ -273,9 +416,9 @@ impl Scanner {
udp_socket.connect(socket).await?;
udp_socket.send(payload).await?;
match io::timeout(wait, udp_socket.recv(&mut buf)).await {
match io_timeout(wait, udp_socket.recv(&mut buf)).await {
Ok(size) => {
debug!("Received {} bytes", size);
debug!("Received {size} bytes");
self.fmt_ports(socket);
Ok(true)
}
@@ -289,7 +432,7 @@ impl Scanner {
}
}
Err(e) => {
println!("Err E binding sock {:?}", e);
debug!("Error binding UDP socket: {e:?}");
Err(e)
}
}
@@ -297,11 +440,22 @@ impl Scanner {
/// Formats and prints the port status
fn fmt_ports(&self, socket: SocketAddr) {
if !self.greppable {
if self.print_open_ports && !self.greppable {
if self.accessible {
println!("Open {socket}");
println_safe(format_args!("Open {socket}"));
} else {
println!("Open {}", socket.to_string().purple());
println_safe(format_args!("Open {}", socket.to_string().purple()));
}
}
}
/// 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()));
}
}
}
@@ -310,218 +464,279 @@ impl Scanner {
#[cfg(test)]
mod tests {
use super::*;
use crate::input::{PortRange, ScanOrder};
use async_std::task::block_on;
use std::{net::IpAddr, time::Duration};
use crate::input::{PortRanges, ScanOrder};
// These tests never open sockets: operations are simulated futures, and
// scanners that run have no target addresses.
fn test_runtime() -> tokio::runtime::Runtime {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.start_paused(true)
.build()
.unwrap()
}
#[test]
fn scanner_runs() {
// Makes sure the program still runs and doesn't panic
fn timeout_preserves_success_including_empty_udp_responses() {
test_runtime().block_on(async {
// A zero-byte UDP datagram is still a successful response.
let result = io_timeout(Duration::from_secs(1), async { Ok(0_usize) }).await;
assert_eq!(result.unwrap(), 0);
});
}
#[test]
fn timeout_preserves_io_errors() {
test_runtime().block_on(async {
for kind in [
io::ErrorKind::ConnectionRefused,
io::ErrorKind::PermissionDenied,
io::ErrorKind::TimedOut,
] {
let result = io_timeout(Duration::from_secs(1), async {
Err::<(), _>(io::Error::new(kind, "socket error"))
})
.await;
let error = result.unwrap_err();
assert_eq!(error.kind(), kind);
assert_eq!(error.to_string(), "socket error");
}
});
}
#[test]
fn ready_operation_wins_at_zero_timeout() {
test_runtime().block_on(async {
assert_eq!(
io_timeout(Duration::ZERO, async { Ok(7) }).await.unwrap(),
7
);
});
}
#[test]
fn timeout_preserves_wakes_from_the_first_poll() {
test_runtime().block_on(async {
let mut polled = false;
let operation = std::future::poll_fn(|context| {
if polled {
std::task::Poll::Ready(Ok(7))
} else {
polled = true;
context.waker().wake_by_ref();
std::task::Poll::Pending
}
});
assert_eq!(
io_timeout(Duration::from_secs(1), operation).await.unwrap(),
7
);
});
}
#[test]
fn expired_timeout_cancels_the_operation() {
use std::cell::Cell;
struct DropGuard<'a>(&'a Cell<bool>);
impl Drop for DropGuard<'_> {
fn drop(&mut self) {
self.0.set(true);
}
}
for duration in [Duration::from_millis(5), Duration::from_millis(80)] {
test_runtime().block_on(async {
let dropped = Cell::new(false);
let started = std::time::Instant::now();
let operation = async {
let _guard = DropGuard(&dropped);
std::future::pending::<io::Result<()>>().await
};
let error = io_timeout(duration, operation).await.unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::TimedOut);
assert!(started.elapsed() >= duration);
assert!(dropped.get(), "the timed-out operation must be dropped");
});
}
}
#[test]
fn operation_can_complete_before_the_precise_timeout_phase() {
test_runtime().block_on(async {
let result = io_timeout(Duration::from_secs(1), async {
tokio::time::sleep(Duration::from_millis(10)).await;
Ok(7)
})
.await;
assert_eq!(result.unwrap(), 7);
});
}
#[test]
fn interval_waits_only_between_included_ports() {
test_runtime().block_on(async {
let interval = Duration::from_millis(250);
let mut scanner = test_scanner().with_interval(interval);
scanner.ips.clear();
scanner.port_strategy = PortStrategy::Manual(vec![80, 443, 8080, 8443]);
scanner.exclude_ports = vec![443];
let started = tokio::time::Instant::now();
assert!(scanner.run_with_delay(tokio::time::sleep).await.is_empty());
assert_eq!(started.elapsed(), interval * 2);
});
}
#[test]
fn dropping_a_scan_cancels_a_pending_interval() {
use std::cell::Cell;
use std::task::Poll;
struct PendingDelay<'a>(&'a Cell<bool>);
impl Future for PendingDelay<'_> {
type Output = ();
fn poll(self: std::pin::Pin<&mut Self>, _: &mut std::task::Context<'_>) -> Poll<()> {
Poll::Pending
}
}
impl Drop for PendingDelay<'_> {
fn drop(&mut self) {
self.0.set(true);
}
}
test_runtime().block_on(async {
let dropped = Cell::new(false);
let mut scanner = test_scanner().with_interval(Duration::from_secs(3600));
scanner.ips.clear();
scanner.port_strategy = PortStrategy::Manual(vec![80, 443]);
let mut scan = Box::pin(scanner.run_with_delay(|_| PendingDelay(&dropped)));
assert!(futures::poll!(scan.as_mut()).is_pending());
assert!(!dropped.get());
drop(scan);
assert!(dropped.get());
});
}
#[test]
fn zero_interval_and_empty_port_sets_do_not_wait() {
test_runtime().block_on(async {
let mut scanner = test_scanner();
scanner.ips.clear();
scanner.port_strategy = PortStrategy::Manual(vec![80, 443]);
let started = tokio::time::Instant::now();
assert!(scanner.run().await.is_empty());
assert_eq!(started.elapsed(), Duration::ZERO);
scanner = scanner.with_interval(Duration::from_secs(1));
scanner.exclude_ports = vec![80, 443];
assert!(scanner.run().await.is_empty());
assert_eq!(started.elapsed(), Duration::ZERO);
scanner.port_strategy = PortStrategy::Manual(Vec::new());
assert!(scanner.run().await.is_empty());
assert_eq!(started.elapsed(), Duration::ZERO);
});
}
fn test_scanner() -> Scanner {
let addrs = vec!["127.0.0.1".parse::<IpAddr>().unwrap()];
let range = PortRange {
start: 1,
end: 1_000,
};
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
let scanner = Scanner::new(
let strategy = PortStrategy::pick(&Some(PortRanges(vec![(1, 1)])), None, ScanOrder::Serial);
Scanner::new(
&addrs,
10,
1,
Duration::from_millis(100),
1,
true,
strategy,
true,
vec![9000],
false,
);
block_on(scanner.run());
// if the scan fails, it wouldn't be able to assert_eq! as it panicked!
assert_eq!(1, 1);
}
#[test]
fn ipv6_scanner_runs() {
// Makes sure the program still runs and doesn't panic
let addrs = vec!["::1".parse::<IpAddr>().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],
false,
);
block_on(scanner.run());
// if the scan fails, it wouldn't be able to assert_eq! as it panicked!
assert_eq!(1, 1);
}
#[test]
fn quad_zero_scanner_runs() {
let addrs = vec!["0.0.0.0".parse::<IpAddr>().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],
Vec::new(),
false,
);
block_on(scanner.run());
assert_eq!(1, 1);
}
#[test]
fn google_dns_runs() {
let addrs = vec!["8.8.8.8".parse::<IpAddr>().unwrap()];
let range = PortRange {
start: 400,
end: 445,
};
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],
false,
);
block_on(scanner.run());
assert_eq!(1, 1);
}
#[test]
fn infer_ulimit_lowering_no_panic() {
// Test behaviour on MacOS where ulimit is not automatically lowered
let addrs = vec!["8.8.8.8".parse::<IpAddr>().unwrap()];
// mac should have this automatically scaled down
let range = PortRange {
start: 400,
end: 600,
};
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],
false,
);
block_on(scanner.run());
assert_eq!(1, 1);
)
}
#[test]
fn udp_scan_runs() {
// Makes sure the program still runs and doesn't panic
let addrs = vec!["127.0.0.1".parse::<IpAddr>().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,
);
block_on(scanner.run());
// if the scan fails, it wouldn't be able to assert_eq! as it panicked!
assert_eq!(1, 1);
fn library_scanner_is_quiet_by_default() {
let scanner = test_scanner();
assert!(!scanner.print_open_ports);
}
#[test]
fn udp_ipv6_runs() {
// Makes sure the program still runs and doesn't panic
let addrs = vec!["::1".parse::<IpAddr>().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,
);
block_on(scanner.run());
// if the scan fails, it wouldn't be able to assert_eq! as it panicked!
assert_eq!(1, 1);
fn cli_can_enable_open_port_output() {
let scanner = test_scanner().with_open_port_output();
assert!(scanner.print_open_ports);
}
#[test]
fn udp_quad_zero_scanner_runs() {
let addrs = vec!["0.0.0.0".parse::<IpAddr>().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,
);
block_on(scanner.run());
assert_eq!(1, 1);
fn closed_ports_are_not_reported_by_default() {
assert!(!test_scanner().report_closed);
}
#[test]
fn udp_google_dns_runs() {
let addrs = vec!["8.8.8.8".parse::<IpAddr>().unwrap()];
let range = PortRange {
start: 100,
end: 150,
};
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,
);
block_on(scanner.run());
assert_eq!(1, 1);
fn closed_port_reporting_is_opt_in() {
assert!(test_scanner().with_closed_ports().report_closed);
}
#[test]
fn no_interval_by_default() {
assert!(test_scanner().interval.is_zero());
}
#[test]
fn with_interval_sets_the_delay_between_ports() {
let scanner = test_scanner().with_interval(Duration::from_millis(250));
assert_eq!(scanner.interval, Duration::from_millis(250));
}
/// 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
/// decoding mangled it into a 28-byte probe that agents never answered.
#[test]
fn udp_snmp_probe_bytes_match_nmap() {
let payload = get_parsed_data()
.iter()
.find(|(ports, _)| ports.contains(&161))
.map(|(_, payload)| payload)
.expect("no UDP payload registered for port 161");
let expected: Vec<u8> = vec![
0x30, 0x1f, 0x02, 0x01, 0x00, 0x04, 0x06, b'p', b'u', b'b', b'l', b'i', b'c', 0xa1,
0x12, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00, 0x30, 0x07, 0x30, 0x05,
0x06, 0x01, 0x00, 0x05, 0x00,
];
assert_eq!(*payload, expected);
}
/// The SSDP probe mixes `\xNN` escapes, `\"` escapes and literal text
/// across two quoted segments: segments must decode and concatenate
/// with no separators.
#[test]
fn udp_ssdp_probe_decodes_escapes_and_literal_text() {
let payload = get_parsed_data()
.iter()
.find(|(ports, _)| ports.contains(&1900))
.map(|(_, payload)| payload)
.expect("no UDP payload registered for port 1900");
let expected =
b"M-SEARCH * HTTP/1.1\r\nHost: 239.255.255.250:1900\r\nMan: \"ssdp:discover\"\r\nMX: 5\r\nST: ssdp:all\r\n\r\n"
.to_vec();
assert_eq!(*payload, expected);
}
}
+101 -29
View File
@@ -1,55 +1,78 @@
use itertools::{iproduct, Product};
use std::iter::FusedIterator;
use std::net::{IpAddr, SocketAddr};
use std::slice;
/// An iterator that receives a slice of IPs and ports and returns a
/// [`SocketAddr`] for each IP/port pair until all combinations are exhausted.
///
/// The goal of this iterator is to walk every IP and port combination
/// *without* allocating a big intermediate buffer — the alternative would be
/// materialising a `Vec<SocketAddr>` of every pair up front.
///
/// # Ordering
///
/// The IP/port order is intentionally reversed inside `product_it`: we want
/// `iproduct!` to iterate *all IPs for one port* before advancing to the next
/// port ("hold the port, go through all the IPs, then advance the port").
///
/// # Example
///
/// `SocketIterator` lives in a private module, so this example is not run as
/// a doctest; `goes_through_every_ip_port_combination` checks the same order.
///
/// ```ignore
/// # use std::net::IpAddr;
/// let ips = [
/// "127.0.0.1".parse::<IpAddr>().unwrap(),
/// "192.168.0.1".parse::<IpAddr>().unwrap(),
/// ];
/// let ports = [80u16, 443];
///
/// let mut it = SocketIterator::new(&ips, &ports);
/// assert_eq!(it.next(), Some("127.0.0.1:80".parse().unwrap()));
/// assert_eq!(it.next(), Some("192.168.0.1:80".parse().unwrap()));
/// assert_eq!(it.next(), Some("127.0.0.1:443".parse().unwrap()));
/// assert_eq!(it.next(), Some("192.168.0.1:443".parse().unwrap()));
/// assert_eq!(it.next(), None);
/// ```
#[derive(Clone)]
pub struct SocketIterator<'s> {
// product_it is a cartesian product iterator over
// the slices of ports and IP addresses.
//
// The IP/port order is intentionally reversed here since we want
// the itertools::iproduct! macro below to generate the pairs with
// all the IPs for one port before moving on to the next one
// ("hold the port, go through all the IPs, then advance the port...").
// See also the comments in the iterator implementation for an example.
product_it:
Product<Box<std::slice::Iter<'s, u16>>, Box<std::slice::Iter<'s, std::net::IpAddr>>>,
// No boxing: the iterator owns the concrete slice iterators directly.
// This keeps `SocketIterator` allocation-free and makes `Clone` free too.
product_it: Product<slice::Iter<'s, u16>, slice::Iter<'s, IpAddr>>,
}
/// An iterator that receives a slice of IPs and ports and returns a Socket
/// for each IP and port pair until all of these combinations are exhausted.
/// The goal of this iterator is to go over every IP and port combination
/// without generating a big memory footprint. The alternative would be
/// generating a vector containing all these combinations.
impl<'s> SocketIterator<'s> {
pub fn new(ips: &'s [IpAddr], ports: &'s [u16]) -> Self {
let ports_it = Box::new(ports.iter());
let ips_it = Box::new(ips.iter());
Self {
product_it: iproduct!(ports_it, ips_it),
// `iproduct!` calls `.into_iter()` on each argument; `slice::Iter`
// is already an iterator, so this is a no-op (no allocation).
product_it: iproduct!(ports.iter(), ips.iter()),
}
}
}
#[allow(clippy::doc_link_with_quotes)]
impl Iterator for SocketIterator<'_> {
type Item = SocketAddr;
/// Returns a socket based on the combination of one of the provided
/// IPs and ports or None when these combinations are exhausted. Every
/// IP will have the same port until a port is incremented.
///
/// let it = SocketIterator::new(&["127.0.0.1", "192.168.0.1"], &[80, 443]);
/// it.next(); // 127.0.0.1:80
/// it.next(); // 192.168.0.1:80
/// it.next(); // 127.0.0.1:443
/// it.next(); // 192.168.0.1:443
/// it.next(); // None
/// Returns the next socket, or `None` when all combinations are exhausted.
/// Every IP is paired with the same port until the port advances.
fn next(&mut self) -> Option<Self::Item> {
self.product_it
.next()
.map(|(port, ip)| SocketAddr::new(*ip, *port))
}
// Delegate the size hint so callers like `collect()` can pre-size buffers.
fn size_hint(&self) -> (usize, Option<usize>) {
self.product_it.size_hint()
}
}
// `Product` and `slice::Iter` are both fused, so we are too.
impl FusedIterator for SocketIterator<'_> {}
#[cfg(test)]
mod tests {
use super::SocketIterator;
@@ -72,4 +95,53 @@ mod tests {
assert_eq!(Some(SocketAddr::new(addrs[1], ports[2])), it.next());
assert_eq!(None, it.next());
}
#[test]
fn size_hint_is_exact() {
let addrs = ["127.0.0.1".parse::<IpAddr>().unwrap()];
let ports: Vec<u16> = vec![22, 80, 443];
let mut it = SocketIterator::new(&addrs, &ports);
assert_eq!(it.size_hint(), (3, Some(3)));
it.next();
assert_eq!(it.size_hint(), (2, Some(2)));
}
#[test]
fn clone_resumes_independently() {
let addrs = [
"127.0.0.1".parse::<IpAddr>().unwrap(),
"192.168.0.1".parse::<IpAddr>().unwrap(),
];
let ports: Vec<u16> = vec![22, 80];
let mut a = SocketIterator::new(&addrs, &ports);
assert_eq!(a.next(), Some(SocketAddr::new(addrs[0], ports[0])));
let b = a.clone(); // b starts where a is now
// 2 IPs x 2 ports = 4 sockets, one of which was consumed before cloning.
let rest = [
SocketAddr::new(addrs[1], ports[0]),
SocketAddr::new(addrs[0], ports[1]),
SocketAddr::new(addrs[1], ports[1]),
];
assert_eq!(a.collect::<Vec<_>>(), rest);
assert_eq!(b.collect::<Vec<_>>(), rest);
}
#[test]
fn empty_inputs_yield_nothing() {
let addrs = ["127.0.0.1".parse::<IpAddr>().unwrap()];
let ports = [22u16];
for mut it in [
SocketIterator::new(&[], &ports),
SocketIterator::new(&addrs, &[]),
] {
assert_eq!(it.size_hint(), (0, Some(0)));
assert_eq!(it.next(), None);
// Stays exhausted, as promised by the `FusedIterator` impl.
assert_eq!(it.next(), None);
}
}
}
+57
View File
@@ -0,0 +1,57 @@
use std::{
future::{poll_fn, Future},
io,
task::Poll,
time::{Duration, Instant},
};
/// Preserve I/O errors and cancel pending operations when their deadline expires.
/// Poll once before allocating a timer so immediate replies pay no timer cost.
pub(super) async fn io_timeout<T>(
duration: Duration,
operation: impl Future<Output = io::Result<T>>,
) -> io::Result<T> {
let started = Instant::now();
futures::pin_mut!(operation);
let mut delay = None;
// Most replies arrive well before a normal scan timeout. Keep their timers
// on Tokio's reactor to avoid registering each socket's deadline with a
// second reactor. Leave 32 ms for the precise timer to allow for coarse
// timer rounding (including Windows). Short timeouts use the
// precise timer immediately; neither phase changes the original deadline.
// Allocate this state only for pending I/O. Keeping it out of the outer
// future also keeps FuturesUnordered entries small for immediate results.
poll_fn(|context| {
if let Poll::Ready(result) = operation.as_mut().poll(context) {
return Poll::Ready(result);
}
let delay = delay.get_or_insert_with(|| {
Box::pin(async move {
let Some(deadline) = started.checked_add(duration) else {
return std::future::pending::<()>().await;
};
const PRECISE_WINDOW: Duration = Duration::from_millis(32);
if deadline.saturating_duration_since(Instant::now()) > PRECISE_WINDOW {
tokio::time::sleep_until(tokio::time::Instant::from_std(
deadline - PRECISE_WINDOW,
))
.await;
}
// OS waits avoid rounding every short timeout to the next millisecond.
async_io::Timer::at(deadline).await;
})
});
if delay.as_mut().poll(context).is_ready() {
Poll::Ready(Err(io::Error::new(
io::ErrorKind::TimedOut,
"future timed out",
)))
} else {
Poll::Pending
}
})
.await
}
+68 -88
View File
@@ -46,12 +46,9 @@
//!
//! - `fixtures/test_rustscan_scripts.toml`
//!
//! Script file examples:
//! Metadata-only test fixture:
//!
//! - `fixtures/test_script.py`
//! - `fixtures/test_script.pl`
//! - `fixtures/test_script.sh`
//! - `fixtures/test_script.txt`
//! - `fixtures/.rustscan_scripts/test_script.txt`
//!
//! `call_format` in script files can be of 2 variants:
//!
@@ -108,16 +105,20 @@ pub fn init_scripts(scripts: &ScriptsRequired) -> Result<Vec<ScriptFile>> {
scripts_to_run.push(default_script);
}
ScriptsRequired::Custom => {
let scripts_dir_base =
dirs::home_dir().ok_or_else(|| anyhow!("Could not infer scripts path."))?;
let script_paths = find_scripts(scripts_dir_base)?;
debug!("Scripts paths \n{:?}", script_paths);
let script_config = ScriptConfig::read_config()?;
debug!("Script config \n{script_config:?}");
let script_dir_base = if let Some(config_directory) = &script_config.directory {
PathBuf::from(config_directory)
} else {
dirs::home_dir().ok_or_else(|| anyhow!("Could not infer scripts path."))?
};
let script_paths = find_scripts(script_dir_base)?;
debug!("Scripts paths \n{script_paths:?}");
let parsed_scripts = parse_scripts(script_paths);
debug!("Scripts parsed \n{:?}", parsed_scripts);
let script_config = ScriptConfig::read_config()?;
debug!("Script config \n{:?}", script_config);
debug!("Scripts parsed \n{parsed_scripts:?}");
// Only Scripts that contain all the tags found in ScriptConfig will be selected.
if let Some(config_hashset) = script_config.tags {
@@ -131,14 +132,14 @@ pub fn init_scripts(scripts: &ScriptsRequired) -> Result<Vec<ScriptFile>> {
} else {
debug!(
"\nScript tags does not match config tags {:?} {}",
&script_hashset,
script_hashset,
script.path.unwrap().display()
);
}
}
}
}
debug!("\nScript(s) to run {:?}", scripts_to_run);
debug!("\nScript(s) to run {scripts_to_run:?}");
}
}
@@ -148,7 +149,7 @@ pub fn init_scripts(scripts: &ScriptsRequired) -> Result<Vec<ScriptFile>> {
pub fn parse_scripts(scripts: Vec<PathBuf>) -> Vec<ScriptFile> {
let mut parsed_scripts: Vec<ScriptFile> = Vec::with_capacity(scripts.len());
for script in scripts {
debug!("Parsing script {}", &script.display());
debug!("Parsing script {}", script.display());
if let Some(script_file) = ScriptFile::new(script) {
parsed_scripts.push(script_file);
}
@@ -220,7 +221,7 @@ impl Script {
// Some variables get changed before read, and compiler throws warning on warn(unused_assignments)
#[allow(unused_assignments)]
pub fn run(self) -> Result<String> {
debug!("run self {:?}", &self);
debug!("run self {:?}", self);
let separator = self.ports_separator.unwrap_or_else(|| ",".into());
@@ -265,14 +266,14 @@ impl Script {
};
to_run = default_template.fill_with_struct(&exec_parts)?;
}
debug!("\nScript format to run {}", to_run);
debug!("\nScript format to run {to_run}");
execute_script(&to_run)
}
}
#[cfg(not(tarpaulin_include))]
fn execute_script(script: &str) -> Result<String> {
debug!("\nScript arguments {}", script);
debug!("\nScript arguments {script}");
let (cmd, arg) = if cfg!(unix) {
("sh", "-c")
@@ -310,16 +311,15 @@ fn execute_script(script: &str) -> Result<String> {
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
Err(error) => {
debug!("Command error {}", error.to_string());
debug!("Command error {error}",);
Err(anyhow!(error.to_string()))
}
}
}
pub fn find_scripts(mut path: PathBuf) -> Result<Vec<PathBuf>> {
path.push(".rustscan_scripts");
pub fn find_scripts(path: PathBuf) -> Result<Vec<PathBuf>> {
if path.is_dir() {
debug!("Scripts folder found {}", &path.display());
debug!("Scripts folder found {}", path.display());
let mut files_vec: Vec<PathBuf> = Vec::new();
for entry in fs::read_dir(path)? {
let entry = entry?;
@@ -357,20 +357,20 @@ impl ScriptFile {
}
}
} else {
debug!("Failed to read file: {}", &real_path.display());
debug!("Failed to read file: {}", real_path.display());
return None;
}
debug!("ScriptFile {} lines\n{}", &real_path.display(), &lines_buf);
debug!("ScriptFile {} lines\n{}", real_path.display(), lines_buf);
match toml::from_str::<ScriptFile>(&lines_buf) {
Ok(mut parsed) => {
debug!("Parsed ScriptFile{} \n{:?}", &real_path.display(), &parsed);
debug!("Parsed ScriptFile{} \n{:?}", real_path.display(), parsed);
parsed.path = Some(real_path);
// parsed_scripts.push(parsed);
Some(parsed)
}
Err(e) => {
debug!("Failed to parse ScriptFile headers {}", e.to_string());
debug!("Failed to parse ScriptFile headers {e}");
None
}
}
@@ -382,6 +382,7 @@ pub struct ScriptConfig {
pub tags: Option<Vec<String>>,
pub ports: Option<Vec<String>>,
pub developer: Option<Vec<String>>,
pub directory: Option<String>,
}
#[cfg(not(tarpaulin_include))]
@@ -400,27 +401,13 @@ impl ScriptConfig {
#[cfg(test)]
mod tests {
use super::{find_scripts, parse_scripts, Script, ScriptFile};
// Function for testing only, it inserts static values into ip and open_ports
// Doesn't use impl in case it's implemented in the super module at some point
fn into_script(script_f: ScriptFile) -> Script {
Script::build(
script_f.path,
"127.0.0.1".parse().unwrap(),
vec![80, 8080],
script_f.port,
script_f.ports_separator,
script_f.tags,
script_f.call_format,
)
}
use super::*;
#[test]
fn find_and_parse_scripts() {
let scripts = find_scripts("fixtures/".into()).unwrap();
let scripts = find_scripts("fixtures/.rustscan_scripts".into()).unwrap();
let scripts = parse_scripts(scripts);
assert_eq!(scripts.len(), 4);
assert_eq!(scripts.len(), 1);
}
#[test]
@@ -436,26 +423,6 @@ mod tests {
.unwrap();
}
#[test]
#[should_panic]
fn open_script_file_invalid_call_format() {
let mut script_f =
ScriptFile::new("fixtures/.rustscan_scripts/test_script.txt".into()).unwrap();
script_f.call_format = Some("qwertyuiop".to_string());
let script: Script = into_script(script_f);
let _output = script.run().unwrap();
}
#[test]
#[should_panic]
fn open_script_file_missing_call_format() {
let mut script_f =
ScriptFile::new("fixtures/.rustscan_scripts/test_script.txt".into()).unwrap();
script_f.call_format = None;
let script: Script = into_script(script_f);
let _output = script.run().unwrap();
}
#[test]
#[should_panic]
fn open_nonexisting_script_file() {
@@ -480,39 +447,52 @@ mod tests {
assert_eq!(script_f.ports_separator, Some(",".to_string()));
assert_eq!(
script_f.call_format,
Some("nmap -vvv -p {{port}} {{ip}}".to_string())
Some("fixture {{ip}} {{port}}".to_string())
);
}
#[test]
#[cfg(unix)]
fn run_bash_script() {
let script_f = ScriptFile::new("fixtures/.rustscan_scripts/test_script.sh".into()).unwrap();
let script: Script = into_script(script_f);
let output = script.run().unwrap();
// output has a newline at the end by default, .trim() trims it
assert_eq!(output.trim(), "127.0.0.1 80,8080");
}
fn test_custom_directory_config() {
// Create test config
let config_str = r#"
tags = ["core_approved", "example"]
directory = "fixtures/.rustscan_scripts"
"#;
#[test]
fn run_python_script() {
let script_f = ScriptFile::new("fixtures/.rustscan_scripts/test_script.py".into()).unwrap();
let script: Script = into_script(script_f);
let output = script.run().unwrap();
// output has a newline at the end by default, .trim() trims it
let config: ScriptConfig = toml::from_str(config_str).unwrap();
assert_eq!(
output.trim(),
"Python script ran with arguments ['fixtures/.rustscan_scripts/test_script.py', '127.0.0.1', '80,8080']"
config.directory,
Some("fixtures/.rustscan_scripts".to_string())
);
// Test that the directory is actually used
let script_dir_base = PathBuf::from(config.directory.unwrap());
let scripts = find_scripts(script_dir_base).unwrap();
// Verify we found the test script
assert!(scripts.iter().any(|p| p
.file_name()
.and_then(|f| f.to_str())
.map(|s| s == "test_script.txt")
.unwrap_or(false)));
}
#[test]
#[cfg(unix)]
fn run_perl_script() {
let script_f = ScriptFile::new("fixtures/.rustscan_scripts/test_script.pl".into()).unwrap();
let script: Script = into_script(script_f);
let output = script.run().unwrap();
// output has a newline at the end by default, .trim() trims it
assert_eq!(output.trim(), "Total args passed to fixtures/.rustscan_scripts/test_script.pl : 2\nArg # 1 : 127.0.0.1\nArg # 2 : 80,8080");
fn test_default_directory_fallback() {
let config_str = r#"
tags = ["core_approved", "example"]
"#;
let config: ScriptConfig = toml::from_str(config_str).unwrap();
assert_eq!(config.directory, None);
// Test fallback to home directory
let script_dir_base = if let Some(config_directory) = &config.directory {
PathBuf::from(config_directory)
} else {
dirs::home_dir().unwrap()
};
assert_eq!(script_dir_base, dirs::home_dir().unwrap());
}
}
+57 -13
View File
@@ -1,20 +1,55 @@
//! Utilities for terminal output during scanning.
use std::io::Write;
/// Prints a single line to stdout without ever panicking.
///
/// `println!` panics when writing to stdout fails, which is fatal under the
/// `panic = "abort"` release profile. That includes the `BrokenPipe` error
/// produced when a downstream consumer (e.g. `rustscan -g ... | head`)
/// closes the pipe, so instead the process exits quietly when that happens.
/// Other output errors are reported on stderr before exiting.
///
/// All user-facing output should go through this function (the `warning!`,
/// `detail!`, `output!` and `funny_opening!` macros already do) instead of
/// `println!`.
pub fn println_safe(args: std::fmt::Arguments<'_>) {
match writeln!(std::io::stdout().lock(), "{args}") {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => {
// the reader end of the pipe is gone; nothing left to print to
std::process::exit(0);
}
Err(e) => {
let _ = writeln!(std::io::stderr(), "rustscan: failed writing to stdout: {e}");
std::process::exit(1);
}
}
}
/// Terminal User Interface Module for RustScan
/// Defines macros to use
#[macro_export]
macro_rules! warning {
($name:expr) => {
println!("{} {}", ansi_term::Colour::Red.bold().paint("[!]"), $name);
$crate::tui::println_safe(format_args!(
"{} {}",
ansi_term::Colour::Red.bold().paint("[!]"),
$name
));
};
($name:expr, $greppable:expr, $accessible:expr) => {
// if not greppable then print, otherwise no else statement so do not print.
if !$greppable {
if $accessible {
// Don't print the ascii art
println!("{}", $name);
$crate::tui::println_safe(format_args!("{}", $name));
} else {
println!("{} {}", ansi_term::Colour::Red.bold().paint("[!]"), $name);
$crate::tui::println_safe(format_args!(
"{} {}",
ansi_term::Colour::Red.bold().paint("[!]"),
$name
));
}
}
};
@@ -23,16 +58,24 @@ macro_rules! warning {
#[macro_export]
macro_rules! detail {
($name:expr) => {
println!("{} {}", ansi_term::Colour::Blue.bold().paint("[~]"), $name);
$crate::tui::println_safe(format_args!(
"{} {}",
ansi_term::Colour::Blue.bold().paint("[~]"),
$name
));
};
($name:expr, $greppable:expr, $accessible:expr) => {
// if not greppable then print, otherwise no else statement so do not print.
if !$greppable {
if $accessible {
// Don't print the ascii art
println!("{}", $name);
$crate::tui::println_safe(format_args!("{}", $name));
} else {
println!("{} {}", ansi_term::Colour::Blue.bold().paint("[~]"), $name);
$crate::tui::println_safe(format_args!(
"{} {}",
ansi_term::Colour::Blue.bold().paint("[~]"),
$name
));
}
}
};
@@ -41,24 +84,24 @@ macro_rules! detail {
#[macro_export]
macro_rules! output {
($name:expr) => {
println!(
$crate::tui::println_safe(format_args!(
"{} {}",
RGansi_term::Colour::RGB(0, 255, 9).bold().paint("[>]"),
ansi_term::Colour::RGB(0, 255, 9).bold().paint("[>]"),
$name
);
));
};
($name:expr, $greppable:expr, $accessible:expr) => {
// if not greppable then print, otherwise no else statement so do not print.
if !$greppable {
if $accessible {
// Don't print the ascii art
println!("{}", $name);
$crate::tui::println_safe(format_args!("{}", $name));
} else {
println!(
$crate::tui::println_safe(format_args!(
"{} {}",
ansi_term::Colour::RGB(0, 255, 9).bold().paint("[>]"),
$name
);
));
}
}
};
@@ -97,9 +140,10 @@ macro_rules! funny_opening {
"RustScan: Exploring the digital landscape, one IP at a time.",
"TreadStone was here 🚀",
"With RustScan, I scan ports so fast, even my firewall gets whiplash 💨",
"Scanning ports so fast, even the internet got a speeding ticket!",
];
let random_quote = quotes.choose(&mut rand::rng()).unwrap();
println!("{}\n", random_quote);
$crate::tui::println_safe(format_args!("{}\n", random_quote));
};
}
-163
View File
@@ -1,163 +0,0 @@
/*
* Test rustscan against different targets with a time limit.
* The tests assumes target/debug/rustscan has already been built.
*
* The tests are #[ignore] to avoid running them during normal development.
*
* Their tests in the timelimits module are run by travis during CI.
*/
use std::process::Command;
use std::time::Duration;
use wait_timeout::ChildExt;
const TIMEOUT_MARGIN: u32 = 3;
#[cfg(not(tarpaulin_include))]
fn run_rustscan_with_timeout(args: &[&str], timeout: Duration) {
println!("Running: target/debug/rustscan: {}", args.join(" "));
use std::time::Instant;
let start = Instant::now();
let mut child = Command::new("target/debug/rustscan")
.args(args)
.spawn()
.unwrap();
let mut tries = TIMEOUT_MARGIN;
loop {
match child.wait_timeout(timeout).unwrap() {
Some(_status) => break,
None => {
tries -= 1;
if tries == 0 {
// child hasn't exited yet
child.kill().unwrap();
child.wait().unwrap().code().unwrap();
panic!("Timeout while running command");
}
}
}
}
let end = Instant::now();
let duration = end.saturating_duration_since(start).as_secs_f32();
println!("time: {:1.1}s", duration);
}
mod timelimits {
#[test]
#[ignore]
fn scan_localhost() {
let timeout = super::Duration::from_secs(25);
super::run_rustscan_with_timeout(&["--greppable", "--no-nmap", "127.0.0.1"], timeout);
}
#[test]
#[ignore]
fn scan_google_com() {
super::run_rustscan_with_timeout(
&[
"--greppable",
"--no-nmap",
"-u",
"5000",
"-b",
"2500",
"google.com",
],
super::Duration::from_secs(28),
);
}
#[test]
#[ignore]
fn scan_example_com() {
super::run_rustscan_with_timeout(
&[
"--greppable",
"--no-nmap",
"-u",
"5000",
"-b",
"2500",
"example.com",
],
super::Duration::from_secs(28),
);
}
#[test]
#[ignore]
fn scan_rustscan_cmnatic_co_uk() {
super::run_rustscan_with_timeout(
&[
"--greppable",
"--no-nmap",
"-u",
"5000",
"-b",
"2500",
"rustscan.cmnatic.co.uk",
],
super::Duration::from_secs(26),
);
}
#[test]
#[ignore]
fn udp_scan_localhost() {
let timeout = super::Duration::from_secs(25);
super::run_rustscan_with_timeout(&["--greppable", "127.0.0.1", "--udp"], timeout);
}
#[test]
#[ignore]
fn udp_scan_google_com() {
super::run_rustscan_with_timeout(
&[
"--udp",
"--greppable",
"-u",
"5000",
"-b",
"2500",
"google.com",
],
super::Duration::from_secs(28),
);
}
#[test]
#[ignore]
fn udp_scan_example_com() {
super::run_rustscan_with_timeout(
&[
"--udp",
"--greppable",
"-u",
"5000",
"-b",
"2500",
"example.com",
],
super::Duration::from_secs(28),
);
}
#[test]
#[ignore]
fn udp_scan_rustscan_cmnatic_co_uk() {
super::run_rustscan_with_timeout(
&[
"--udp",
"--greppable",
"-u",
"5000",
"-b",
"2500",
"rustscan.cmnatic.co.uk",
],
super::Duration::from_secs(26),
);
}
}
+32
View File
@@ -0,0 +1,32 @@
use rustscan::generated::get_parsed_data;
use rustscan::scanner::build_udp_payload_lookup;
#[test]
fn udp_payload_lookup_contains_common_udp_ports() {
let udp_map = get_parsed_data();
let lookup = build_udp_payload_lookup(udp_map);
// These are common UDP services; the payload database should include them.
assert!(
lookup.contains_key(&53),
"expected UDP payload for DNS (53)"
);
assert!(
lookup.contains_key(&123),
"expected UDP payload for NTP (123)"
);
}
#[test]
fn udp_payload_lookup_payloads_are_non_empty_for_known_ports() {
let udp_map = get_parsed_data();
let lookup = build_udp_payload_lookup(udp_map);
// Don't assert exact bytes (the generated payload set may evolve),
// but it should not be empty for these well-known protocols.
let dns = lookup.get(&53).expect("missing DNS payload");
assert!(!dns.is_empty(), "DNS payload should not be empty");
let ntp = lookup.get(&123).expect("missing NTP payload");
assert!(!ntp.is_empty(), "NTP payload should not be empty");
}