Compare commits

..
Author SHA1 Message Date
Autumn (Bee) bb9f88ecbc Update Cargo.toml 2024-04-22 07:42:19 +01:00
45 changed files with 2237 additions and 5569 deletions

No files matched your search

-4
View File
@@ -7,10 +7,6 @@ assignees: ''
---
IMPORTANT: If you do not fill this out, your issue will be automatically closed.
As of November 2024, RustScan no longer offiically supports any packages other than `cargo`. If you have an issue with Docker or a `.deb`, please bring it up with whoever created it :)
**Describe the bug**
A clear and concise description of what the bug is.
+78 -187
View File
@@ -1,201 +1,92 @@
name: CD Pipeline
name: Build
on:
pull_request:
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
- master
schedule:
- cron: '00 01 * * *'
jobs:
build-nix:
env:
IN_PIPELINE: true
runs-on: ${{ matrix.os }}
check:
name: Check
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: Swatinem/rust-cache@v2
- name: Run cargo check
uses: actions-rs/cargo@v1
with:
command: check
test:
name: Test Suite
strategy:
matrix:
type: [ubuntu-x64, ubuntu-x86, armv7, aarch64]
include:
- type: ubuntu-x64
os: ubuntu-latest
target: x86_64-unknown-linux-musl
name: x86_64-linux-rustscan
path: target/x86_64-unknown-linux-musl/release/rustscan
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
- type: ubuntu-x86
os: ubuntu-latest
target: i686-unknown-linux-musl
name: x86-linux-rustscan
path: target/i686-unknown-linux-musl/release/rustscan
pkg_config_path: /usr/lib/i686-linux-gnu/pkgconfig
- type: armv7
os: ubuntu-latest
target: armv7-unknown-linux-gnueabihf
name: armv7-linux-rustscan
path: target/armv7-unknown-linux-gnueabihf/release/rustscan
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
- type: aarch64
os: ubuntu-latest
target: aarch64-unknown-linux-gnu
name: aarch64-linux-rustscan
path: target/aarch64-unknown-linux-gnu/release/rustscan
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
steps:
- uses: actions/checkout@v7
- name: Cache cargo & target directories
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Build binary
uses: houseabsolute/actions-rust-cross@21b0f18dc621b25bfae556ff2791fca4173121e8 # v1.0.8
with:
command: build
target: ${{ matrix.target }}
args: "--locked --release"
strip: true
toolchain: stable
- name: Package and upload artifacts
uses: houseabsolute/actions-rust-release@9e126fd5fc18f4cecf358b3909f3991ad59663dc # v1.0.1
with:
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@v7
- name: Install cargo-deb
run: cargo install -f cargo-deb
- uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: musl-tools # provides musl-gcc
version: 1.0
- name: Install musl toolchain
run: rustup target add x86_64-unknown-linux-musl
- name: Deb Build
run: cargo deb --target=x86_64-unknown-linux-musl
- name: Upload Deb Artifact
uses: actions/upload-artifact@v7
with:
name: rustscan.deb
path: ./target/x86_64-unknown-linux-musl/debian/*
build-macos:
env:
IN_PIPELINE: true
runs-on: macos-latest
steps:
- uses: actions/checkout@v7
- name: Cache cargo & target directories
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Build binary
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: Package and upload artifacts
uses: houseabsolute/actions-rust-release@9e126fd5fc18f4cecf358b3909f3991ad59663dc # v1.0.1
with:
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
steps:
- uses: actions/checkout@v7
- name: Cache cargo & target directories
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Build binary
uses: houseabsolute/actions-rust-cross@21b0f18dc621b25bfae556ff2791fca4173121e8 # v1.0.8
with:
command: build
target: aarch64-apple-darwin
args: "--locked --release"
strip: true
toolchain: stable
- name: Package and upload artifacts
uses: houseabsolute/actions-rust-release@9e126fd5fc18f4cecf358b3909f3991ad59663dc # v1.0.1
with:
executable-name: rustscan
target: aarch64-apple-darwin
archive-name: aarch64-macos-rustscan
changes-file: ""
build-windows:
env:
IN_PIPELINE: true
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable]
runs-on: ${{ matrix.os }}
strategy:
matrix:
type: [windows-x64, windows-x86]
include:
- type: windows-x64
os: windows-latest
target: x86_64-pc-windows-msvc
name: x86_64-windows-rustscan
- type: windows-x86
os: windows-latest
target: i686-pc-windows-msvc
name: x86-windows-rustscan
steps:
- uses: actions/checkout@v7
- name: Cache cargo & target directories
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Build binary
uses: houseabsolute/actions-rust-cross@21b0f18dc621b25bfae556ff2791fca4173121e8 # v1.0.8
with:
command: build
target: ${{ matrix.target }}
args: "--locked --release"
strip: true
toolchain: stable
- name: Package and upload artifacts
uses: houseabsolute/actions-rust-release@9e126fd5fc18f4cecf358b3909f3991ad59663dc # v1.0.1
with:
executable-name: rustscan
target: ${{ matrix.target }}
archive-name: ${{ matrix.name }}
changes-file: ""
- name: Checkout sources
uses: actions/checkout@v4
# 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
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
override: true
- uses: Swatinem/rust-cache@v2
- name: Run cargo test
uses: actions-rs/cargo@v1
with:
command: test
lints:
name: Lints
runs-on: ubuntu-latest
permissions:
contents: write # create the release / upload release assets
steps:
- name: Download build artifacts
uses: actions/download-artifact@v8
- name: Checkout sources
uses: actions/checkout@v4
with:
merge-multiple: true
- name: Publish artifacts to GitHub release
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
submodules: true
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
fail_on_unmatched_files: true
files: |
*.deb
*.sha256
*.tar.gz
*.zip
profile: minimal
toolchain: stable
override: true
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- name: Run cargo fmt
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- name: Run cargo clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: -- -D warnings
- name: Run rustdoc lints
uses: actions-rs/cargo@v1
with:
command: doc
args: --workspace --all-features --no-deps --document-private-items
+38
View File
@@ -0,0 +1,38 @@
name: Publish Docker image
on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'
release:
types: [published]
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Log in to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
password: ${{ secrets.DOCKERHUB_USERNAME }}
username: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: rustscan/rustscan
flavor: latest=true
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: ${{ contains(fromJson('["push", "release"]'), github.event_name) }} # Publish to docker registry only on push event or new release.
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+19
View File
@@ -0,0 +1,19 @@
name: Check Markdown links
on:
push:
branches:
- master
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'
+245
View File
@@ -0,0 +1,245 @@
name: Release
on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'
pull_request:
branches:
- master
paths:
- 'Cargo.toml'
- 'Cargo.lock'
- '.github/workflows/release.yml'
env:
BIN_NAME: rustscan
PROJECT_NAME: rustscan
REPO_NAME: RustScan/RustScan
# BREW_TAP: RustScan/homebrew-tap
jobs:
dist:
name: Dist
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false # don't fail other jobs if one fails
matrix:
build: [x86_64-linux, x86_64-macos, x86_64-windows] #, x86_64-win-gnu, win32-msvc
include:
- build: x86_64-linux
os: ubuntu-20.04
rust: stable
target: x86_64-unknown-linux-gnu
cross: false
- build: x86_64-macos
os: macos-latest
rust: stable
target: x86_64-apple-darwin
cross: false
- build: x86_64-windows
os: windows-2019
rust: stable
target: x86_64-pc-windows-msvc
cross: false
# - build: aarch64-linux
# os: ubuntu-20.04
# rust: stable
# target: aarch64-unknown-linux-gnu
# - build: aarch64-macos
# os: macos-latest
# rust: stable
# target: aarch64-apple-darwin
# - build: x86_64-win-gnu
# os: windows-2019
# rust: stable-x86_64-gnu
# target: x86_64-pc-windows-gnu
# - build: win32-msvc
# os: windows-2019
# rust: stable
# target: i686-pc-windows-msvc
steps:
- name: Set Git config. (windows only)
if: contains(matrix.build, 'windows') # Windows is missing some dependencies
run: |
git config --global pack.windowMemory "100m"
git config --global pack.packSizeLimit "100m"
git config --global pack.threads "1"
git config --global pack.deltaCacheSize "512m"
- name: Install python 3.10 for windows
if: contains(matrix.build, 'windows') # Windows is missing some dependencies
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Checkout sources
uses: actions/checkout@v4
with:
submodules: true
- name: Install ${{ matrix.rust }} toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
override: true
- name: Run cargo test
uses: actions-rs/cargo@v1
with:
use-cross: ${{ matrix.cross }}
command: test
args: --release --locked --target ${{ matrix.target }}
- name: Build release binary
uses: actions-rs/cargo@v1
with:
use-cross: ${{ matrix.cross }}
command: build
args: --release --locked --target ${{ matrix.target }}
- name: Calculate tag name
if: contains(matrix.build, 'linux')
run: |
name=dev
if [[ ${GITHUB_REF} =~ refs/tags/[0-9]+.[0-9]+.[0-9]+ ]]; then
name=${GITHUB_REF#refs/tags/}
fi
echo ::set-output name=val::$name
echo TAG=$name >> $GITHUB_ENV
id: tagname
- name: Build deb package (linux only)
if: contains(matrix.build, 'linux')
run: |
cargo install cargo-deb
cargo deb --target ${{ matrix.target }} --deb-version ${TAG}
- name: Build archive
shell: bash
run: |
mkdir dist
if [[ "${{ matrix.build }}" =~ "windows" ]]; then
cp "target/${{ matrix.target }}/release/$BIN_NAME.exe" "dist/"
else
cp "target/${{ matrix.target }}/release/$BIN_NAME" "dist/"
fi
if [[ "${{ matrix.build }}" =~ "linux" ]]; then
cp "target/${{ matrix.target }}/debian/rustscan_${TAG}_amd64.deb" "dist/"
fi
- uses: actions/[email protected]
with:
name: bins-${{ matrix.build }}
path: dist
publish:
name: Publish
needs: [dist]
if: contains(fromJson('["push", "schedule"]'), github.event_name) && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
with:
submodules: false
- uses: actions/download-artifact@v4
# with:
# path: dist
# - run: ls -al ./dist
- run: ls -al bins-*
- name: Calculate tag name
run: |
name=dev
if [[ ${GITHUB_REF} =~ refs/tags/[0-9]+.[0-9]+.[0-9]+ ]]; then
name=${GITHUB_REF#refs/tags/}
fi
echo ::set-output name=val::$name
echo TAG=$name >> $GITHUB_ENV
id: tagname
- name: Build archive
shell: bash
run: |
set -ex
rm -rf tmp
mkdir tmp
mkdir dist
for dir in bins-* ; do
platform=${dir#"bins-"}
unset exe
if [[ $platform =~ "windows" ]]; then
exe=".exe"
fi
pkgname=$PROJECT_NAME-$TAG-$platform
mkdir tmp/$pkgname
# cp LICENSE README.md tmp/$pkgname
mv bins-$platform/$BIN_NAME$exe tmp/$pkgname
chmod +x tmp/$pkgname/$BIN_NAME$exe
if [[ $platform =~ "linux" ]]; then
mv "bins-$platform/rustscan_${TAG}_amd64.deb" dist/
fi
if [ "$exe" = "" ]; then
tar cJf dist/$pkgname.tar.xz -C tmp $pkgname
else
(cd tmp && 7z a -r ../dist/$pkgname.zip $pkgname)
fi
done
- name: Upload binaries to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: dist/*
file_glob: true
tag: ${{ steps.tagname.outputs.val }}
overwrite: true
- name: Extract version
id: extract-version
run: |
printf "::set-output name=%s::%s\n" tag-name "${GITHUB_REF#refs/tags/}"
- uses: mislav/bump-homebrew-formula-action@v3
with:
formula-path: ${{env.PROJECT_NAME}}.rb
homebrew-tap: ${{ env.BREW_TAP }}
download-url: 'https://github.com/${{ env.REPO_NAME }}/releases/download/${{ steps.extract-version.outputs.tag-name }}/${{env.PROJECT_NAME}}-${{ steps.extract-version.outputs.tag-name }}-x86_64-macos.tar.xz'
commit-message: updating formula for ${{ env.PROJECT_NAME }}
env:
COMMITTER_TOKEN: ${{ secrets.COMMITTER_TOKEN }}
#
# you can use this initial file in your homebrew-tap if you don't have an initial formula:
# <projectname>.rb
#
# class <Projectname capitalized> < Formula
# desc "A test formula"
# homepage "http://www.example.com"
# url "-----"
# version "-----"
# sha256 "-----"
# def install
# bin.install "<bin-name>"
# end
# end
# Uncomment this section if you want to release your package to crates.io
# Before publishing, make sure you have filled out the following fields:
# license or license-file, description, homepage, documentation, repository, readme.
# Read more: https://doc.rust-lang.org/cargo/reference/publishing.html
- name: Install ${{ matrix.rust }} toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
- run: cargo publish --token ${CRATES_TOKEN}
env:
CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }}
-45
View File
@@ -1,45 +0,0 @@
name: Test
on:
pull_request:
branches: [master]
push:
branches:
- master
permissions:
contents: read
jobs:
test:
name: Test Suite
strategy:
matrix:
# 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@v7
- name: Setup Rust
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
with:
toolchain: ${{ matrix.rust }}
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@v4
- name: Run tests
run: just test
+1 -3
View File
@@ -1,8 +1,6 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# generated file by build.rs for mapping nmap-payloads to rust
src/generated.rs
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
@@ -123,4 +121,4 @@ fabric.properties
.idea
.idea*
settings.json
settings.json
+5
View File
@@ -17,7 +17,12 @@ 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
+817 -1469
View File
File diff suppressed because it is too large. Load diff
+18 -29
View File
@@ -1,15 +1,15 @@
[package]
name = "rustscan"
version = "2.4.1"
version = "2.2.2"
authors = ["Autumn <[email protected]>"]
edition = "2018"
description = "Faster Nmap Scanning with Rust"
homepage = "https://github.com/rustscan/rustscan"
repository = "https://github.com/rustscan/rustscan"
license = "GPL-3.0-only"
license = "MIT"
keywords = ["port", "scanning", "nmap"]
categories = ["command-line-utilities"]
readme = "README.md"
readme="README.md"
exclude = [
".github/*",
"pictures/*",
@@ -17,34 +17,30 @@ exclude = [
]
[dependencies]
clap = { version = "4.6.1", features = ["derive", "wrap_help"] }
colored = "3.1.1"
tokio = { version = "1.53.1", features = ["rt", "net", "time"] }
colored = "2.1.0"
structopt = "0.3.20"
async-std = "1.7.0"
futures = "0.3"
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"
rlimit = "0.8.3"
log = "0.4.0"
env_logger = "0.8.2"
dirs = "3.0.1"
gcd = "2.0.1"
rand = "0.10.1"
colorful = "0.3.2"
rand = "0.7.3"
colorful = "0.2.1"
ansi_term = "0.12.1"
toml = "1.1.2"
toml = "0.5.7"
serde = "1.0.124"
serde_derive = "1.0.116"
cidr-utils = "0.7.1"
itertools = "0.15.0"
hickory-resolver = { version = "0.24.3", features = ["dns-over-rustls"] }
cidr-utils = "0.5.1"
itertools = "0.9.0"
trust-dns-resolver = { version = "0.23.2", features = ["dns-over-rustls"] }
anyhow = "1.0.40"
subprocess = "0.2.6"
text_placeholder = { version = "0.5", features = ["struct_context"] }
once_cell = "1.21.4"
[dev-dependencies]
tokio = { version = "1.53.1", features = ["test-util"] }
parameterized = "2.0.0"
criterion = { version = "0.8", features = ["html_reports"] }
wait-timeout = "0.2"
[package.metadata.deb]
depends = "$auto, nmap"
@@ -59,10 +55,3 @@ strip = true
[[bin]]
name = "rustscan"
path = "src/main.rs"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(tarpaulin_include)"] }
[[bench]]
name = "benchmark_helpers"
harness = false
+25
View File
@@ -0,0 +1,25 @@
# Build Stage
FROM rust:alpine as builder
LABEL maintainer="RustScan <https://github.com/RustScan>"
RUN apk add --no-cache build-base
# Encourage some layer caching here rather then copying entire directory that includes docs to builder container ~CMN
WORKDIR /app/rustscan
COPY Cargo.toml Cargo.lock ./
COPY src/ src/
RUN cargo build --release
# Release Stage
FROM alpine:3.19.1 as release
LABEL author="Hydragyrum <https://github.com/Hydragyrum>"
LABEL author="LeoFVO <https://github.com/LeoFVO>"
RUN addgroup -S rustscan && \
adduser -S -G rustscan rustscan && \
ulimit -n 100000 && \
apk add --no-cache nmap nmap-scripts wget ca-certificates bind-tools
USER rustscan
COPY --from=builder /app/rustscan/target/release/rustscan /usr/local/bin/rustscan
ENTRYPOINT [ "/usr/local/bin/rustscan" ]
+31 -33
View File
@@ -9,6 +9,16 @@
![Arch Linux package][badge-1] ![Built with Rust][badge-2] ![GitHub All Releases][badge-3] ![Crates.io][badge-4] ![Discord][badge-5] ![Actions][badge-6]
---
| 🐋 `(Recommended)` | 👩‍💻 | 🏗️ | 🔧 |
| :---------------------------------------------------------: | :---------------------------: | :-------------------------: | :-------------------------: |
| [![Docker][DockerPic]][Docker] | [![Kali][Kali1]][Kali/Debian] | [![Arch][Arch]][Arch-Linux] | [![apple][apple]][Homebrew] |
| `docker pull rustscan/rustscan:2.1.1`</br>[Docker][usage-0] | [Link to Documentation][kali] | `yay -S rustscan` | `brew install rustscan` |
---
</div>
# 🤔 What is this?
@@ -17,32 +27,6 @@
The Modern Port Scanner. **Find ports quickly (3 seconds at its fastest)**. Run scripts through our scripting engine (Python, Lua, Shell supported).
# 🛠️ Installation
You can install RustScan's binary from our [releases page](https://github.com/RustScan/RustScan/releases).
We would prefer you to install with a package manager so it is tested and works for your system.
RustScan is in many repositories already. Install it with whatever tools you wish:
[![Packaging status](https://repology.org/badge/vertical-allrepos/rustscan.svg)](https://repology.org/project/rustscan/versions)
RustScan only officially supports Cargo installations, if you want to use that please install Rust and then `cargo install rustscan`
Example installations include:
MacOS:
```
brew install rustscan
```
Arch:
```
pacman -S rustscan
```
# ✨ Features
- Scans all 65k ports in **3 seconds**.
@@ -55,16 +39,15 @@ Arch:
| <!--Installation Guide--> | <!--Documentation--> | <!--Discord--> |
| :----------------------------------------: | :------------------------------------: | :-------------------------: |
| :book: [Installation Guide][toc-install] | :books: [Documentation][links-table-2] | :parrot: [Discord][discord] |
| | :penguin: [ARM Support](docs/arm-support.md) | |
| :book: [Installation Guide][links-table-1] | :books: [Documentation][links-table-2] | :parrot: [Discord][discord] |
## 🙋 Table of Contents
- 📖 [Installation Guide][toc-install]
- 🐋 [Docker Usage][toc-docker-usage]
- 🐧 [ARM Support](docs/arm-support.md)
- 🦜 [Discord][discord]
- 🤸 [Usage][usage-1]
- 🎪 [Community][toc-community]
# 🔭 Why RustScan?
@@ -122,6 +105,21 @@ RustScan is one of the first penetration testing tools that aims to be entirely
RustScan has continuous integration testing that aims to ensure it is accessible, and we are constantly working on ways to improve our accessibility and ensure _everyone_ can use RustScan.
# 📖 Full Installation Guide
You can find our guide [here][install-1].
## 🦊 Community Distributions
Here are all of RustScan's community distributions.
If you maintain a community distribution and want it listed here, leave an issue / pull request / Discord message or however, you want to let us know.
- [Open Suse][distributions-1]
- [Fedora/CentOS][distributions-2]
[![Packaging status][rustscan-svg]][repology-1]
# 🤸 Usage
We have 2 usage guides. [Basic Usage][usage-1] and [Things you may want to do][usage-2].
@@ -147,7 +145,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://skerritt.blog"><img src="https://avatars3.githubusercontent.com/u/10378052?v=4" width="100px;" alt=""/><br /><sub><b>Bee</b></sub></a><br /><a href="#infra-beeskerritt" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/RustScan/RustScan/commits?author=beeskerritt" title="Tests">⚠️</a> <a href="https://github.com/RustScan/RustScan/commits?author=beesan" title="Code">💻</a> <a href="#design-beeskerritt" title="Design">🎨</a></td>
<td align="center"><a href="https://skerritt.blog"><img src="https://avatars3.githubusercontent.com/u/10378052?v=4" width="100px;" alt=""/><br /><sub><b>Brandon</b></sub></a><br /><a href="#infra-brandonskerritt" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/RustScan/RustScan/commits?author=brandonskerritt" title="Tests">⚠️</a> <a href="https://github.com/RustScan/RustScan/commits?author=brandonskerritt" title="Code">💻</a> <a href="#design-brandonskerritt" title="Design">🎨</a></td>
<td align="center"><a href="https://sakiir.ovh"><img src="https://avatars1.githubusercontent.com/u/9950578?v=4" width="100px;" alt=""/><br /><sub><b>SakiiR</b></sub></a><br /><a href="https://github.com/RustScan/RustScan/commits?author=SakiiR" title="Code">💻</a> <a href="https://github.com/RustScan/RustScan/issues?q=author%3ASakiiR" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/smackhack"><img src="https://avatars2.githubusercontent.com/u/48143394?v=4" width="100px;" alt=""/><br /><sub><b>smackhack</b></sub></a><br /><a href="#ideas-smackhack" title="Ideas, Planning, & Feedback">🤔</a> <a href="#example-smackhack" title="Examples">💡</a></td>
<td align="center"><a href="http://bernardoamc.github.io/"><img src="https://avatars0.githubusercontent.com/u/428984?v=4" width="100px;" alt=""/><br /><sub><b>Bernardo Araujo</b></sub></a><br /><a href="https://github.com/RustScan/RustScan/commits?author=bernardoamc" title="Code">💻</a> <a href="https://github.com/RustScan/RustScan/issues?q=author%3Abernardoamc" title="Bug reports">🐛</a> <a href="#design-bernardoamc" title="Design">🎨</a></td>
@@ -195,7 +193,7 @@ This project follows the [all-contributors](https://github.com/all-contributors/
[kali]: https://github.com/RustScan/RustScan/wiki/Installation-Guide#%EF%B8%8F-debian--kali "Read the install guide"
[Kali/Debian]: https://github.com/RustScan/RustScan/releases "Kali Debian"
[Arch-Linux]: https://archlinux.org/packages/extra/x86_64/rustscan/ "Arch Linux installation of Rustscan"
[Homebrew]: https://formulae.brew.sh/formula/rustscan "Homebrew install of Rustscan"
[Homebrew]: https://crates.io/crates/rustscan/ "Homebrew install of Rustscan"
[usage-1]: https://github.com/RustScan/RustScan/wiki/Usage "Basic Usage of Rustscan"
[usage-0]: https://github.com/RustScan/RustScan/wiki/Installation-Guide#docker-whale "Use Docker Rustscan"
[config-file-here]: https://github.com/RustScan/RustScan/wiki/Config-File "RustScan Configuration File"
@@ -211,7 +209,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://github.com/bee-san/RustScan/wiki "Rustscan"
[links-table-2]: https://rustscan.github.io/RustScan/ "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"
@@ -234,4 +232,4 @@ This project follows the [all-contributors](https://github.com/all-contributors/
[badge-3]: https://img.shields.io/github/downloads/rustscan/rustscan/total?label=GitHub%20Downloads
[badge-4]: https://img.shields.io/crates/d/rustscan?label=Cargo%20Downloads
[badge-5]: https://img.shields.io/discord/754001738184392704
[badge-6]: https://github.com/RustScan/RustScan/actions/workflows/build.yml/badge.svg?branch=master
[badge-6]: https://github.com/RustScan/RustScan/workflows/Continuous%20integration/badge.svg?branch=master
-140
View File
@@ -1,140 +0,0 @@
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);
-306
View File
@@ -1,306 +0,0 @@
use std::collections::BTreeMap;
use std::fs::{self, File};
use std::env;
use std::io::{BufReader, Read};
use std::path::PathBuf;
use std::process::Command;
// Reads in a file with payloads based on port
pub fn main() {
let mut file_path = env::current_dir().expect("cant find curr dir");
file_path.push("./nmap-payloads");
let mut data = String::new();
let file = File::open(&file_path).expect("File not found.");
let mut file_buf = BufReader::new(file);
file_buf
.read_to_string(&mut data)
.expect("unable to read file");
let mut fp_map: BTreeMap<i32, String> = BTreeMap::new();
let mut count = 0;
let mut capturing = false;
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;
}
if line.starts_with("udp") {
if !curr.is_empty() {
fp_map.insert(count, curr);
curr = String::new();
}
capturing = true;
count += 1;
}
if capturing {
if !curr.is_empty() {
curr.push(' ');
}
curr.push_str(line);
}
}
let pb_linenr = ports_v(&fp_map);
let payb_linenr = payloads_v(&fp_map);
let map = port_payload_map(pb_linenr, payb_linenr);
generate_code(map);
}
/// Generates a file called Generated.rs and calls cargo fmt from the command line
///
/// # Arguments
///
/// * `port_payload_map` - A BTreeMap mapping port numbers to payload data
fn generate_code(port_payload_map: BTreeMap<Vec<u16>, Vec<u8>>) {
let dest_path = PathBuf::from("src/generated.rs");
let mut generated_code = String::new();
generated_code.push_str("use std::collections::BTreeMap;\n");
generated_code.push_str("use once_cell::sync::Lazy;\n\n");
generated_code.push_str("fn generated_data() -> BTreeMap<Vec<u16>, Vec<u8>> {\n");
generated_code.push_str(" let mut map = BTreeMap::new();\n");
for (ports, payloads) in port_payload_map {
generated_code.push_str(" map.insert(vec![");
generated_code.push_str(
&ports
.iter()
.map(|&p| p.to_string())
.collect::<Vec<_>>()
.join(","),
);
generated_code.push_str("], vec![");
generated_code.push_str(
&payloads
.iter()
.map(|&p| p.to_string())
.collect::<Vec<_>>()
.join(","),
);
generated_code.push_str("]);\n");
}
generated_code.push_str(" map\n");
generated_code.push_str("}\n\n");
generated_code.push_str(
"static PARSED_DATA: Lazy<BTreeMap<Vec<u16>, Vec<u8>>> = Lazy::new(generated_data);\n",
);
generated_code.push_str("pub fn get_parsed_data() -> &'static BTreeMap<Vec<u16>, Vec<u8>> {\n");
generated_code.push_str(" &PARSED_DATA\n");
generated_code.push_str("}\n");
fs::write(dest_path, generated_code).unwrap();
// format the generated code
Command::new("cargo")
.arg("fmt")
.arg("--all")
.output()
.expect("Failed to execute cargo fmt");
}
/// Creates a BTreeMap of line numbers mapped to a Vec<u16> of ports
///
/// # Arguments
///
/// * `fp_map` - A BTreeMap containing the parsed file data
///
/// # Returns
///
/// A BTreeMap where keys are line numbers and values are vectors of ports
fn ports_v(fp_map: &BTreeMap<i32, String>) -> BTreeMap<i32, Vec<u16>> {
let mut pb_linenr: BTreeMap<i32, Vec<u16>> = BTreeMap::new();
let mut port_list: Vec<u16> = Vec::new();
for (&line_nr, ports) in fp_map {
if ports.contains("udp ") {
let remain = &ports[4..];
let mut start = remain.split(' ');
let ports = start.next().unwrap();
let port_segments: Vec<&str> = ports.split(',').collect();
for segment in port_segments {
if segment.contains('-') {
let range: Vec<&str> = segment.trim().split('-').collect();
let start = range[0].parse::<u16>().unwrap();
let end = range[1].parse::<u16>().unwrap();
for port in start..end {
port_list.push(port);
}
} else if !segment.is_empty() {
match segment.parse::<u16>() {
Ok(port) => port_list.push(port),
Err(_) => println!("Error parsing port: {segment}"),
}
}
}
}
pb_linenr.insert(line_nr, port_list.clone());
port_list.clear();
}
pb_linenr
}
/// Parses out the Payloads into a BTreeMap of line numbers mapped to vectors of payload bytes
///
/// # Arguments
///
/// * `fp_map` - A BTreeMap containing the parsed file data
///
/// # Returns
///
/// A BTreeMap where keys are line numbers and values are vectors of payload bytes
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");
// 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 quoted Nmap payload block to bytes.
///
/// # Arguments
///
/// * `payload` - The raw block text starting at its first opening quote:
/// one or more `"..."` segments separated by whitespace
///
/// # Returns
///
/// 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 mut bytes: Vec<u8> = Vec::new();
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;
}
}
}
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
///
/// * `pb_linenr` - A BTreeMap mapping line numbers to vectors of ports
/// * `payb_linenr` - A BTreeMap mapping line numbers to vectors of payload bytes
///
/// # Returns
///
/// A BTreeMap mapping vectors of ports to vectors of payload bytes
fn port_payload_map(
pb_linenr: BTreeMap<i32, Vec<u16>>,
payb_linenr: BTreeMap<i32, Vec<u8>>,
) -> BTreeMap<Vec<u16>, Vec<u8>> {
let mut ppm_fin: BTreeMap<Vec<u16>, Vec<u8>> = BTreeMap::new();
for (port_linenr, ports) in pb_linenr {
for (pay_linenr, payloads) in &payb_linenr {
if pay_linenr == &port_linenr {
ppm_fin.insert(ports.to_vec(), payloads.to_vec());
}
}
}
ppm_fin
}
+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 rustfmt for code formatting and clippy for linting.
RUN rustup component add rustfmt clippy
# 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
+2 -7
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 can check the command-line interface without starting a scan:
You are now ready to use RustScan:
```bash
root@container:/rustscan# cargo run -- --help
root@container:/rustscan# cargo run -- -b 2000 -t 5000 -a 127.0.0.1
```
You can also format, lint with `clippy` and test the code with the following commands:
@@ -67,8 +67,3 @@ 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
@@ -1,38 +0,0 @@
# 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
@@ -1,49 +0,0 @@
--- 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
@@ -1,25 +0,0 @@
{"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
@@ -1,229 +0,0 @@
# 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
@@ -1,104 +0,0 @@
# 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
@@ -1,4 +0,0 @@
tags = ["core_approved", "example"]
ports = ["80", "443", "8080"]
developer = ["example", "https://example.org"]
directory = "fixtures/.rustscan_scripts"
+23
View File
@@ -0,0 +1,23 @@
#!/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
@@ -0,0 +1,13 @@
#!/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
@@ -0,0 +1,12 @@
#!/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 $@
+5 -3
View File
@@ -1,7 +1,9 @@
Metadata-only test fixture; not executable.
#!intentional_blank_line
#tags = ["core_approved", "example"]
#developer = [ "example", "https://example.org" ]
#ports_separator = ","
#call_format = "fixture {{ip}} {{port}}"
#call_format = "nmap -vvv -p {{port}} {{ip}}"
The parser reads only the metadata above. This file contains no script body.
# 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"
@@ -1,7 +1,7 @@
Metadata-only test fixture; not executable.
#!intentional_blank_line
#tags = "core_approved"
#developer = [ "example", "https://example.org" ]
#ports_separator = ","
#call_format = "fixture {{ip}} {{port}}"
#call_format = "nmap -vvv -p {{port}} {{ip}}"
# 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
@@ -0,0 +1,6 @@
127.0.0.1
google.com
example.com
66666666666666.666666666666666666.66666666666666666.6666666666
....
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.radiatorFixtures
-18
View File
@@ -1,18 +0,0 @@
test:
cargo test
cargo clippy -- --deny warnings
cargo clippy --tests -- --deny warnings
cargo fmt --check
cargo doc --workspace --all-features --no-deps --document-private-items
fix:
cargo fmt
cargo clippy --fix
# release only runs if test runs
release: test
git checkout master
git pull
cargo generate-lockfile
cargo publish
git push
-500
View File
@@ -1,500 +0,0 @@
# Nmap nmap payload database -*- mode: fundamental; -*-
# $Id$
#
# These payloads are sent with every host discovery or port scan probe
# by default. This database should only include payloads that are
# unlikely to crash services, trip IDS alerts, or change state on the
# server. The idea behind these is to evoke a response using a payload.
# Some of them are taken from nmap-service-probes.
#
# This collection of data is (C) 1996-2022 by Nmap Software LLC. It
# is distributed under the Nmap Public Source license as provided in
# the LICENSE file of the source distribution or at
# https://nmap.org/npsl/. Note that this free license does not allow
# incorporation of Nmap or its data files within proprietary
# software. We sell a separate Nmap OEM for that as described
# (including pricing) at https://nmap.org/npsl/.
#
# Each entry begins with a protocol (only "udp" is supported) followed
# by a comma-separated list of ports, followed by one or more quoted
# strings containing the payload. These elements may be broken across
# several lines. For future expansion, additional keywords may follow
# the payload data. Any data following one of these keywords must be on
# the same line as the keyword so that unknown keywords can be ignored
# by the parser. Currently this file contains some entries with the
# "source" keyword to specify a desired source port, but it is not
# honored by Nmap.
#
# Multiple payloads may be defined for a single protocol and port, in
# which case they will be all be sent concurrently. There is a limit
# of 255 payloads per port.
#
# Lines longer than 1024 characters will be ignored.
#
# Example:
# udp 1234 "payloaddatapayloaddata"
# "payloaddatapayloaddata"
# source 5678
# GenericLines. Use for the echo service.
udp 7 "\x0D\x0A\x0D\x0A"
# DNSStatusRequest
# Sent for TFTP (69) as well due to its ability to provoke an error response
udp 53,69,5353,26198 "\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00"
# DNS VER
udp 53,5353,26198
"\x77\x77\x01\x00\x00"
"\x01\x00\x00\x00\x00\x00\x00\x07version\x04bind\x00\x00\x10\x00\x03"
# DHCP INFORM
udp 67
"\x01\x01\x06\x00"
"\x01\x23\x45\x67\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x35\xd4\xd8\x51\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x63\x82\x53\x63\x35\x01"
"\x08\xff"
# TFTP GET
udp 69 "\x00\x01r7tftp.txt\x00octet\x00"
# QUIC packet with unsupported version Q999
# Also found on 443, but need to check whether DTLS or QUIC is more prevalent
udp 80 "\r12345678Q999\x00"
# RPCCheck
udp 111
"\x72\xFE\x1D\x13\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\x86\xA0"
"\x00\x01\x97\x7C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00"
# ONCRPC CALL
udp 111,2049,4045,32768-65535
"\x3e\xec\xe3\xca\x00\x00\x00\x00\x00\x00\x00\x02\x00"
"\xbc\x61\x4e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
# NTPRequest
udp 123
"\xE3\x00\x04\xFA\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\xC5\x4F\x23\x4B\x71\xB1\x52\xF3"
# NTP REQ
udp 123
"\xd9\x00\x0a\xfa\x00\x00\x00"
"\x00\x00\x01\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc6"
"\xf1\x5e\xdb\x78\x00\x00\x00"
# DCERPC CALL
udp 135,1025-1199
"\x05\x00\x0b\x03\x10\x00\x00\x00\x48\x00"
"\x00\x00\x01\x00\x00\x00\xb8\x10\xb8\x10\x00\x00\x00\x00\x01\x00\x00"
"\x00\x00\x00\x01\x00\x01\x23\x45\x67\x89\xab\xcd\xef\x01\x23\x45\x67"
"\x89\xab\xcd\xef\xe7\x03\x00\x00\xfe\xdc\xba\x98\x76\x54\x32\x10\x01"
"\x23\x45\x67\x89\xab\xcd\xef\xe7\x03\x00\x00"
# NBTStat
udp 137
"\x80\xF0\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00"
"\x20CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00\x21\x00\x01"
# CIFS NS NAME QUERY UC
udp 137
"\x01\x91\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00"
"\x20CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00\x21\x00\x01"
# CIFS NS NAME QUERY BC
udp 137
"\x01\x91\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00"
"\x20CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00\x21\x00\x01"
# SNMPv3GetRequest
udp 161,260,3401
"\x30\x3A\x02\x01\x03\x30\x0F\x02\x02\x4A\x69\x02\x03\x00\xFF\xE3"
"\x04\x01\x04\x02\x01\x03\x04\x10\x30\x0E\x04\x00\x02\x01\x00\x02"
"\x01\x00\x04\x00\x04\x00\x04\x00\x30\x12\x04\x00\x04\x00\xA0\x0C"
"\x02\x02\x37\xF0\x02\x01\x00\x02\x01\x00\x30\x00"
# SNMP PUBLIC WALK
udp 161,260,3401
"\x30\x1f\x02\x01\x00\x04\x06public\xa1\x12\x02\x01\x00\x02"
"\x01\x00\x02\x01\x00\x30\x07\x30\x05\x06\x01\x00\x05\x00"
# Sqlping - disabled because it trips a Snort rule with SID 2049
# ("MS-SQL ping attempt").
# udp 1434 "\x02"
# xdmcp - X Display Manager Control Protocol. Version 1, packet type
# Query (2), no authorization names. We expect a Willing or Unwilling
# packet in reply.
# http://cgit.freedesktop.org/xorg/doc/xorg-docs/plain/hardcopy/XDMCP/xdmcp.PS.gz
udp 177 "\x00\x01\x00\x02\x00\x01\x00"
# Connectionless LDAP - used by Microsoft Active Directory
udp 389
"\x30\x84\x00\x00\x00\x2d\x02\x01\x07\x63\x84\x00\x00\x00\x24\x04\x00"
"\x0a\x01\x00\x0a\x01\x00\x02\x01\x00\x02\x01\x64\x01\x01\x00\x87\x0b"
"objectClass0\x84\x00\x00\x00\x00"
# svrloc
udp 427
"\x02\x01\x00\x006 \x00\x00\x00\x00\x00\x01\x00\x02en\x00\x00\x00\x15"
"service:service-agent\x00\x07default\x00\x00\x00\x00"
# DTLS
udp 443,853,3391,4433,4740,5349,5684,5868,6514,6636,8232,10161,10162,12346,12446,12546,12646,12746,12846,12946,13046
# DTLS 1.0, length 52
"\x16\xfe\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x36"
# ClientHello, length 40, sequence 0, offset 0
"\x01\x00\x00\x2a\x00\x00\x00\x00\x00\x00\x00\x2a"
# DTLS 1.2
"\xfe\xfd"
# Random
"\x00\x00\x00\x00\x7c\x77\x40\x1e\x8a\xc8\x22\xa0\xa0\x18\xff\x93"
"\x08\xca\xac\x0a\x64\x2f\xc9\x22\x64\xbc\x08\xa8\x16\x89\x19\x3f"
# Session id length 0, cookie length 0
"\x00\x00"
# Cipher suites, mandatory TLS_RSA_WITH_AES_128_CBC_SHA
"\x00\x02\x00\x2f"
# Compressors (NULL)
"\x01\x00"
# Internet Key Exchange version 1, phase 1 Main Mode. We offer every
# combination of (DES, 3DES) and (MD5, SHA) in the hope that one of them will
# be acceptable. Because we use a fixed cookie, we set the association lifetime
# to 1 second to reduce the chance that repeated probes will look like
# retransmissions (and therefore not get a response). This payload comes from
# ike-scan --lifetime 1 --cookie 0011223344556677 --trans=5,2,1,2 --trans=5,1,1,2 --trans=1,2,1,2 --trans=1,1,1,2
# We expect another phase 1 message in response. This payload works better with
# a source port of 500 or a randomized initiator cookie.
udp 500
# Initiator cookie 0x0011223344556677, responder cookie 0x0000000000000000.
"\x00\x11\x22\x33\x44\x55\x66\x77\x00\x00\x00\x00\x00\x00\x00\x00"
# Version 1, Main Mode, flags 0x00, message ID 0x00000000, length 192.
"\x01\x10\x02\x00\x00\x00\x00\x00\x00\x00\x00\xC0"
# Security Association payload, length 164, IPSEC, IDENTITY.
"\x00\x00\x00\xA4\x00\x00\x00\x01\x00\x00\x00\x01"
# Proposal 1, length 152, ISAKMP, 4 transforms.
"\x00\x00\x00\x98\x01\x01\x00\x04"
# Transform 1, 3DES-CBC, SHA, PSK, group 2.
"\x03\x00\x00\x24\x01\x01\x00\x00\x80\x01\x00\x05\x80\x02\x00\x02"
"\x80\x03\x00\x01\x80\x04\x00\x02"
"\x80\x0B\x00\x01\x00\x0C\x00\x04\x00\x00\x00\x01"
# Transform 2, 3DES-CBC, MD5, PSK, group 2.
"\x03\x00\x00\x24\x02\x01\x00\x00\x80\x01\x00\x05\x80\x02\x00\x01"
"\x80\x03\x00\x01\x80\x04\x00\x02"
"\x80\x0B\x00\x01\x00\x0C\x00\x04\x00\x00\x00\x01"
# Transform 3, DES-CBC, SHA, PSK, group 2.
"\x03\x00\x00\x24\x03\x01\x00\x00\x80\x01\x00\x01\x80\x02\x00\x02"
"\x80\x03\x00\x01\x80\x04\x00\x02"
"\x80\x0B\x00\x01\x00\x0C\x00\x04\x00\x00\x00\x01"
# Transform 4, DES-CBC, MD5, PSK, group 2.
"\x00\x00\x00\x24\x04\x01\x00\x00\x80\x01\x00\x01\x80\x02\x00\x01"
"\x80\x03\x00\x01\x80\x04\x00\x02"
"\x80\x0B\x00\x01\x00\x0C\x00\x04\x00\x00\x00\x01"
# IPSEC START
udp 500,4500
"\x31\x27\xfc"
"\xb0\x38\x10\x9e\x89\x00\x00\x00\x00\x00\x00\x00\x00\x01\x10\x02\x00"
"\x00\x00\x00\x00\x00\x00\x00\xcc\x0d\x00\x00\x5c\x00\x00\x00\x01\x00"
"\x00\x00\x01\x00\x00\x00\x50\x01\x01\x00\x02\x03\x00\x00\x24\x01\x01"
"\x00\x00\x80\x01\x00\x05\x80\x02\x00\x02\x80\x04\x00\x02\x80\x03\x00"
"\x03\x80\x0b\x00\x01\x00\x0c\x00\x04\x00\x00\x0e\x10\x00\x00\x00\x24"
"\x02\x01\x00\x00\x80\x01\x00\x05\x80\x02\x00\x01\x80\x04\x00\x02\x80"
"\x03\x00\x03\x80\x0b\x00\x01\x00\x0c\x00\x04\x00\x00\x0e\x10\x0d\x00"
"\x00\x18\x1e\x2b\x51\x69\x05\x99\x1c\x7d\x7c\x96\xfc\xbf\xb5\x87\xe4"
"\x61\x00\x00\x00\x04\x0d\x00\x00\x14\x40\x48\xb7\xd5\x6e\xbc\xe8\x85"
"\x25\xe7\xde\x7f\x00\xd6\xc2\xd3\x0d\x00\x00\x14\x90\xcb\x80\x91\x3e"
"\xbb\x69\x6e\x08\x63\x81\xb5\xec\x42\x7b\x1f\x00\x00\x00\x14\x26\x24"
"\x4d\x38\xed\xdb\x61\xb3\x17\x2a\x36\xe3\xd0\xcf\xb8\x19"
# Routing Information Protocol version 1. Special-case request for the entire
# routing table (address family 0, address 0.0.0.0, metric 16). RFC 1058,
# section 3.4.1.
udp 520
"\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x10"
# RMCP ASF ping
udp 623
"\x06\x00\xff\x06" # RMCP version 6, sequence 0xff, normal RMCP class ASF
"\x00\x00\x11\xbe" # IAN ASF code 4542
"\x80\x00\x00\x00" # payload-less ASF presence ping
# IPMI
# RMCP Get Channel Auth Capabilities
udp 623
"\x06\x00\xff\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x20\x18"
"\xc8\x81\x00\x38\x8e\x04\xb5"
# serialnumberd. This service runs on Mac OS X Server. This probe
# requests the serial number of another server. In response we expect a
# packet starting with "SNRESPS:", followed by some data whose purpose
# is not known.
udp 626 "SNQUERY: 127.0.0.1:AAAAAA:xsvr"
# OpenVPN P_CONTROL_HARD_RESET_CLIENT_V2
# Byte 0; 0x38 opcode
# Byte 1-8: Session ID, random
# Byte 9: Message packet-id array length (0)
# Byte 10-13: Message packet-id (0)
udp 1194 "8d\xc1x\x01\xb8\x9b\xcb\x8f\0\0\0\0\0"
# OpenVPN when in PKI mode and without the "HMAC Firewall" setting enabled
# (tls-auth) should respond to the following probe, which is
# 0x38<8 random bytes><4 null bytes>
udp 1194
"\x38\x01\x02\x03\x04\x05\x06\x07\x08\x00\x00\x00\x00"
# Citrix MetaFrame application browser service
# Original idea from http://sh0dan.org/oldfiles/hackingcitrix.html
# Payload contents copied from Wireshark capture of Citrix Program
# Neighborhood client application. The application uses this payload to
# locate Citrix servers on the local network. Response to this probe is
# a 48 byte UDP payload as shown here:
#
# 0000 30 00 02 31 02 fd a8 e3 02 00 06 44 c0 a8 80 55
# 0010 00 00 00 00 00 00 00 00 00 00 00 00 02 00 06 44
# 0020 c0 a8 80 56 00 00 00 00 00 00 00 00 00 00 00 00
#
# The first 12 bytes appear to be the same in all responses.
#
# Bytes 0x00 appears to be a packet length field
# Bytes 0x0C - 0x0F are the IP address of the server
# Bytes 0x10 - 0x13 may vary, 0x14 - 0x1F do not appear to
# Bytes 0x20 - 0x23 are the IP address of the primary system in a server farm
# configuration
# Bytes 0x24 - 0x27 can vary, 0x28 - 0x2F do not appear to
udp 1604
"\x1e\x00\x01\x30\x02\xfd\xa8\xe3\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
# RADIUS Access-Request. This is a degenerate packet with no username or
# password; we expect an Access-Reject in response. The Identifier and Request
# Authenticator are both 0. It was generated by running
# echo 'User-Password = ""' | radclient <ip> auth ""
# and then manually stripping out the password.
#
# Section 2 of the RFC says "A request from a client for which the
# RADIUS server does not have a shared secret MUST be silently
# discarded." So this payload only works when the server is configured
# (or misconfigured) to know the scanning machine as a client.
#
# RFC 2865: "The early deployment of RADIUS was done using UDP port
# number 1645, which conflicts with the "datametrics" service. The
# officially assigned port number for RADIUS is 1812.
udp 1645,1812
"\x01\x00\x00\x14"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
# L2TP ICRQ
udp 1701
"\xc8\x02"
"\x00\x3c\x00\x00\x00\x00\x00\x00\x00\x00\x80\x08\x00\x00\x00\x00\x00"
"\x01\x80\x08\x00\x00\x00\x02\x01\x00\x80\x0e\x00\x00\x00\x07"
"nxp-scan\x80\x0a\x00\x00\x00\x03\x00\x00\x00\x03\x80"
"\x08\x00\x00\x00\x09\x00\x00"
# UPNP MSEARCH
udp 1900
"M-SEARCH * HTTP/1.1\r\nHost: 239.255.255.250:1900\r\n"
"Man: \"ssdp:discover\"\r\nMX: 5\r\nST: ssdp:all\r\n\r\n"
# NFS version 2, RFC 1831. XID 0x00000000, program 100003 (NFS), procedure
# NFSPROC_NULL (does nothing, see section 2.2.1), null authentication (see
# section 9.1).
udp 2049
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\x86\xA3"
"\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
"\x00\x00\x00\x00\x00\x00\x00\x00"
# GPRS Tunneling Protocol (GTP)
udp 2123,2152
# GTPv1, protocol 1
"\x32"
# EchoRequest
"\x01"
# message length
"\x00\x04"
# Tunnel endpoint identifier
"\x00\x00\x42\x00"
# sequence number
"\x13\x37"
# N-PDU number
"\x00"
# next extension header type
"\x00"
# GPRS Tunneling Protocol (GTP) "prime" v2
# This same packet can be used for GTP v2 on ports 2123 and 2152 if you change
# the first byte from \x4e to \x40
udp 3386
# GTP'v2
"\x4e"
# EchoRequest
"\x01"
# message length
"\x00\x04"
# sequence number
"\xde\xfe\xc8\x00"
# Freelancer game server status query
# http://sourceforge.net/projects/gameq/
# (relevant files: games.ini, packets.ini, freelancer.php)
udp 2302 "\x00\x02\xf1\x26\x01\x26\xf0\x90\xa6\xf0\x26\x57\x4e\xac\xa0\xec\xf8\x68\xe4\x8d\x21"
# Apple Remote Desktop (ARD)
udp 3283 "\0\x14\0\x01\x03"
# STUN Binding request, see RFC 5389 Section 6
# message type = 0x001, Binding (see Section 18.1)
# message length = 0
# magic cookie = 0x2112a442
# transaction ID = "\x00"*12
udp 3478 "\x00\x01\x00\x00\x21\x12\xa4\x42\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
# Sun Service Tag Discovery protocol (stdiscover)
# http://arc.opensolaris.org/caselog/PSARC/2006/638/stdiscover_protocolv2.pdf
# Would work better with a varying cookie; the second and later sends of this
# probe will be interpreted as resends by the server and will be ignored.
udp 6481 "[PROBE] 0000"
# NAT-PMP external IP address request. See section 3.2 of
# http://files.dns-sd.org/draft-cheshire-nat-pmp.txt.
udp 5351 "\x00\x00"
# DNS Service Discovery (DNS-SD) service query, as used in Zeroconf.
# Transaction ID 0x0000, flags 0x0000, 1 question: PTR query for
# _services._dns-sd._udp.local. If the remote host supports DNS-SD it will send
# back a list of all its services. This is the same as a packet capture of
# dns-sd -B _services._dns-sd._udp .
# See section 9 of
# http://files.dns-sd.org/draft-cheshire-dnsext-dns-sd.txt.
# This first probe is a QU probe, meaning a unicast response is desired
udp 5353
"\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00"
"\x09_services\x07_dns-sd\x04_udp\x05local\x00\x00\x0C\x80\x01"
# This second probe is a QM probe, meaning a unicast response is desired
udp 5353
"\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00"
"\x09_services\x07_dns-sd\x04_udp\x05local\x00\x00\x0C\x00\x01"
# PCANY STATUS
udp 5632 "ST"
# CoAP GET .well-known/core
udp 5683 "@\x01\x01\xce\xbb.well-known\x04core"
# UT2K PING
udp 7777 "None\x00"
# Ubiquiti Discovery Service - v1
udp 10001 "\x01\x00\x00\x00"
# Amanda backup service noop request. I think that this does nothing on the
# server but only asks it to send back its feature list. In reply we expect an
# ACK or (more likely) an ERROR. I couldn't find good online documentation of
# the Amanda network protocol. There is parsing code in the Amanda source at
# common-src/security-util.c. This is based on a packet capture of
# amcheck <config> <host>
udp 10080
"Amanda 2.6 REQ HANDLE 000-00000000 SEQ 0\n"
"SERVICE noop\n"
# VxWorks Wind River Debugger
udp 17185
# Random XID
"\x00\x00\x00\x00"
# RPC version 2 procedure call
"\x00\x00\x00\x00\x00\x00\x00\x02"
# WDB version 1
"\x55\x55\x55\x55\x00\x00\x00\x01"
# WDB_TARGET_PING
"\x00\x00\x00\x00"
# RPC Auth NULL
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
# Checksum
"\xff\xff\x55\x13"
# WDB wrapper (length and sequence number)
"\x00\x00\x00\x30\x00\x00\x00\x01"
# Empty data?
"\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00"
# VXWORKS DEBUG (alternative?)
udp 17185
"\x72\x37\x72\x37\x00\x00\x00"
"\x00\x00\x00\x00\x02\x55\x55\x55\x55\x00\x00\x00\x01\x00\x00\x00\x01"
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff"
"\xff\x55\x10\x00\x00\x00\x3c\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00"
"\x00\x00\x00\x00\x00\x00"
# Quake 2 and Quake 3 game servers (and servers of derived games like Nexuiz).
# Gets game information from the server (see probe responses in
# nmap-service-probes). These services typically run on a base port or a
# few numbers higher.
# Quake 2. Typical ports: 27910-27914.
udp 27910-27914 "\xff\xff\xff\xffstatus"
# Quake 3. Typical ports:
# 26000-26004: Nexuiz
# 27960-27964: Various games
# 30720-30724: Tremulous
# 44400: Warsow
udp 26000-26004,27960-27964,30720-30724,44400 "\xff\xff\xff\xffgetstatus"
# Murmur 1.2.X (Mumble server)
# UDP ping. "abcdefgh" is an identifier. See
# http://mumble.sourceforge.net/Protocol.
udp 64738 "\x00\x00\x00\x00abcdefgh"
# Ventrilo 2.1.2+
# UDP general status request (encrypted).
# See http://aluigi.altervista.org/papers.htm#ventrilo
udp 3784
"\x01\xe7\xe5\x75\x31\xa3\x17\x0b\x21\xcf\xbf\x2b\x99\x4e\xdd\x19\xac\xde\x08\x5f\x8b\x24\x0a\x11\x19\xb6\x73\x6f\xad\x28\x13\xd2\x0a\xb9\x12\x75"
# Kademlia (kad) as used by various P2P applications. Send a Kademlia ping
# 4665, 4666, 4672, 6429: eDonkey/eMule and variants
udp 4665,4666,4672,6429 "\xE4\x60"
# TeamSpeak 2
# UDP login request
# See http://wiki.wireshark.org/TeamSpeak2
udp 8767
"\xf4\xbe\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x002x\xba\x85\tTeamSpeak\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\nWindows XP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00 \x00<\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08nickname\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
# TS3INIT1
udp 9987
"TS3INIT1\x00\x65\x00\x00\x88\x0a\x39\x7b\x0f\x00\x5b\x55\x72\xef\xdc\x78\x32\x6b\x00\x00\x00\x00\x00\x00\x00\x00"
# TeamSpeak 3
# UDP login request (encrypted)
# http://seclists.org/nmap-dev/2013/q3/72
udp 9987
"\x05\xca\x7f\x16\x9c\x11\xf9\x89\x00\x00\x00\x00\x02\x9d\x74\x8b\x45\xaa\x7b\xef\xb9\x9e\xfe\xad\x08\x19\xba\xcf\x41\xe0\x16\xa2\x32\x6c\xf3\xcf\xf4\x8e\x3c\x44\x83\xc8\x8d\x51\x45\x6f\x90\x95\x23\x3e\x00\x97\x2b\x1c\x71\xb2\x4e\xc0\x61\xf1\xd7\x6f\xc5\x7e\xf6\x48\x52\xbf\x82\x6a\xa2\x3b\x65\xaa\x18\x7a\x17\x38\xc3\x81\x27\xc3\x47\xfc\xa7\x35\xba\xfc\x0f\x9d\x9d\x72\x24\x9d\xfc\x02\x17\x6d\x6b\xb1\x2d\x72\xc6\xe3\x17\x1c\x95\xd9\x69\x99\x57\xce\xdd\xdf\x05\xdc\x03\x94\x56\x04\x3a\x14\xe5\xad\x9a\x2b\x14\x30\x3a\x23\xa3\x25\xad\xe8\xe6\x39\x8a\x85\x2a\xc6\xdf\xe5\x5d\x2d\xa0\x2f\x5d\x9c\xd7\x2b\x24\xfb\xb0\x9c\xc2\xba\x89\xb4\x1b\x17\xa2\xb6"
# Memcached
# version request (shorter response than stats)
# https://github.com/memcached/memcached/blob/master/doc/protocol.txt
udp 11211
"\0\x01\0\0\0\x01\0\0version\r\n"
# Steam, typically using a port in 27015-27030. Send a "Source Engine query"
udp 27015-27030
"\xff\xff\xff\xffTSource Engine Query\x00"
# TRIN00 UNIX PING
udp 27444 "png l44adsl"
# BO PING
udp 31337
"\xce\x63\xd1\xd2\x16\xe7\x13\xcf\x38"
"\xa5\xa5\x86\xb2\x75\x4b\x99\xaa\x32\x58"
# TRIN00 WIN PING
udp 34555 "png []..Ks"
# Beckhoff ADS discovery request
# https://github.com/ONE75/adsclient/blob/master/src/AdsClient.Finder/DeviceFinder.cs#L49-L64
udp 48899
"\x03\x66\x14\x71\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x01\x10\x27\x00\x00\x00\x00"
-471
View File
@@ -1,471 +0,0 @@
//! Provides functions to parse input IP addresses, CIDRs or files.
use std::collections::BTreeSet;
use std::fs::{self, File};
use std::io::{prelude::*, BufReader};
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
use std::path::Path;
use std::str::FromStr;
use cidr_utils::cidr::{IpCidr, IpInet};
use hickory_resolver::{
config::{NameServerConfig, Protocol, ResolverConfig, ResolverOpts},
Resolver,
};
use log::debug;
use crate::input::Opts;
use crate::warning;
/// Parses the string(s) into IP addresses.
///
/// Goes through all possible IP inputs (files or via argparsing).
///
/// ```rust
/// # use rustscan::input::Opts;
/// # use rustscan::address::parse_addresses;
/// let mut opts = Opts::default();
/// opts.addresses = vec!["192.168.0.0/30".to_owned()];
///
/// let ips = parse_addresses(&opts);
/// ```
///
/// Finally, any duplicates are removed to avoid excessive scans.
pub fn parse_addresses(input: &Opts) -> Vec<IpAddr> {
let mut ips: Vec<IpAddr> = Vec::new();
let mut unresolved_addresses: Vec<&str> = Vec::new();
let backup_resolver = get_resolver(&input.resolver);
for address in &input.addresses {
let parsed_ips = parse_address(address, &backup_resolver);
if !parsed_ips.is_empty() {
ips.extend(parsed_ips);
} else {
unresolved_addresses.push(address);
}
}
// If we got to this point this can only be a file path or the wrong input.
for file_path in unresolved_addresses {
let file_path = Path::new(file_path);
if !file_path.is_file() {
warning!(
format!("Host {file_path:?} could not be resolved."),
input.greppable,
input.accessible
);
continue;
}
if let Ok(x) = read_ips_from_file(file_path, &backup_resolver) {
ips.extend(x);
} else {
warning!(
format!("Host {file_path:?} could not be resolved."),
input.greppable,
input.accessible
);
}
}
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_cidrs.iter().any(|cidr| cidr.contains(ip)));
ips
}
/// Given a string, parse it as a host, IP address, or CIDR.
///
/// This allows us to pass files as hosts or cidr or IPs easily
/// Call this every time you have a possible IP-or-host.
///
/// If the address is a domain, we can self-resolve the domain locally
/// or resolve it by dns resolver list.
///
/// ```rust
/// # use rustscan::address::parse_address;
/// # use hickory_resolver::Resolver;
/// let ips = parse_address("127.0.0.1", &Resolver::default().unwrap());
/// ```
pub fn parse_address(address: &str, resolver: &Resolver) -> Vec<IpAddr> {
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
fn resolve_ips_from_host(source: &str, backup_resolver: &Resolver) -> Vec<IpAddr> {
let mut ips: Vec<IpAddr> = Vec::new();
if let Ok(addrs) = source.to_socket_addrs() {
for ip in addrs {
ips.push(ip.ip());
}
} else if let Ok(addrs) = backup_resolver.lookup_ip(source) {
ips.extend(addrs.iter());
}
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:
/// 1. assume the parameter is a path and attempt to read IPs.
/// 2. parse the input as a comma-separated list of IPs.
/// 2. if `resolver` is not set:
/// 1. attempt to derive a resolver from the system config. (e.g.
/// `/etc/resolv.conf` on *nix).
/// 2. finally, build a CloudFlare-based resolver (default
/// behaviour).
fn get_resolver(resolver: &Option<String>) -> Resolver {
match resolver {
Some(r) => {
let mut config = ResolverConfig::new();
let resolver_ips = match read_resolver_from_file(r) {
Ok(ips) => ips,
Err(_) => r
.split(',')
.filter_map(|r| IpAddr::from_str(r).ok())
.collect::<Vec<_>>(),
};
for ip in resolver_ips {
config.add_name_server(NameServerConfig::new(
SocketAddr::new(ip, 53),
Protocol::Udp,
));
}
Resolver::new(config, resolver_opts()).unwrap()
}
None => match system_resolver() {
Ok(resolver) => resolver,
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)?
.lines()
.filter_map(|line| IpAddr::from_str(line.trim()).ok())
.collect();
Ok(ips)
}
#[cfg(not(tarpaulin_include))]
/// Parses an input file of IPs and uses those
fn read_ips_from_file(
ips: &std::path::Path,
backup_resolver: &Resolver,
) -> Result<Vec<IpAddr>, std::io::Error> {
let file = File::open(ips)?;
let reader = BufReader::new(file);
let mut ips: Vec<IpAddr> = Vec::new();
for address_line in reader.lines() {
if let Ok(address) = address_line {
ips.extend(parse_address(&address, backup_resolver));
} else {
debug!("Line in file is not valid");
}
}
Ok(ips)
}
#[cfg(test)]
mod tests {
use super::{parse_addresses, Opts};
use std::net::{IpAddr, Ipv4Addr};
#[test]
fn parse_correct_addresses() {
let opts = Opts {
addresses: vec!["127.0.0.1".to_owned(), "192.168.0.0/30".to_owned()],
..Default::default()
};
let ips = parse_addresses(&opts);
assert_eq!(
ips,
[
Ipv4Addr::new(127, 0, 0, 1),
Ipv4Addr::new(192, 168, 0, 0),
Ipv4Addr::new(192, 168, 0, 1),
Ipv4Addr::new(192, 168, 0, 2),
Ipv4Addr::new(192, 168, 0, 3)
]
);
}
#[test]
fn parse_addresses_with_address_exclusions() {
let opts = Opts {
addresses: vec!["192.168.0.0/30".to_owned()],
exclude_addresses: Some(vec!["192.168.0.1".to_owned()]),
..Default::default()
};
let ips = parse_addresses(&opts);
assert_eq!(
ips,
[
Ipv4Addr::new(192, 168, 0, 0),
Ipv4Addr::new(192, 168, 0, 2),
Ipv4Addr::new(192, 168, 0, 3)
]
);
}
#[test]
fn parse_addresses_with_cidr_exclusions() {
let opts = Opts {
addresses: vec!["192.168.0.0/29".to_owned()],
exclude_addresses: Some(vec!["192.168.0.0/30".to_owned()]),
..Default::default()
};
let ips = parse_addresses(&opts);
assert_eq!(
ips,
[
Ipv4Addr::new(192, 168, 0, 4),
Ipv4Addr::new(192, 168, 0, 5),
Ipv4Addr::new(192, 168, 0, 6),
Ipv4Addr::new(192, 168, 0, 7),
]
);
}
#[test]
fn parse_addresses_with_incorrect_address_exclusions() {
let opts = Opts {
addresses: vec!["192.168.0.0/30".to_owned()],
exclude_addresses: Some(vec!["192.168.0.1".to_owned()]),
..Default::default()
};
let ips = parse_addresses(&opts);
assert_eq!(
ips,
[
Ipv4Addr::new(192, 168, 0, 0),
Ipv4Addr::new(192, 168, 0, 2),
Ipv4Addr::new(192, 168, 0, 3)
]
);
}
#[test]
fn parse_duplicate_cidrs() {
let opts = Opts {
addresses: vec!["79.98.104.0/21".to_owned(), "79.98.104.0/24".to_owned()],
..Default::default()
};
let ips = parse_addresses(&opts);
assert_eq!(ips.len(), 2_048);
}
#[test]
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 {
addresses: vec!["192.128.1.1/24".to_owned()],
..Default::default()
};
let ips = parse_addresses(&opts);
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),
]
);
}
}
+7 -5
View File
@@ -42,11 +42,13 @@ impl Benchmark {
let mut summary = String::from("\nRustScan Benchmark Summary");
for timer in &self.named_timers {
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));
}
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));
}
}
summary
+96 -430
View File
@@ -1,129 +1,70 @@
//! 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;
use structopt::{clap::arg_enum, StructOpt};
const LOWEST_PORT_NUMBER: u16 = 1;
const TOP_PORT_NUMBER: u16 = 65535;
/// Represents the strategy in which the port scanning will run.
/// - Serial will run from start to end, for example 1 to 1_000.
/// - Random will randomize the order in which ports will be scanned.
#[derive(Deserialize, Debug, ValueEnum, Clone, Copy, PartialEq, Eq)]
pub enum ScanOrder {
Serial,
Random,
}
/// Represents the scripts variant.
/// - none will avoid running any script, only portscan results will be shown.
/// - default will run the default embedded nmap script, that's part of RustScan since the beginning.
/// - custom will read the ScriptConfig file and the available scripts in the predefined folders
#[derive(Deserialize, Debug, ValueEnum, Clone, PartialEq, Eq, Copy)]
pub enum ScriptsRequired {
None,
Default,
Custom,
}
/// 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))
arg_enum! {
/// Represents the strategy in which the port scanning will run.
/// - Serial will run from start to end, for example 1 to 1_000.
/// - Random will randomize the order in which ports will be scanned.
#[derive(Deserialize, Debug, StructOpt, Clone, Copy, PartialEq, Eq)]
pub enum ScanOrder {
Serial,
Random,
}
}
arg_enum! {
/// Represents the scripts variant.
/// - none will avoid running any script, only portscan results will be shown.
/// - default will run the default embedded nmap script, that's part of RustScan since the beginning.
/// - custom will read the ScriptConfig file and the available scripts in the predefined folders
#[derive(Deserialize, Debug, StructOpt, Clone, PartialEq, Eq, Copy)]
pub enum ScriptsRequired {
None,
Default,
Custom,
}
}
/// Represents the range of ports to be scanned.
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct PortRange {
pub start: u16,
pub end: u16,
}
#[cfg(not(tarpaulin_include))]
/// 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()?;
fn parse_range(input: &str) -> Result<PortRange, String> {
let range = input
.split('-')
.map(str::parse)
.collect::<Result<Vec<u16>, std::num::ParseIntError>>();
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());
if range.is_err() {
return 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)
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.",
)),
}
}
#[derive(Parser, Debug, Clone)]
#[command(
name = "rustscan",
version = env!("CARGO_PKG_VERSION"),
max_term_width = 120,
help_template = "{bin} {version}\n{about}\n\nUSAGE:\n {usage}\n\nOPTIONS:\n{options}",
)]
#[derive(StructOpt, Debug, Clone)]
#[structopt(name = "rustscan", setting = structopt::clap::AppSettings::TrailingVarArg)]
#[allow(clippy::struct_excessive_bools)]
/// Fast Port Scanner built in Rust.
/// WARNING Do not use this program against sensitive infrastructure since the
@@ -132,74 +73,65 @@ fn parse_ranges(input: &str) -> Result<PortRanges, String> {
/// - GitHub <https://github.com/RustScan/RustScan>
pub struct Opts {
/// A comma-delimited list or newline-delimited file of separated CIDRs, IPs, or hosts to be scanned.
#[arg(short, long, value_delimiter = ',')]
#[structopt(short, long, use_delimiter = true)]
pub addresses: Vec<String>,
/// A list of comma separated ports to be scanned. Example: 80,443,8080.
#[arg(short, long, value_delimiter = ',')]
#[structopt(short, long, use_delimiter = true)]
pub ports: Option<Vec<u16>>,
/// 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>,
/// A range of ports with format start-end. Example: 1-1000.
#[structopt(short, long, conflicts_with = "ports", parse(try_from_str = parse_range))]
pub range: Option<PortRange>,
/// Whether to ignore the configuration file or not.
#[arg(short, long)]
#[structopt(short, long)]
pub no_config: bool,
/// Hide the banner
#[arg(long)]
pub no_banner: bool,
/// Custom path to config file
#[arg(short, long, value_parser)]
#[structopt(short, long, parse(from_os_str))]
pub config_path: Option<PathBuf>,
/// Greppable mode. Only output the ports. No Nmap. Useful for grep or outputting to a file.
#[arg(short, long)]
#[structopt(short, long)]
pub greppable: bool,
/// Accessible mode. Turns off features which negatively affect screen readers.
#[arg(long)]
#[structopt(long)]
pub accessible: bool,
/// A comma-delimited list or file of DNS resolvers.
#[arg(long)]
pub resolver: Option<String>,
/// The batch size for port scanning, it increases or slows the speed of
/// scanning. Depends on the open file limit of your OS. If you do 65535
/// 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: usize,
#[structopt(short, long, default_value = "4500")]
pub batch_size: u16,
/// The timeout in milliseconds before a port is assumed to be closed.
#[arg(short, long, default_value = "1500")]
#[structopt(short, long, default_value = "1500")]
pub timeout: u32,
/// The number of tries before a port is assumed to be closed.
/// If set to 0, rustscan will correct it to 1.
#[arg(long, default_value = "1")]
#[structopt(long, default_value = "1")]
pub tries: u8,
/// Automatically increases the Unix file-descriptor limit.
#[cfg_attr(not(unix), arg(hide = true))]
#[arg(short, long)]
pub ulimit: Option<usize>,
/// Automatically ups the ULIMIT with the value you provided.
#[structopt(short, long)]
pub ulimit: Option<u64>,
/// The order of scanning to be performed. The "serial" option will
/// scan ports in ascending order while the "random" option will scan
/// ports randomly.
#[arg(long, value_enum, ignore_case = true, default_value = "serial")]
#[structopt(long, possible_values = &ScanOrder::variants(), case_insensitive = true, default_value = "serial")]
pub scan_order: ScanOrder,
/// Level of scripting required for the run.
#[arg(long, value_enum, ignore_case = true, default_value = "default")]
#[structopt(long, possible_values = &ScriptsRequired::variants(), case_insensitive = true, default_value = "default")]
pub scripts: ScriptsRequired,
/// Use the top 1000 ports.
#[arg(long)]
#[structopt(long)]
pub top: bool,
/// The Script arguments to run.
@@ -207,65 +139,29 @@ pub struct Opts {
/// Example: 'rustscan -t 1500 -a 127.0.0.1 -- -A -sC'.
/// This command adds -Pn -vvv -p $PORTS automatically to nmap.
/// For things like --script '(safe and vuln)' enclose it in quotations marks \"'(safe and vuln)'\"
#[arg(last = true)]
#[structopt(last = true)]
pub command: Vec<String>,
/// A list of comma separated ports to be excluded from scanning. Example: 80,443,8080.
#[arg(short, long, value_delimiter = ',')]
#[structopt(short, long, use_delimiter = true)]
pub exclude_ports: Option<Vec<u16>>,
/// A list of comma separated CIDRs, IPs, or hosts to be excluded from scanning.
#[arg(short = 'x', long = "exclude-addresses", value_delimiter = ',')]
pub exclude_addresses: Option<Vec<String>>,
/// 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))]
impl Opts {
pub fn read() -> Self {
let mut opts = Opts::parse();
let mut opts = Opts::from_args();
if opts.ports.is_none() && opts.range.is_none() {
opts.range = Some(PortRanges(vec![(LOWEST_PORT_NUMBER, TOP_PORT_NUMBER)]));
opts.range = Some(PortRange {
start: LOWEST_PORT_NUMBER,
end: 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) {
@@ -288,7 +184,7 @@ impl Opts {
merge_required!(
addresses, greppable, accessible, batch_size, timeout, tries, scan_order, scripts,
command, udp, no_banner, closed, interval
command
);
}
@@ -305,10 +201,14 @@ impl Opts {
// Only use top ports when the user asks for them
if self.top && config.ports.is_some() {
self.ports = config.ports.clone();
let mut ports: Vec<u16> = Vec::with_capacity(config.ports.clone().unwrap().len());
for entry in config.ports.clone().unwrap().keys() {
ports.push(entry.parse().unwrap());
}
self.ports = Some(ports);
}
merge_optional!(range, resolver, ulimit, exclude_ports, exclude_addresses);
merge_optional!(range, ulimit, exclude_ports);
}
}
@@ -325,18 +225,12 @@ impl Default for Opts {
ulimit: None,
command: vec![],
accessible: false,
resolver: None,
scan_order: ScanOrder::Serial,
no_config: true,
no_banner: false,
top: false,
scripts: ScriptsRequired::Default,
config_path: None,
exclude_ports: None,
exclude_addresses: None,
udp: false,
closed: false,
interval: 0,
}
}
}
@@ -348,29 +242,22 @@ impl Default for Opts {
#[derive(Debug, Deserialize)]
pub struct Config {
addresses: Option<Vec<String>>,
ports: Option<Vec<u16>>,
range: Option<PortRanges>,
ports: Option<HashMap<String, u16>>,
range: Option<PortRange>,
greppable: Option<bool>,
accessible: Option<bool>,
batch_size: Option<usize>,
batch_size: Option<u16>,
timeout: Option<u32>,
tries: Option<u8>,
ulimit: Option<usize>,
resolver: Option<String>,
ulimit: Option<u64>,
scan_order: Option<ScanOrder>,
command: Option<Vec<String>>,
scripts: Option<ScriptsRequired>,
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))]
#[allow(clippy::doc_link_with_quotes)]
#[allow(clippy::manual_unwrap_or_default)]
impl Config {
/// Reads the configuration file with TOML format and parses it into a
/// Config struct.
@@ -380,29 +267,12 @@ impl Config {
/// addresses = ["127.0.0.1", "127.0.0.1"]
/// ports = [80, 443, 8080]
/// greppable = true
/// scan_order = "Serial"
/// 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(|| {
// 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()
});
let config_path = custom_config_path.unwrap_or_else(default_config_path);
if config_path.exists() {
content = match fs::read_to_string(config_path) {
Ok(content) => content,
@@ -413,9 +283,7 @@ impl Config {
let config: Config = match toml::from_str(&content) {
Ok(config) => config,
Err(e) => {
crate::tui::println_safe(format_args!(
"Found {e} in configuration file.\nAborting scan.\n"
));
println!("Found {e} in configuration file.\nAborting scan.\n");
std::process::exit(1);
}
};
@@ -424,30 +292,8 @@ impl Config {
}
}
/// 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.
/// Constructs default path to config toml
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.");
};
@@ -457,11 +303,7 @@ pub fn old_default_config_path() -> PathBuf {
#[cfg(test)]
mod tests {
use clap::{CommandFactory, Parser};
use parameterized::parameterized;
use super::{Config, Opts, PortRanges, ScanOrder, ScriptsRequired};
use super::{Config, Opts, PortRange, ScanOrder, ScriptsRequired};
impl Config {
fn default() -> Self {
Self {
@@ -475,120 +317,13 @@ mod tests {
ulimit: None,
command: Some(vec!["-A".to_owned()]),
accessible: Some(true),
resolver: None,
scan_order: Some(ScanOrder::Random),
scripts: None,
exclude_ports: None,
exclude_addresses: None,
udp: Some(false),
no_banner: None,
closed: Some(false),
interval: None,
}
}
}
#[test]
fn verify_cli() {
Opts::command().debug_assert();
}
#[parameterized(input = {
vec!["rustscan", "--addresses", "127.0.0.1"],
vec!["rustscan", "--addresses", "127.0.0.1", "--", "-sCV"],
vec!["rustscan", "--addresses", "127.0.0.1", "--", "-A"],
vec!["rustscan", "-t", "1500", "-a", "127.0.0.1", "--", "-A", "-sC"],
vec!["rustscan", "--addresses", "127.0.0.1", "--", "--script", r#""'(safe and vuln)'""#],
}, command = {
vec![],
vec!["-sCV".to_owned()],
vec!["-A".to_owned()],
vec!["-A".to_owned(), "-sC".to_owned()],
vec!["--script".to_owned(), "\"'(safe and vuln)'\"".to_owned()],
})]
fn parse_trailing_command(input: Vec<&str>, command: Vec<String>) {
let opts = Opts::parse_from(input);
assert_eq!(vec!["127.0.0.1".to_owned()], opts.addresses);
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();
@@ -624,84 +359,15 @@ mod tests {
fn opts_merge_optional_arguments() {
let mut opts = Opts::default();
let mut config = Config::default();
config.range = Some(PortRanges(vec![(1, 1_000)]));
config.range = Some(PortRange {
start: 1,
end: 1_000,
});
config.ulimit = Some(1_000);
config.resolver = Some("1.1.1.1".to_owned());
opts.merge_optional(&config);
assert_eq!(opts.range, config.range);
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);
}
}
+17 -28
View File
@@ -7,40 +7,33 @@
//! [`Scanner`](crate::scanner::Scanner) which in turn requires a
//! [`PortStrategy`](crate::port_strategy::PortStrategy):
//!
//! 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
//! ```rust
//! use async_std::task::block_on;
//! use std::{net::IpAddr, time::Duration};
//!
//! use rustscan::input::{PortRanges, ScanOrder};
//! use rustscan::input::{PortRange, ScanOrder};
//! use rustscan::port_strategy::PortStrategy;
//! use rustscan::scanner::Scanner;
//!
//! fn main() {
//! let addrs = vec!["127.0.0.1".parse::<IpAddr>().unwrap()];
//! 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 range = PortRange {
//! start: 1,
//! end: 1_000,
//! };
//! let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
//! let scanner = Scanner::new(
//! &addrs, // the addresses to scan
//! 10, // batch_size is how many ports at a time should be scanned
//! Duration::from_millis(100), //T imeout is the time RustScan should wait before declaring a port closed. As datatype Duration.
//! 1, // Tries, how many retries should RustScan do?
//! true, // greppable is whether or not RustScan should print things, or wait until the end to print only the ip
//! strategy, // the port strategy used
//! true, // accessible, should the output be A11Y compliant?
//! vec![9000], // What ports should RustScan exclude?
//! false, // is this a UDP scan?
//! &addrs,
//! 10,
//! Duration::from_millis(100),
//! 1,
//! true,
//! strategy,
//! true,
//! vec![9000],
//! );
//!
//! let runtime = tokio::runtime::Builder::new_current_thread()
//! .enable_all()
//! .build()
//! .unwrap();
//! let scan_result = runtime.block_on(scanner.run());
//! let scan_result = block_on(scanner.run());
//!
//! println!("{:?}", scan_result);
//! }
@@ -58,7 +51,3 @@ pub mod port_strategy;
pub mod benchmark;
pub mod scripts;
pub mod address;
pub mod generated;
+253 -181
View File
@@ -5,28 +5,33 @@
use rustscan::benchmark::{Benchmark, NamedTimer};
use rustscan::input::{self, Config, Opts, ScriptsRequired};
use rustscan::port_strategy::PortStrategy;
use rustscan::scanner::{PortStatus, Scanner};
use rustscan::scanner::Scanner;
use rustscan::scripts::{init_scripts, Script, ScriptFile};
use rustscan::tui::println_safe;
use rustscan::{detail, funny_opening, output, warning};
use cidr_utils::cidr::IpCidr;
use colorful::{Color, Colorful};
use futures::executor::block_on;
use std::collections::HashMap;
use std::net::IpAddr;
use std::fs::File;
use std::io::{prelude::*, BufReader};
use std::net::{IpAddr, ToSocketAddrs};
use std::path::Path;
use std::string::ToString;
use std::time::Duration;
use rustscan::address::parse_addresses;
use trust_dns_resolver::{
config::{ResolverConfig, ResolverOpts},
Resolver,
};
extern crate colorful;
extern crate dirs;
// Average value for Ubuntu
#[cfg(unix)]
const DEFAULT_FILE_DESCRIPTORS_LIMIT: usize = 8000;
const DEFAULT_FILE_DESCRIPTORS_LIMIT: u64 = 8000;
// Safest batch size based on experimentation
#[cfg(unix)]
const AVERAGE_BATCH_SIZE: usize = 3000;
const AVERAGE_BATCH_SIZE: u16 = 3000;
#[macro_use]
extern crate log;
@@ -36,9 +41,6 @@ extern crate log;
/// Faster Nmap scanning with Rust
/// If you're looking for the actual scanning, check out the module Scanner
fn main() {
#[cfg(not(unix))]
let _ = ansi_term::enable_ansi_support();
env_logger::init();
let mut benchmarks = Benchmark::init();
let mut rustscan_bench = NamedTimer::start("RustScan");
@@ -47,14 +49,9 @@ fn main() {
let config = Config::read(opts.config_path.clone());
opts.merge(&config);
if let Err(message) = opts.validate_platform() {
eprintln!("error: {message}");
std::process::exit(2);
}
debug!("Main() `opts` arguments are {:?}", opts);
debug!("Main() `opts` arguments are {opts:?}");
let scripts_to_run: Vec<ScriptFile> = match init_scripts(&opts.scripts) {
let scripts_to_run: Vec<ScriptFile> = match init_scripts(opts.scripts) {
Ok(scripts_to_run) => scripts_to_run,
Err(e) => {
warning!(
@@ -66,9 +63,9 @@ fn main() {
}
};
debug!("Scripts initialized {scripts_to_run:?}");
debug!("Scripts initialized {:?}", &scripts_to_run);
if !opts.greppable && !opts.accessible && !opts.no_banner {
if !opts.greppable && !opts.accessible {
print_opening(&opts);
}
@@ -83,10 +80,16 @@ fn main() {
std::process::exit(1);
}
let batch_size = effective_batch_size(&opts);
debug!("Effective batch size: {batch_size}");
#[cfg(unix)]
let batch_size: u16 = infer_batch_size(&opts, adjust_ulimit_size(&opts));
let mut scanner = Scanner::new(
#[cfg(not(unix))]
let batch_size: u16 = AVERAGE_BATCH_SIZE;
// Added by wasuaje - 01/26/2024:
// exclude_ports is an exclusion port list
//
let scanner = Scanner::new(
&ips,
batch_size,
Duration::from_millis(opts.timeout.into()),
@@ -95,38 +98,21 @@ fn main() {
PortStrategy::pick(&opts.range, opts.ports, opts.scan_order),
opts.accessible,
opts.exclude_ports.unwrap_or_default(),
opts.udp,
)
.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:?}");
);
debug!("Scanner finished building: {:?}", scanner);
let mut portscan_bench = NamedTimer::start("Portscan");
// 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);
let scan_result = block_on(scanner.run());
portscan_bench.end();
benchmarks.push(portscan_bench);
let mut ports_per_ip: HashMap<IpAddr, Vec<u16>> = HashMap::new();
let mut closed_ports_per_ip: HashMap<IpAddr, Vec<u16>> = HashMap::new();
let mut ports_per_ip = HashMap::new();
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 socket in scan_result {
ports_per_ip
.entry(socket.ip())
.or_insert_with(Vec::new)
.push(socket.port());
}
for ip in ips {
@@ -141,7 +127,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,
batch_size,
opts.batch_size,
"'rustscan -b <batch_size> -a <ip address>'");
warning!(x, opts.greppable, opts.accessible);
}
@@ -155,19 +141,17 @@ fn main() {
// if option scripts is none, no script will be spawned
if opts.greppable || opts.scripts == ScriptsRequired::None {
println_safe(format_args!("{ip} -> [{ports_str}]"));
println!("{} -> [{}]", &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 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();
for mut script_f in scripts_to_run.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(' ');
@@ -177,7 +161,7 @@ fn main() {
opts.greppable,
opts.accessible
);
debug!("Call format {call_f}");
debug!("Call format {}", call_f);
script_f.call_format = Some(call_f);
}
}
@@ -194,7 +178,7 @@ fn main() {
);
match script.run() {
Ok(script_result) => {
detail!(script_result.clone(), opts.greppable, opts.accessible);
detail!(script_result.to_string(), opts.greppable, opts.accessible);
}
Err(e) => {
warning!(&format!("Error {e}"), opts.greppable, opts.accessible);
@@ -203,61 +187,36 @@ 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) {
debug!("Printing opening");
let s = r#".----. .-. .-. .----..---. .----. .---. .--. .-. .-.
| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |
| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |
`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'
The Modern Day Port Scanner."#;
println_safe(format_args!("{}", s.gradient(Color::Green).bold()));
let info = r#"________________________________________
: http://discord.skerritt.blog :
: https://github.com/RustScan/RustScan :
--------------------------------------"#;
println_safe(format_args!("{}", info.gradient(Color::Yellow).bold()));
let s = format!(
"{}\n{}\n{}\n{}\n{}",
r#".----. .-. .-. .----..---. .----. .---. .--. .-. .-."#,
r#"| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |"#,
r#"| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |"#,
r#"`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'"#,
r#"The Modern Day Port Scanner."#
);
println!("{}", s.gradient(Color::Green).bold());
let info = format!(
"{}\n{}\n{}\n{}",
r#"________________________________________"#,
r#": http://discord.skerritt.blog :"#,
r#": https://github.com/RustScan/RustScan :"#,
r#" --------------------------------------"#
);
println!("{}", info.gradient(Color::Yellow).bold());
funny_opening!();
let config_path = opts
@@ -270,26 +229,111 @@ 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
);
/// Goes through all possible IP inputs (files or via argparsing)
/// Parses the string(s) into IPs
fn parse_addresses(input: &Opts) -> Vec<IpAddr> {
let mut ips: Vec<IpAddr> = Vec::new();
let mut unresolved_addresses: Vec<&str> = Vec::new();
let backup_resolver =
Resolver::new(ResolverConfig::cloudflare_tls(), ResolverOpts::default()).unwrap();
for address in &input.addresses {
let parsed_ips = parse_address(address, &backup_resolver);
if !parsed_ips.is_empty() {
ips.extend(parsed_ips);
} else {
unresolved_addresses.push(address);
}
}
// If we got to this point this can only be a file path or the wrong input.
for file_path in unresolved_addresses {
let file_path = Path::new(file_path);
if !file_path.is_file() {
warning!(
format!("Host {file_path:?} could not be resolved."),
input.greppable,
input.accessible
);
continue;
}
if let Ok(x) = read_ips_from_file(file_path, &backup_resolver) {
ips.extend(x);
} else {
warning!(
format!("Host {file_path:?} could not be resolved."),
input.greppable,
input.accessible
);
}
}
ips
}
/// Given a string, parse it as a host, IP address, or CIDR.
/// This allows us to pass files as hosts or cidr or IPs easily
/// Call this every time you have a possible IP_or_host
fn parse_address(address: &str, resolver: &Resolver) -> Vec<IpAddr> {
IpCidr::from_str(address)
.map(|cidr| cidr.iter().collect())
.ok()
.or_else(|| {
format!("{}:{}", &address, 80)
.to_socket_addrs()
.ok()
.map(|mut iter| vec![iter.next().unwrap().ip()])
})
.unwrap_or_else(|| resolve_ips_from_host(address, resolver))
}
/// Uses DNS to get the IPS associated with host
fn resolve_ips_from_host(source: &str, backup_resolver: &Resolver) -> Vec<IpAddr> {
let mut ips: Vec<std::net::IpAddr> = Vec::new();
if let Ok(addrs) = source.to_socket_addrs() {
for ip in addrs {
ips.push(ip.ip());
}
} else if let Ok(addrs) = backup_resolver.lookup_ip(source) {
ips.extend(addrs.iter());
}
ips
}
#[cfg(not(tarpaulin_include))]
/// Parses an input file of IPs and uses those
fn read_ips_from_file(
ips: &std::path::Path,
backup_resolver: &Resolver,
) -> Result<Vec<std::net::IpAddr>, std::io::Error> {
let file = File::open(ips)?;
let reader = BufReader::new(file);
let mut ips: Vec<std::net::IpAddr> = Vec::new();
for address_line in reader.lines() {
if let Ok(address) = address_line {
ips.extend(parse_address(&address, backup_resolver));
} else {
debug!("Line in file is not valid");
}
}
Ok(ips)
}
#[cfg(unix)]
fn adjust_ulimit_size(opts: &Opts) -> usize {
fn adjust_ulimit_size(opts: &Opts) -> u64 {
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}."),
@@ -306,15 +350,14 @@ fn adjust_ulimit_size(opts: &Opts) -> usize {
}
let (soft, _) = Resource::NOFILE.get().unwrap();
// 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)
soft
}
#[cfg(unix)]
fn infer_batch_size(opts: &Opts, ulimit: usize) -> usize {
let mut batch_size = opts.batch_size;
fn infer_batch_size(opts: &Opts, ulimit: u64) -> u16 {
use std::convert::TryInto;
let mut batch_size: u64 = opts.batch_size.into();
// Adjust the batch size when the ulimit value is lower than the desired batch size
if ulimit < batch_size {
@@ -325,8 +368,8 @@ fn infer_batch_size(opts: &Opts, ulimit: usize) -> usize {
// 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 {
// ulimit is smaller than average batch size
if ulimit < AVERAGE_BATCH_SIZE.into() {
// ulimit is smaller than aveage 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);
@@ -334,7 +377,7 @@ fn infer_batch_size(opts: &Opts, ulimit: usize) -> usize {
batch_size = ulimit / 2;
} else if ulimit > DEFAULT_FILE_DESCRIPTORS_LIMIT {
info!("Batch size is now average batch size");
batch_size = AVERAGE_BATCH_SIZE;
batch_size = AVERAGE_BATCH_SIZE.into();
} else {
batch_size = ulimit - 100;
}
@@ -347,23 +390,22 @@ fn infer_batch_size(opts: &Opts, ulimit: usize) -> usize {
}
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};
use super::{parse_addresses, print_opening, Opts};
use std::net::Ipv4Addr;
#[test]
#[cfg(unix)]
fn batch_size_lowered() {
let opts = Opts {
batch_size: 50_000,
..Default::default()
};
let mut opts = Opts::default();
opts.batch_size = 50_000;
let batch_size = infer_batch_size(&opts, 120);
assert!(batch_size < opts.batch_size);
@@ -372,92 +414,122 @@ mod tests {
#[test]
#[cfg(unix)]
fn batch_size_lowered_average_size() {
let opts = Opts {
batch_size: 50_000,
..Default::default()
};
let mut opts = Opts::default();
opts.batch_size = 50_000;
let batch_size = infer_batch_size(&opts, 9_000);
assert_eq!(batch_size, 3_000);
assert!(batch_size == 3_000);
}
#[test]
#[cfg(unix)]
fn batch_size_equals_ulimit_lowered() {
// because ulimit and batch size are same size, batch size is lowered
// to ULIMIT - 100
let opts = Opts {
batch_size: 50_000,
..Default::default()
};
let mut opts = Opts::default();
opts.batch_size = 50_000;
let batch_size = infer_batch_size(&opts, 5_000);
assert_eq!(batch_size, 4_900);
assert!(batch_size == 4_900);
}
#[test]
#[cfg(unix)]
fn batch_size_adjusted_2000() {
// ulimit == batch_size
let opts = Opts {
batch_size: 50_000,
ulimit: Some(2_000),
..Default::default()
};
let mut opts = Opts::default();
opts.batch_size = 50_000;
opts.ulimit = Some(2_000);
let batch_size = adjust_ulimit_size(&opts);
assert_eq!(batch_size, 2_000);
assert!(batch_size == 2_000);
}
#[test]
#[cfg(unix)]
fn test_high_ulimit_no_greppable_mode() {
let opts = Opts {
batch_size: 10,
greppable: false,
..Default::default()
};
let batch_size = infer_batch_size(&opts, 1_000_000);
assert_eq!(batch_size, opts.batch_size);
}
#[test]
fn test_print_opening_no_panic() {
let opts = Opts::default();
let mut opts = Opts::default();
opts.ulimit = Some(2_000);
// 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()
};
#[cfg(unix)]
fn test_high_ulimit_no_greppable_mode() {
let mut opts = Opts::default();
opts.batch_size = 10;
opts.greppable = false;
assert_eq!(effective_batch_size(&opts), 50);
let batch_size = infer_batch_size(&opts, 1_000_000);
assert!(batch_size == opts.batch_size);
}
#[test]
#[cfg(windows)]
fn windows_batch_size_preserves_large_requested_value() {
let opts = Opts {
batch_size: 12_345,
..Default::default()
};
fn parse_correct_addresses() {
let mut opts = Opts::default();
opts.addresses = vec!["127.0.0.1".to_owned(), "192.168.0.0/30".to_owned()];
let ips = parse_addresses(&opts);
assert_eq!(effective_batch_size(&opts), 12_345);
assert_eq!(
ips,
[
Ipv4Addr::new(127, 0, 0, 1),
Ipv4Addr::new(192, 168, 0, 0),
Ipv4Addr::new(192, 168, 0, 1),
Ipv4Addr::new(192, 168, 0, 2),
Ipv4Addr::new(192, 168, 0, 3)
]
);
}
#[test]
#[cfg(windows)]
fn windows_batch_size_preserves_single_connection() {
let opts = Opts {
batch_size: 1,
..Default::default()
};
fn parse_correct_host_addresses() {
let mut opts = Opts::default();
opts.addresses = vec!["google.com".to_owned()];
let ips = parse_addresses(&opts);
assert_eq!(effective_batch_size(&opts), 1);
assert_eq!(ips.len(), 1);
}
#[test]
fn parse_correct_and_incorrect_addresses() {
let mut opts = Opts::default();
opts.addresses = vec!["127.0.0.1".to_owned(), "im_wrong".to_owned()];
let ips = parse_addresses(&opts);
assert_eq!(ips, [Ipv4Addr::new(127, 0, 0, 1),]);
}
#[test]
fn parse_incorrect_addresses() {
let mut opts = Opts::default();
opts.addresses = vec!["im_wrong".to_owned(), "300.10.1.1".to_owned()];
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 mut opts = Opts::default();
opts.addresses = vec!["fixtures/hosts.txt".to_owned()];
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 mut opts = Opts::default();
opts.addresses = vec!["fixtures/empty_hosts.txt".to_owned()];
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 mut opts = Opts::default();
opts.addresses = vec!["fixtures/naughty_string.txt".to_owned()];
let ips = parse_addresses(&opts);
assert_eq!(ips.len(), 0);
}
}
+29 -39
View File
@@ -1,8 +1,8 @@
//! Provides a means to hold configuration options specifically for port scanning.
mod range_iterator;
use crate::input::{PortRanges, ScanOrder};
use rand::rng;
use crate::input::{PortRange, ScanOrder};
use rand::seq::SliceRandom;
use rand::thread_rng;
use range_iterator::RangeIterator;
/// Represents options of port scanning.
@@ -17,23 +17,25 @@ pub enum PortStrategy {
}
impl PortStrategy {
pub fn pick(range: &Option<PortRanges>, ports: Option<Vec<u16>>, order: ScanOrder) -> Self {
pub fn pick(range: &Option<PortRange>, ports: Option<Vec<u16>>, order: ScanOrder) -> Self {
match order {
ScanOrder::Serial if ports.is_none() => {
let port_ranges = range.as_ref().unwrap();
let range = range.as_ref().unwrap();
PortStrategy::Serial(SerialRange {
range: port_ranges.0.clone(),
start: range.start,
end: range.end,
})
}
ScanOrder::Random if ports.is_none() => {
let port_ranges = range.as_ref().unwrap();
let range = range.as_ref().unwrap();
PortStrategy::Random(RandomRange {
range: port_ranges.0.clone(),
start: range.start,
end: range.end,
})
}
ScanOrder::Serial => PortStrategy::Manual(ports.unwrap()),
ScanOrder::Random => {
let mut rng = rng();
let mut rng = thread_rng();
let mut ports = ports.unwrap();
ports.shuffle(&mut rng);
PortStrategy::Manual(ports)
@@ -60,12 +62,13 @@ trait RangeOrder {
/// ascending order.
#[derive(Debug)]
pub struct SerialRange {
range: Vec<(u16, u16)>,
start: u16,
end: u16,
}
impl RangeOrder for SerialRange {
fn generate(&self) -> Vec<u16> {
RangeIterator::new_serial(&self.range).collect()
(self.start..=self.end).collect()
}
}
@@ -73,7 +76,8 @@ impl RangeOrder for SerialRange {
/// a random order. This vector is built following the LCG algorithm.
#[derive(Debug)]
pub struct RandomRange {
range: Vec<(u16, u16)>,
start: u16,
end: u16,
}
impl RangeOrder for RandomRange {
@@ -87,47 +91,33 @@ 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_random(&self.range).collect()
RangeIterator::new(self.start.into(), self.end.into()).collect()
}
}
#[cfg(test)]
mod tests {
use super::PortStrategy;
use crate::input::{PortRanges, ScanOrder};
use std::collections::HashSet;
use crate::input::{PortRange, ScanOrder};
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 ranges = PortRanges(vec![(1u16, 10u16), (20u16, 30u16), (100u16, 110u16)]);
let strategy = PortStrategy::pick(&Some(ranges.clone()), None, ScanOrder::Serial);
let range = PortRange { start: 1, end: 100 };
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Serial);
let result = strategy.order();
let expected = expected_ports_from_ranges(&ranges.0);
assert_eq!(expected, result);
let expected_range = (1..=100).into_iter().collect::<Vec<u16>>();
assert_eq!(expected_range, result);
}
#[test]
fn random_strategy_with_range() {
let ranges = PortRanges(vec![(1u16, 10u16), (20u16, 30u16), (100u16, 110u16)]);
let strategy = PortStrategy::pick(&Some(ranges.clone()), None, ScanOrder::Random);
let range = PortRange { start: 1, end: 100 };
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
let mut result = strategy.order();
let expected = expected_ports_from_ranges(&ranges.0);
let expected_range = (1..=100).into_iter().collect::<Vec<u16>>();
assert_ne!(expected_range, result);
assert_ne!(expected, result);
result.sort_unstable();
assert_eq!(expected, result);
assert_eq!(expected_range, result);
}
#[test]
@@ -141,10 +131,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 = (1..10).collect::<Vec<u16>>();
assert_ne!(expected, result);
let expected_range = (1..10).into_iter().collect::<Vec<u16>>();
assert_ne!(expected_range, result);
result.sort_unstable();
assert_eq!(expected, result);
assert_eq!(expected_range, result);
}
}
+61 -255
View File
@@ -1,196 +1,71 @@
use gcd::Gcd;
use rand::RngExt;
use rand::Rng;
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,
total: u32,
normalized_end: 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 {
/// Construct a randomized iterator (LCG permutation).
/// 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.
///
/// 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();
/// 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);
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 first = rng.random_range(0..total);
// Randomly choose a number within the range to be the first
// and assign it as a pick.
let mut rng = rand::thread_rng();
let normalized_first_pick = rng.gen_range(0, normalized_end);
Self {
active: true,
total,
normalized_first_pick: first,
normalized_pick: first,
normalized_end,
step,
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),
normalized_first_pick,
normalized_pick: normalized_first_pick,
actual_start: start,
}
}
}
impl Iterator for RangeIterator {
type Item = u16;
/// 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`.
///
// 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.
fn next(&mut self) -> Option<Self::Item> {
if !self.active {
return None;
}
// 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;
}
let current_pick = self.normalized_pick;
let next_pick = (current_pick + self.step) % self.normalized_end;
// 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 {
// 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 {
self.active = false;
}
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)
self.normalized_pick = next_pick;
Some(
(self.actual_start + current_pick)
.try_into()
.expect("Could not convert u32 to u16"),
)
}
}
@@ -208,14 +83,14 @@ fn pick_random_coprime(end: u32) -> u32 {
let range_boundary = end / 4;
let lower_range = range_boundary;
let upper_range = end - range_boundary;
let mut rng = rand::rng();
let mut candidate = rng.random_range(lower_range..upper_range);
let mut rng = rand::thread_rng();
let mut candidate = rng.gen_range(lower_range, upper_range);
for _ in 0..10 {
if end.gcd(candidate) == 1 {
return candidate;
}
candidate = rng.random_range(lower_range..upper_range);
candidate = rng.gen_range(lower_range, upper_range);
}
end - 1
@@ -223,105 +98,36 @@ fn pick_random_coprime(end: u32) -> u32 {
#[cfg(test)]
mod tests {
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
}
use super::RangeIterator;
#[test]
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);
fn range_iterator_iterates_through_the_entire_range() {
let result = generate_sorted_range(1, 10);
let expected_range = (1..=10).into_iter().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, 100);
let expected_range = (1..=100).into_iter().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, 1000);
let expected_range = (1..=1000).into_iter().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(1, 65_535);
let expected_range = (1..=65_535).into_iter().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);
let result = generate_sorted_range(1000, 2000);
let expected_range = (1000..=2000).into_iter().collect::<Vec<u16>>();
assert_eq!(expected_range, result);
}
#[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.
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();
// 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);
result
}
}
+153 -580
View File
@@ -1,49 +1,21 @@
//! 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::io;
use async_std::net::TcpStream;
use async_std::prelude::*;
use colored::Colorize;
use futures::{stream::FuturesUnordered, StreamExt};
use std::collections::BTreeMap;
use futures::stream::FuturesUnordered;
use std::{
collections::{HashMap, HashSet},
future::Future,
io,
collections::HashSet,
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
@@ -51,31 +23,19 @@ pub fn build_udp_payload_lookup(udp_map: &'static BTreeMap<Vec<u16>, Vec<u8>>) -
/// batch_size is how many ports at a time should be scanned
/// Timeout is the time RustScan should wait before declaring a port closed. As datatype Duration.
/// greppable is whether or not RustScan should print things, or wait until the end to print only the ip and open ports.
/// Added by wasuaje - 01/26/2024:
/// exclude_ports is an exclusion port list
#[cfg(not(tarpaulin_include))]
#[derive(Debug)]
pub struct Scanner {
ips: Vec<IpAddr>,
batch_size: usize,
batch_size: u16,
timeout: Duration,
tries: NonZeroU8,
greppable: bool,
port_strategy: PortStrategy,
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.
@@ -83,14 +43,13 @@ pub enum PortStatus {
impl Scanner {
pub fn new(
ips: &[IpAddr],
batch_size: usize,
batch_size: u16,
timeout: Duration,
tries: u8,
greppable: bool,
port_strategy: PortStrategy,
accessible: bool,
exclude_ports: Vec<u16>,
udp: bool,
) -> Self {
Self {
batch_size,
@@ -101,82 +60,15 @@ impl Scanner {
ips: ips.iter().map(ToOwned::to_owned).collect(),
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 sockets.
///
/// Must be awaited inside a Tokio runtime with I/O and time enabled.
/// Returns all open ports as `Vec<u16>`
/// Added by wasuaje - 01/26/2024:
/// Filtering port against exclude port list
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()
@@ -184,73 +76,32 @@ impl Scanner {
.filter(|&port| !self.exclude_ports.contains(port))
.copied()
.collect();
let mut found_sockets: Vec<PortStatus> = Vec::new();
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 errors: HashSet<String> = HashSet::new();
// Build UDP payload lookup once (only if we are scanning UDP).
// This avoids cloning a big map into every spawned future and turns
// 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) = sockets.next() {
ftrs.push(self.scan_socket(socket, udp_payloads.clone()));
if let Some(socket) = socket_iterator.next() {
ftrs.push(self.scan_socket(socket));
} 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) = sockets.next() {
ftrs.push(self.scan_socket(socket, udp_payloads.clone()));
if let Some(socket) = socket_iterator.next() {
ftrs.push(self.scan_socket(socket));
}
match result {
Ok(status) => found_sockets.push(status),
Ok(socket) => open_sockets.push(socket),
Err(e) => {
let error_string = e.to_string();
if errors.len() < self.ips.len() * 1000 {
@@ -259,6 +110,9 @@ impl Scanner {
}
}
}
debug!("Typical socket connection errors {:?}", errors);
debug!("Open Sockets found: {:?}", &open_sockets);
open_sockets
}
/// Given a socket, scan it self.tries times.
@@ -275,39 +129,31 @@ impl Scanner {
/// ```
///
/// Note: `self` must contain `self.ip`.
async fn scan_socket(
&self,
socket: SocketAddr,
udp_payloads: Option<Arc<UdpPayloadLookup>>,
) -> io::Result<PortStatus> {
if self.udp {
return self.scan_udp_socket(socket, udp_payloads).await;
}
async fn scan_socket(&self, socket: SocketAddr) -> io::Result<SocketAddr> {
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
.into_std()
.and_then(|stream| stream.shutdown(Shutdown::Both))
{
debug!("Shutdown stream error {}", e);
Ok(x) => {
debug!(
"Connection was successful, shutting down stream {}",
&socket
);
if let Err(e) = x.shutdown(Shutdown::Both) {
debug!("Shutdown stream error {}", &e);
}
if !self.greppable {
if self.accessible {
println!("Open {socket}");
} else {
println!("Open {}", socket.to_string().purple());
}
}
self.fmt_ports(socket);
debug!("Return Ok after {nr_try} tries");
return Ok(PortStatus::Open(socket));
debug!("Return Ok after {} tries", nr_try);
return Ok(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.");
@@ -315,7 +161,7 @@ impl Scanner {
if nr_try == tries {
error_string.push(' ');
error_string.push_str(&socket.ip().to_string());
return Err(io::Error::other(error_string));
return Err(io::Error::new(io::ErrorKind::Other, error_string));
}
}
};
@@ -323,30 +169,6 @@ impl Scanner {
unreachable!();
}
async fn scan_udp_socket(
&self,
socket: SocketAddr,
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(PortStatus::Open(socket)),
Ok(false) => continue,
Err(e) => return Err(e),
}
}
Err(io::Error::other(format!(
"UDP scan timed-out for all tries on socket {socket}"
)))
}
/// Performs the connection to the socket with timeout
/// # Example
///
@@ -362,381 +184,132 @@ impl Scanner {
/// ```
///
async fn connect(&self, socket: SocketAddr) -> io::Result<TcpStream> {
io_timeout(self.timeout, TcpStream::connect(socket)).await
}
/// Binds to a UDP socket so we can send and receive packets
/// # Example
///
/// ```compile_fail
/// # use std::net::{IpAddr, Ipv6Addr, SocketAddr};
/// let port: u16 = 80;
/// // ip is an IpAddr type
/// let ip = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1));
/// let socket = SocketAddr::new(ip, port);
/// scanner.udp_bind(socket);
/// // returns Result which is either Ok(stream) for port is open, or Err for port is closed.
/// // Timeout occurs after self.timeout seconds
/// ```
///
async fn udp_bind(&self, socket: SocketAddr) -> io::Result<UdpSocket> {
let local_addr = match socket {
SocketAddr::V4(_) => "0.0.0.0:0".parse::<SocketAddr>().unwrap(),
SocketAddr::V6(_) => "[::]:0".parse::<SocketAddr>().unwrap(),
};
UdpSocket::bind(local_addr).await
}
/// Performs a UDP scan on the specified socket with a payload and wait duration
/// # Example
///
/// ```compile_fail
/// # use std::net::{IpAddr, Ipv6Addr, SocketAddr};
/// # use std::time::Duration;
/// let port: u16 = 123;
/// // ip is an IpAddr type
/// let ip = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1));
/// let socket = SocketAddr::new(ip, port);
/// let payload = vec![0, 1, 2, 3];
/// let wait = Duration::from_secs(1);
/// let result = scanner.udp_scan(socket, payload, wait).await;
/// // returns Result which is either Ok(true) if response received, or Ok(false) if timed out.
/// // Err is returned for other I/O errors.
async fn udp_scan(
&self,
socket: SocketAddr,
payload: &[u8],
wait: Duration,
) -> io::Result<bool> {
match self.udp_bind(socket).await {
Ok(udp_socket) => {
let mut buf = [0u8; 1024];
udp_socket.connect(socket).await?;
udp_socket.send(payload).await?;
match io_timeout(wait, udp_socket.recv(&mut buf)).await {
Ok(size) => {
debug!("Received {size} bytes");
self.fmt_ports(socket);
Ok(true)
}
Err(e) => {
if e.kind() == io::ErrorKind::TimedOut {
Ok(false)
} else {
Err(e)
}
}
}
}
Err(e) => {
debug!("Error binding UDP socket: {e:?}");
Err(e)
}
}
}
/// Formats and prints the port status
fn fmt_ports(&self, socket: SocketAddr) {
if self.print_open_ports && !self.greppable {
if self.accessible {
println_safe(format_args!("Open {socket}"));
} else {
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()));
}
}
let stream = io::timeout(
self.timeout,
async move { TcpStream::connect(socket).await },
)
.await?;
Ok(stream)
}
}
#[cfg(test)]
mod tests {
use super::*;
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()
}
use crate::input::{PortRange, ScanOrder};
use async_std::task::block_on;
use std::{net::IpAddr, time::Duration};
#[test]
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 {
fn scanner_runs() {
// Makes sure the program still runs and doesn't panic
let addrs = vec!["127.0.0.1".parse::<IpAddr>().unwrap()];
let strategy = PortStrategy::pick(&Some(PortRanges(vec![(1, 1)])), None, ScanOrder::Serial);
Scanner::new(
let range = PortRange {
start: 1,
end: 1_000,
};
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
let scanner = Scanner::new(
&addrs,
1,
10,
Duration::from_millis(100),
1,
false,
true,
strategy,
false,
Vec::new(),
false,
)
true,
vec![9000],
);
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 library_scanner_is_quiet_by_default() {
let scanner = test_scanner();
assert!(!scanner.print_open_ports);
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],
);
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 cli_can_enable_open_port_output() {
let scanner = test_scanner().with_open_port_output();
assert!(scanner.print_open_ports);
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],
);
block_on(scanner.run());
assert_eq!(1, 1);
}
#[test]
fn closed_ports_are_not_reported_by_default() {
assert!(!test_scanner().report_closed);
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],
);
block_on(scanner.run());
assert_eq!(1, 1);
}
#[test]
fn closed_port_reporting_is_opt_in() {
assert!(test_scanner().with_closed_ports().report_closed);
}
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()];
#[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);
// 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],
);
block_on(scanner.run());
assert_eq!(1, 1);
}
}
+30 -102
View File
@@ -1,78 +1,55 @@
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> {
// 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>>,
// 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>>>,
}
/// 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 {
// `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()),
product_it: iproduct!(ports_it, ips_it),
}
}
}
impl Iterator for SocketIterator<'_> {
#[allow(clippy::doc_link_with_quotes)]
impl<'s> Iterator for SocketIterator<'s> {
type Item = SocketAddr;
/// Returns the next socket, or `None` when all combinations are exhausted.
/// Every IP is paired with the same port until the port advances.
/// 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
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;
@@ -95,53 +72,4 @@ 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
@@ -1,57 +0,0 @@
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
}
+122 -126
View File
@@ -46,9 +46,12 @@
//!
//! - `fixtures/test_rustscan_scripts.toml`
//!
//! Metadata-only test fixture:
//! Script file examples:
//!
//! - `fixtures/.rustscan_scripts/test_script.txt`
//! - `fixtures/test_script.py`
//! - `fixtures/test_script.pl`
//! - `fixtures/test_script.sh`
//! - `fixtures/test_script.txt`
//!
//! `call_format` in script files can be of 2 variants:
//!
@@ -76,80 +79,83 @@ use crate::input::ScriptsRequired;
use anyhow::{anyhow, Result};
use log::debug;
use serde_derive::{Deserialize, Serialize};
use std::collections::HashSet;
use std::convert::TryInto;
use std::fs::{self, File};
use std::io::{self, prelude::*};
use std::net::IpAddr;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::string::ToString;
use subprocess::{Exec, ExitStatus};
use text_placeholder::Template;
#[cfg(unix)]
use std::os::unix::process::ExitStatusExt;
static DEFAULT: &str = r#"tags = ["core_approved", "RustScan", "default"]
developer = [ "RustScan", "https://github.com/RustScan" ]
ports_separator = ","
call_format = "nmap -vvv -p {{port}} -{{ipversion}} {{ip}}"
call_format = "nmap -vvv -p {{port}} {{ip}}"
"#;
#[cfg(not(tarpaulin_include))]
pub fn init_scripts(scripts: &ScriptsRequired) -> Result<Vec<ScriptFile>> {
pub fn init_scripts(scripts: ScriptsRequired) -> Result<Vec<ScriptFile>> {
let mut scripts_to_run: Vec<ScriptFile> = Vec::new();
match scripts {
ScriptsRequired::None => {}
ScriptsRequired::None => Ok(scripts_to_run),
ScriptsRequired::Default => {
let default_script =
toml::from_str::<ScriptFile>(DEFAULT).expect("Failed to parse Script file.");
scripts_to_run.push(default_script);
Ok(scripts_to_run)
}
ScriptsRequired::Custom => {
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 Some(scripts_dir_base) = dirs::home_dir() else {
return Err(anyhow!("Could not infer scripts path."));
};
let script_paths = find_scripts(script_dir_base)?;
debug!("Scripts paths \n{script_paths:?}");
let script_paths = match find_scripts(scripts_dir_base) {
Ok(script_paths) => script_paths,
Err(e) => return Err(anyhow!(e)),
};
debug!("Scripts paths \n{:?}", script_paths);
let parsed_scripts = parse_scripts(script_paths);
debug!("Scripts parsed \n{parsed_scripts:?}");
debug!("Scripts parsed \n{:?}", parsed_scripts);
let script_config = match ScriptConfig::read_config() {
Ok(script_config) => script_config,
Err(e) => return Err(anyhow!(e)),
};
debug!("Script config \n{:?}", script_config);
// Only Scripts that contain all the tags found in ScriptConfig will be selected.
if let Some(config_hashset) = script_config.tags {
for script in parsed_scripts {
if let Some(script_hashset) = &script.tags {
if script_hashset
.iter()
.all(|tag| config_hashset.contains(tag))
{
scripts_to_run.push(script);
if script_config.tags.is_some() {
let config_hashset: HashSet<String> =
script_config.tags.unwrap().into_iter().collect();
for script in &parsed_scripts {
if script.tags.is_some() {
let script_hashset: HashSet<String> =
script.tags.clone().unwrap().into_iter().collect();
if config_hashset.is_subset(&script_hashset) {
scripts_to_run.push(script.clone());
} else {
debug!(
"\nScript tags does not match config tags {:?} {}",
script_hashset,
script.path.unwrap().display()
&script_hashset,
script.path.clone().unwrap().display()
);
}
}
}
}
debug!("\nScript(s) to run {scripts_to_run:?}");
debug!("\nScript(s) to run {:?}", scripts_to_run);
Ok(scripts_to_run)
}
}
Ok(scripts_to_run)
}
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);
}
@@ -187,14 +193,12 @@ struct ExecPartsScript {
script: String,
ip: String,
port: String,
ipversion: String,
}
#[derive(Serialize)]
struct ExecParts {
ip: String,
port: String,
ipversion: String,
}
impl Script {
@@ -221,7 +225,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());
@@ -249,77 +253,49 @@ impl Script {
script: self.path.unwrap().to_str().unwrap().to_string(),
ip: self.ip.to_string(),
port: ports_str,
ipversion: match &self.ip {
IpAddr::V4(_) => String::from("4"),
IpAddr::V6(_) => String::from("6"),
},
};
to_run = default_template.fill_with_struct(&exec_parts_script)?;
} else {
let exec_parts: ExecParts = ExecParts {
ip: self.ip.to_string(),
port: ports_str,
ipversion: match &self.ip {
IpAddr::V4(_) => String::from("4"),
IpAddr::V6(_) => String::from("6"),
},
};
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}");
let (cmd, arg) = if cfg!(unix) {
("sh", "-c")
} else {
("cmd.exe", "/c")
};
match Command::new(cmd)
.args([arg, script])
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.output()
{
Ok(output) => {
let status = output.status;
let es = match status.code() {
Some(code) => code,
_ => {
#[cfg(unix)]
{
status.signal().unwrap()
}
#[cfg(windows)]
{
return Err(anyhow!("Unknown exit status"));
}
}
debug!("\nScript arguments {}", script);
let process = Exec::shell(script);
match process.capture() {
Ok(c) => {
let es = match c.exit_status {
ExitStatus::Exited(c) => c.try_into().unwrap(),
ExitStatus::Signaled(c) => c.into(),
ExitStatus::Other(c) => c,
ExitStatus::Undetermined => -1,
};
if es != 0 {
return Err(anyhow!("Exit code = {}", es));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
Ok(c.stdout_str())
}
Err(error) => {
debug!("Command error {error}",);
debug!("Command error {}", error.to_string());
Err(anyhow!(error.to_string()))
}
}
}
pub fn find_scripts(path: PathBuf) -> Result<Vec<PathBuf>> {
pub fn find_scripts(mut path: PathBuf) -> Result<Vec<PathBuf>> {
path.push(".rustscan_scripts");
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 +333,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}");
debug!("Failed to parse ScriptFile headers {}", e.to_string());
None
}
}
@@ -382,7 +358,6 @@ 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))]
@@ -401,13 +376,27 @@ impl ScriptConfig {
#[cfg(test)]
mod tests {
use super::*;
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,
)
}
#[test]
fn find_and_parse_scripts() {
let scripts = find_scripts("fixtures/.rustscan_scripts".into()).unwrap();
let scripts = find_scripts("fixtures/".into()).unwrap();
let scripts = parse_scripts(scripts);
assert_eq!(scripts.len(), 1);
assert_eq!(scripts.len(), 4);
}
#[test]
@@ -423,6 +412,26 @@ 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() {
@@ -447,52 +456,39 @@ mod tests {
assert_eq!(script_f.ports_separator, Some(",".to_string()));
assert_eq!(
script_f.call_format,
Some("fixture {{ip}} {{port}}".to_string())
Some("nmap -vvv -p {{port}} {{ip}}".to_string())
);
}
#[test]
fn test_custom_directory_config() {
// Create test config
let config_str = r#"
tags = ["core_approved", "example"]
directory = "fixtures/.rustscan_scripts"
"#;
#[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");
}
let config: ScriptConfig = toml::from_str(config_str).unwrap();
#[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
assert_eq!(
config.directory,
Some("fixtures/.rustscan_scripts".to_string())
output.trim(),
"Python script ran with arguments ['fixtures/.rustscan_scripts/test_script.py', '127.0.0.1', '80,8080']"
);
// 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]
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());
#[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");
}
}
+15 -63
View File
@@ -1,55 +1,20 @@
//! 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) => {
$crate::tui::println_safe(format_args!(
"{} {}",
ansi_term::Colour::Red.bold().paint("[!]"),
$name
));
println!("{} {}", 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
$crate::tui::println_safe(format_args!("{}", $name));
println!("{}", $name);
} else {
$crate::tui::println_safe(format_args!(
"{} {}",
ansi_term::Colour::Red.bold().paint("[!]"),
$name
));
println!("{} {}", ansi_term::Colour::Red.bold().paint("[!]"), $name);
}
}
};
@@ -58,24 +23,16 @@ macro_rules! warning {
#[macro_export]
macro_rules! detail {
($name:expr) => {
$crate::tui::println_safe(format_args!(
"{} {}",
ansi_term::Colour::Blue.bold().paint("[~]"),
$name
));
println!("{} {}", 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
$crate::tui::println_safe(format_args!("{}", $name));
println!("{}", $name);
} else {
$crate::tui::println_safe(format_args!(
"{} {}",
ansi_term::Colour::Blue.bold().paint("[~]"),
$name
));
println!("{} {}", ansi_term::Colour::Blue.bold().paint("[~]"), $name);
}
}
};
@@ -84,24 +41,24 @@ macro_rules! detail {
#[macro_export]
macro_rules! output {
($name:expr) => {
$crate::tui::println_safe(format_args!(
println!(
"{} {}",
ansi_term::Colour::RGB(0, 255, 9).bold().paint("[>]"),
RGansi_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
$crate::tui::println_safe(format_args!("{}", $name));
println!("{}", $name);
} else {
$crate::tui::println_safe(format_args!(
println!(
"{} {}",
ansi_term::Colour::RGB(0, 255, 9).bold().paint("[>]"),
$name
));
);
}
}
};
@@ -111,7 +68,7 @@ macro_rules! output {
macro_rules! funny_opening {
// prints a funny quote / opening
() => {
use rand::seq::IndexedRandom;
use rand::seq::SliceRandom;
let quotes = vec![
"Nmap? More like slowmap.🐢",
"🌍HACK THE PLANET🌍",
@@ -132,18 +89,13 @@ macro_rules! funny_opening {
"TCP handshake? More like a friendly high-five!",
"Scanning ports: The virtual equivalent of knocking on doors.",
"RustScan: Making sure 'closed' isn't just a state of mind.",
"RustScan: allowing you to send UDP packets into the void 1200x faster than NMAP",
"Port scanning: Because every port has a story to tell.",
"I scanned ports so fast, even my computer was surprised.",
"Scanning ports faster than you can say 'SYN ACK'",
"RustScan: Where '404 Not Found' meets '200 OK'.",
"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();
let random_quote = quotes.choose(&mut rand::thread_rng()).unwrap();
$crate::tui::println_safe(format_args!("{}\n", random_quote));
println!("{}\n", random_quote);
};
}
+109
View File
@@ -0,0 +1,109 @@
/*
* 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),
);
}
}
-32
View File
@@ -1,32 +0,0 @@
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");
}