Compare commits

..
Author SHA1 Message Date
Autumn (Bee) bb9f88ecbc Update Cargo.toml 2024-04-22 07:42:19 +01:00
36 changed files with 1879 additions and 3579 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 -165
View File
@@ -1,179 +1,92 @@
name: CD Pipeline
on: [push]
name: Build
on:
pull_request:
push:
branches:
- master
schedule:
- cron: '00 01 * * *'
jobs:
build-nix:
env:
IN_PIPELINE: true
runs-on: ${{ matrix.os }}
if: github.ref == 'refs/heads/master'
strategy:
matrix:
type: [ubuntu-x64, ubuntu-x86, armv7, aarch64]
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@v2
- name: Build binary
uses: houseabsolute/actions-rust-cross@v1
with:
command: build
target: ${{ matrix.target }}
args: "--locked --release"
strip: true
toolchain: stable
- name: Build tar.gz for homebrew installs
if: matrix.type == 'ubuntu-x64'
run: |
tar czf ${{ matrix.name }}.tar.gz -C target/x86_64-unknown-linux-musl/release rustscan
- uses: actions/upload-artifact@v7
with:
name: ${{ matrix.name }}
path: ${{ matrix.path }}
- uses: actions/upload-artifact@v7
if: matrix.type == 'ubuntu-x64'
with:
name: ${{ matrix.name }}.tar.gz
path: ${{ matrix.name }}.tar.gz
build-deb:
needs: [build-nix]
check:
name: Check
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@v1
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/*
- name: Checkout sources
uses: actions/checkout@v4
build-macos:
env:
IN_PIPELINE: true
runs-on: macos-latest
if: github.ref == 'refs/heads/master'
steps:
- uses: actions/checkout@v7
- name: Cache cargo & target directories
uses: Swatinem/rust-cache@v2
- name: Build binary
uses: houseabsolute/actions-rust-cross@v1
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
command: build
target: x86_64-apple-darwin
args: "--locked --release"
strip: true
profile: minimal
toolchain: stable
- name: Build tar.gz for homebrew installs
run: |
tar czf x86_64-macos-rustscan.tar.gz -C target/x86_64-apple-darwin/release rustscan
- uses: actions/upload-artifact@v7
with:
name: x86_64-macos-rustscan
path: target/x86_64-apple-darwin/release/rustscan
- uses: actions/upload-artifact@v7
with:
name: x86_64-macos-rustscan.tar.gz
path: x86_64-macos-rustscan.tar.gz
build-macos-aarch64:
env:
IN_PIPELINE: true
runs-on: macos-latest
if: github.ref == 'refs/heads/master'
steps:
- uses: actions/checkout@v7
- name: Cache cargo & target directories
uses: Swatinem/rust-cache@v2
- name: Build binary
uses: houseabsolute/actions-rust-cross@v1
with:
command: build
target: aarch64-apple-darwin
args: "--locked --release"
strip: true
toolchain: stable
- name: Build tar.gz for homebrew installs
run: |
tar czf aarch64-macos-rustscan.tar.gz -C target/aarch64-apple-darwin/release rustscan
- uses: actions/upload-artifact@v7
with:
name: aarch64-macos-rustscan
path: target/aarch64-apple-darwin/release/rustscan
- uses: actions/upload-artifact@v7
with:
name: aarch64-macos-rustscan.tar.gz
path: aarch64-macos-rustscan.tar.gz
override: true
build-windows:
env:
IN_PIPELINE: true
runs-on: ${{ matrix.os }}
if: github.ref == 'refs/heads/master'
- uses: Swatinem/rust-cache@v2
- name: Run cargo check
uses: actions-rs/cargo@v1
with:
command: check
test:
name: Test Suite
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.exe
path: target\x86_64-pc-windows-msvc\release\rustscan.exe
- type: windows-x86
os: windows-latest
target: i686-pc-windows-msvc
name: x86-windows-rustscan.exe
path: target\i686-pc-windows-msvc\release\rustscan.exe
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- name: Cache cargo & target directories
uses: Swatinem/rust-cache@v2
- name: Build binary
uses: houseabsolute/actions-rust-cross@v1
- name: Checkout sources
uses: actions/checkout@v4
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
command: build
target: ${{ matrix.target }}
args: "--locked --release"
strip: true
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
steps:
- name: Checkout sources
uses: actions/checkout@v4
with:
submodules: true
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
- uses: actions/upload-artifact@v7
override: true
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- name: Run cargo fmt
uses: actions-rs/cargo@v1
with:
name: ${{ matrix.name }}
path: ${{ matrix.path }}
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
-14
View File
@@ -1,14 +0,0 @@
name: Dependabot Auto-Merge
jobs:
dependabot:
runs-on: ubuntu-latest
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Enable auto-merge for Dependabot PRs
run: |
gh pr review --approve "$PR_URL"
gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+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 }}
-34
View File
@@ -1,34 +0,0 @@
name: Test
on:
pull_request:
branches: [master]
push:
branches:
- master
jobs:
test:
name: Test Suite
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v7
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
override: true
- uses: taiki-e/install-action@nextest
- uses: Swatinem/rust-cache@v2
- name: Install Just
uses: extractions/setup-just@v4
- name: Run just
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
+1
View File
@@ -17,6 +17,7 @@ jobs:
script:
- cargo build
- cargo test
- "ulimit -n 5000; cargo test timelimits:: -- --ignored --test-threads=1 --show-output"
- cargo fmt -- --check
after_failure:
- wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh
Generated
+711 -1617
View File
File diff suppressed because it is too large. Load diff
+18 -27
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,32 +17,30 @@ exclude = [
]
[dependencies]
clap = { version = "4.6.1", features = ["derive", "wrap_help"] }
colored = "3.1.1"
async-std = "1.13.2"
colored = "2.1.0"
structopt = "0.3.20"
async-std = "1.7.0"
futures = "0.3"
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]
parameterized = "2.0.0"
criterion = { version = "0.8", features = ["html_reports"] }
wait-timeout = "0.2"
[package.metadata.deb]
depends = "$auto, nmap"
@@ -57,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 -31
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,7 +39,7 @@ Arch:
| <!--Installation Guide--> | <!--Documentation--> | <!--Discord--> |
| :----------------------------------------: | :------------------------------------: | :-------------------------: |
| :book: [Installation Guide][toc-install] | :books: [Documentation][links-table-2] | :parrot: [Discord][discord] |
| :book: [Installation Guide][links-table-1] | :books: [Documentation][links-table-2] | :parrot: [Discord][discord] |
## 🙋 Table of Contents
@@ -63,6 +47,7 @@ Arch:
- 🐋 [Docker Usage][toc-docker-usage]
- 🦜 [Discord][discord]
- 🤸 [Usage][usage-1]
- 🎪 [Community][toc-community]
# 🔭 Why RustScan?
@@ -120,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].
@@ -145,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>
@@ -193,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"
@@ -209,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"
@@ -232,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
-52
View File
@@ -1,52 +0,0 @@
use criterion::{criterion_group, criterion_main, Criterion};
use rustscan::input::{Opts, PortRange, ScanOrder};
use rustscan::port_strategy::PortStrategy;
use std::net::IpAddr;
use std::time::Duration;
fn bench_address() {
let _addrs = ["127.0.0.1".parse::<IpAddr>().unwrap()];
}
fn bench_port_strategy() {
let range = PortRange {
start: 1,
end: 1_000,
};
let _strategy = PortStrategy::pick(&Some(range.clone()), None, ScanOrder::Serial);
}
fn 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);
}
fn criterion_benchmark(c: &mut Criterion) {
// 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();
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
-234
View File
@@ -1,234 +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') {
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");
let payloads = &data[start + 1..];
payb_linenr.insert(line_nr, parser(payloads.trim()));
}
}
payb_linenr
}
/// Converts a hexadecimal string to a Vec<u8>
///
/// # Arguments
///
/// * `payload` - A string slice containing the hexadecimal payload
///
/// # Returns
///
/// A vector of bytes representing the decoded payload
fn parser(payload: &str) -> Vec<u8> {
let payload = payload.trim_matches('"');
let mut tmp_str = String::new();
let mut bytes: Vec<u8> = Vec::new();
for (idx, char) in payload.chars().enumerate() {
if char == '\\' && payload.chars().nth(idx + 1) == Some('x') {
continue;
} else if char.is_ascii_hexdigit() {
tmp_str.push(char);
if tmp_str.len() == 2 {
bytes.push(u8::from_str_radix(&tmp_str, 16).unwrap());
tmp_str.clear();
}
}
}
bytes
}
/// 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
}
+1 -3
View File
@@ -1,7 +1,5 @@
# Dockerfile for RustScan development environment
# Provides a containerized setup with Rust, nmap, and development tools
FROM rust
# Install nmap first.
RUN apt-get update -qy && apt-get install -qy nmap
# Then install rustfmt for code formatting and clippy for linting.
# Then install rustfmt and clippy for cargo.
RUN rustup component add rustfmt clippy
-5
View File
@@ -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.
-4
View File
@@ -1,4 +0,0 @@
tags = ["core_approved", "example"]
ports = ["80", "443", "8080"]
developer = ["example", "https://example.org"]
directory = "fixtures/.rustscan_scripts"
+2 -2
View File
@@ -4,7 +4,7 @@
#ports_separator = ","
#call_format = "perl {{script}} {{ip}} {{port}}"
# The Scriptfile parser stops parsing at the first blank line.
# 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
@@ -20,4 +20,4 @@ print "Total args passed to $scriptname : $total\n";
foreach my $a(@ARGV) {
print "Arg # $counter : $a\n";
$counter++;
}
}
+2 -2
View File
@@ -4,10 +4,10 @@
#trigger_port = "80"
#call_format = "python3 {{script}} {{ip}} {{port}}"
# The Scriptfile parser stops parsing at the first blank line.
# 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))
print('Python script ran with arguments', str(sys.argv))
+2 -2
View File
@@ -4,9 +4,9 @@
#ports_separator = ","
#call_format = "bash {{script}} {{ip}} {{port}}"
# The Scriptfile parser stops parsing at the first blank line.
# 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 $@
echo $@
+2 -2
View File
@@ -4,6 +4,6 @@
#ports_separator = ","
#call_format = "nmap -vvv -p {{port}} {{ip}}"
# The Scriptfile parser stops parsing at the first blank line.
# 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"
# Unused field: trigger_port = "80"
-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"
-345
View File
@@ -1,345 +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, ResolverOpts::default()).unwrap()
}
None => match Resolver::from_system_conf() {
Ok(resolver) => resolver,
Err(_) => {
Resolver::new(ResolverConfig::cloudflare_tls(), ResolverOpts::default()).unwrap()
}
},
}
}
/// 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::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);
}
}
+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
+55 -128
View File
@@ -1,30 +1,35 @@
//! 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,
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,
}
}
/// 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,
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.
@@ -58,13 +63,8 @@ fn parse_range(input: &str) -> Result<PortRange, String> {
}
}
#[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
@@ -73,73 +73,65 @@ fn parse_range(input: &str) -> Result<PortRange, 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>>,
/// A range of ports with format start-end. Example: 1-1000.
#[arg(short, long, conflicts_with = "ports", value_parser = parse_range)]
#[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 ups the ULIMIT with the value you provided.
#[arg(short, long)]
pub ulimit: Option<usize>,
#[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.
@@ -147,26 +139,18 @@ 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,
}
#[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(PortRange {
@@ -200,7 +184,7 @@ impl Opts {
merge_required!(
addresses, greppable, accessible, batch_size, timeout, tries, scan_order, scripts,
command, udp, no_banner
command
);
}
@@ -217,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);
}
}
@@ -237,16 +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,
}
}
}
@@ -258,27 +242,22 @@ impl Default for Opts {
#[derive(Debug, Deserialize)]
pub struct Config {
addresses: Option<Vec<String>>,
ports: Option<Vec<u16>>,
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>,
}
#[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.
@@ -288,20 +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
///
pub fn read(custom_config_path: Option<PathBuf>) -> Self {
let mut content = String::new();
let config_path = custom_config_path.unwrap_or_else(|| {
let path = default_config_path();
match path.exists() {
true => path,
false => old_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,
@@ -323,15 +294,6 @@ impl Config {
/// 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.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.");
};
@@ -341,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, PortRange, ScanOrder, ScriptsRequired};
impl Config {
fn default() -> Self {
Self {
@@ -359,42 +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,
}
}
}
#[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 opts_no_merge_when_config_is_ignored() {
let mut opts = Opts::default();
@@ -435,12 +364,10 @@ mod tests {
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);
}
}
+10 -17
View File
@@ -7,9 +7,7 @@
//! [`Scanner`](crate::scanner::Scanner) which in turn requires a
//! [`PortStrategy`](crate::port_strategy::PortStrategy):
//!
//! 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};
//!
@@ -23,17 +21,16 @@
//! start: 1,
//! end: 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 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 scan_result = block_on(scanner.run());
@@ -54,7 +51,3 @@ pub mod port_strategy;
pub mod benchmark;
pub mod scripts;
pub mod address;
pub mod generated;
+240 -82
View File
@@ -9,23 +9,29 @@ use rustscan::scanner::Scanner;
use rustscan::scripts::{init_scripts, Script, ScriptFile};
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
const AVERAGE_BATCH_SIZE: usize = 3000;
const AVERAGE_BATCH_SIZE: u16 = 3000;
#[macro_use]
extern crate log;
@@ -35,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");
@@ -46,9 +49,9 @@ fn main() {
let config = Config::read(opts.config_path.clone());
opts.merge(&config);
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!(
@@ -60,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);
}
@@ -78,11 +81,14 @@ fn main() {
}
#[cfg(unix)]
let batch_size: usize = infer_batch_size(&opts, adjust_ulimit_size(&opts));
let batch_size: u16 = infer_batch_size(&opts, adjust_ulimit_size(&opts));
#[cfg(not(unix))]
let batch_size: usize = AVERAGE_BATCH_SIZE;
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,
@@ -92,9 +98,8 @@ fn main() {
PortStrategy::pick(&opts.range, opts.ports, opts.scan_order),
opts.accessible,
opts.exclude_ports.unwrap_or_default(),
opts.udp,
);
debug!("Scanner finished building: {scanner:?}");
debug!("Scanner finished building: {:?}", scanner);
let mut portscan_bench = NamedTimer::start("Portscan");
let scan_result = block_on(scanner.run());
@@ -136,7 +141,7 @@ fn main() {
// if option scripts is none, no script will be spawned
if opts.greppable || opts.scripts == ScriptsRequired::None {
println!("{ip} -> [{ports_str}]");
println!("{} -> [{}]", &ip, ports_str);
continue;
}
detail!("Starting Script(s)", opts.greppable, opts.accessible);
@@ -146,7 +151,7 @@ fn main() {
// This part allows us to add commandline arguments to the Script call_format, appending them to the end of the command.
if !opts.command.is_empty() {
let user_extra_args = &opts.command.join(" ");
debug!("Extra args vec {user_extra_args:?}");
debug!("Extra args vec {:?}", user_extra_args);
if script_f.call_format.is_some() {
let mut call_f = script_f.call_format.unwrap();
call_f.push(' ');
@@ -156,7 +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);
}
}
@@ -173,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);
@@ -187,7 +192,7 @@ fn main() {
benchmarks.push(script_bench);
rustscan_bench.end();
benchmarks.push(rustscan_bench);
debug!("Benchmarks raw {benchmarks:?}");
debug!("Benchmarks raw {:?}", benchmarks);
info!("{}", benchmarks.summary());
}
@@ -195,17 +200,22 @@ fn main() {
#[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."#;
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 = r#"________________________________________
: http://discord.skerritt.blog :
: https://github.com/RustScan/RustScan :
--------------------------------------"#;
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!();
@@ -219,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}."),
@@ -255,12 +350,14 @@ fn adjust_ulimit_size(opts: &Opts) -> usize {
}
let (soft, _) = Resource::NOFILE.get().unwrap();
soft.try_into().unwrap_or(usize::MAX)
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 {
@@ -271,7 +368,7 @@ 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 {
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
@@ -280,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;
}
@@ -293,21 +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(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);
@@ -316,62 +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]
fn test_print_opening_no_panic() {
let mut opts = Opts::default();
opts.ulimit = Some(2_000);
// print opening should not panic
print_opening(&opts);
}
#[test]
#[cfg(unix)]
fn test_high_ulimit_no_greppable_mode() {
let opts = Opts {
batch_size: 10,
greppable: false,
..Default::default()
};
let mut opts = Opts::default();
opts.batch_size = 10;
opts.greppable = false;
let batch_size = infer_batch_size(&opts, 1_000_000);
assert_eq!(batch_size, opts.batch_size);
assert!(batch_size == opts.batch_size);
}
#[test]
fn test_print_opening_no_panic() {
let opts = Opts {
ulimit: Some(2_000),
..Default::default()
};
// print opening should not panic
print_opening(&opts);
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!(
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_correct_host_addresses() {
let mut opts = Opts::default();
opts.addresses = vec!["google.com".to_owned()];
let ips = parse_addresses(&opts);
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);
}
}
+5 -5
View File
@@ -1,8 +1,8 @@
//! Provides a means to hold configuration options specifically for port scanning.
mod range_iterator;
use crate::input::{PortRange, ScanOrder};
use rand::rng;
use rand::seq::SliceRandom;
use rand::thread_rng;
use range_iterator::RangeIterator;
/// Represents options of port scanning.
@@ -35,7 +35,7 @@ impl PortStrategy {
}
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)
@@ -105,7 +105,7 @@ mod tests {
let range = PortRange { start: 1, end: 100 };
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Serial);
let result = strategy.order();
let expected_range = (1..=100).collect::<Vec<u16>>();
let expected_range = (1..=100).into_iter().collect::<Vec<u16>>();
assert_eq!(expected_range, result);
}
#[test]
@@ -113,7 +113,7 @@ mod tests {
let range = PortRange { start: 1, end: 100 };
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
let mut result = strategy.order();
let expected_range = (1..=100).collect::<Vec<u16>>();
let expected_range = (1..=100).into_iter().collect::<Vec<u16>>();
assert_ne!(expected_range, result);
result.sort_unstable();
@@ -131,7 +131,7 @@ mod tests {
fn random_strategy_with_ports() {
let strategy = PortStrategy::pick(&None, Some((1..10).collect()), ScanOrder::Random);
let mut result = strategy.order();
let expected_range = (1..10).collect::<Vec<u16>>();
let expected_range = (1..10).into_iter().collect::<Vec<u16>>();
assert_ne!(expected_range, result);
result.sort_unstable();
+11 -11
View File
@@ -1,5 +1,5 @@
use gcd::Gcd;
use rand::RngExt;
use rand::Rng;
use std::convert::TryInto;
pub struct RangeIterator {
@@ -27,8 +27,8 @@ impl RangeIterator {
// Randomly choose a number within the range to be the first
// and assign it as a pick.
let mut rng = rand::rng();
let normalized_first_pick = rng.random_range(0..normalized_end);
let mut rng = rand::thread_rng();
let normalized_first_pick = rng.gen_range(0, normalized_end);
Self {
active: true,
@@ -83,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
@@ -103,23 +103,23 @@ mod tests {
#[test]
fn range_iterator_iterates_through_the_entire_range() {
let result = generate_sorted_range(1, 10);
let expected_range = (1..=10).collect::<Vec<u16>>();
let expected_range = (1..=10).into_iter().collect::<Vec<u16>>();
assert_eq!(expected_range, result);
let result = generate_sorted_range(1, 100);
let expected_range = (1..=100).collect::<Vec<u16>>();
let expected_range = (1..=100).into_iter().collect::<Vec<u16>>();
assert_eq!(expected_range, result);
let result = generate_sorted_range(1, 1000);
let expected_range = (1..=1000).collect::<Vec<u16>>();
let expected_range = (1..=1000).into_iter().collect::<Vec<u16>>();
assert_eq!(expected_range, result);
let result = generate_sorted_range(1, 65_535);
let expected_range = (1..=65_535).collect::<Vec<u16>>();
let expected_range = (1..=65_535).into_iter().collect::<Vec<u16>>();
assert_eq!(expected_range, result);
let result = generate_sorted_range(1000, 2000);
let expected_range = (1000..=2000).collect::<Vec<u16>>();
let expected_range = (1000..=2000).into_iter().collect::<Vec<u16>>();
assert_eq!(expected_range, result);
}
+145 -134
View File
@@ -1,17 +1,15 @@
//! Core functionality for actual scanning behaviour.
use crate::generated::get_parsed_data;
use crate::port_strategy::PortStrategy;
use log::debug;
mod socket_iterator;
use socket_iterator::SocketIterator;
use async_std::io;
use async_std::net::TcpStream;
use async_std::prelude::*;
use async_std::{io, net::UdpSocket};
use colored::Colorize;
use futures::stream::FuturesUnordered;
use std::collections::BTreeMap;
use std::{
collections::HashSet,
net::{IpAddr, Shutdown, SocketAddr},
@@ -25,18 +23,19 @@ use std::{
/// 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,
}
// Allowing too many arguments for clippy.
@@ -44,14 +43,13 @@ pub struct Scanner {
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,
@@ -62,13 +60,14 @@ impl Scanner {
ips: ips.iter().map(ToOwned::to_owned).collect(),
accessible,
exclude_ports,
udp,
}
}
/// Runs scan_range with chunk sizes
/// If you want to run RustScan normally, this is the entry point used
/// Returns all open ports as `Vec<u16>`
/// Added by wasuaje - 01/26/2024:
/// Filtering port against exclude port list
pub async fn run(&self) -> Vec<SocketAddr> {
let ports: Vec<u16> = self
.port_strategy
@@ -81,11 +80,10 @@ impl Scanner {
let mut open_sockets: Vec<SocketAddr> = Vec::new();
let mut ftrs = FuturesUnordered::new();
let mut errors: HashSet<String> = HashSet::new();
let udp_map = get_parsed_data();
for _ in 0..self.batch_size {
if let Some(socket) = socket_iterator.next() {
ftrs.push(self.scan_socket(socket, udp_map.clone()));
ftrs.push(self.scan_socket(socket));
} else {
break;
}
@@ -94,12 +92,12 @@ impl Scanner {
debug!("Start scanning sockets. \nBatch size {}\nNumber of ip-s {}\nNumber of ports {}\nTargets all together {} ",
self.batch_size,
self.ips.len(),
ports.len(),
&ports.len(),
(self.ips.len() * ports.len()));
while let Some(result) = ftrs.next().await {
if let Some(socket) = socket_iterator.next() {
ftrs.push(self.scan_socket(socket, udp_map.clone()));
ftrs.push(self.scan_socket(socket));
}
match result {
@@ -112,8 +110,8 @@ impl Scanner {
}
}
}
debug!("Typical socket connection errors {errors:?}");
debug!("Open Sockets found: {:?}", open_sockets);
debug!("Typical socket connection errors {:?}", errors);
debug!("Open Sockets found: {:?}", &open_sockets);
open_sockets
}
@@ -131,26 +129,28 @@ impl Scanner {
/// ```
///
/// Note: `self` must contain `self.ip`.
async fn scan_socket(
&self,
socket: SocketAddr,
udp_map: BTreeMap<Vec<u16>, Vec<u8>>,
) -> io::Result<SocketAddr> {
if self.udp {
return self.scan_udp_socket(socket, udp_map).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.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");
debug!("Return Ok after {} tries", nr_try);
return Ok(socket);
}
Err(e) => {
@@ -161,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));
}
}
};
@@ -169,32 +169,6 @@ impl Scanner {
unreachable!();
}
async fn scan_udp_socket(
&self,
socket: SocketAddr,
udp_map: BTreeMap<Vec<u16>, Vec<u8>>,
) -> io::Result<SocketAddr> {
let mut payload: Vec<u8> = Vec::new();
for (key, value) in udp_map {
if key.contains(&socket.port()) {
payload = value;
}
}
let tries = self.tries.get();
for _ in 1..=tries {
match self.udp_scan(socket, &payload, self.timeout).await {
Ok(true) => return Ok(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
///
@@ -217,88 +191,125 @@ impl Scanner {
.await?;
Ok(stream)
}
}
/// 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(),
#[cfg(test)]
mod tests {
use super::*;
use crate::input::{PortRange, ScanOrder};
use async_std::task::block_on;
use std::{net::IpAddr, time::Duration};
#[test]
fn scanner_runs() {
// Makes sure the program still runs and doesn't panic
let addrs = vec!["127.0.0.1".parse::<IpAddr>().unwrap()];
let range = PortRange {
start: 1,
end: 1_000,
};
UdpSocket::bind(local_addr).await
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);
}
/// 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) => {
println!("Err E binding sock {e:?}");
Err(e)
}
}
#[test]
fn ipv6_scanner_runs() {
// Makes sure the program still runs and doesn't panic
let addrs = vec!["::1".parse::<IpAddr>().unwrap()];
let range = PortRange {
start: 1,
end: 1_000,
};
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
let scanner = Scanner::new(
&addrs,
10,
Duration::from_millis(100),
1,
true,
strategy,
true,
vec![9000],
);
block_on(scanner.run());
// if the scan fails, it wouldn't be able to assert_eq! as it panicked!
assert_eq!(1, 1);
}
#[test]
fn quad_zero_scanner_runs() {
let addrs = vec!["0.0.0.0".parse::<IpAddr>().unwrap()];
let range = PortRange {
start: 1,
end: 1_000,
};
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
let scanner = Scanner::new(
&addrs,
10,
Duration::from_millis(100),
1,
true,
strategy,
true,
vec![9000],
);
block_on(scanner.run());
assert_eq!(1, 1);
}
#[test]
fn google_dns_runs() {
let addrs = vec!["8.8.8.8".parse::<IpAddr>().unwrap()];
let range = PortRange {
start: 400,
end: 445,
};
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
let scanner = Scanner::new(
&addrs,
10,
Duration::from_millis(100),
1,
true,
strategy,
true,
vec![9000],
);
block_on(scanner.run());
assert_eq!(1, 1);
}
#[test]
fn infer_ulimit_lowering_no_panic() {
// Test behaviour on MacOS where ulimit is not automatically lowered
let addrs = vec!["8.8.8.8".parse::<IpAddr>().unwrap()];
/// Formats and prints the port status
fn fmt_ports(&self, socket: SocketAddr) {
if !self.greppable {
if self.accessible {
println!("Open {socket}");
} else {
println!("Open {}", socket.to_string().purple());
}
}
// 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);
}
}
+1 -1
View File
@@ -30,7 +30,7 @@ impl<'s> SocketIterator<'s> {
}
#[allow(clippy::doc_link_with_quotes)]
impl Iterator for SocketIterator<'_> {
impl<'s> Iterator for SocketIterator<'s> {
type Item = SocketAddr;
/// Returns a socket based on the combination of one of the provided
+117 -124
View File
@@ -79,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);
}
@@ -190,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 {
@@ -224,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());
@@ -252,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?;
@@ -360,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
}
}
@@ -385,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))]
@@ -404,11 +376,25 @@ 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(), 4);
}
@@ -426,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() {
@@ -455,47 +461,34 @@ mod tests {
}
#[test]
fn test_custom_directory_config() {
// Create test config
let config_str = r#"
tags = ["core_approved", "example"]
directory = "fixtures/.rustscan_scripts"
"#;
let config: ScriptConfig = toml::from_str(config_str).unwrap();
assert_eq!(
config.directory,
Some("fixtures/.rustscan_scripts".to_string())
);
// Test that the directory is actually used
let script_dir_base = PathBuf::from(config.directory.unwrap());
let scripts = find_scripts(script_dir_base).unwrap();
// Verify we found the test script
assert!(scripts.iter().any(|p| p
.file_name()
.and_then(|f| f.to_str())
.map(|s| s == "test_script.txt")
.unwrap_or(false)));
#[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");
}
#[test]
fn test_default_directory_fallback() {
let config_str = r#"
tags = ["core_approved", "example"]
"#;
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!(
output.trim(),
"Python script ran with arguments ['fixtures/.rustscan_scripts/test_script.py', '127.0.0.1', '80,8080']"
);
}
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());
#[test]
#[cfg(unix)]
fn run_perl_script() {
let script_f = ScriptFile::new("fixtures/.rustscan_scripts/test_script.pl".into()).unwrap();
let script: Script = into_script(script_f);
let output = script.run().unwrap();
// output has a newline at the end by default, .trim() trims it
assert_eq!(output.trim(), "Total args passed to fixtures/.rustscan_scripts/test_script.pl : 2\nArg # 1 : 127.0.0.1\nArg # 2 : 80,8080");
}
}
+3 -8
View File
@@ -43,7 +43,7 @@ macro_rules! output {
($name:expr) => {
println!(
"{} {}",
ansi_term::Colour::RGB(0, 255, 9).bold().paint("[>]"),
RGansi_term::Colour::RGB(0, 255, 9).bold().paint("[>]"),
$name
);
};
@@ -68,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🌍",
@@ -89,17 +89,12 @@ 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();
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),
);
}
}