mirror of
https://github.com/bee-san/RustScan.git
synced 2026-11-07 09:46:04 -10:00
Compare commits
327
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70241a292e | ||
|
|
cfb8645901 | ||
|
|
d7cdad29c6 | ||
|
|
ea9e6f0736 | ||
|
|
2c854eb551 | ||
|
|
1c3fd8c378 | ||
|
|
4e64b5f00a | ||
|
|
faa21b7d14 | ||
|
|
bf74950995 | ||
|
|
c027ebc7da | ||
|
|
183eafd22e | ||
|
|
e497adf530 | ||
|
|
9ef667caed | ||
|
|
002f736073 | ||
|
|
1c2a982320 | ||
|
|
9644b3a2cd | ||
|
|
a21bbb31bb | ||
|
|
8b143f8ad7 | ||
|
|
111316f446 | ||
|
|
8be97b841d | ||
|
|
7446a42e56 | ||
|
|
e9ba3fa547 | ||
|
|
9a45d26e99 | ||
|
|
6cb99747e1 | ||
|
|
bc80fbbad4 | ||
|
|
3168580b86 | ||
|
|
2e78c22644 | ||
|
|
996f818b5e | ||
|
|
90c5c1766b | ||
|
|
f570d9ff8e | ||
|
|
42a3c87c83 | ||
|
|
1e93f44d8b | ||
|
|
bf290d7c7f | ||
|
|
160dc7a3a2 | ||
|
|
2d52f239ca | ||
|
|
5900974fda | ||
|
|
d0d388c813 | ||
|
|
5492dd729c | ||
|
|
b84f1fa046 | ||
|
|
3d4db2b6a1 | ||
|
|
1967598d96 | ||
|
|
94be5711d9 | ||
|
|
55c9f2a1c8 | ||
|
|
ce880ebe8c | ||
|
|
64f60a97ed | ||
|
|
a084c1b3b9 | ||
|
|
c668636a61 | ||
|
|
730f709fd6 | ||
|
|
e9dadb4a30 | ||
|
|
036297fbb9 | ||
|
|
e7db81a478 | ||
|
|
7c7018530b | ||
|
|
3abd1abe68 | ||
|
|
a4099a4ec2 | ||
|
|
827acd9cb0 | ||
|
|
e724d1953c | ||
|
|
f9bc5abf21 | ||
|
|
82c1a9373a | ||
|
|
45ece51c63 | ||
|
|
e234127874 | ||
|
|
053a3eb0eb | ||
|
|
b03379890e | ||
|
|
52ba449b70 | ||
|
|
79768931e3 | ||
|
|
3c3a69b06b | ||
|
|
487acbe382 | ||
|
|
8115b2d62e | ||
|
|
99c3c1a650 | ||
|
|
f366ce741d | ||
|
|
6f388ae152 | ||
|
|
a11b624644 | ||
|
|
85c080ee56 | ||
|
|
b367fbe48e | ||
|
|
50aed26d29 | ||
|
|
1ceb758acf | ||
|
|
07aa04a94d | ||
|
|
42d2f6128e | ||
|
|
f71ed28ff7 | ||
|
|
bb8aa7fa80 | ||
|
|
cd9852ae9d | ||
|
|
6cd10727ee | ||
|
|
034cbccf7e | ||
|
|
6bae2a1aa2 | ||
|
|
36e303e6ad | ||
|
|
3c2c869ba9 | ||
|
|
7f2fc87aa3 | ||
|
|
6440aed8a7 | ||
|
|
51efbca7aa | ||
|
|
c78975fa87 | ||
|
|
3a7230127f | ||
|
|
6dbf9f51cb | ||
|
|
c7a265e270 | ||
|
|
e3d31b3957 | ||
|
|
b0965dc946 | ||
|
|
2c66b4203a | ||
|
|
c8596c7679 | ||
|
|
aeef44c6e0 | ||
|
|
b228abfaa9 | ||
|
|
7fbd96be7c | ||
|
|
4a7ca7c00b | ||
|
|
60a5ae0102 | ||
|
|
1b74455a35 | ||
|
|
00d1003afa | ||
|
|
4c056a9039 | ||
|
|
a40c4ed17f | ||
|
|
59c6f10fad | ||
|
|
ec2d714e39 | ||
|
|
c19726c74f | ||
|
|
2600036621 | ||
|
|
9596712a19 | ||
|
|
fe98a3b6ef | ||
|
|
f4411e551a | ||
|
|
0640d94f53 | ||
|
|
d3508f8192 | ||
|
|
aacde90cf1 | ||
|
|
81a367e40b | ||
|
|
cc418f08b4 | ||
|
|
6781108db1 | ||
|
|
ea8015747f | ||
|
|
c09fb7bcfd | ||
|
|
6b96e84878 | ||
|
|
6f8b7f42f4 | ||
|
|
24ff49622c | ||
|
|
87399a1583 | ||
|
|
578457f6bd | ||
|
|
e315f82d03 | ||
|
|
63c9fdc05a | ||
|
|
6a1dd069b1 | ||
|
|
c89fad6c58 | ||
|
|
ac83fe7da0 | ||
|
|
3a06c22b43 | ||
|
|
9524b3503d | ||
|
|
eb11342a66 | ||
|
|
7e8230f762 | ||
|
|
d51fbb7411 | ||
|
|
56a234b41f | ||
|
|
59b645aa0e | ||
|
|
f5cbfef10f | ||
|
|
0772c4948d | ||
|
|
06e37f114e | ||
|
|
18a71f2625 | ||
|
|
48f826e545 | ||
|
|
f1006e4b3c | ||
|
|
a7257acbbb | ||
|
|
23724292e4 | ||
|
|
e1e5e7a020 | ||
|
|
2e85a6e31d | ||
|
|
9b43d1c6ce | ||
|
|
7f7de101dc | ||
|
|
c353cf5fd8 | ||
|
|
94b9c0e30a | ||
|
|
042875afec | ||
|
|
edc90a4db3 | ||
|
|
028554c18f | ||
|
|
f979926275 | ||
|
|
cf1eaf8e5c | ||
|
|
618b8aa9a9 | ||
|
|
0f23a671a5 | ||
|
|
3bdac9c9c7 | ||
|
|
1085a723c4 | ||
|
|
e1c8c254a1 | ||
|
|
05e5c50f49 | ||
|
|
f42f887da3 | ||
|
|
d5b7f4a024 | ||
|
|
c5d54880fc | ||
|
|
179d7fdfb8 | ||
|
|
2176262f22 | ||
|
|
447283cf23 | ||
|
|
8b317cec1f | ||
|
|
08ccf624c1 | ||
|
|
16e45cea2b | ||
|
|
a5c36c3f52 | ||
|
|
1124c46f37 | ||
|
|
8330e4363d | ||
|
|
08ee05ff2d | ||
|
|
34c285af75 | ||
|
|
e4eeeac6ef | ||
|
|
a70bd8b374 | ||
|
|
2884a448be | ||
|
|
6ec01bb154 | ||
|
|
c09baef4f8 | ||
|
|
cf426427ed | ||
|
|
293056ef19 | ||
|
|
2888336657 | ||
|
|
800cb3ba39 | ||
|
|
096ebbf73d | ||
|
|
5e6cc410e7 | ||
|
|
5739c6175e | ||
|
|
c134747788 | ||
|
|
2c2a55723a | ||
|
|
c5fb1b006c | ||
|
|
083a20269f | ||
|
|
815b4b5e05 | ||
|
|
b7734da3a7 | ||
|
|
36d71894b9 | ||
|
|
8a6334dfe8 | ||
|
|
e7c4205ec5 | ||
|
|
1553e58cf7 | ||
|
|
2e5e5fd71e | ||
|
|
ea2e8f6fbe | ||
|
|
d6bc7e7219 | ||
|
|
5d66529231 | ||
|
|
9e3c547615 | ||
|
|
a8c723fc68 | ||
|
|
bbb8237d19 | ||
|
|
5f8e015686 | ||
|
|
9d6bca1119 | ||
|
|
4168bf612e | ||
|
|
6a1b49e7a3 | ||
|
|
c123457734 | ||
|
|
727a64d934 | ||
|
|
b1f008a9c9 | ||
|
|
b9d5cee67a | ||
|
|
cbd0369dbd | ||
|
|
f1ef3be688 | ||
|
|
71091bfe86 | ||
|
|
e797458f43 | ||
|
|
2b409c9d33 | ||
|
|
ef8852aeff | ||
|
|
98794d8187 | ||
|
|
9221567d25 | ||
|
|
c5e518889e | ||
|
|
981c1741fe | ||
|
|
a88d311ecf | ||
|
|
c782b07041 | ||
|
|
971f2523f5 | ||
|
|
d1cd45ec29 | ||
|
|
a6213bc1a3 | ||
|
|
e1689e506c | ||
|
|
85f4eb5a30 | ||
|
|
34744424d7 | ||
|
|
ba5d51788a | ||
|
|
b17b733580 | ||
|
|
3b06a34885 | ||
|
|
e3f129e6d1 | ||
|
|
604ff2acf0 | ||
|
|
a8c52917c3 | ||
|
|
cbf0f94ca1 | ||
|
|
4a7101b2f5 | ||
|
|
07e7b30f51 | ||
|
|
43c43ae91d | ||
|
|
4f19e42ebb | ||
|
|
f1810702ab | ||
|
|
0c0433d628 | ||
|
|
de98d0044e | ||
|
|
55e388241e | ||
|
|
d969699fff | ||
|
|
4542798c7b | ||
|
|
69629c4fc3 | ||
|
|
8676de1c19 | ||
|
|
90708aec1d | ||
|
|
094a6f00e7 | ||
|
|
840daac9d2 | ||
|
|
b171ed7c88 | ||
|
|
5a9cfeeb40 | ||
|
|
6a25fb1d98 | ||
|
|
bddda4d2c8 | ||
|
|
75c7d6215b | ||
|
|
d9dec00197 | ||
|
|
1634bab1b4 | ||
|
|
c58c389b1e | ||
|
|
d150802c04 | ||
|
|
dd9e075e56 | ||
|
|
d3229ff9a9 | ||
|
|
aae43ef47e | ||
|
|
387edc7b05 | ||
|
|
bb01c3b5ee | ||
|
|
89541eddfe | ||
|
|
45537cf6cd | ||
|
|
b039db77f0 | ||
|
|
8350947caf | ||
|
|
c7776873bb | ||
|
|
306df13861 | ||
|
|
88c58f3170 | ||
|
|
4e3c2a5649 | ||
|
|
d6fa72ae34 | ||
|
|
9ce001f60f | ||
|
|
f81312d63b | ||
|
|
04224b6a72 | ||
|
|
086812f2a2 | ||
|
|
85959b8c13 | ||
|
|
e29aa7747c | ||
|
|
640003f22c | ||
|
|
66de4e7ee0 | ||
|
|
7edff6d767 | ||
|
|
5198994cbd | ||
|
|
cabeb6b1cb | ||
|
|
2956073d0d | ||
|
|
2c55f1ae58 | ||
|
|
e3bd50c922 | ||
|
|
d5ed38ef60 | ||
|
|
e4937567b8 | ||
|
|
f90d56f02b | ||
|
|
fdd74c9dab | ||
|
|
8b253edaed | ||
|
|
9ebc04cece | ||
|
|
26f387adc0 | ||
|
|
a46096dab5 | ||
|
|
9c0992dcd0 | ||
|
|
347985ea52 | ||
|
|
cafcc10b2e | ||
|
|
371ef81c7b | ||
|
|
54bc6bf191 | ||
|
|
042a1c7b6c | ||
|
|
aed0d71de8 | ||
|
|
ea428a1a22 | ||
|
|
da41cb062d | ||
|
|
0a40d1539a | ||
|
|
c3d24feebe | ||
|
|
45fa80acd4 | ||
|
|
a6f04c5c4a | ||
|
|
0a09da40aa | ||
|
|
6ffd5f741e | ||
|
|
efc42af2aa | ||
|
|
4f964b7d47 | ||
|
|
3887dbb93f | ||
|
|
9c2c03f68c | ||
|
|
d6cd9392d6 | ||
|
|
12dfefaf4d | ||
|
|
663a3df7ed | ||
|
|
7d8b36aaef | ||
|
|
4564036218 | ||
|
|
5b87f96451 | ||
|
|
033e2810f2 | ||
|
|
9f35cd43bc | ||
|
|
68e26cc0fc | ||
|
|
9c63b56142 |
No files matched your search
@@ -7,6 +7,10 @@ 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.
|
||||
|
||||
|
||||
+186
-81
@@ -1,96 +1,201 @@
|
||||
name: Build
|
||||
name: CD Pipeline
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
schedule:
|
||||
- cron: '00 01 * * *'
|
||||
- "master"
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
# Build jobs only need to read the repository. The publish job below is the
|
||||
# only job that gets write access, and it only runs for tag pushes.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo check
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
|
||||
test:
|
||||
name: Test Suite
|
||||
build-nix:
|
||||
env:
|
||||
IN_PIPELINE: true
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
rust: [stable]
|
||||
runs-on: ${{ matrix.os }}
|
||||
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:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
- uses: actions/checkout@v7
|
||||
- name: Cache cargo & target directories
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
- name: Build binary
|
||||
uses: houseabsolute/actions-rust-cross@21b0f18dc621b25bfae556ff2791fca4173121e8 # v1.0.8
|
||||
with:
|
||||
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
|
||||
command: build
|
||||
target: ${{ matrix.target }}
|
||||
args: "--locked --release"
|
||||
strip: true
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo fmt
|
||||
uses: actions-rs/cargo@v1
|
||||
- name: Package and upload artifacts
|
||||
uses: houseabsolute/actions-rust-release@9e126fd5fc18f4cecf358b3909f3991ad59663dc # v1.0.1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
executable-name: rustscan
|
||||
target: ${{ matrix.target }}
|
||||
archive-name: ${{ matrix.name }}
|
||||
changes-file: ""
|
||||
|
||||
- name: Run cargo clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
build-deb:
|
||||
needs: [build-nix]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
IN_PIPELINE: true
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Install cargo-deb
|
||||
run: cargo install -f cargo-deb
|
||||
- uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
command: clippy
|
||||
args: -- -D warnings
|
||||
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: Run rustdoc lints
|
||||
uses: actions-rs/cargo@v1
|
||||
build-macos:
|
||||
env:
|
||||
IN_PIPELINE: true
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Cache cargo & target directories
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
- name: Build binary
|
||||
uses: houseabsolute/actions-rust-cross@21b0f18dc621b25bfae556ff2791fca4173121e8 # v1.0.8
|
||||
with:
|
||||
command: doc
|
||||
args: --workspace --all-features --no-deps --document-private-items
|
||||
command: build
|
||||
target: x86_64-apple-darwin
|
||||
args: "--locked --release"
|
||||
strip: true
|
||||
toolchain: stable
|
||||
- name: Package and upload artifacts
|
||||
uses: houseabsolute/actions-rust-release@9e126fd5fc18f4cecf358b3909f3991ad59663dc # v1.0.1
|
||||
with:
|
||||
executable-name: rustscan
|
||||
target: x86_64-apple-darwin
|
||||
archive-name: x86_64-macos-rustscan
|
||||
changes-file: ""
|
||||
|
||||
build-macos-aarch64:
|
||||
env:
|
||||
IN_PIPELINE: true
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Cache cargo & target directories
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
- name: Build binary
|
||||
uses: houseabsolute/actions-rust-cross@21b0f18dc621b25bfae556ff2791fca4173121e8 # v1.0.8
|
||||
with:
|
||||
command: build
|
||||
target: aarch64-apple-darwin
|
||||
args: "--locked --release"
|
||||
strip: true
|
||||
toolchain: stable
|
||||
- name: Package and upload artifacts
|
||||
uses: houseabsolute/actions-rust-release@9e126fd5fc18f4cecf358b3909f3991ad59663dc # v1.0.1
|
||||
with:
|
||||
executable-name: rustscan
|
||||
target: aarch64-apple-darwin
|
||||
archive-name: aarch64-macos-rustscan
|
||||
changes-file: ""
|
||||
|
||||
build-windows:
|
||||
env:
|
||||
IN_PIPELINE: true
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
type: [windows-x64, windows-x86]
|
||||
include:
|
||||
- type: windows-x64
|
||||
os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
name: x86_64-windows-rustscan
|
||||
- type: windows-x86
|
||||
os: windows-latest
|
||||
target: i686-pc-windows-msvc
|
||||
name: x86-windows-rustscan
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Cache cargo & target directories
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
- name: Build binary
|
||||
uses: houseabsolute/actions-rust-cross@21b0f18dc621b25bfae556ff2791fca4173121e8 # v1.0.8
|
||||
with:
|
||||
command: build
|
||||
target: ${{ matrix.target }}
|
||||
args: "--locked --release"
|
||||
strip: true
|
||||
toolchain: stable
|
||||
- name: Package and upload artifacts
|
||||
uses: houseabsolute/actions-rust-release@9e126fd5fc18f4cecf358b3909f3991ad59663dc # v1.0.1
|
||||
with:
|
||||
executable-name: rustscan
|
||||
target: ${{ matrix.target }}
|
||||
archive-name: ${{ matrix.name }}
|
||||
changes-file: ""
|
||||
|
||||
# Runs only for tag pushes (tags can only be created by maintainers), and
|
||||
# only after every build job succeeded, so a release never ships partially.
|
||||
publish:
|
||||
if: github.ref_type == 'tag'
|
||||
needs:
|
||||
- build-nix
|
||||
- build-deb
|
||||
- build-macos
|
||||
- build-macos-aarch64
|
||||
- build-windows
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # create the release / upload release assets
|
||||
steps:
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
merge-multiple: true
|
||||
- name: Publish artifacts to GitHub release
|
||||
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
|
||||
with:
|
||||
fail_on_unmatched_files: true
|
||||
files: |
|
||||
*.deb
|
||||
*.sha256
|
||||
*.tar.gz
|
||||
*.zip
|
||||
@@ -1,38 +0,0 @@
|
||||
name: Release - Distribute 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@v6
|
||||
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 }}
|
||||
@@ -1,21 +0,0 @@
|
||||
name: Check Markdown links
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
schedule:
|
||||
# Run everyday at 9:00 AM (See https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07)
|
||||
- cron: "0 9 * * *"
|
||||
|
||||
jobs:
|
||||
markdown-link-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: gaurav-nelson/github-action-markdown-link-check@v1
|
||||
with:
|
||||
use-quiet-mode: 'yes'
|
||||
config-file: './.github/workflows/mlc_config.json'
|
||||
@@ -1,216 +0,0 @@
|
||||
name: Release - Distribute binaries
|
||||
on:
|
||||
# If we want to make release using github interface.
|
||||
# release:
|
||||
# types: [published]
|
||||
# If we want to make release by pushing new tag.
|
||||
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:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false # don't fail other jobs if one fails
|
||||
matrix:
|
||||
# aarch64-linux is failing due to python3 missing issues during tests. I'm removing it for now.
|
||||
# build: [x86_64-linux, aarch64-linux, x86_64-macos, aarch64-macos, x86_64-windows, x86_64-win-gnu]
|
||||
build: [x86_64-linux, x86_64-macos, aarch64-macos, x86_64-windows, x86_64-win-gnu]
|
||||
include:
|
||||
- build: aarch64-macos
|
||||
os: macos-latest
|
||||
rust: stable
|
||||
target: aarch64-apple-darwin
|
||||
cross: true
|
||||
# - build: aarch64-linux
|
||||
# os: ubuntu-20.04
|
||||
# rust: stable
|
||||
# target: aarch64-unknown-linux-gnu
|
||||
# cross: true
|
||||
- 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: x86_64-win-gnu
|
||||
os: windows-2019
|
||||
rust: stable-x86_64-gnu
|
||||
target: x86_64-pc-windows-gnu
|
||||
cross: false
|
||||
|
||||
steps:
|
||||
- name: Set Git config (windows only)
|
||||
if: contains(matrix.os, '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 dependencies
|
||||
if: contains(matrix.build, 'aarch64-linux') # Windows is missing some dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu python3
|
||||
|
||||
- name: Install python 3.10
|
||||
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')
|
||||
shell: bash
|
||||
run: |
|
||||
name=dev
|
||||
if [[ ${GITHUB_REF} =~ refs/tags/[0-9]+.[0-9]+.[0-9]+ ]]; then
|
||||
name=${GITHUB_REF#refs/tags/}
|
||||
fi
|
||||
echo "TAG=$name" >> $GITHUB_ENV
|
||||
id: tagname
|
||||
|
||||
- name: Build and package artifacts
|
||||
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
|
||||
cargo install cargo-deb
|
||||
cargo deb --target ${{ matrix.target }} --deb-version ${TAG}
|
||||
cp "target/${{ matrix.target }}/debian/rustscan_${TAG}_amd64.deb" "dist/" || true
|
||||
fi
|
||||
|
||||
- uses: actions/[email protected]
|
||||
with:
|
||||
name: bins-${{ matrix.build }}
|
||||
path: dist
|
||||
|
||||
package:
|
||||
name: Package
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dist
|
||||
|
||||
- name: Calculate tag name
|
||||
# if: contains(matrix.build, 'linux')
|
||||
shell: bash
|
||||
run: |
|
||||
name=dev
|
||||
if [[ ${GITHUB_REF} =~ refs/tags/[0-9]+.[0-9]+.[0-9]+ ]]; then
|
||||
name=${GITHUB_REF#refs/tags/}
|
||||
fi
|
||||
echo "TAG=$name" >> $GITHUB_ENV
|
||||
id: tagname
|
||||
|
||||
- name: Build archive
|
||||
shell: bash
|
||||
run: |
|
||||
set -ex
|
||||
rm -rf tmp
|
||||
mkdir tmp
|
||||
for dir in dist/bins-* ; do
|
||||
platform=${dir#"dist/bins-"}
|
||||
unset exe
|
||||
if [[ $platform =~ "win" ]]; then
|
||||
exe=".exe"
|
||||
fi
|
||||
pkgname=$PROJECT_NAME-$TAG-$platform
|
||||
mkdir tmp/$pkgname
|
||||
cp $dir/$BIN_NAME$exe dist/ || true
|
||||
mv $dir/$BIN_NAME$exe tmp/$pkgname
|
||||
chmod +x tmp/$pkgname/$BIN_NAME$exe
|
||||
|
||||
if [[ $platform =~ "linux" ]]; then
|
||||
mv "$dir/rustscan_${TAG}_amd64.deb" dist/ || true
|
||||
fi
|
||||
|
||||
tar cJf dist/$pkgname.tar.xz -C tmp $pkgname
|
||||
7z a dist/$pkgname.zip tmp/$pkgname
|
||||
done
|
||||
|
||||
- name: Upload binaries to release
|
||||
if: (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) || github.event_name == 'release'
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: dist/*
|
||||
file_glob: true
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
|
||||
# - 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/${{ github.ref }}/${{env.PROJECT_NAME}}-${{ github.ref }}-x86_64-macos.tar.xz'
|
||||
# commit-message: updating formula for ${{ env.PROJECT_NAME }}
|
||||
# env:
|
||||
# COMMITTER_TOKEN: ${{ secrets.COMMITTER_TOKEN }}
|
||||
|
||||
- run: cargo publish --token ${CRATES_TOKEN}
|
||||
if: (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) || github.event_name == 'release'
|
||||
env:
|
||||
CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }}
|
||||
@@ -0,0 +1,45 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test Suite
|
||||
strategy:
|
||||
matrix:
|
||||
# ubuntu-24.04-arm is the GitHub-hosted arm64 Linux runner;
|
||||
# macos-latest already runs on Apple Silicon (arm64).
|
||||
os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest]
|
||||
rust: [stable]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
components: clippy, rustfmt
|
||||
# Caching is handled by Swatinem/rust-cache below.
|
||||
cache: false
|
||||
# Keep RUSTFLAGS unset; `just test` already denies Clippy warnings.
|
||||
rustflags: ""
|
||||
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install Just
|
||||
uses: extractions/setup-just@v4
|
||||
|
||||
- name: Run tests
|
||||
run: just test
|
||||
+3
-1
@@ -1,6 +1,8 @@
|
||||
# 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
|
||||
@@ -121,4 +123,4 @@ fabric.properties
|
||||
.idea
|
||||
.idea*
|
||||
|
||||
settings.json
|
||||
settings.json
|
||||
@@ -17,12 +17,7 @@ jobs:
|
||||
script:
|
||||
- cargo build
|
||||
- cargo test
|
||||
- "ulimit -n 5000; cargo test timelimits:: -- --ignored --test-threads=1 --show-output"
|
||||
- cargo fmt -- --check
|
||||
after_failure:
|
||||
- wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh
|
||||
- chmod +x send.sh
|
||||
- "./send.sh failure $WEBHOOK_URL"
|
||||
after_success: |
|
||||
if [ -n "${GITHUB_TOKEN}" ]; then
|
||||
[ "${TRAVIS_BRANCH} " = master ] &&
|
||||
|
||||
Generated
+1318
-590
File diff suppressed because it is too large.
Load diff
+25
-18
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "rustscan"
|
||||
version = "2.2.4-rc1"
|
||||
version = "2.4.1"
|
||||
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 = "MIT"
|
||||
license = "GPL-3.0-only"
|
||||
keywords = ["port", "scanning", "nmap"]
|
||||
categories = ["command-line-utilities"]
|
||||
readme = "README.md"
|
||||
@@ -17,32 +17,32 @@ exclude = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.8", features = ["derive", "wrap_help"] }
|
||||
colored = "2.1.0"
|
||||
async-std = "1.7.0"
|
||||
clap = { version = "4.6.1", features = ["derive", "wrap_help"] }
|
||||
colored = "3.1.1"
|
||||
async-std = "1.13.2"
|
||||
futures = "0.3"
|
||||
rlimit = "0.10.1"
|
||||
log = "0.4.22"
|
||||
env_logger = "0.11.3"
|
||||
anstream = "=0.6.14"
|
||||
dirs = "5.0.1"
|
||||
rlimit = "0.11.0"
|
||||
log = "0.4.32"
|
||||
env_logger = "0.11.10"
|
||||
anstream = "=1.0.0"
|
||||
dirs = "7.0.0"
|
||||
gcd = "2.0.1"
|
||||
rand = "0.8.5"
|
||||
colorful = "0.2.1"
|
||||
rand = "0.10.1"
|
||||
colorful = "0.3.2"
|
||||
ansi_term = "0.12.1"
|
||||
toml = "0.8.14"
|
||||
toml = "1.1.2"
|
||||
serde = "1.0.124"
|
||||
serde_derive = "1.0.116"
|
||||
cidr-utils = "0.6.1"
|
||||
itertools = "0.13.0"
|
||||
hickory-resolver = { version = "0.24.0", features = ["dns-over-rustls"] }
|
||||
cidr-utils = "0.7.1"
|
||||
itertools = "0.15.0"
|
||||
hickory-resolver = { version = "0.24.3", 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"
|
||||
wait-timeout = "0.2"
|
||||
criterion = { version = "0.8", features = ["html_reports"] }
|
||||
|
||||
[package.metadata.deb]
|
||||
depends = "$auto, nmap"
|
||||
@@ -57,3 +57,10 @@ 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
@@ -1,25 +0,0 @@
|
||||
# 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.20.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" ]
|
||||
@@ -9,16 +9,6 @@
|
||||
|
||||
![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:latest`</br>[Docker][usage-0] | [Link to Documentation][kali] | `yay -S rustscan` | `brew install rustscan` |
|
||||
|
||||
---
|
||||
|
||||
</div>
|
||||
|
||||
# 🤔 What is this?
|
||||
@@ -27,6 +17,32 @@
|
||||
|
||||
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:
|
||||
|
||||
[](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**.
|
||||
@@ -39,15 +55,16 @@ The Modern Port Scanner. **Find ports quickly (3 seconds at its fastest)**. Run
|
||||
|
||||
| <!--Installation Guide--> | <!--Documentation--> | <!--Discord--> |
|
||||
| :----------------------------------------: | :------------------------------------: | :-------------------------: |
|
||||
| :book: [Installation Guide][links-table-1] | :books: [Documentation][links-table-2] | :parrot: [Discord][discord] |
|
||||
| :book: [Installation Guide][toc-install] | :books: [Documentation][links-table-2] | :parrot: [Discord][discord] |
|
||||
| | :penguin: [ARM Support](docs/arm-support.md) | |
|
||||
|
||||
## 🙋 Table of Contents
|
||||
|
||||
- 📖 [Installation Guide][toc-install]
|
||||
- 🐋 [Docker Usage][toc-docker-usage]
|
||||
- 🐧 [ARM Support](docs/arm-support.md)
|
||||
- 🦜 [Discord][discord]
|
||||
- 🤸 [Usage][usage-1]
|
||||
- 🎪 [Community][toc-community]
|
||||
|
||||
# 🔭 Why RustScan?
|
||||
|
||||
@@ -105,21 +122,6 @@ 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 +147,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>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://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://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 +195,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://crates.io/crates/rustscan/ "Homebrew install of Rustscan"
|
||||
[Homebrew]: https://formulae.brew.sh/formula/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 +211,7 @@ This project follows the [all-contributors](https://github.com/all-contributors/
|
||||
[speed-3]: https://github.com/RustScan/RustScan/wiki/Increasing-Speed-&-Accuracy "Increasing Speed & Accuracy"
|
||||
[toc-community]: https://github.com/RustScan/RustScan#-community "Community"
|
||||
[links-table-1]: https://github.com/RustScan/RustScan#-full-installation-guide "Full installation guide"
|
||||
[links-table-2]: https://rustscan.github.io/RustScan/ "Rustscan"
|
||||
[links-table-2]: https://github.com/bee-san/RustScan/wiki "Rustscan"
|
||||
[discord]: http://discord.skerritt.blog "Discord blog"
|
||||
[toc-install]: https://github.com/RustScan/RustScan/wiki/Installation-Guide "Installation Guide Wiki"
|
||||
[toc-docker-usage]: https://github.com/RustScan/RustScan/wiki/Installation-Guide#docker- "Docker Installation Guide Wiki"
|
||||
@@ -232,4 +234,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/workflows/Continuous%20integration/badge.svg?branch=master
|
||||
[badge-6]: https://github.com/RustScan/RustScan/actions/workflows/build.yml/badge.svg?branch=master
|
||||
@@ -0,0 +1,89 @@
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use rustscan::generated::get_parsed_data;
|
||||
use rustscan::input::{Opts, PortRanges, ScanOrder};
|
||||
use rustscan::port_strategy::PortStrategy;
|
||||
use rustscan::scanner::build_udp_payload_lookup;
|
||||
use std::collections::BTreeMap;
|
||||
use std::hint::black_box;
|
||||
use std::net::IpAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
fn bench_address() {
|
||||
let _addrs = ["127.0.0.1".parse::<IpAddr>().unwrap()];
|
||||
}
|
||||
|
||||
fn bench_port_strategy() {
|
||||
let range = PortRanges(vec![(1, 1_000)]);
|
||||
let _strategy = PortStrategy::pick(&Some(range.clone()), None, ScanOrder::Serial);
|
||||
}
|
||||
|
||||
fn bench_address_parsing() {
|
||||
let opts = Opts {
|
||||
addresses: vec![
|
||||
"127.0.0.1".to_owned(),
|
||||
"10.2.0.1".to_owned(),
|
||||
"192.168.0.0/24".to_owned(),
|
||||
],
|
||||
exclude_addresses: Some(vec![
|
||||
"10.0.0.0/8".to_owned(),
|
||||
"172.16.0.0/12".to_owned(),
|
||||
"192.168.0.0/16".to_owned(),
|
||||
"172.16.0.1".to_owned(),
|
||||
]),
|
||||
..Default::default()
|
||||
};
|
||||
let _ips = rustscan::address::parse_addresses(&opts);
|
||||
}
|
||||
|
||||
// Replicates the old UDP payload selection behavior:
|
||||
// scan the whole UDP payload map and find the last payload whose port list contains `port`.
|
||||
fn old_payload_for_port(udp_map: &'static BTreeMap<Vec<u16>, Vec<u8>>, port: u16) -> &'static [u8] {
|
||||
let mut payload: &'static [u8] = b"";
|
||||
for (ports, value) in udp_map.iter() {
|
||||
if ports.contains(&port) {
|
||||
payload = value.as_slice();
|
||||
}
|
||||
}
|
||||
payload
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// UDP payload lookup micro-benchmark: compares the old linear scan of the
|
||||
// payload map with the precomputed port -> payload lookup. No sockets.
|
||||
let udp_map = get_parsed_data();
|
||||
let lookup = build_udp_payload_lookup(udp_map);
|
||||
let ports: Vec<u16> = (1..=4096).collect();
|
||||
|
||||
c.bench_function("udp payload lookup/old scan map 1..4096", |b| {
|
||||
b.iter(|| {
|
||||
for &p in ports.iter() {
|
||||
let payload = old_payload_for_port(black_box(udp_map), black_box(p));
|
||||
black_box(payload);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("udp payload lookup/new hashmap 1..4096", |b| {
|
||||
b.iter(|| {
|
||||
for &p in ports.iter() {
|
||||
let payload = lookup.get(&p).copied().unwrap_or(b"");
|
||||
black_box(payload);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
@@ -0,0 +1,306 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::{self, File};
|
||||
|
||||
use std::env;
|
||||
use std::io::{BufReader, Read};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
// Reads in a file with payloads based on port
|
||||
pub fn main() {
|
||||
let mut file_path = env::current_dir().expect("cant find curr dir");
|
||||
file_path.push("./nmap-payloads");
|
||||
|
||||
let mut data = String::new();
|
||||
let file = File::open(&file_path).expect("File not found.");
|
||||
let mut file_buf = BufReader::new(file);
|
||||
file_buf
|
||||
.read_to_string(&mut data)
|
||||
.expect("unable to read file");
|
||||
|
||||
let mut fp_map: BTreeMap<i32, String> = BTreeMap::new();
|
||||
|
||||
let mut count = 0;
|
||||
let mut capturing = false;
|
||||
let mut curr = String::new();
|
||||
|
||||
for line in data.trim().split('\n') {
|
||||
// Strip a trailing carriage return so CRLF checkouts parse
|
||||
// identically to LF ones (otherwise `\r` pollutes port tokens and
|
||||
// blank lines stop looking blank).
|
||||
let line = line.strip_suffix('\r').unwrap_or(line);
|
||||
if line.contains('#') || line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if line.starts_with("udp") {
|
||||
if !curr.is_empty() {
|
||||
fp_map.insert(count, curr);
|
||||
curr = String::new();
|
||||
}
|
||||
capturing = true;
|
||||
count += 1;
|
||||
}
|
||||
|
||||
if capturing {
|
||||
if !curr.is_empty() {
|
||||
curr.push(' ');
|
||||
}
|
||||
curr.push_str(line);
|
||||
}
|
||||
}
|
||||
|
||||
let pb_linenr = ports_v(&fp_map);
|
||||
let payb_linenr = payloads_v(&fp_map);
|
||||
let map = port_payload_map(pb_linenr, payb_linenr);
|
||||
|
||||
generate_code(map);
|
||||
}
|
||||
|
||||
/// Generates a file called Generated.rs and calls cargo fmt from the command line
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `port_payload_map` - A BTreeMap mapping port numbers to payload data
|
||||
fn generate_code(port_payload_map: BTreeMap<Vec<u16>, Vec<u8>>) {
|
||||
let dest_path = PathBuf::from("src/generated.rs");
|
||||
|
||||
let mut generated_code = String::new();
|
||||
generated_code.push_str("use std::collections::BTreeMap;\n");
|
||||
generated_code.push_str("use once_cell::sync::Lazy;\n\n");
|
||||
|
||||
generated_code.push_str("fn generated_data() -> BTreeMap<Vec<u16>, Vec<u8>> {\n");
|
||||
generated_code.push_str(" let mut map = BTreeMap::new();\n");
|
||||
|
||||
for (ports, payloads) in port_payload_map {
|
||||
generated_code.push_str(" map.insert(vec![");
|
||||
generated_code.push_str(
|
||||
&ports
|
||||
.iter()
|
||||
.map(|&p| p.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
);
|
||||
generated_code.push_str("], vec![");
|
||||
generated_code.push_str(
|
||||
&payloads
|
||||
.iter()
|
||||
.map(|&p| p.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
);
|
||||
generated_code.push_str("]);\n");
|
||||
}
|
||||
|
||||
generated_code.push_str(" map\n");
|
||||
generated_code.push_str("}\n\n");
|
||||
|
||||
generated_code.push_str(
|
||||
"static PARSED_DATA: Lazy<BTreeMap<Vec<u16>, Vec<u8>>> = Lazy::new(generated_data);\n",
|
||||
);
|
||||
generated_code.push_str("pub fn get_parsed_data() -> &'static BTreeMap<Vec<u16>, Vec<u8>> {\n");
|
||||
generated_code.push_str(" &PARSED_DATA\n");
|
||||
generated_code.push_str("}\n");
|
||||
|
||||
fs::write(dest_path, generated_code).unwrap();
|
||||
|
||||
// format the generated code
|
||||
Command::new("cargo")
|
||||
.arg("fmt")
|
||||
.arg("--all")
|
||||
.output()
|
||||
.expect("Failed to execute cargo fmt");
|
||||
}
|
||||
|
||||
/// Creates a BTreeMap of line numbers mapped to a Vec<u16> of ports
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `fp_map` - A BTreeMap containing the parsed file data
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A BTreeMap where keys are line numbers and values are vectors of ports
|
||||
fn ports_v(fp_map: &BTreeMap<i32, String>) -> BTreeMap<i32, Vec<u16>> {
|
||||
let mut pb_linenr: BTreeMap<i32, Vec<u16>> = BTreeMap::new();
|
||||
let mut port_list: Vec<u16> = Vec::new();
|
||||
|
||||
for (&line_nr, ports) in fp_map {
|
||||
if ports.contains("udp ") {
|
||||
let remain = &ports[4..];
|
||||
let mut start = remain.split(' ');
|
||||
|
||||
let ports = start.next().unwrap();
|
||||
let port_segments: Vec<&str> = ports.split(',').collect();
|
||||
|
||||
for segment in port_segments {
|
||||
if segment.contains('-') {
|
||||
let range: Vec<&str> = segment.trim().split('-').collect();
|
||||
let start = range[0].parse::<u16>().unwrap();
|
||||
let end = range[1].parse::<u16>().unwrap();
|
||||
|
||||
for port in start..end {
|
||||
port_list.push(port);
|
||||
}
|
||||
} else if !segment.is_empty() {
|
||||
match segment.parse::<u16>() {
|
||||
Ok(port) => port_list.push(port),
|
||||
Err(_) => println!("Error parsing port: {segment}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pb_linenr.insert(line_nr, port_list.clone());
|
||||
port_list.clear();
|
||||
}
|
||||
|
||||
pb_linenr
|
||||
}
|
||||
|
||||
/// Parses out the Payloads into a BTreeMap of line numbers mapped to vectors of payload bytes
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `fp_map` - A BTreeMap containing the parsed file data
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A BTreeMap where keys are line numbers and values are vectors of payload bytes
|
||||
fn payloads_v(fp_map: &BTreeMap<i32, String>) -> BTreeMap<i32, Vec<u8>> {
|
||||
let mut payb_linenr: BTreeMap<i32, Vec<u8>> = BTreeMap::new();
|
||||
|
||||
for (&line_nr, data) in fp_map {
|
||||
if data.contains('"') {
|
||||
let start = data.find('"').expect("payload opening \" not found");
|
||||
// Pass from the opening quote: the decoder pairs quotes itself
|
||||
// to split multi-line segments.
|
||||
payb_linenr.insert(line_nr, parser(data[start..].trim()));
|
||||
}
|
||||
}
|
||||
|
||||
payb_linenr
|
||||
}
|
||||
|
||||
/// Converts a quoted Nmap payload block to bytes.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `payload` - The raw block text starting at its first opening quote:
|
||||
/// one or more `"..."` segments separated by whitespace
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The decoded probe bytes: segments are decoded with [`decode_segment`]
|
||||
/// and concatenated with no separators, so multi-line probes reassemble
|
||||
/// exactly.
|
||||
fn parser(payload: &str) -> Vec<u8> {
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
let mut rest = payload.trim();
|
||||
while let Some(open) = rest.find('"') {
|
||||
let after_open = &rest[open + 1..];
|
||||
match split_segment(after_open) {
|
||||
Some((segment, remainder)) => {
|
||||
decode_segment(segment, &mut bytes);
|
||||
rest = remainder;
|
||||
}
|
||||
None => {
|
||||
// Unterminated trailing quote: decode what's left literally.
|
||||
decode_segment(after_open, &mut bytes);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Splits off the first `"..."` segment: `text` must start just after an
|
||||
/// opening quote. Escaped quotes (`\"`) do not terminate the segment.
|
||||
///
|
||||
/// Returns the segment body and the remainder after the closing quote.
|
||||
fn split_segment(text: &str) -> Option<(&str, &str)> {
|
||||
let mut escaped = false;
|
||||
for (i, c) in text.char_indices() {
|
||||
if escaped {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if c == '\\' {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if c == '"' {
|
||||
return Some((&text[..i], &text[i + 1..]));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Decodes one quoted Nmap payload segment in C-style escape format:
|
||||
/// `\xNN` becomes a byte, common single-char escapes (`\\`, `\"`,
|
||||
/// `\0`, `\n`, `\r`, `\t`) decode to their value, and every other
|
||||
/// character contributes its literal bytes.
|
||||
///
|
||||
/// Preserving literal text matters: several probes embed plain-text
|
||||
/// protocol words (the SNMP `public` community string, NetBIOS names,
|
||||
/// LDAP `objectClass`, SSDP headers). The previous hexdigits-only
|
||||
/// decoding mangled them into wrong bytes, so agents never answered.
|
||||
fn decode_segment(segment: &str, bytes: &mut Vec<u8>) {
|
||||
let mut chars = segment.chars();
|
||||
while let Some(c) = chars.next() {
|
||||
if c != '\\' {
|
||||
let mut buf = [0; 4];
|
||||
bytes.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
|
||||
continue;
|
||||
}
|
||||
match chars.next() {
|
||||
Some('x') => {
|
||||
let hi = chars.next().unwrap_or('0');
|
||||
let lo = chars.next().unwrap_or('0');
|
||||
let hex: String = [hi, lo].iter().collect();
|
||||
bytes.push(u8::from_str_radix(&hex, 16).unwrap_or(0));
|
||||
}
|
||||
Some('0') => bytes.push(0),
|
||||
Some('n') => bytes.push(b'\n'),
|
||||
Some('r') => bytes.push(b'\r'),
|
||||
Some('t') => bytes.push(b'\t'),
|
||||
Some('\\') => bytes.push(b'\\'),
|
||||
Some('"') => bytes.push(b'"'),
|
||||
// Unknown escape: keep the character literally, matching Nmap,
|
||||
// which passes unrecognized escapes through untouched.
|
||||
Some(other) => {
|
||||
let mut buf = [0; 4];
|
||||
bytes.extend_from_slice(other.encode_utf8(&mut buf).as_bytes());
|
||||
}
|
||||
None => bytes.push(b'\\'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Combines the ports BTreeMap and the Payloads BTreeMap
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pb_linenr` - A BTreeMap mapping line numbers to vectors of ports
|
||||
/// * `payb_linenr` - A BTreeMap mapping line numbers to vectors of payload bytes
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A BTreeMap mapping vectors of ports to vectors of payload bytes
|
||||
fn port_payload_map(
|
||||
pb_linenr: BTreeMap<i32, Vec<u16>>,
|
||||
payb_linenr: BTreeMap<i32, Vec<u8>>,
|
||||
) -> BTreeMap<Vec<u16>, Vec<u8>> {
|
||||
let mut ppm_fin: BTreeMap<Vec<u16>, Vec<u8>> = BTreeMap::new();
|
||||
|
||||
for (port_linenr, ports) in pb_linenr {
|
||||
for (pay_linenr, payloads) in &payb_linenr {
|
||||
if pay_linenr == &port_linenr {
|
||||
ppm_fin.insert(ports.to_vec(), payloads.to_vec());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ppm_fin
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
# Dockerfile for RustScan development environment
|
||||
# Provides a containerized setup with Rust and development tools
|
||||
FROM rust
|
||||
# Install nmap first.
|
||||
RUN apt-get update -qy && apt-get install -qy nmap
|
||||
# Then install rustfmt and clippy for cargo.
|
||||
RUN rustup component add rustfmt clippy
|
||||
# Install rustfmt for code formatting and clippy for linting.
|
||||
RUN rustup component add rustfmt clippy
|
||||
+7
-2
@@ -54,10 +54,10 @@ You can now modify RustScan files with your favorite editor, once you want to co
|
||||
root@container:/rustscan# cargo build
|
||||
```
|
||||
|
||||
You are now ready to use RustScan:
|
||||
You can check the command-line interface without starting a scan:
|
||||
|
||||
```bash
|
||||
root@container:/rustscan# cargo run -- -b 2000 -t 5000 -a 127.0.0.1
|
||||
root@container:/rustscan# cargo run -- --help
|
||||
```
|
||||
|
||||
You can also format, lint with `clippy` and test the code with the following commands:
|
||||
@@ -67,3 +67,8 @@ root@container:/rustscan# cargo fmt
|
||||
root@container:/rustscan# cargo clippy
|
||||
root@container:/rustscan# cargo test
|
||||
```
|
||||
|
||||
Automated tests and benchmarks must run without network traffic or external script
|
||||
execution. Keep coverage focused on parsing, configuration, port ordering, socket
|
||||
address iteration, and timing helpers. Do not add tests that run TCP/UDP scans
|
||||
(including localhost), resolve hostnames, or launch Nmap or other scripts.
|
||||
@@ -0,0 +1,38 @@
|
||||
# ARM support
|
||||
|
||||
This page covers which ARM targets RustScan's CI builds and tests, and how to build ARM binaries yourself.
|
||||
|
||||
## What CI covers
|
||||
|
||||
| Target | Built by | Tested by |
|
||||
| --- | --- | --- |
|
||||
| `aarch64-unknown-linux-gnu` (64-bit ARM Linux) | `build-nix` job in [build.yml](../.github/workflows/build.yml) | `Test Suite (ubuntu-24.04-arm)` in [test.yml](../.github/workflows/test.yml) |
|
||||
| `armv7-unknown-linux-gnueabihf` (32-bit ARM Linux) | `build-nix` job in [build.yml](../.github/workflows/build.yml) | not tested in CI |
|
||||
| `aarch64-apple-darwin` (Apple Silicon macOS) | `build-macos-aarch64` job in [build.yml](../.github/workflows/build.yml) | `Test Suite (macos-latest)` in [test.yml](../.github/workflows/test.yml) |
|
||||
|
||||
- **Builds** use [`houseabsolute/actions-rust-cross`](https://github.com/houseabsolute/actions-rust-cross). It cross-compiles the Linux ARM targets with [`cross`](https://github.com/cross-rs/cross) on x86_64 runners.
|
||||
- **Tests** run natively on GitHub-hosted arm64 runners: `ubuntu-24.04-arm` for Linux, and `macos-latest`, which runs on Apple Silicon. They run the same `just test` recipe as every other platform.
|
||||
|
||||
## Building ARM binaries locally
|
||||
|
||||
The simplest option is `cross`, which runs the build in a container that already has the right toolchain and linker. It needs Docker or Podman.
|
||||
|
||||
```sh
|
||||
cargo install cross
|
||||
cross build --locked --release --target aarch64-unknown-linux-gnu
|
||||
# or, for 32-bit ARM:
|
||||
cross build --locked --release --target armv7-unknown-linux-gnueabihf
|
||||
```
|
||||
|
||||
The binary ends up at `target/<target>/release/rustscan`.
|
||||
|
||||
On an ARM machine (for example a Raspberry Pi running a 64-bit OS, or an Apple Silicon Mac) you don't need `cross`; a normal native build works:
|
||||
|
||||
```sh
|
||||
cargo build --locked --release
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **`error[E0463]: can't find crate for std`, or linker errors:** you're cross-compiling with plain `cargo`. Either use `cross`, or install the target (`rustup target add <target>`) along with a matching cross linker.
|
||||
- **`cross` can't pull its image:** check that Docker or Podman is running and that your user can reach it.
|
||||
@@ -0,0 +1,4 @@
|
||||
tags = ["core_approved", "example"]
|
||||
ports = ["80", "443", "8080"]
|
||||
developer = ["example", "https://example.org"]
|
||||
directory = "fixtures/.rustscan_scripts"
|
||||
@@ -1,23 +0,0 @@
|
||||
#!/usr/bin/perl
|
||||
#tags = ["core_approved", "example",]
|
||||
#developer = [ "example", "https://example.org" ]
|
||||
#ports_separator = ","
|
||||
#call_format = "perl {{script}} {{ip}} {{port}}"
|
||||
|
||||
# Sriptfile parser stops at the first blank line with parsing.
|
||||
# This script will run itself as an argument with the system installed perl interpreter, ports will be concatenated with "," .
|
||||
# Unused field: trigger_port = "80"
|
||||
# get total arg passed to this script
|
||||
my $total = $#ARGV + 1;
|
||||
my $counter = 1;
|
||||
|
||||
# get script name
|
||||
my $scriptname = $0;
|
||||
|
||||
print "Total args passed to $scriptname : $total\n";
|
||||
|
||||
# Use loop to print all args stored in an array called @ARGV
|
||||
foreach my $a(@ARGV) {
|
||||
print "Arg # $counter : $a\n";
|
||||
$counter++;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
#tags = ["core_approved", "example",]
|
||||
#developer = [ "example", "https://example.org" ]
|
||||
#trigger_port = "80"
|
||||
#call_format = "python3 {{script}} {{ip}} {{port}}"
|
||||
|
||||
# Sriptfile parser stops at the first blank line with parsing.
|
||||
# This script will run itself as an argument with the system installed python interpreter, only scanning port 80.
|
||||
# Unused filed: ports_separator = ","
|
||||
|
||||
import sys
|
||||
|
||||
print('Python script ran with arguments', str(sys.argv))
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
#tags = ["core_approved", "example",]
|
||||
#developer = [ "example", "https://example.org" ]
|
||||
#ports_separator = ","
|
||||
#call_format = "bash {{script}} {{ip}} {{port}}"
|
||||
|
||||
# Sriptfile parser stops at the first blank line with parsing.
|
||||
# This script will run itself as an argument with the system installed bash interpreter, scanning all ports concatenated with "," .
|
||||
# Unused filed: trigger_port = "80"
|
||||
|
||||
# print all arguments passed to the script
|
||||
echo $@
|
||||
@@ -1,9 +1,7 @@
|
||||
#!intentional_blank_line
|
||||
Metadata-only test fixture; not executable.
|
||||
#tags = ["core_approved", "example"]
|
||||
#developer = [ "example", "https://example.org" ]
|
||||
#ports_separator = ","
|
||||
#call_format = "nmap -vvv -p {{port}} {{ip}}"
|
||||
#call_format = "fixture {{ip}} {{port}}"
|
||||
|
||||
# Scriptfile parser stops at the first blank line with parsing.
|
||||
# This script will run the system installed nmap, ports will be concatenated with "," .
|
||||
# Unused field: trigger_port = "80"
|
||||
The parser reads only the metadata above. This file contains no script body.
|
||||
@@ -1,7 +1,7 @@
|
||||
#!intentional_blank_line
|
||||
Metadata-only test fixture; not executable.
|
||||
#tags = "core_approved"
|
||||
#developer = [ "example", "https://example.org" ]
|
||||
#ports_separator = ","
|
||||
#call_format = "nmap -vvv -p {{port}} {{ip}}"
|
||||
#call_format = "fixture {{ip}} {{port}}"
|
||||
|
||||
# tags has to be an array, thus this file won't be parsed
|
||||
# tags has to be an array, thus this file won't be parsed
|
||||
Whitespace-only changes.
@@ -1,6 +0,0 @@
|
||||
127.0.0.1
|
||||
google.com
|
||||
example.com
|
||||
66666666666666.666666666666666666.66666666666666666.6666666666
|
||||
....
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.radiatorFixtures
|
||||
@@ -0,0 +1,18 @@
|
||||
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
@@ -0,0 +1,500 @@
|
||||
# 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"
|
||||
+253
-63
@@ -1,11 +1,12 @@
|
||||
//! 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;
|
||||
use cidr_utils::cidr::{IpCidr, IpInet};
|
||||
use hickory_resolver::{
|
||||
config::{NameServerConfig, Protocol, ResolverConfig, ResolverOpts},
|
||||
Resolver,
|
||||
@@ -27,6 +28,8 @@ use crate::warning;
|
||||
///
|
||||
/// 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();
|
||||
@@ -66,6 +69,12 @@ pub fn parse_addresses(input: &Opts) -> Vec<IpAddr> {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -83,16 +92,21 @@ pub fn parse_addresses(input: &Opts) -> Vec<IpAddr> {
|
||||
/// let ips = parse_address("127.0.0.1", &Resolver::default().unwrap());
|
||||
/// ```
|
||||
pub fn parse_address(address: &str, resolver: &Resolver) -> Vec<IpAddr> {
|
||||
IpCidr::from_str(address)
|
||||
.map(|cidr| cidr.iter().map(|c| c.address()).collect())
|
||||
.ok()
|
||||
.or_else(|| {
|
||||
format!("{}:{}", &address, 80)
|
||||
.to_socket_addrs()
|
||||
.ok()
|
||||
.map(|mut iter| vec![iter.next().unwrap().ip()])
|
||||
})
|
||||
.unwrap_or_else(|| resolve_ips_from_host(address, resolver))
|
||||
if let Ok(addr) = IpAddr::from_str(address) {
|
||||
// `address` is an IP string
|
||||
vec![addr]
|
||||
} else if let Ok(net_addr) = IpInet::from_str(address) {
|
||||
// `address` is a CIDR string
|
||||
net_addr.network().into_iter().addresses().collect()
|
||||
} else {
|
||||
// `address` is a hostname or DNS name
|
||||
// attempt default DNS lookup
|
||||
match format!("{address}:80").to_socket_addrs() {
|
||||
Ok(mut iter) => vec![iter.next().unwrap().ip()],
|
||||
// default lookup didn't work, so try again with the dedicated resolver
|
||||
Err(_) => resolve_ips_from_host(address, resolver),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses DNS to get the IPS associated with host
|
||||
@@ -110,6 +124,46 @@ fn resolve_ips_from_host(source: &str, backup_resolver: &Resolver) -> Vec<IpAddr
|
||||
ips
|
||||
}
|
||||
|
||||
/// Parses excluded networks from a list of addresses.
|
||||
///
|
||||
/// This function handles three types of inputs:
|
||||
/// 1. CIDR notation (e.g. "192.168.0.0/24")
|
||||
/// 2. Single IP addresses (e.g. "192.168.0.1")
|
||||
/// 3. Hostnames that need to be resolved (e.g. "example.com")
|
||||
///
|
||||
/// ```rust
|
||||
/// # use rustscan::address::parse_excluded_networks;
|
||||
/// # use hickory_resolver::Resolver;
|
||||
/// let resolver = Resolver::default().unwrap();
|
||||
/// let excluded = parse_excluded_networks(&Some(vec!["192.168.0.0/24".to_owned()]), &resolver);
|
||||
/// ```
|
||||
pub fn parse_excluded_networks(
|
||||
exclude_addresses: &Option<Vec<String>>,
|
||||
resolver: &Resolver,
|
||||
) -> Vec<IpCidr> {
|
||||
exclude_addresses
|
||||
.iter()
|
||||
.flatten()
|
||||
.flat_map(|addr| parse_single_excluded_address(addr, resolver))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Parses a single address into an IpCidr, handling CIDR notation, IP addresses, and hostnames.
|
||||
fn parse_single_excluded_address(addr: &str, resolver: &Resolver) -> Vec<IpCidr> {
|
||||
if let Ok(cidr) = IpCidr::from_str(addr) {
|
||||
return vec![cidr];
|
||||
}
|
||||
|
||||
if let Ok(ip) = IpAddr::from_str(addr) {
|
||||
return vec![IpCidr::new_host(ip)];
|
||||
}
|
||||
|
||||
resolve_ips_from_host(addr, resolver)
|
||||
.into_iter()
|
||||
.map(IpCidr::new_host)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Derive a DNS resolver.
|
||||
///
|
||||
/// 1. if the `resolver` parameter has been set:
|
||||
@@ -137,17 +191,56 @@ fn get_resolver(resolver: &Option<String>) -> Resolver {
|
||||
Protocol::Udp,
|
||||
));
|
||||
}
|
||||
Resolver::new(config, ResolverOpts::default()).unwrap()
|
||||
Resolver::new(config, resolver_opts()).unwrap()
|
||||
}
|
||||
None => match Resolver::from_system_conf() {
|
||||
None => match system_resolver() {
|
||||
Ok(resolver) => resolver,
|
||||
Err(_) => {
|
||||
Resolver::new(ResolverConfig::cloudflare_tls(), ResolverOpts::default()).unwrap()
|
||||
}
|
||||
Err(_) => Resolver::new(ResolverConfig::cloudflare_tls(), resolver_opts()).unwrap(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// `true` on Windows when the `SystemRoot` environment variable is unset.
|
||||
///
|
||||
/// hickory-resolver locates the hosts file via
|
||||
/// `std::env::var_os("SystemRoot").expect(...)`, which panics when the
|
||||
/// variable is missing. Processes spawned with a minimal environment
|
||||
/// (services, scheduled tasks, WMI) can lack `SystemRoot`, and the panic is
|
||||
/// fatal under this crate's `panic = "abort"` release profile.
|
||||
fn windows_system_root_missing() -> bool {
|
||||
cfg!(windows) && std::env::var_os("SystemRoot").is_none()
|
||||
}
|
||||
|
||||
/// Derives a resolver from the system configuration, e.g. `/etc/resolv.conf`
|
||||
/// on *nix or the registry on Windows.
|
||||
///
|
||||
/// Returns an error without touching the system configuration when doing so
|
||||
/// would panic inside hickory-resolver (see [`windows_system_root_missing`]).
|
||||
fn system_resolver() -> std::io::Result<Resolver> {
|
||||
if windows_system_root_missing() {
|
||||
debug!("SystemRoot is not set; skipping system resolver configuration");
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
"SystemRoot environment variable is not set",
|
||||
));
|
||||
}
|
||||
|
||||
Resolver::from_system_conf()
|
||||
}
|
||||
|
||||
/// Resolver options that are safe to use in the current environment.
|
||||
///
|
||||
/// hickory-resolver eagerly loads the hosts file when `use_hosts_file` is set
|
||||
/// (the default), which hits the same missing-`SystemRoot` panic described in
|
||||
/// [`windows_system_root_missing`]; disable it in that case.
|
||||
fn resolver_opts() -> ResolverOpts {
|
||||
let mut opts = ResolverOpts::default();
|
||||
if windows_system_root_missing() {
|
||||
opts.use_hosts_file = false;
|
||||
}
|
||||
opts
|
||||
}
|
||||
|
||||
/// Parses and input file of IPs for use in DNS resolution.
|
||||
fn read_resolver_from_file(path: &str) -> Result<Vec<IpAddr>, std::io::Error> {
|
||||
let ips = fs::read_to_string(path)?
|
||||
@@ -182,13 +275,16 @@ fn read_ips_from_file(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{get_resolver, parse_addresses, Opts};
|
||||
use std::net::Ipv4Addr;
|
||||
use super::{parse_addresses, Opts};
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
#[test]
|
||||
fn parse_correct_addresses() {
|
||||
let mut opts = Opts::default();
|
||||
opts.addresses = vec!["127.0.0.1".to_owned(), "192.168.0.0/30".to_owned()];
|
||||
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!(
|
||||
@@ -204,78 +300,172 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_correct_host_addresses() {
|
||||
let mut opts = Opts::default();
|
||||
opts.addresses = vec!["google.com".to_owned()];
|
||||
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.len(), 1);
|
||||
assert_eq!(
|
||||
ips,
|
||||
[
|
||||
Ipv4Addr::new(192, 168, 0, 0),
|
||||
Ipv4Addr::new(192, 168, 0, 2),
|
||||
Ipv4Addr::new(192, 168, 0, 3)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[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()];
|
||||
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(127, 0, 0, 1),]);
|
||||
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_incorrect_addresses() {
|
||||
let mut opts = Opts::default();
|
||||
opts.addresses = vec!["im_wrong".to_owned(), "300.10.1.1".to_owned()];
|
||||
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!(ips.is_empty());
|
||||
assert_eq!(
|
||||
ips,
|
||||
[
|
||||
Ipv4Addr::new(192, 168, 0, 0),
|
||||
Ipv4Addr::new(192, 168, 0, 2),
|
||||
Ipv4Addr::new(192, 168, 0, 3)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[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()];
|
||||
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(), 3);
|
||||
|
||||
assert_eq!(ips.len(), 2_048);
|
||||
}
|
||||
|
||||
#[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()];
|
||||
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(), 0);
|
||||
|
||||
assert_eq!(ips.len(), 256);
|
||||
}
|
||||
|
||||
#[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()];
|
||||
fn parse_non_canonical_cidr_mid_block() {
|
||||
// 192.168.1.13/29: .13 = 0000 1101, mask clears last 3 bits → .8 = 0000 1000
|
||||
// network is 192.168.1.8/29, spanning .8 through .15
|
||||
let opts = Opts {
|
||||
addresses: vec!["192.168.1.13/29".to_owned()],
|
||||
..Default::default()
|
||||
};
|
||||
let ips = parse_addresses(&opts);
|
||||
assert_eq!(ips.len(), 0);
|
||||
assert_eq!(
|
||||
ips,
|
||||
[
|
||||
Ipv4Addr::new(192, 168, 1, 8),
|
||||
Ipv4Addr::new(192, 168, 1, 9),
|
||||
Ipv4Addr::new(192, 168, 1, 10),
|
||||
Ipv4Addr::new(192, 168, 1, 11),
|
||||
Ipv4Addr::new(192, 168, 1, 12),
|
||||
Ipv4Addr::new(192, 168, 1, 13),
|
||||
Ipv4Addr::new(192, 168, 1, 14),
|
||||
Ipv4Addr::new(192, 168, 1, 15),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolver_default_cloudflare() {
|
||||
let opts = Opts::default();
|
||||
|
||||
let resolver = get_resolver(&opts.resolver);
|
||||
let lookup = resolver.lookup_ip("www.example.com.").unwrap();
|
||||
|
||||
assert!(opts.resolver.is_none());
|
||||
assert!(lookup.iter().next().is_some());
|
||||
fn parse_non_canonical_cidr_last_in_block() {
|
||||
// 192.168.1.15/29: last address in the block, should still resolve to same .8–.15 network
|
||||
let opts = Opts {
|
||||
addresses: vec!["192.168.1.15/29".to_owned()],
|
||||
..Default::default()
|
||||
};
|
||||
let ips = parse_addresses(&opts);
|
||||
assert_eq!(
|
||||
ips,
|
||||
[
|
||||
Ipv4Addr::new(192, 168, 1, 8),
|
||||
Ipv4Addr::new(192, 168, 1, 9),
|
||||
Ipv4Addr::new(192, 168, 1, 10),
|
||||
Ipv4Addr::new(192, 168, 1, 11),
|
||||
Ipv4Addr::new(192, 168, 1, 12),
|
||||
Ipv4Addr::new(192, 168, 1, 13),
|
||||
Ipv4Addr::new(192, 168, 1, 14),
|
||||
Ipv4Addr::new(192, 168, 1, 15),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolver_args_google_dns() {
|
||||
let mut opts = Opts::default();
|
||||
// https://developers.google.com/speed/public-dns
|
||||
opts.resolver = Some("8.8.8.8,8.8.4.4".to_owned());
|
||||
fn parse_non_canonical_cidr_crosses_third_octet() {
|
||||
// 192.168.1.5/23: host bits span into the third octet
|
||||
// .1.5 in 23-bit context → network is 192.168.0.0/23, spanning .0.0 through .1.255 (512 addresses)
|
||||
let opts = Opts {
|
||||
addresses: vec!["192.168.1.5/23".to_owned()],
|
||||
..Default::default()
|
||||
};
|
||||
let ips = parse_addresses(&opts);
|
||||
assert_eq!(
|
||||
ips.first(),
|
||||
Some(&IpAddr::V4(Ipv4Addr::new(192, 168, 0, 0)))
|
||||
);
|
||||
assert_eq!(
|
||||
ips.last(),
|
||||
Some(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 255)))
|
||||
);
|
||||
assert_eq!(ips.len(), 512);
|
||||
}
|
||||
|
||||
let resolver = get_resolver(&opts.resolver);
|
||||
let lookup = resolver.lookup_ip("www.example.com.").unwrap();
|
||||
|
||||
assert!(lookup.iter().next().is_some());
|
||||
#[test]
|
||||
fn parse_non_canonical_cidr_slash30() {
|
||||
// 10.0.0.7/30: .7 = 0000 0111, mask clears last 2 bits → .4 = 0000 0100
|
||||
// network is 10.0.0.4/30, spanning .4 through .7
|
||||
let opts = Opts {
|
||||
addresses: vec!["10.0.0.7/30".to_owned()],
|
||||
..Default::default()
|
||||
};
|
||||
let ips = parse_addresses(&opts);
|
||||
assert_eq!(
|
||||
ips,
|
||||
[
|
||||
Ipv4Addr::new(10, 0, 0, 4),
|
||||
Ipv4Addr::new(10, 0, 0, 5),
|
||||
Ipv4Addr::new(10, 0, 0, 6),
|
||||
Ipv4Addr::new(10, 0, 0, 7),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -42,13 +42,11 @@ impl Benchmark {
|
||||
let mut summary = String::from("\nRustScan Benchmark Summary");
|
||||
|
||||
for timer in &self.named_timers {
|
||||
if timer.start.is_some() && timer.end.is_some() {
|
||||
let runtime_secs = timer
|
||||
.end
|
||||
.unwrap()
|
||||
.saturating_duration_since(timer.start.unwrap())
|
||||
.as_secs_f32();
|
||||
summary.push_str(&format!("\n{0: <10} | {1: <10}s", timer.name, runtime_secs));
|
||||
if let Some(start) = timer.start {
|
||||
if let Some(end) = timer.end {
|
||||
let runtime_secs = end.saturating_duration_since(start).as_secs_f32();
|
||||
summary.push_str(&format!("\n{0: <10} | {1: <10}s", timer.name, runtime_secs));
|
||||
}
|
||||
}
|
||||
}
|
||||
summary
|
||||
|
||||
+470
-60
@@ -1,7 +1,9 @@
|
||||
//! Provides a means to read, parse and hold configuration options for scans.
|
||||
use clap::{Parser, ValueEnum};
|
||||
use clap::parser::ValueSource;
|
||||
use clap::{CommandFactory, FromArgMatches, Parser, ValueEnum};
|
||||
use serde_derive::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -28,35 +30,94 @@ pub enum ScriptsRequired {
|
||||
Custom,
|
||||
}
|
||||
|
||||
/// Represents the range of ports to be scanned.
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PortRange {
|
||||
pub start: u16,
|
||||
pub end: u16,
|
||||
/// The port ranges to scan, as inclusive `(start, end)` pairs.
|
||||
///
|
||||
/// Parsed from `-r/--range` (e.g. `1-500,1000-2500`) and from the `range`
|
||||
/// key of the configuration file.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PortRanges(pub Vec<(u16, u16)>);
|
||||
|
||||
/// Accepted spellings of `range` in the configuration file.
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum PortRangesConfig {
|
||||
/// `range = { start = 1, end = 1000 }`: the format used before multiple
|
||||
/// ranges were supported, kept so existing configuration files still work.
|
||||
Single { start: u16, end: u16 },
|
||||
/// `range = "1-500,1000-2500"`: the same syntax as `--range`.
|
||||
Text(String),
|
||||
/// `range = [[1, 500], [1000, 2500]]`.
|
||||
Pairs(Vec<(u16, u16)>),
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for PortRanges {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let pairs = match PortRangesConfig::deserialize(deserializer)? {
|
||||
PortRangesConfig::Single { start, end } => vec![(start, end)],
|
||||
PortRangesConfig::Text(text) => {
|
||||
return parse_ranges(&text).map_err(serde::de::Error::custom)
|
||||
}
|
||||
PortRangesConfig::Pairs(pairs) => pairs,
|
||||
};
|
||||
|
||||
if pairs.is_empty() {
|
||||
return Err(serde::de::Error::custom("expected at least one port range"));
|
||||
}
|
||||
if let Some(&(start, end)) = pairs.iter().find(|&&(start, end)| start > end) {
|
||||
return Err(serde::de::Error::custom(format!(
|
||||
"invalid port range {start}-{end}: start must not be greater than end"
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(PortRanges(pairs))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn parse_range(input: &str) -> Result<PortRange, String> {
|
||||
let range = input
|
||||
.split('-')
|
||||
.map(str::parse)
|
||||
.collect::<Result<Vec<u16>, std::num::ParseIntError>>();
|
||||
/// Parse a single `start-end` token (e.g. "100-200") into `(start, end)`.
|
||||
/// Returns `None` when the token is malformed, cannot be parsed as `u16`,
|
||||
/// or `start > end`.
|
||||
fn parse_range(input: &str) -> Option<(u16, u16)> {
|
||||
let mut parts = input.trim().splitn(2, '-').map(str::trim);
|
||||
let a = parts.next()?;
|
||||
let b = parts.next()?;
|
||||
|
||||
if range.is_err() {
|
||||
return Err(String::from(
|
||||
"the range format must be 'start-end'. Example: 1-1000.",
|
||||
));
|
||||
let start = a.parse::<u16>().ok()?;
|
||||
let end = b.parse::<u16>().ok()?;
|
||||
|
||||
if start <= end {
|
||||
Some((start, end))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
/// Parse a comma-separated list of `start-end` ranges into `PortRanges`.
|
||||
///
|
||||
/// Errors with a helpful message identifying the bad token.
|
||||
fn parse_ranges(input: &str) -> Result<PortRanges, String> {
|
||||
let s = input.trim();
|
||||
if s.is_empty() {
|
||||
return Err("empty input: expected one or more comma-separated 'start-end' pairs".into());
|
||||
}
|
||||
|
||||
match range.unwrap().as_slice() {
|
||||
[start, end] => Ok(PortRange {
|
||||
start: *start,
|
||||
end: *end,
|
||||
}),
|
||||
_ => Err(String::from(
|
||||
"the range format must be 'start-end'. Example: 1-1000.",
|
||||
)),
|
||||
}
|
||||
let ranges_res: Result<Vec<(u16, u16)>, String> = s
|
||||
.split(',')
|
||||
.map(|token| {
|
||||
let t = token.trim();
|
||||
parse_range(t).ok_or_else(|| {
|
||||
format!(
|
||||
"invalid range token `{}` — expected `start-end` with 0 <= start <= end <= 65535",
|
||||
t
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
ranges_res.map(PortRanges)
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
@@ -81,14 +142,18 @@ pub struct Opts {
|
||||
#[arg(short, long, value_delimiter = ',')]
|
||||
pub ports: Option<Vec<u16>>,
|
||||
|
||||
/// A range of ports with format start-end. Example: 1-1000.
|
||||
#[arg(short, long, conflicts_with = "ports", value_parser = parse_range)]
|
||||
pub range: Option<PortRange>,
|
||||
/// Ranges of ports as comma-separated start-end pairs. Example: 1-500,1000-2500,4000-7000
|
||||
#[arg(short, long, conflicts_with = "ports", value_parser = parse_ranges)]
|
||||
pub range: Option<PortRanges>,
|
||||
|
||||
/// Whether to ignore the configuration file or not.
|
||||
#[arg(short, long)]
|
||||
pub no_config: bool,
|
||||
|
||||
/// Hide the banner
|
||||
#[arg(long)]
|
||||
pub no_banner: bool,
|
||||
|
||||
/// Custom path to config file
|
||||
#[arg(short, long, value_parser)]
|
||||
pub config_path: Option<PathBuf>,
|
||||
@@ -110,7 +175,7 @@ pub struct Opts {
|
||||
/// it will do every port at the same time. Although, your OS may not
|
||||
/// support this.
|
||||
#[arg(short, long, default_value = "4500")]
|
||||
pub batch_size: u16,
|
||||
pub batch_size: usize,
|
||||
|
||||
/// The timeout in milliseconds before a port is assumed to be closed.
|
||||
#[arg(short, long, default_value = "1500")]
|
||||
@@ -121,9 +186,10 @@ pub struct Opts {
|
||||
#[arg(long, default_value = "1")]
|
||||
pub tries: u8,
|
||||
|
||||
/// Automatically ups the ULIMIT with the value you provided.
|
||||
/// Automatically increases the Unix file-descriptor limit.
|
||||
#[cfg_attr(not(unix), arg(hide = true))]
|
||||
#[arg(short, long)]
|
||||
pub ulimit: Option<u64>,
|
||||
pub ulimit: Option<usize>,
|
||||
|
||||
/// The order of scanning to be performed. The "serial" option will
|
||||
/// scan ports in ascending order while the "random" option will scan
|
||||
@@ -151,28 +217,91 @@ pub struct Opts {
|
||||
#[arg(short, long, value_delimiter = ',')]
|
||||
pub exclude_ports: Option<Vec<u16>>,
|
||||
|
||||
/// A list of comma separated CIDRs, IPs, or hosts to be excluded from scanning.
|
||||
#[arg(short = 'x', long = "exclude-addresses", value_delimiter = ',')]
|
||||
pub exclude_addresses: Option<Vec<String>>,
|
||||
|
||||
/// UDP scanning mode, finds UDP ports that send back responses
|
||||
#[arg(long)]
|
||||
pub udp: bool,
|
||||
|
||||
/// Also list TCP ports that actively refused the connection (closed).
|
||||
/// Closed ports are only printed; scripts are never run against them.
|
||||
#[arg(long)]
|
||||
pub closed: bool,
|
||||
|
||||
/// Milliseconds to wait after scanning a port (on every address) before
|
||||
/// scanning the next one, for slow, low-noise scans. 0 disables the delay.
|
||||
#[arg(long, default_value = "0", value_name = "MS")]
|
||||
pub interval: u64,
|
||||
|
||||
/// Ids of the options given explicitly on the command line. The
|
||||
/// configuration file never overrides these (see [`Opts::merge`]).
|
||||
#[arg(skip)]
|
||||
pub cli_args: HashSet<String>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl Opts {
|
||||
pub fn read() -> Self {
|
||||
let mut opts = Opts::parse();
|
||||
Self::try_read_from(std::env::args_os()).unwrap_or_else(|e| e.exit())
|
||||
}
|
||||
|
||||
/// Parses `args` like [`Opts::read`] does, returning an error instead of
|
||||
/// exiting.
|
||||
fn try_read_from<I, T>(args: I) -> Result<Self, clap::Error>
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<OsString> + Clone,
|
||||
{
|
||||
let mut command = Self::command();
|
||||
let mut matches = command.try_get_matches_from_mut(args)?;
|
||||
|
||||
// clap fills in default values, so the parsed struct alone cannot
|
||||
// tell `-b 4500` apart from no `-b` at all. Record what the user
|
||||
// actually typed so that `merge` lets it win over the config file.
|
||||
let cli_args = command
|
||||
.get_arguments()
|
||||
.map(|arg| arg.get_id().as_str())
|
||||
.filter(|id| matches.value_source(id) == Some(ValueSource::CommandLine))
|
||||
.map(str::to_owned)
|
||||
.collect();
|
||||
|
||||
let mut opts =
|
||||
Self::from_arg_matches_mut(&mut matches).map_err(|e| e.format(&mut command))?;
|
||||
opts.cli_args = cli_args;
|
||||
|
||||
if opts.ports.is_none() && opts.range.is_none() {
|
||||
opts.range = Some(PortRange {
|
||||
start: LOWEST_PORT_NUMBER,
|
||||
end: TOP_PORT_NUMBER,
|
||||
});
|
||||
opts.range = Some(PortRanges(vec![(LOWEST_PORT_NUMBER, TOP_PORT_NUMBER)]));
|
||||
}
|
||||
|
||||
opts
|
||||
Ok(opts)
|
||||
}
|
||||
|
||||
/// Validates options whose availability or semantics depend on the
|
||||
/// operating system.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error when an option is unsupported on the current platform.
|
||||
pub fn validate_platform(&self) -> Result<(), String> {
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
if self.ulimit.is_some() {
|
||||
return Err(
|
||||
"--ulimit is only supported on Unix-like operating systems. \
|
||||
On Windows, use --batch-size (-b) to control scan concurrency."
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads the command line arguments into an Opts struct and merge
|
||||
/// values found within the user configuration file.
|
||||
/// values found within the user configuration file. Options given on the
|
||||
/// command line take precedence over the configuration file.
|
||||
pub fn merge(&mut self, config: &Config) {
|
||||
if !self.no_config {
|
||||
self.merge_required(config);
|
||||
@@ -185,7 +314,9 @@ impl Opts {
|
||||
($($field: ident),+) => {
|
||||
$(
|
||||
if let Some(e) = &config.$field {
|
||||
self.$field = e.clone();
|
||||
if !self.cli_args.contains(stringify!($field)) {
|
||||
self.$field = e.clone();
|
||||
}
|
||||
}
|
||||
)+
|
||||
}
|
||||
@@ -193,7 +324,7 @@ impl Opts {
|
||||
|
||||
merge_required!(
|
||||
addresses, greppable, accessible, batch_size, timeout, tries, scan_order, scripts,
|
||||
command, udp
|
||||
command, udp, no_banner, closed, interval
|
||||
);
|
||||
}
|
||||
|
||||
@@ -201,7 +332,7 @@ impl Opts {
|
||||
macro_rules! merge_optional {
|
||||
($($field: ident),+) => {
|
||||
$(
|
||||
if config.$field.is_some() {
|
||||
if config.$field.is_some() && !self.cli_args.contains(stringify!($field)) {
|
||||
self.$field = config.$field.clone();
|
||||
}
|
||||
)+
|
||||
@@ -210,14 +341,10 @@ impl Opts {
|
||||
|
||||
// Only use top ports when the user asks for them
|
||||
if self.top && config.ports.is_some() {
|
||||
let mut ports: Vec<u16> = Vec::with_capacity(config.ports.as_ref().unwrap().len());
|
||||
for entry in config.ports.as_ref().unwrap().keys() {
|
||||
ports.push(entry.parse().unwrap());
|
||||
}
|
||||
self.ports = Some(ports);
|
||||
self.ports = config.ports.clone();
|
||||
}
|
||||
|
||||
merge_optional!(range, resolver, ulimit, exclude_ports);
|
||||
merge_optional!(range, resolver, ulimit, exclude_ports, exclude_addresses);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,11 +364,16 @@ impl Default for Opts {
|
||||
resolver: None,
|
||||
scan_order: ScanOrder::Serial,
|
||||
no_config: true,
|
||||
no_banner: false,
|
||||
top: false,
|
||||
scripts: ScriptsRequired::Default,
|
||||
config_path: None,
|
||||
exclude_ports: None,
|
||||
exclude_addresses: None,
|
||||
udp: false,
|
||||
closed: false,
|
||||
interval: 0,
|
||||
cli_args: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,24 +385,29 @@ impl Default for Opts {
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
addresses: Option<Vec<String>>,
|
||||
ports: Option<HashMap<String, u16>>,
|
||||
range: Option<PortRange>,
|
||||
ports: Option<Vec<u16>>,
|
||||
range: Option<PortRanges>,
|
||||
greppable: Option<bool>,
|
||||
accessible: Option<bool>,
|
||||
batch_size: Option<u16>,
|
||||
batch_size: Option<usize>,
|
||||
timeout: Option<u32>,
|
||||
tries: Option<u8>,
|
||||
ulimit: Option<u64>,
|
||||
ulimit: Option<usize>,
|
||||
resolver: Option<String>,
|
||||
scan_order: Option<ScanOrder>,
|
||||
command: Option<Vec<String>>,
|
||||
scripts: Option<ScriptsRequired>,
|
||||
exclude_ports: Option<Vec<u16>>,
|
||||
exclude_addresses: Option<Vec<String>>,
|
||||
udp: Option<bool>,
|
||||
no_banner: Option<bool>,
|
||||
closed: Option<bool>,
|
||||
interval: Option<u64>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
#[allow(clippy::doc_link_with_quotes)]
|
||||
#[allow(clippy::manual_unwrap_or_default)]
|
||||
impl Config {
|
||||
/// Reads the configuration file with TOML format and parses it into a
|
||||
/// Config struct.
|
||||
@@ -280,13 +417,29 @@ impl Config {
|
||||
/// addresses = ["127.0.0.1", "127.0.0.1"]
|
||||
/// ports = [80, 443, 8080]
|
||||
/// greppable = true
|
||||
/// scan_order: "Serial"
|
||||
/// scan_order = "Serial"
|
||||
/// exclude_ports = [8080, 9090, 80]
|
||||
/// udp = false
|
||||
/// closed = false
|
||||
/// interval = 0
|
||||
///
|
||||
pub fn read(custom_config_path: Option<PathBuf>) -> Self {
|
||||
let mut content = String::new();
|
||||
let config_path = custom_config_path.unwrap_or_else(default_config_path);
|
||||
let config_path = custom_config_path.unwrap_or_else(|| {
|
||||
// Try the XDG-idiomatic location first, then fall back to legacy
|
||||
// paths so existing users keep working unchanged.
|
||||
for path in [
|
||||
default_config_path(),
|
||||
legacy_dot_config_path(),
|
||||
old_default_config_path(),
|
||||
] {
|
||||
if path.exists() {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
default_config_path()
|
||||
});
|
||||
|
||||
if config_path.exists() {
|
||||
content = match fs::read_to_string(config_path) {
|
||||
Ok(content) => content,
|
||||
@@ -297,7 +450,9 @@ impl Config {
|
||||
let config: Config = match toml::from_str(&content) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
println!("Found {e} in configuration file.\nAborting scan.\n");
|
||||
crate::tui::println_safe(format_args!(
|
||||
"Found {e} in configuration file.\nAborting scan.\n"
|
||||
));
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
@@ -306,8 +461,30 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs default path to config toml
|
||||
/// Returns the preferred config file path: `$XDG_CONFIG_HOME/rustscan/config.toml`
|
||||
/// on Linux (with the usual `~/.config` fallback when the variable is unset),
|
||||
/// and the platform-equivalent `dirs::config_dir()` location on macOS / Windows.
|
||||
pub fn default_config_path() -> PathBuf {
|
||||
let Some(mut config_path) = dirs::config_dir() else {
|
||||
panic!("Could not infer config file path.");
|
||||
};
|
||||
config_path.push("rustscan");
|
||||
config_path.push("config.toml");
|
||||
config_path
|
||||
}
|
||||
|
||||
/// Returns the transitional `$XDG_CONFIG_HOME/.rustscan.toml` path that older
|
||||
/// builds wrote to. Kept readable for backwards compatibility.
|
||||
pub fn legacy_dot_config_path() -> PathBuf {
|
||||
let Some(mut config_path) = dirs::config_dir() else {
|
||||
panic!("Could not infer config file path.");
|
||||
};
|
||||
config_path.push(".rustscan.toml");
|
||||
config_path
|
||||
}
|
||||
|
||||
/// Returns the deprecated home directory config path used for backwards compatibility.
|
||||
pub fn old_default_config_path() -> PathBuf {
|
||||
let Some(mut config_path) = dirs::home_dir() else {
|
||||
panic!("Could not infer config file path.");
|
||||
};
|
||||
@@ -319,8 +496,9 @@ pub fn default_config_path() -> PathBuf {
|
||||
mod tests {
|
||||
use clap::{CommandFactory, Parser};
|
||||
use parameterized::parameterized;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::{Config, Opts, PortRange, ScanOrder, ScriptsRequired};
|
||||
use super::{Config, Opts, PortRanges, ScanOrder, ScriptsRequired};
|
||||
|
||||
impl Config {
|
||||
fn default() -> Self {
|
||||
@@ -339,7 +517,11 @@ mod tests {
|
||||
scan_order: Some(ScanOrder::Random),
|
||||
scripts: None,
|
||||
exclude_ports: None,
|
||||
exclude_addresses: None,
|
||||
udp: Some(false),
|
||||
no_banner: None,
|
||||
closed: Some(false),
|
||||
interval: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -369,6 +551,82 @@ mod tests {
|
||||
assert_eq!(command, opts.command);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_explicit_batch_size() {
|
||||
let opts = Opts::parse_from(["rustscan", "-a", "127.0.0.1", "-b", "1234"]);
|
||||
|
||||
assert_eq!(opts.batch_size, 1234);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_explicit_long_batch_size() {
|
||||
let opts = Opts::parse_from(["rustscan", "-a", "127.0.0.1", "--batch-size", "4321"]);
|
||||
|
||||
assert_eq!(opts.batch_size, 4321);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn windows_platform_validation_accepts_batch_size() {
|
||||
let opts = Opts::parse_from(["rustscan", "-a", "127.0.0.1", "--batch-size", "500"]);
|
||||
|
||||
assert!(opts.validate_platform().is_ok());
|
||||
assert_eq!(opts.batch_size, 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn windows_rejects_ulimit() {
|
||||
let opts = Opts::parse_from(["rustscan", "-a", "127.0.0.1", "--ulimit", "5000"]);
|
||||
|
||||
let error = opts
|
||||
.validate_platform()
|
||||
.expect_err("Windows must reject --ulimit");
|
||||
|
||||
assert!(error.contains("--ulimit"));
|
||||
assert!(error.contains("Unix"));
|
||||
assert!(error.contains("--batch-size"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn windows_hides_ulimit_from_help() {
|
||||
let help = Opts::command().render_long_help().to_string();
|
||||
|
||||
assert!(
|
||||
!help.contains("--ulimit"),
|
||||
"--ulimit should not be advertised on Windows"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn windows_rejects_ulimit_from_config_merge() {
|
||||
let mut opts = Opts {
|
||||
no_config: false,
|
||||
..Default::default()
|
||||
};
|
||||
let mut config = Config::default();
|
||||
config.ulimit = Some(5_000);
|
||||
|
||||
opts.merge(&config);
|
||||
|
||||
let error = opts
|
||||
.validate_platform()
|
||||
.expect_err("Windows must reject --ulimit supplied by configuration");
|
||||
|
||||
assert!(error.contains("--ulimit"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn unix_platform_validation_accepts_ulimit() {
|
||||
let opts = Opts::parse_from(["rustscan", "-a", "127.0.0.1", "--ulimit", "5000"]);
|
||||
|
||||
assert!(opts.validate_platform().is_ok());
|
||||
assert_eq!(opts.ulimit, Some(5_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opts_no_merge_when_config_is_ignored() {
|
||||
let mut opts = Opts::default();
|
||||
@@ -404,10 +662,7 @@ mod tests {
|
||||
fn opts_merge_optional_arguments() {
|
||||
let mut opts = Opts::default();
|
||||
let mut config = Config::default();
|
||||
config.range = Some(PortRange {
|
||||
start: 1,
|
||||
end: 1_000,
|
||||
});
|
||||
config.range = Some(PortRanges(vec![(1, 1_000)]));
|
||||
config.ulimit = Some(1_000);
|
||||
config.resolver = Some("1.1.1.1".to_owned());
|
||||
|
||||
@@ -417,4 +672,159 @@ mod tests {
|
||||
assert_eq!(opts.ulimit, config.ulimit);
|
||||
assert_eq!(opts.resolver, config.resolver);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_arguments_override_config() {
|
||||
// Regression test for #722: the config file used to silently replace
|
||||
// options given on the command line, e.g. `-r` with its `range`.
|
||||
let mut opts = Opts::try_read_from([
|
||||
"rustscan",
|
||||
"-a",
|
||||
"192.168.0.1",
|
||||
"-r",
|
||||
"8600-8650",
|
||||
"-b",
|
||||
"100",
|
||||
// Passing clap's default value explicitly must still win.
|
||||
"-t",
|
||||
"1500",
|
||||
"--tries",
|
||||
"3",
|
||||
"--scan-order",
|
||||
"serial",
|
||||
"--scripts",
|
||||
"none",
|
||||
"-g",
|
||||
"--",
|
||||
"-sV",
|
||||
])
|
||||
.unwrap();
|
||||
let config: Config = toml::from_str(
|
||||
r#"
|
||||
addresses = ["127.0.0.1"]
|
||||
range = { start = 1, end = 100 }
|
||||
batch_size = 25000
|
||||
timeout = 1000
|
||||
tries = 1
|
||||
scan_order = "Random"
|
||||
scripts = "Custom"
|
||||
greppable = false
|
||||
command = ["-A"]
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
opts.merge(&config);
|
||||
|
||||
assert_eq!(opts.addresses, vec!["192.168.0.1".to_owned()]);
|
||||
assert_eq!(opts.range, Some(PortRanges(vec![(8_600, 8_650)])));
|
||||
assert_eq!(opts.batch_size, 100);
|
||||
assert_eq!(opts.timeout, 1_500);
|
||||
assert_eq!(opts.tries, 3);
|
||||
assert_eq!(opts.scan_order, ScanOrder::Serial);
|
||||
assert_eq!(opts.scripts, ScriptsRequired::None);
|
||||
assert!(opts.greppable);
|
||||
assert_eq!(opts.command, vec!["-sV".to_owned()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_fills_arguments_not_given_on_cli() {
|
||||
let mut opts = Opts::try_read_from(["rustscan", "-a", "192.168.0.1"]).unwrap();
|
||||
let config: Config = toml::from_str(
|
||||
r#"
|
||||
addresses = ["127.0.0.1"]
|
||||
range = { start = 1, end = 100 }
|
||||
batch_size = 25000
|
||||
timeout = 1000
|
||||
tries = 3
|
||||
scan_order = "Random"
|
||||
greppable = true
|
||||
command = ["-A"]
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
opts.merge(&config);
|
||||
|
||||
// Only `-a` was typed; clap defaults such as `-b 4500` must not count.
|
||||
assert_eq!(opts.cli_args, HashSet::from(["addresses".to_owned()]));
|
||||
assert_eq!(opts.addresses, vec!["192.168.0.1".to_owned()]);
|
||||
assert_eq!(opts.range, Some(PortRanges(vec![(1, 100)])));
|
||||
assert_eq!(opts.batch_size, 25_000);
|
||||
assert_eq!(opts.timeout, 1_000);
|
||||
assert_eq!(opts.tries, 3);
|
||||
assert_eq!(opts.scan_order, ScanOrder::Random);
|
||||
assert!(opts.greppable);
|
||||
assert_eq!(opts.command, vec!["-A".to_owned()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_comma_separated_ranges() {
|
||||
let opts = Opts::parse_from([
|
||||
"rustscan",
|
||||
"-a",
|
||||
"127.0.0.1",
|
||||
"-r",
|
||||
"1-100, 200-300,5000-5100",
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
opts.range,
|
||||
Some(PortRanges(vec![(1, 100), (200, 300), (5_000, 5_100)]))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_malformed_ranges() {
|
||||
for range in ["", "300-200", "1-100,", "1-2-3", "a-b", "1-70000"] {
|
||||
assert!(
|
||||
Opts::try_parse_from(["rustscan", "-a", "127.0.0.1", "-r", range]).is_err(),
|
||||
"{:?} should be rejected",
|
||||
range
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_range_accepts_legacy_table() {
|
||||
let config: Config = toml::from_str("range = { start = 1, end = 1000 }").unwrap();
|
||||
|
||||
assert_eq!(config.range, Some(PortRanges(vec![(1, 1_000)])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_range_accepts_string_and_pairs() {
|
||||
let expected = Some(PortRanges(vec![(1, 100), (200, 300)]));
|
||||
|
||||
let config: Config = toml::from_str("range = \"1-100,200-300\"").unwrap();
|
||||
assert_eq!(config.range, expected);
|
||||
|
||||
let config: Config = toml::from_str("range = [[1, 100], [200, 300]]").unwrap();
|
||||
assert_eq!(config.range, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_range_rejects_invalid_ranges() {
|
||||
for range in [
|
||||
"range = { start = 10, end = 1 }",
|
||||
"range = [[10, 1]]",
|
||||
"range = []",
|
||||
"range = \"10-1\"",
|
||||
] {
|
||||
assert!(
|
||||
toml::from_str::<Config>(range).is_err(),
|
||||
"{:?} should be rejected",
|
||||
range
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_interval_in_milliseconds() {
|
||||
let opts = Opts::parse_from(["rustscan", "-a", "127.0.0.1", "--interval", "250"]);
|
||||
assert_eq!(opts.interval, 250);
|
||||
|
||||
let opts = Opts::parse_from(["rustscan", "-a", "127.0.0.1"]);
|
||||
assert_eq!(opts.interval, 0);
|
||||
}
|
||||
}
|
||||
+16
-17
@@ -7,31 +7,30 @@
|
||||
//! [`Scanner`](crate::scanner::Scanner) which in turn requires a
|
||||
//! [`PortStrategy`](crate::port_strategy::PortStrategy):
|
||||
//!
|
||||
//! ```rust
|
||||
//! This example is compiled but never executed by the test suite.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use async_std::task::block_on;
|
||||
//! use std::{net::IpAddr, time::Duration};
|
||||
//!
|
||||
//! use rustscan::input::{PortRange, ScanOrder};
|
||||
//! use rustscan::input::{PortRanges, ScanOrder};
|
||||
//! use rustscan::port_strategy::PortStrategy;
|
||||
//! use rustscan::scanner::Scanner;
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let addrs = vec!["127.0.0.1".parse::<IpAddr>().unwrap()];
|
||||
//! let range = PortRange {
|
||||
//! start: 1,
|
||||
//! end: 1_000,
|
||||
//! };
|
||||
//! let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
|
||||
//! let range = PortRanges(vec![(1, 1_000)]);
|
||||
//! let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random); // can be serial, random or manual https://github.com/RustScan/RustScan/blob/master/src/port_strategy/mod.rs
|
||||
//! let scanner = Scanner::new(
|
||||
//! &addrs,
|
||||
//! 10,
|
||||
//! Duration::from_millis(100),
|
||||
//! 1,
|
||||
//! true,
|
||||
//! strategy,
|
||||
//! true,
|
||||
//! vec![9000],
|
||||
//! false,
|
||||
//! &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?
|
||||
//! );
|
||||
//!
|
||||
//! let scan_result = block_on(scanner.run());
|
||||
@@ -55,4 +54,4 @@ pub mod scripts;
|
||||
|
||||
pub mod address;
|
||||
|
||||
pub mod udp_packets;
|
||||
pub mod generated;
|
||||
+174
-80
@@ -5,8 +5,9 @@
|
||||
use rustscan::benchmark::{Benchmark, NamedTimer};
|
||||
use rustscan::input::{self, Config, Opts, ScriptsRequired};
|
||||
use rustscan::port_strategy::PortStrategy;
|
||||
use rustscan::scanner::Scanner;
|
||||
use rustscan::scanner::{PortStatus, Scanner};
|
||||
use rustscan::scripts::{init_scripts, Script, ScriptFile};
|
||||
use rustscan::tui::println_safe;
|
||||
use rustscan::{detail, funny_opening, output, warning};
|
||||
|
||||
use colorful::{Color, Colorful};
|
||||
@@ -23,9 +24,10 @@ extern crate dirs;
|
||||
|
||||
// Average value for Ubuntu
|
||||
#[cfg(unix)]
|
||||
const DEFAULT_FILE_DESCRIPTORS_LIMIT: u64 = 8000;
|
||||
const DEFAULT_FILE_DESCRIPTORS_LIMIT: usize = 8000;
|
||||
// Safest batch size based on experimentation
|
||||
const AVERAGE_BATCH_SIZE: u16 = 3000;
|
||||
#[cfg(unix)]
|
||||
const AVERAGE_BATCH_SIZE: usize = 3000;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
@@ -35,6 +37,9 @@ 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");
|
||||
@@ -43,9 +48,14 @@ fn main() {
|
||||
let config = Config::read(opts.config_path.clone());
|
||||
opts.merge(&config);
|
||||
|
||||
debug!("Main() `opts` arguments are {:?}", opts);
|
||||
if let Err(message) = opts.validate_platform() {
|
||||
eprintln!("error: {message}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
|
||||
let scripts_to_run: Vec<ScriptFile> = match init_scripts(opts.scripts) {
|
||||
debug!("Main() `opts` arguments are {opts:?}");
|
||||
|
||||
let scripts_to_run: Vec<ScriptFile> = match init_scripts(&opts.scripts) {
|
||||
Ok(scripts_to_run) => scripts_to_run,
|
||||
Err(e) => {
|
||||
warning!(
|
||||
@@ -57,9 +67,9 @@ fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
debug!("Scripts initialized {:?}", &scripts_to_run);
|
||||
debug!("Scripts initialized {scripts_to_run:?}");
|
||||
|
||||
if !opts.greppable && !opts.accessible {
|
||||
if !opts.greppable && !opts.accessible && !opts.no_banner {
|
||||
print_opening(&opts);
|
||||
}
|
||||
|
||||
@@ -74,18 +84,10 @@ fn main() {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
let batch_size: u16 = infer_batch_size(&opts, adjust_ulimit_size(&opts));
|
||||
let batch_size = effective_batch_size(&opts);
|
||||
debug!("Effective batch size: {batch_size}");
|
||||
|
||||
#[cfg(not(unix))]
|
||||
let batch_size: u16 = AVERAGE_BATCH_SIZE;
|
||||
|
||||
// Added by wasuaje - 01/26/2024:
|
||||
// exclude_ports is an exclusion port list
|
||||
//
|
||||
// Added by brendanglancy - 5/19/2024:
|
||||
// udp is an option to do a udp scan
|
||||
let scanner = Scanner::new(
|
||||
let mut scanner = Scanner::new(
|
||||
&ips,
|
||||
batch_size,
|
||||
Duration::from_millis(opts.timeout.into()),
|
||||
@@ -95,21 +97,30 @@ fn main() {
|
||||
opts.accessible,
|
||||
opts.exclude_ports.unwrap_or_default(),
|
||||
opts.udp,
|
||||
);
|
||||
debug!("Scanner finished building: {:?}", scanner);
|
||||
)
|
||||
.with_open_port_output();
|
||||
if opts.closed {
|
||||
scanner = scanner.with_closed_ports();
|
||||
}
|
||||
if opts.interval > 0 {
|
||||
scanner = scanner.with_interval(Duration::from_millis(opts.interval));
|
||||
}
|
||||
debug!("Scanner finished building: {scanner:?}");
|
||||
|
||||
let mut portscan_bench = NamedTimer::start("Portscan");
|
||||
let scan_result = block_on(scanner.run());
|
||||
let scan_result = block_on(scanner.run_with_status());
|
||||
portscan_bench.end();
|
||||
benchmarks.push(portscan_bench);
|
||||
|
||||
let mut ports_per_ip = HashMap::new();
|
||||
let mut ports_per_ip: HashMap<IpAddr, Vec<u16>> = HashMap::new();
|
||||
let mut closed_ports_per_ip: HashMap<IpAddr, Vec<u16>> = HashMap::new();
|
||||
|
||||
for socket in scan_result {
|
||||
ports_per_ip
|
||||
.entry(socket.ip())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(socket.port());
|
||||
for status in scan_result {
|
||||
let (map, socket) = match status {
|
||||
PortStatus::Open(socket) => (&mut ports_per_ip, socket),
|
||||
PortStatus::Closed(socket) => (&mut closed_ports_per_ip, socket),
|
||||
};
|
||||
map.entry(socket.ip()).or_default().push(socket.port());
|
||||
}
|
||||
|
||||
for ip in ips {
|
||||
@@ -124,7 +135,7 @@ fn main() {
|
||||
\n*I used {} batch size, consider lowering it with {} or a comfortable number for your system.
|
||||
\n Alternatively, increase the timeout if your ping is high. Rustscan -t 2000 for 2000 milliseconds (2s) timeout.\n",
|
||||
ip,
|
||||
opts.batch_size,
|
||||
batch_size,
|
||||
"'rustscan -b <batch_size> -a <ip address>'");
|
||||
warning!(x, opts.greppable, opts.accessible);
|
||||
}
|
||||
@@ -138,17 +149,19 @@ fn main() {
|
||||
|
||||
// if option scripts is none, no script will be spawned
|
||||
if opts.greppable || opts.scripts == ScriptsRequired::None {
|
||||
println!("{} -> [{}]", &ip, ports_str);
|
||||
println_safe(format_args!("{ip} -> [{ports_str}]"));
|
||||
continue;
|
||||
}
|
||||
detail!("Starting Script(s)", opts.greppable, opts.accessible);
|
||||
|
||||
// Run all the scripts we found and parsed based on the script config file tags field.
|
||||
for mut script_f in scripts_to_run.clone() {
|
||||
for script_f in &scripts_to_run {
|
||||
// Clone only the script being run instead of the whole list per IP.
|
||||
let mut script_f = script_f.clone();
|
||||
// This part allows us to add commandline arguments to the Script call_format, appending them to the end of the command.
|
||||
if !opts.command.is_empty() {
|
||||
let user_extra_args = &opts.command.join(" ");
|
||||
debug!("Extra args vec {:?}", user_extra_args);
|
||||
debug!("Extra args vec {user_extra_args:?}");
|
||||
if script_f.call_format.is_some() {
|
||||
let mut call_f = script_f.call_format.unwrap();
|
||||
call_f.push(' ');
|
||||
@@ -158,7 +171,7 @@ fn main() {
|
||||
opts.greppable,
|
||||
opts.accessible
|
||||
);
|
||||
debug!("Call format {}", call_f);
|
||||
debug!("Call format {call_f}");
|
||||
script_f.call_format = Some(call_f);
|
||||
}
|
||||
}
|
||||
@@ -175,7 +188,7 @@ fn main() {
|
||||
);
|
||||
match script.run() {
|
||||
Ok(script_result) => {
|
||||
detail!(script_result.to_string(), opts.greppable, opts.accessible);
|
||||
detail!(script_result.clone(), opts.greppable, opts.accessible);
|
||||
}
|
||||
Err(e) => {
|
||||
warning!(&format!("Error {e}"), opts.greppable, opts.accessible);
|
||||
@@ -184,36 +197,61 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Closed ports are only listed; scripts are never run against them.
|
||||
if opts.closed {
|
||||
println_safe(format_args!("closed ports:"));
|
||||
for (ip, ports) in &closed_ports_per_ip {
|
||||
let ports_str = ports
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
println_safe(format_args!("{ip} -> [{ports_str}]"));
|
||||
}
|
||||
}
|
||||
|
||||
// To use the runtime benchmark, run the process as: RUST_LOG=info ./rustscan
|
||||
script_bench.end();
|
||||
benchmarks.push(script_bench);
|
||||
rustscan_bench.end();
|
||||
benchmarks.push(rustscan_bench);
|
||||
debug!("Benchmarks raw {:?}", benchmarks);
|
||||
debug!("Benchmarks raw {benchmarks:?}");
|
||||
info!("{}", benchmarks.summary());
|
||||
}
|
||||
|
||||
/// Determines the actual batch size used by the scanner.
|
||||
///
|
||||
/// Unix systems may reduce the requested batch size according to the process
|
||||
/// file-descriptor limit. Other platforms, including Windows, use the batch
|
||||
/// size explicitly requested by the user.
|
||||
fn effective_batch_size(opts: &Opts) -> usize {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
infer_batch_size(opts, adjust_ulimit_size(opts))
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
opts.batch_size
|
||||
}
|
||||
}
|
||||
|
||||
/// Prints the opening title of RustScan
|
||||
#[allow(clippy::items_after_statements, clippy::needless_raw_string_hashes)]
|
||||
fn print_opening(opts: &Opts) {
|
||||
debug!("Printing opening");
|
||||
let s = format!(
|
||||
"{}\n{}\n{}\n{}\n{}",
|
||||
r#".----. .-. .-. .----..---. .----. .---. .--. .-. .-."#,
|
||||
r#"| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |"#,
|
||||
r#"| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |"#,
|
||||
r#"`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'"#,
|
||||
r#"The Modern Day Port Scanner."#
|
||||
);
|
||||
println!("{}", s.gradient(Color::Green).bold());
|
||||
let info = format!(
|
||||
"{}\n{}\n{}\n{}",
|
||||
r#"________________________________________"#,
|
||||
r#": http://discord.skerritt.blog :"#,
|
||||
r#": https://github.com/RustScan/RustScan :"#,
|
||||
r#" --------------------------------------"#
|
||||
);
|
||||
println!("{}", info.gradient(Color::Yellow).bold());
|
||||
let s = r#".----. .-. .-. .----..---. .----. .---. .--. .-. .-.
|
||||
| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |
|
||||
| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |
|
||||
`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'
|
||||
The Modern Day Port Scanner."#;
|
||||
|
||||
println_safe(format_args!("{}", s.gradient(Color::Green).bold()));
|
||||
let info = r#"________________________________________
|
||||
: http://discord.skerritt.blog :
|
||||
: https://github.com/RustScan/RustScan :
|
||||
--------------------------------------"#;
|
||||
println_safe(format_args!("{}", info.gradient(Color::Yellow).bold()));
|
||||
funny_opening!();
|
||||
|
||||
let config_path = opts
|
||||
@@ -226,13 +264,26 @@ fn print_opening(opts: &Opts) {
|
||||
opts.greppable,
|
||||
opts.accessible
|
||||
);
|
||||
|
||||
if opts.config_path.is_none() {
|
||||
let old_config_path = input::old_default_config_path();
|
||||
detail!(
|
||||
format!(
|
||||
"For backwards compatibility, the config file may also be at {old_config_path:?}"
|
||||
),
|
||||
opts.greppable,
|
||||
opts.accessible
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn adjust_ulimit_size(opts: &Opts) -> u64 {
|
||||
fn adjust_ulimit_size(opts: &Opts) -> usize {
|
||||
use rlimit::Resource;
|
||||
use std::convert::TryInto;
|
||||
|
||||
if let Some(limit) = opts.ulimit {
|
||||
let limit = limit as u64;
|
||||
if Resource::NOFILE.set(limit, limit).is_ok() {
|
||||
detail!(
|
||||
format!("Automatically increasing ulimit value to {limit}."),
|
||||
@@ -249,14 +300,15 @@ fn adjust_ulimit_size(opts: &Opts) -> u64 {
|
||||
}
|
||||
|
||||
let (soft, _) = Resource::NOFILE.get().unwrap();
|
||||
soft
|
||||
// On 32-bit targets the soft limit (e.g. RLIM_INFINITY) may not fit in a
|
||||
// usize. Fall back to the conservative default rather than usize::MAX,
|
||||
// which would skip every batch-size adjustment in infer_batch_size.
|
||||
soft.try_into().unwrap_or(DEFAULT_FILE_DESCRIPTORS_LIMIT)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn infer_batch_size(opts: &Opts, ulimit: u64) -> u16 {
|
||||
use std::convert::TryInto;
|
||||
|
||||
let mut batch_size: u64 = opts.batch_size.into();
|
||||
fn infer_batch_size(opts: &Opts, ulimit: usize) -> usize {
|
||||
let mut batch_size = opts.batch_size;
|
||||
|
||||
// Adjust the batch size when the ulimit value is lower than the desired batch size
|
||||
if ulimit < batch_size {
|
||||
@@ -267,8 +319,8 @@ fn infer_batch_size(opts: &Opts, ulimit: u64) -> u16 {
|
||||
// When the OS supports high file limits like 8000, but the user
|
||||
// selected a batch size higher than this we should reduce it to
|
||||
// a lower number.
|
||||
if ulimit < AVERAGE_BATCH_SIZE.into() {
|
||||
// ulimit is smaller than aveage batch size
|
||||
if ulimit < AVERAGE_BATCH_SIZE {
|
||||
// ulimit is smaller than average batch size
|
||||
// user must have very small ulimit
|
||||
// decrease batch size to half of ulimit
|
||||
warning!("Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'. ", opts.greppable, opts.accessible);
|
||||
@@ -276,7 +328,7 @@ fn infer_batch_size(opts: &Opts, ulimit: u64) -> u16 {
|
||||
batch_size = ulimit / 2;
|
||||
} else if ulimit > DEFAULT_FILE_DESCRIPTORS_LIMIT {
|
||||
info!("Batch size is now average batch size");
|
||||
batch_size = AVERAGE_BATCH_SIZE.into();
|
||||
batch_size = AVERAGE_BATCH_SIZE;
|
||||
} else {
|
||||
batch_size = ulimit - 100;
|
||||
}
|
||||
@@ -289,12 +341,12 @@ fn infer_batch_size(opts: &Opts, ulimit: u64) -> u16 {
|
||||
}
|
||||
|
||||
batch_size
|
||||
.try_into()
|
||||
.expect("Couldn't fit the batch size into a u16.")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(windows)]
|
||||
use super::effective_batch_size;
|
||||
#[cfg(unix)]
|
||||
use super::{adjust_ulimit_size, infer_batch_size};
|
||||
use super::{print_opening, Opts};
|
||||
@@ -302,8 +354,10 @@ mod tests {
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn batch_size_lowered() {
|
||||
let mut opts = Opts::default();
|
||||
opts.batch_size = 50_000;
|
||||
let opts = Opts {
|
||||
batch_size: 50_000,
|
||||
..Default::default()
|
||||
};
|
||||
let batch_size = infer_batch_size(&opts, 120);
|
||||
|
||||
assert!(batch_size < opts.batch_size);
|
||||
@@ -312,52 +366,92 @@ mod tests {
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn batch_size_lowered_average_size() {
|
||||
let mut opts = Opts::default();
|
||||
opts.batch_size = 50_000;
|
||||
let opts = Opts {
|
||||
batch_size: 50_000,
|
||||
..Default::default()
|
||||
};
|
||||
let batch_size = infer_batch_size(&opts, 9_000);
|
||||
|
||||
assert!(batch_size == 3_000);
|
||||
assert_eq!(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 mut opts = Opts::default();
|
||||
opts.batch_size = 50_000;
|
||||
let opts = Opts {
|
||||
batch_size: 50_000,
|
||||
..Default::default()
|
||||
};
|
||||
let batch_size = infer_batch_size(&opts, 5_000);
|
||||
|
||||
assert!(batch_size == 4_900);
|
||||
assert_eq!(batch_size, 4_900);
|
||||
}
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn batch_size_adjusted_2000() {
|
||||
// ulimit == batch_size
|
||||
let mut opts = Opts::default();
|
||||
opts.batch_size = 50_000;
|
||||
opts.ulimit = Some(2_000);
|
||||
let opts = Opts {
|
||||
batch_size: 50_000,
|
||||
ulimit: Some(2_000),
|
||||
..Default::default()
|
||||
};
|
||||
let batch_size = adjust_ulimit_size(&opts);
|
||||
|
||||
assert!(batch_size == 2_000);
|
||||
assert_eq!(batch_size, 2_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_high_ulimit_no_greppable_mode() {
|
||||
let mut opts = Opts::default();
|
||||
opts.batch_size = 10;
|
||||
opts.greppable = false;
|
||||
let opts = Opts {
|
||||
batch_size: 10,
|
||||
greppable: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let batch_size = infer_batch_size(&opts, 1_000_000);
|
||||
|
||||
assert!(batch_size == opts.batch_size);
|
||||
assert_eq!(batch_size, opts.batch_size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_print_opening_no_panic() {
|
||||
let mut opts = Opts::default();
|
||||
opts.ulimit = Some(2_000);
|
||||
let opts = Opts::default();
|
||||
// print opening should not panic
|
||||
print_opening(&opts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn windows_batch_size_uses_requested_value() {
|
||||
let opts = Opts {
|
||||
batch_size: 50,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(effective_batch_size(&opts), 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn windows_batch_size_preserves_large_requested_value() {
|
||||
let opts = Opts {
|
||||
batch_size: 12_345,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(effective_batch_size(&opts), 12_345);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn windows_batch_size_preserves_single_connection() {
|
||||
let opts = Opts {
|
||||
batch_size: 1,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(effective_batch_size(&opts), 1);
|
||||
}
|
||||
}
|
||||
+39
-29
@@ -1,8 +1,8 @@
|
||||
//! Provides a means to hold configuration options specifically for port scanning.
|
||||
mod range_iterator;
|
||||
use crate::input::{PortRange, ScanOrder};
|
||||
use crate::input::{PortRanges, ScanOrder};
|
||||
use rand::rng;
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::thread_rng;
|
||||
use range_iterator::RangeIterator;
|
||||
|
||||
/// Represents options of port scanning.
|
||||
@@ -17,25 +17,23 @@ pub enum PortStrategy {
|
||||
}
|
||||
|
||||
impl PortStrategy {
|
||||
pub fn pick(range: &Option<PortRange>, ports: Option<Vec<u16>>, order: ScanOrder) -> Self {
|
||||
pub fn pick(range: &Option<PortRanges>, ports: Option<Vec<u16>>, order: ScanOrder) -> Self {
|
||||
match order {
|
||||
ScanOrder::Serial if ports.is_none() => {
|
||||
let range = range.as_ref().unwrap();
|
||||
let port_ranges = range.as_ref().unwrap();
|
||||
PortStrategy::Serial(SerialRange {
|
||||
start: range.start,
|
||||
end: range.end,
|
||||
range: port_ranges.0.clone(),
|
||||
})
|
||||
}
|
||||
ScanOrder::Random if ports.is_none() => {
|
||||
let range = range.as_ref().unwrap();
|
||||
let port_ranges = range.as_ref().unwrap();
|
||||
PortStrategy::Random(RandomRange {
|
||||
start: range.start,
|
||||
end: range.end,
|
||||
range: port_ranges.0.clone(),
|
||||
})
|
||||
}
|
||||
ScanOrder::Serial => PortStrategy::Manual(ports.unwrap()),
|
||||
ScanOrder::Random => {
|
||||
let mut rng = thread_rng();
|
||||
let mut rng = rng();
|
||||
let mut ports = ports.unwrap();
|
||||
ports.shuffle(&mut rng);
|
||||
PortStrategy::Manual(ports)
|
||||
@@ -62,13 +60,12 @@ trait RangeOrder {
|
||||
/// ascending order.
|
||||
#[derive(Debug)]
|
||||
pub struct SerialRange {
|
||||
start: u16,
|
||||
end: u16,
|
||||
range: Vec<(u16, u16)>,
|
||||
}
|
||||
|
||||
impl RangeOrder for SerialRange {
|
||||
fn generate(&self) -> Vec<u16> {
|
||||
(self.start..=self.end).collect()
|
||||
RangeIterator::new_serial(&self.range).collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,8 +73,7 @@ impl RangeOrder for SerialRange {
|
||||
/// a random order. This vector is built following the LCG algorithm.
|
||||
#[derive(Debug)]
|
||||
pub struct RandomRange {
|
||||
start: u16,
|
||||
end: u16,
|
||||
range: Vec<(u16, u16)>,
|
||||
}
|
||||
|
||||
impl RangeOrder for RandomRange {
|
||||
@@ -91,33 +87,47 @@ impl RangeOrder for RandomRange {
|
||||
// port numbers close to each other are pretty slim due to the way the
|
||||
// algorithm works.
|
||||
fn generate(&self) -> Vec<u16> {
|
||||
RangeIterator::new(self.start.into(), self.end.into()).collect()
|
||||
RangeIterator::new_random(&self.range).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::PortStrategy;
|
||||
use crate::input::{PortRange, ScanOrder};
|
||||
use crate::input::{PortRanges, ScanOrder};
|
||||
use std::collections::HashSet;
|
||||
|
||||
fn expected_ports_from_ranges(input: &[(u16, u16)]) -> Vec<u16> {
|
||||
let mut s = HashSet::new();
|
||||
for &(start, end) in input {
|
||||
for p in start..=end {
|
||||
s.insert(p);
|
||||
}
|
||||
}
|
||||
let mut v: Vec<u16> = s.into_iter().collect();
|
||||
v.sort_unstable();
|
||||
v
|
||||
}
|
||||
#[test]
|
||||
fn serial_strategy_with_range() {
|
||||
let range = PortRange { start: 1, end: 100 };
|
||||
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Serial);
|
||||
let ranges = PortRanges(vec![(1u16, 10u16), (20u16, 30u16), (100u16, 110u16)]);
|
||||
let strategy = PortStrategy::pick(&Some(ranges.clone()), None, ScanOrder::Serial);
|
||||
let result = strategy.order();
|
||||
let expected_range = (1..=100).into_iter().collect::<Vec<u16>>();
|
||||
assert_eq!(expected_range, result);
|
||||
let expected = expected_ports_from_ranges(&ranges.0);
|
||||
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
#[test]
|
||||
fn random_strategy_with_range() {
|
||||
let range = PortRange { start: 1, end: 100 };
|
||||
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
|
||||
let ranges = PortRanges(vec![(1u16, 10u16), (20u16, 30u16), (100u16, 110u16)]);
|
||||
let strategy = PortStrategy::pick(&Some(ranges.clone()), None, ScanOrder::Random);
|
||||
let mut result = strategy.order();
|
||||
let expected_range = (1..=100).into_iter().collect::<Vec<u16>>();
|
||||
assert_ne!(expected_range, result);
|
||||
let expected = expected_ports_from_ranges(&ranges.0);
|
||||
|
||||
assert_ne!(expected, result);
|
||||
result.sort_unstable();
|
||||
assert_eq!(expected_range, result);
|
||||
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -131,10 +141,10 @@ mod tests {
|
||||
fn random_strategy_with_ports() {
|
||||
let strategy = PortStrategy::pick(&None, Some((1..10).collect()), ScanOrder::Random);
|
||||
let mut result = strategy.order();
|
||||
let expected_range = (1..10).into_iter().collect::<Vec<u16>>();
|
||||
assert_ne!(expected_range, result);
|
||||
let expected = (1..10).collect::<Vec<u16>>();
|
||||
assert_ne!(expected, result);
|
||||
|
||||
result.sort_unstable();
|
||||
assert_eq!(expected_range, result);
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
}
|
||||
@@ -1,71 +1,196 @@
|
||||
use gcd::Gcd;
|
||||
use rand::Rng;
|
||||
use rand::RngExt;
|
||||
use std::convert::TryInto;
|
||||
|
||||
/// Yields ports produced from a collection of (possibly overlapping)
|
||||
/// inclusive `u16` ranges in two modes:
|
||||
///
|
||||
/// **Randomized** — created by `RangeIterator::new_random`:
|
||||
/// - The algorithm generates a permutation of indices `0..N-1` using the
|
||||
/// additive congruential step `x_{i+1} = (x_i + step) % N`.
|
||||
/// - `step` is chosen so `gcd(step, N) == 1` to ensure the sequence is a
|
||||
/// full-length cycle (visits each index exactly once).
|
||||
/// - `x_0` (the seed stored as `normalized_first_pick`) is chosen uniformly
|
||||
/// in `0..N`.
|
||||
///
|
||||
/// For more information: <https://en.wikipedia.org/wiki/Linear_congruential_generator>
|
||||
///
|
||||
/// **Serial** — `RangeIterator::new_serial`:
|
||||
/// Iterates the input ranges in the **original input order** and yields
|
||||
/// each port the first time it is encountered. Duplicate ports (from
|
||||
/// overlapping ranges) are skipped using a 65_536-entry "seen" table.
|
||||
pub struct RangeIterator {
|
||||
active: bool,
|
||||
normalized_end: u32,
|
||||
total: u32,
|
||||
normalized_first_pick: u32,
|
||||
normalized_pick: u32,
|
||||
actual_start: u32,
|
||||
step: u32,
|
||||
ranges: Vec<(u32, u32)>,
|
||||
prefix: Vec<u32>,
|
||||
serial_itr: Option<Box<dyn Iterator<Item = u16>>>,
|
||||
/// `seen[port]` is true once `port` has been yielded (serial mode only).
|
||||
serial_seen: Option<Vec<bool>>,
|
||||
}
|
||||
|
||||
/// An iterator that follows the `Linear Congruential Generator` algorithm.
|
||||
///
|
||||
/// For more information: <https://en.wikipedia.org/wiki/Linear_congruential_generator>
|
||||
impl RangeIterator {
|
||||
/// Receives the start and end of a range and normalize
|
||||
/// these values before selecting a coprime for the end of the range
|
||||
/// which will server as the step for the algorithm.
|
||||
/// Construct a randomized iterator (LCG permutation).
|
||||
///
|
||||
/// For example, the range `1000-2500` will be normalized to `0-1500`
|
||||
/// before going through the algorithm.
|
||||
pub fn new(start: u32, end: u32) -> Self {
|
||||
let normalized_end = end - start + 1;
|
||||
let step = pick_random_coprime(normalized_end);
|
||||
/// Preconditions:
|
||||
/// - `input` must contain at least one `(u16,u16)`
|
||||
/// and each pair must satisfy `start <= end`.
|
||||
pub fn new_random(input: &[(u16, u16)]) -> Self {
|
||||
// normalize & merge into (start, len) u32 pairs
|
||||
// Example: [(10,12),(11,15)] -> merged [(10,6)]
|
||||
let mut ranges: Vec<(u32, u32)> = input
|
||||
.iter()
|
||||
.map(|(s, e)| {
|
||||
let start = *s as u32;
|
||||
let end_excl = (*e as u32) + 1; // convert inclusive -> exclusive
|
||||
(start, end_excl)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Randomly choose a number within the range to be the first
|
||||
// and assign it as a pick.
|
||||
let mut rng = rand::thread_rng();
|
||||
let normalized_first_pick = rng.gen_range(0..normalized_end);
|
||||
ranges.sort_unstable_by_key(|&(s, _)| s);
|
||||
|
||||
let mut merged: Vec<(u32, u32)> = Vec::with_capacity(ranges.len());
|
||||
if !ranges.is_empty() {
|
||||
let mut iter = ranges.into_iter();
|
||||
let (mut cur_s, mut cur_end) = iter.next().unwrap(); // cur_end exclusive
|
||||
for (s, end_excl) in iter {
|
||||
if s <= cur_end {
|
||||
// overlap/adjacent -> extend
|
||||
if end_excl > cur_end {
|
||||
cur_end = end_excl;
|
||||
}
|
||||
} else {
|
||||
// push disjoint segment as (start, len)
|
||||
merged.push((cur_s, cur_end - cur_s)); // len = excl - start
|
||||
cur_s = s;
|
||||
cur_end = end_excl;
|
||||
}
|
||||
}
|
||||
merged.push((cur_s, cur_end - cur_s));
|
||||
}
|
||||
// build prefix sums (prefix[0] = 0; prefix.len() == merged.len() + 1)
|
||||
let prefix = merged.iter().fold(vec![0u32], |mut acc, (_, len)| {
|
||||
let last = acc.last().unwrap();
|
||||
acc.push(last.saturating_add(*len));
|
||||
acc
|
||||
});
|
||||
|
||||
// total is guaranteed > 0 by precondition (input.len() >= 1)
|
||||
let total = *prefix.last().unwrap();
|
||||
|
||||
// pick step and seed
|
||||
let step = pick_random_coprime(total);
|
||||
let mut rng = rand::rng();
|
||||
let first = rng.random_range(0..total);
|
||||
|
||||
Self {
|
||||
active: true,
|
||||
normalized_end,
|
||||
total,
|
||||
normalized_first_pick: first,
|
||||
normalized_pick: first,
|
||||
step,
|
||||
normalized_first_pick,
|
||||
normalized_pick: normalized_first_pick,
|
||||
actual_start: start,
|
||||
ranges: merged,
|
||||
prefix,
|
||||
serial_itr: None,
|
||||
serial_seen: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a serial iterator that yields ports in original input order,
|
||||
/// skipping duplicates. The deduplication is done on the fly with a seen-table.
|
||||
///
|
||||
/// Preconditions:
|
||||
/// - `input` must contain at least one `(u16,u16)` and each pair must satisfy `start <= end`.
|
||||
pub fn new_serial(input: &[(u16, u16)]) -> Self {
|
||||
// Build a serial iterator that yields ports in *input order* (start..=end).
|
||||
// We keep the merged ranges/prefix empty here (they are not needed for serial mode).
|
||||
let input = input.to_vec();
|
||||
let serial_itr = input.into_iter().flat_map(|(start, end)| start..=end);
|
||||
|
||||
let serial_itr_boxed: Box<dyn Iterator<Item = u16>> = Box::new(serial_itr);
|
||||
// One entry per possible port (0..=65535).
|
||||
let seen = vec![false; usize::from(u16::MAX) + 1];
|
||||
|
||||
Self {
|
||||
active: true,
|
||||
total: 0,
|
||||
normalized_first_pick: 0,
|
||||
normalized_pick: 0,
|
||||
step: 0,
|
||||
ranges: Vec::new(),
|
||||
prefix: Vec::new(),
|
||||
serial_itr: Some(serial_itr_boxed),
|
||||
serial_seen: Some(seen),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for RangeIterator {
|
||||
type Item = u16;
|
||||
|
||||
// The next step is always bound by the formula: N+1 = (N + STEP) % TOP_OF_THE_RANGE
|
||||
// It will only stop once we generate a number equal to the first generated number.
|
||||
/// Advance the iterator by one port.
|
||||
///
|
||||
/// 1. Read the current normalized index `cur`.
|
||||
/// 2. Compute `next = (cur + step) % total` and update `normalized_pick`.
|
||||
/// 3. If `next == normalized_first_pick` mark `active = false` (we completed the cycle).
|
||||
/// 4. Map the returned index `cur` into the merged ranges via the prefix array:
|
||||
/// - find the range index `idx` where `prefix[idx] <= cur < prefix[idx+1]`,
|
||||
/// - offset = `cur - prefix[idx]`,
|
||||
/// - port = `ranges[idx].0 + offset`.
|
||||
/// 5. Return `port as u16`.
|
||||
///
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if !self.active {
|
||||
return None;
|
||||
}
|
||||
|
||||
let current_pick = self.normalized_pick;
|
||||
let next_pick = (current_pick + self.step) % self.normalized_end;
|
||||
// SERIAL iterator fast-path: preserve original input order but skip duplicates.
|
||||
if let (Some(it), Some(seen)) = (self.serial_itr.as_mut(), self.serial_seen.as_mut()) {
|
||||
for p in it.by_ref() {
|
||||
// Yield each port only the first time it is seen.
|
||||
if !std::mem::replace(&mut seen[usize::from(p)], true) {
|
||||
return Some(p);
|
||||
}
|
||||
// otherwise skip duplicate and continue
|
||||
}
|
||||
// serial iterator exhausted: drop it and mark inactive
|
||||
self.serial_itr = None;
|
||||
self.serial_seen = None;
|
||||
self.active = false;
|
||||
return None;
|
||||
}
|
||||
|
||||
// If the next pick is equal to the first pick this means that
|
||||
// we have iterated through the entire range.
|
||||
if next_pick == self.normalized_first_pick {
|
||||
// RANDOMIZED (LCG) path
|
||||
let cur = self.normalized_pick;
|
||||
let next = (cur + self.step) % self.total;
|
||||
|
||||
// if next equals the original seed we finished the cycle after returning cur
|
||||
if next == self.normalized_first_pick {
|
||||
self.active = false;
|
||||
}
|
||||
|
||||
self.normalized_pick = next_pick;
|
||||
Some(
|
||||
(self.actual_start + current_pick)
|
||||
.try_into()
|
||||
.expect("Could not convert u32 to u16"),
|
||||
)
|
||||
self.normalized_pick = next;
|
||||
|
||||
// Map cur -> port using prefix + ranges (binary search)
|
||||
let mut lo: usize = 0;
|
||||
let mut hi: usize = self.ranges.len();
|
||||
while lo < hi {
|
||||
let mid = (lo + hi) / 2;
|
||||
if self.prefix[mid + 1] > cur {
|
||||
hi = mid;
|
||||
} else {
|
||||
lo = mid + 1;
|
||||
}
|
||||
}
|
||||
let idx = lo;
|
||||
let offset = cur - self.prefix[idx];
|
||||
let (start, _len) = self.ranges[idx];
|
||||
let port = (start + offset)
|
||||
.try_into()
|
||||
.expect("Could not convert u32 to u16");
|
||||
Some(port)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,14 +208,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::thread_rng();
|
||||
let mut candidate = rng.gen_range(lower_range..upper_range);
|
||||
let mut rng = rand::rng();
|
||||
let mut candidate = rng.random_range(lower_range..upper_range);
|
||||
|
||||
for _ in 0..10 {
|
||||
if end.gcd(candidate) == 1 {
|
||||
return candidate;
|
||||
}
|
||||
candidate = rng.gen_range(lower_range..upper_range);
|
||||
candidate = rng.random_range(lower_range..upper_range);
|
||||
}
|
||||
|
||||
end - 1
|
||||
@@ -98,36 +223,105 @@ fn pick_random_coprime(end: u32) -> u32 {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::RangeIterator;
|
||||
use super::*;
|
||||
use std::collections::HashSet;
|
||||
|
||||
// Helper: collect, sort and return ports produced by randomized RangeIterator
|
||||
fn generate_sorted_from_ranges_random(input: &[(u16, u16)]) -> Vec<u16> {
|
||||
let mut it = RangeIterator::new_random(input);
|
||||
let mut v: Vec<u16> = it.by_ref().collect();
|
||||
v.sort_unstable();
|
||||
v
|
||||
}
|
||||
|
||||
// Helper: collect, sort and return ports produced by serial RangeIterator
|
||||
fn generate_sorted_from_ranges_serial(input: &[(u16, u16)]) -> Vec<u16> {
|
||||
let mut it = RangeIterator::new_serial(input);
|
||||
let mut v: Vec<u16> = it.by_ref().collect();
|
||||
v.sort_unstable();
|
||||
v
|
||||
}
|
||||
|
||||
// Build expected sorted unique ports from input ranges (inclusive)
|
||||
fn expected_ports_from_ranges(input: &[(u16, u16)]) -> Vec<u16> {
|
||||
let mut s = HashSet::new();
|
||||
for &(start, end) in input {
|
||||
for p in start..=end {
|
||||
s.insert(p);
|
||||
}
|
||||
}
|
||||
let mut v: Vec<u16> = s.into_iter().collect();
|
||||
v.sort_unstable();
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_iterator_iterates_through_the_entire_range() {
|
||||
let result = generate_sorted_range(1, 10);
|
||||
let expected_range = (1..=10).into_iter().collect::<Vec<u16>>();
|
||||
assert_eq!(expected_range, result);
|
||||
fn random_range_iterator_test() {
|
||||
// small disjoint ranges
|
||||
let input = &[(1u16, 10u16), (20u16, 30u16), (100u16, 110u16)];
|
||||
let result = generate_sorted_from_ranges_random(input);
|
||||
let expected = expected_ports_from_ranges(input);
|
||||
assert_eq!(expected, result);
|
||||
|
||||
let result = generate_sorted_range(1, 100);
|
||||
let expected_range = (1..=100).into_iter().collect::<Vec<u16>>();
|
||||
assert_eq!(expected_range, result);
|
||||
// larger disjoint ranges
|
||||
let input = &[(1u16, 100u16), (200u16, 500u16)];
|
||||
let result = generate_sorted_from_ranges_random(input);
|
||||
let expected = expected_ports_from_ranges(input);
|
||||
assert_eq!(expected, result);
|
||||
|
||||
let result = generate_sorted_range(1, 1000);
|
||||
let expected_range = (1..=1000).into_iter().collect::<Vec<u16>>();
|
||||
assert_eq!(expected_range, result);
|
||||
// overlapping and adjacent
|
||||
let input = &[(10u16, 20u16), (15u16, 25u16), (26u16, 30u16)];
|
||||
let result = generate_sorted_from_ranges_random(input);
|
||||
let expected = expected_ports_from_ranges(input);
|
||||
assert_eq!(expected, result);
|
||||
|
||||
let result = generate_sorted_range(1, 65_535);
|
||||
let expected_range = (1..=65_535).into_iter().collect::<Vec<u16>>();
|
||||
assert_eq!(expected_range, result);
|
||||
// near-full domain (heavy): we only assert lengths & equality
|
||||
let input = &[(1u16, 65_535u16)];
|
||||
let result = generate_sorted_from_ranges_random(input);
|
||||
let expected = expected_ports_from_ranges(input);
|
||||
assert_eq!(expected.len(), result.len());
|
||||
assert_eq!(expected, result);
|
||||
|
||||
let result = generate_sorted_range(1000, 2000);
|
||||
let expected_range = (1000..=2000).into_iter().collect::<Vec<u16>>();
|
||||
assert_eq!(expected_range, result);
|
||||
// multiple disjoint ranges - check dedupe & coverage
|
||||
let input = &[(50u16, 100u16), (1000u16, 2000u16), (30000u16, 30010u16)];
|
||||
let result = generate_sorted_from_ranges_random(input);
|
||||
let set_len = result.iter().copied().collect::<HashSet<u16>>().len();
|
||||
assert_eq!(set_len, result.len());
|
||||
let expected = expected_ports_from_ranges(input);
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
|
||||
fn generate_sorted_range(start: u32, end: u32) -> Vec<u16> {
|
||||
let range = RangeIterator::new(start, end);
|
||||
let mut result = range.into_iter().collect::<Vec<u16>>();
|
||||
result.sort_unstable();
|
||||
#[test]
|
||||
fn serial_range_iterator_test() {
|
||||
// serial should preserve input-order semantics but here we only assert
|
||||
// coverage (no duplicates) by sorting results and comparing expected set.
|
||||
|
||||
result
|
||||
// small disjoint ranges
|
||||
let input = &[(1u16, 10u16), (20u16, 30u16), (100u16, 110u16)];
|
||||
let result = generate_sorted_from_ranges_serial(input);
|
||||
let expected = expected_ports_from_ranges(input);
|
||||
assert_eq!(expected, result);
|
||||
|
||||
// overlapping and adjacent
|
||||
let input = &[(10u16, 20u16), (15u16, 25u16), (26u16, 30u16)];
|
||||
let result = generate_sorted_from_ranges_serial(input);
|
||||
let expected = expected_ports_from_ranges(input);
|
||||
assert_eq!(expected, result);
|
||||
|
||||
// multiple disjoint ranges
|
||||
let input = &[(50u16, 100u16), (1000u16, 2000u16), (30000u16, 30010u16)];
|
||||
let result = generate_sorted_from_ranges_serial(input);
|
||||
let set_len = result.iter().copied().collect::<HashSet<u16>>().len();
|
||||
assert_eq!(set_len, result.len());
|
||||
let expected = expected_ports_from_ranges(input);
|
||||
assert_eq!(expected, result);
|
||||
|
||||
// all possible inputs
|
||||
let input = &[(u16::MIN, u16::MAX)];
|
||||
let result = generate_sorted_from_ranges_serial(input);
|
||||
let set_len = result.iter().copied().collect::<HashSet<u16>>().len();
|
||||
assert_eq!(set_len, result.len());
|
||||
let expected = expected_ports_from_ranges(input);
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
}
|
||||
+280
-247
@@ -1,6 +1,7 @@
|
||||
//! Core functionality for actual scanning behaviour.
|
||||
use crate::generated::get_parsed_data;
|
||||
use crate::port_strategy::PortStrategy;
|
||||
use crate::udp_packets::udp_payload::cust_payload;
|
||||
use crate::tui::println_safe;
|
||||
use log::debug;
|
||||
|
||||
mod socket_iterator;
|
||||
@@ -8,29 +9,52 @@ use socket_iterator::SocketIterator;
|
||||
|
||||
use async_std::net::TcpStream;
|
||||
use async_std::prelude::*;
|
||||
use async_std::task::sleep;
|
||||
use async_std::{io, net::UdpSocket};
|
||||
use colored::Colorize;
|
||||
use futures::stream::FuturesUnordered;
|
||||
use std::collections::BTreeMap;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
collections::{HashMap, HashSet},
|
||||
net::{IpAddr, Shutdown, SocketAddr},
|
||||
num::NonZeroU8,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
/// UDP payload lookup: port -> payload bytes
|
||||
///
|
||||
/// `get_parsed_data()` returns a `&'static BTreeMap<...>`, so we can store
|
||||
/// references to the payload bytes without cloning them.
|
||||
#[doc(hidden)]
|
||||
pub type UdpPayloadLookup = HashMap<u16, &'static [u8]>;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn build_udp_payload_lookup(udp_map: &'static BTreeMap<Vec<u16>, Vec<u8>>) -> UdpPayloadLookup {
|
||||
let mut lookup: UdpPayloadLookup = HashMap::new();
|
||||
|
||||
for (ports, payload_vec) in udp_map.iter() {
|
||||
let payload: &'static [u8] = payload_vec.as_slice();
|
||||
for &port in ports.iter() {
|
||||
// Preserve existing behavior: if duplicates exist, last insert wins.
|
||||
lookup.insert(port, payload);
|
||||
}
|
||||
}
|
||||
|
||||
lookup
|
||||
}
|
||||
|
||||
/// The class for the scanner
|
||||
/// IP is data type IpAddr and is the IP address
|
||||
/// start & end is where the port scan starts and ends
|
||||
/// 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: u16,
|
||||
batch_size: usize,
|
||||
timeout: Duration,
|
||||
tries: NonZeroU8,
|
||||
greppable: bool,
|
||||
@@ -38,6 +62,19 @@ pub struct Scanner {
|
||||
accessible: bool,
|
||||
exclude_ports: Vec<u16>,
|
||||
udp: bool,
|
||||
print_open_ports: bool,
|
||||
report_closed: bool,
|
||||
interval: Duration,
|
||||
}
|
||||
|
||||
/// The outcome for a single socket, as returned by [`Scanner::run_with_status`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PortStatus {
|
||||
/// The TCP connection succeeded, or the UDP target answered.
|
||||
Open(SocketAddr),
|
||||
/// The target actively refused the TCP connection (for example with a
|
||||
/// RST). Only reported when [`Scanner::with_closed_ports`] is enabled.
|
||||
Closed(SocketAddr),
|
||||
}
|
||||
|
||||
// Allowing too many arguments for clippy.
|
||||
@@ -45,7 +82,7 @@ pub struct Scanner {
|
||||
impl Scanner {
|
||||
pub fn new(
|
||||
ips: &[IpAddr],
|
||||
batch_size: u16,
|
||||
batch_size: usize,
|
||||
timeout: Duration,
|
||||
tries: u8,
|
||||
greppable: bool,
|
||||
@@ -64,15 +101,62 @@ impl Scanner {
|
||||
accessible,
|
||||
exclude_ports,
|
||||
udp,
|
||||
print_open_ports: false,
|
||||
report_closed: false,
|
||||
interval: Duration::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
/// Enables the CLI's incremental open-port output.
|
||||
///
|
||||
/// Library callers are quiet by default and can inspect the sockets returned by [`Self::run`].
|
||||
#[must_use]
|
||||
pub fn with_open_port_output(mut self) -> Self {
|
||||
self.print_open_ports = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Also reports TCP ports that actively refuse the connection, as
|
||||
/// [`PortStatus::Closed`] from [`Self::run_with_status`].
|
||||
///
|
||||
/// Ports that time out are still treated as filtered and are not reported.
|
||||
/// UDP scans never report closed ports.
|
||||
#[must_use]
|
||||
pub fn with_closed_ports(mut self) -> Self {
|
||||
self.report_closed = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Waits `interval` after scanning one port (on every address) before
|
||||
/// scanning the next port, for slow, low-noise scans.
|
||||
///
|
||||
/// Within a port, up to `batch_size` addresses are still scanned
|
||||
/// concurrently. A zero interval (the default) scans all sockets in
|
||||
/// batches without any delay.
|
||||
#[must_use]
|
||||
pub fn with_interval(mut self, interval: Duration) -> Self {
|
||||
self.interval = interval;
|
||||
self
|
||||
}
|
||||
|
||||
/// Runs scan_range with chunk sizes
|
||||
/// If you want to run RustScan normally, this is the entry point used
|
||||
/// Returns all open ports as `Vec<u16>`
|
||||
/// Added by wasuaje - 01/26/2024:
|
||||
/// Filtering port against exclude port list
|
||||
/// Returns all open sockets.
|
||||
pub async fn run(&self) -> Vec<SocketAddr> {
|
||||
self.run_with_status()
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|status| match status {
|
||||
PortStatus::Open(socket) => Some(socket),
|
||||
PortStatus::Closed(_) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Like [`Self::run`], but returns the status of every socket that gave a
|
||||
/// definitive answer: open sockets, plus closed sockets when
|
||||
/// [`Self::with_closed_ports`] is enabled.
|
||||
pub async fn run_with_status(&self) -> Vec<PortStatus> {
|
||||
let ports: Vec<u16> = self
|
||||
.port_strategy
|
||||
.order()
|
||||
@@ -80,32 +164,73 @@ impl Scanner {
|
||||
.filter(|&port| !self.exclude_ports.contains(port))
|
||||
.copied()
|
||||
.collect();
|
||||
let mut socket_iterator: SocketIterator = SocketIterator::new(&self.ips, &ports);
|
||||
let mut open_sockets: Vec<SocketAddr> = Vec::new();
|
||||
let mut ftrs = FuturesUnordered::new();
|
||||
let mut found_sockets: Vec<PortStatus> = Vec::new();
|
||||
let mut errors: HashSet<String> = HashSet::new();
|
||||
|
||||
// Build UDP payload lookup once (only if we are scanning UDP).
|
||||
// This avoids cloning a big map into every spawned future and turns
|
||||
// payload selection from O(n) to O(1).
|
||||
let udp_payloads: Option<Arc<UdpPayloadLookup>> = if self.udp {
|
||||
Some(Arc::new(build_udp_payload_lookup(get_parsed_data())))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
debug!("Start scanning sockets. \nBatch size {}\nNumber of ip-s {}\nNumber of ports {}\nTargets all together {}\nInterval between ports {:?}",
|
||||
self.batch_size,
|
||||
self.ips.len(),
|
||||
ports.len(),
|
||||
(self.ips.len() * ports.len()),
|
||||
self.interval);
|
||||
|
||||
if self.interval.is_zero() {
|
||||
let sockets = SocketIterator::new(&self.ips, &ports);
|
||||
self.scan_sockets(sockets, &udp_payloads, &mut found_sockets, &mut errors)
|
||||
.await;
|
||||
} else {
|
||||
// Scan one port (on every address) at a time and wait `interval`
|
||||
// before moving on to the next port.
|
||||
for (i, port) in ports.iter().enumerate() {
|
||||
if i > 0 {
|
||||
sleep(self.interval).await;
|
||||
}
|
||||
let sockets = SocketIterator::new(&self.ips, std::slice::from_ref(port));
|
||||
self.scan_sockets(sockets, &udp_payloads, &mut found_sockets, &mut errors)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Typical socket connection errors {errors:?}");
|
||||
debug!("Sockets found: {:?}", found_sockets);
|
||||
found_sockets
|
||||
}
|
||||
|
||||
/// Scans every socket yielded by `sockets`, keeping at most `batch_size`
|
||||
/// connection attempts in flight.
|
||||
async fn scan_sockets(
|
||||
&self,
|
||||
mut sockets: SocketIterator<'_>,
|
||||
udp_payloads: &Option<Arc<UdpPayloadLookup>>,
|
||||
found_sockets: &mut Vec<PortStatus>,
|
||||
errors: &mut HashSet<String>,
|
||||
) {
|
||||
let mut ftrs = FuturesUnordered::new();
|
||||
|
||||
for _ in 0..self.batch_size {
|
||||
if let Some(socket) = socket_iterator.next() {
|
||||
ftrs.push(self.scan_socket(socket));
|
||||
if let Some(socket) = sockets.next() {
|
||||
ftrs.push(self.scan_socket(socket, udp_payloads.clone()));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Start scanning sockets. \nBatch size {}\nNumber of ip-s {}\nNumber of ports {}\nTargets all together {} ",
|
||||
self.batch_size,
|
||||
self.ips.len(),
|
||||
&ports.len(),
|
||||
(self.ips.len() * ports.len()));
|
||||
|
||||
while let Some(result) = ftrs.next().await {
|
||||
if let Some(socket) = socket_iterator.next() {
|
||||
ftrs.push(self.scan_socket(socket));
|
||||
if let Some(socket) = sockets.next() {
|
||||
ftrs.push(self.scan_socket(socket, udp_payloads.clone()));
|
||||
}
|
||||
|
||||
match result {
|
||||
Ok(socket) => open_sockets.push(socket),
|
||||
Ok(status) => found_sockets.push(status),
|
||||
Err(e) => {
|
||||
let error_string = e.to_string();
|
||||
if errors.len() < self.ips.len() * 1000 {
|
||||
@@ -114,9 +239,6 @@ impl Scanner {
|
||||
}
|
||||
}
|
||||
}
|
||||
debug!("Typical socket connection errors {:?}", errors);
|
||||
debug!("Open Sockets found: {:?}", &open_sockets);
|
||||
open_sockets
|
||||
}
|
||||
|
||||
/// Given a socket, scan it self.tries times.
|
||||
@@ -133,38 +255,36 @@ impl Scanner {
|
||||
/// ```
|
||||
///
|
||||
/// Note: `self` must contain `self.ip`.
|
||||
async fn scan_socket(&self, socket: SocketAddr) -> io::Result<SocketAddr> {
|
||||
async fn scan_socket(
|
||||
&self,
|
||||
socket: SocketAddr,
|
||||
udp_payloads: Option<Arc<UdpPayloadLookup>>,
|
||||
) -> io::Result<PortStatus> {
|
||||
if self.udp {
|
||||
let payload = cust_payload(socket.port());
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
return Ok(socket);
|
||||
return self.scan_udp_socket(socket, udp_payloads).await;
|
||||
}
|
||||
|
||||
let tries = self.tries.get();
|
||||
for nr_try in 1..=tries {
|
||||
match self.connect(socket).await {
|
||||
Ok(tcp_stream) => {
|
||||
debug!(
|
||||
"Connection was successful, shutting down stream {}",
|
||||
&socket
|
||||
);
|
||||
debug!("Connection was successful, shutting down stream {}", socket);
|
||||
if let Err(e) = tcp_stream.shutdown(Shutdown::Both) {
|
||||
debug!("Shutdown stream error {}", &e);
|
||||
debug!("Shutdown stream error {}", e);
|
||||
}
|
||||
self.fmt_ports(socket);
|
||||
|
||||
debug!("Return Ok after {} tries", nr_try);
|
||||
return Ok(socket);
|
||||
debug!("Return Ok after {nr_try} tries");
|
||||
return Ok(PortStatus::Open(socket));
|
||||
}
|
||||
Err(e) => {
|
||||
// A refused connection is a definitive answer, so there is
|
||||
// no point in retrying it.
|
||||
if self.report_closed && e.kind() == io::ErrorKind::ConnectionRefused {
|
||||
self.fmt_closed_port(socket);
|
||||
return Ok(PortStatus::Closed(socket));
|
||||
}
|
||||
|
||||
let mut error_string = e.to_string();
|
||||
|
||||
assert!(!error_string.to_lowercase().contains("too many open files"), "Too many open files. Please reduce batch size. The default is 5000. Try -b 2500.");
|
||||
@@ -172,7 +292,7 @@ impl Scanner {
|
||||
if nr_try == tries {
|
||||
error_string.push(' ');
|
||||
error_string.push_str(&socket.ip().to_string());
|
||||
return Err(io::Error::new(io::ErrorKind::Other, error_string));
|
||||
return Err(io::Error::other(error_string));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -180,6 +300,30 @@ impl Scanner {
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
async fn scan_udp_socket(
|
||||
&self,
|
||||
socket: SocketAddr,
|
||||
udp_payloads: Option<Arc<UdpPayloadLookup>>,
|
||||
) -> io::Result<PortStatus> {
|
||||
let payload: &[u8] = udp_payloads
|
||||
.as_ref()
|
||||
.and_then(|m| m.get(&socket.port()).copied())
|
||||
.unwrap_or(b"");
|
||||
|
||||
let tries = self.tries.get();
|
||||
for _ in 1..=tries {
|
||||
match self.udp_scan(socket, payload, self.timeout).await {
|
||||
Ok(true) => return Ok(PortStatus::Open(socket)),
|
||||
Ok(false) => continue,
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
Err(io::Error::other(format!(
|
||||
"UDP scan timed-out for all tries on socket {socket}"
|
||||
)))
|
||||
}
|
||||
|
||||
/// Performs the connection to the socket with timeout
|
||||
/// # Example
|
||||
///
|
||||
@@ -203,7 +347,7 @@ impl Scanner {
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
/// Binds to a UDP socket so we can send and recieve packets
|
||||
/// Binds to a UDP socket so we can send and receive packets
|
||||
/// # Example
|
||||
///
|
||||
/// ```compile_fail
|
||||
@@ -256,7 +400,7 @@ impl Scanner {
|
||||
|
||||
match io::timeout(wait, udp_socket.recv(&mut buf)).await {
|
||||
Ok(size) => {
|
||||
debug!("Received {} bytes", size);
|
||||
debug!("Received {size} bytes");
|
||||
self.fmt_ports(socket);
|
||||
Ok(true)
|
||||
}
|
||||
@@ -270,7 +414,7 @@ impl Scanner {
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Err E binding sock {:?}", e);
|
||||
debug!("Error binding UDP socket: {e:?}");
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
@@ -278,11 +422,22 @@ impl Scanner {
|
||||
|
||||
/// Formats and prints the port status
|
||||
fn fmt_ports(&self, socket: SocketAddr) {
|
||||
if !self.greppable {
|
||||
if self.print_open_ports && !self.greppable {
|
||||
if self.accessible {
|
||||
println!("Open {socket}");
|
||||
println_safe(format_args!("Open {socket}"));
|
||||
} else {
|
||||
println!("Open {}", socket.to_string().purple());
|
||||
println_safe(format_args!("Open {}", socket.to_string().purple()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prints a closed port (CLI output only, never in greppable mode).
|
||||
fn fmt_closed_port(&self, socket: SocketAddr) {
|
||||
if self.print_open_ports && !self.greppable {
|
||||
if self.accessible {
|
||||
println_safe(format_args!("Closed {socket}"));
|
||||
} else {
|
||||
println_safe(format_args!("Closed {}", socket.to_string().red()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -291,218 +446,96 @@ impl Scanner {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::input::{PortRange, ScanOrder};
|
||||
use async_std::task::block_on;
|
||||
use std::{net::IpAddr, time::Duration};
|
||||
|
||||
#[test]
|
||||
fn scanner_runs() {
|
||||
// Makes sure the program still runs and doesn't panic
|
||||
use crate::input::{PortRanges, ScanOrder};
|
||||
|
||||
// These tests never open sockets: they only build a `Scanner` or inspect
|
||||
// the payload table generated by build.rs.
|
||||
|
||||
fn test_scanner() -> Scanner {
|
||||
let addrs = vec!["127.0.0.1".parse::<IpAddr>().unwrap()];
|
||||
let range = PortRange {
|
||||
start: 1,
|
||||
end: 1_000,
|
||||
};
|
||||
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
|
||||
let scanner = Scanner::new(
|
||||
let strategy = PortStrategy::pick(&Some(PortRanges(vec![(1, 1)])), None, ScanOrder::Serial);
|
||||
Scanner::new(
|
||||
&addrs,
|
||||
10,
|
||||
1,
|
||||
Duration::from_millis(100),
|
||||
1,
|
||||
true,
|
||||
strategy,
|
||||
true,
|
||||
vec![9000],
|
||||
false,
|
||||
);
|
||||
block_on(scanner.run());
|
||||
// if the scan fails, it wouldn't be able to assert_eq! as it panicked!
|
||||
assert_eq!(1, 1);
|
||||
}
|
||||
#[test]
|
||||
fn ipv6_scanner_runs() {
|
||||
// Makes sure the program still runs and doesn't panic
|
||||
let addrs = vec!["::1".parse::<IpAddr>().unwrap()];
|
||||
let range = PortRange {
|
||||
start: 1,
|
||||
end: 1_000,
|
||||
};
|
||||
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
|
||||
let scanner = Scanner::new(
|
||||
&addrs,
|
||||
10,
|
||||
Duration::from_millis(100),
|
||||
1,
|
||||
true,
|
||||
strategy,
|
||||
true,
|
||||
vec![9000],
|
||||
false,
|
||||
);
|
||||
block_on(scanner.run());
|
||||
// if the scan fails, it wouldn't be able to assert_eq! as it panicked!
|
||||
assert_eq!(1, 1);
|
||||
}
|
||||
#[test]
|
||||
fn quad_zero_scanner_runs() {
|
||||
let addrs = vec!["0.0.0.0".parse::<IpAddr>().unwrap()];
|
||||
let range = PortRange {
|
||||
start: 1,
|
||||
end: 1_000,
|
||||
};
|
||||
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
|
||||
let scanner = Scanner::new(
|
||||
&addrs,
|
||||
10,
|
||||
Duration::from_millis(100),
|
||||
1,
|
||||
true,
|
||||
strategy,
|
||||
true,
|
||||
vec![9000],
|
||||
Vec::new(),
|
||||
false,
|
||||
);
|
||||
block_on(scanner.run());
|
||||
assert_eq!(1, 1);
|
||||
}
|
||||
#[test]
|
||||
fn google_dns_runs() {
|
||||
let addrs = vec!["8.8.8.8".parse::<IpAddr>().unwrap()];
|
||||
let range = PortRange {
|
||||
start: 400,
|
||||
end: 445,
|
||||
};
|
||||
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
|
||||
let scanner = Scanner::new(
|
||||
&addrs,
|
||||
10,
|
||||
Duration::from_millis(100),
|
||||
1,
|
||||
true,
|
||||
strategy,
|
||||
true,
|
||||
vec![9000],
|
||||
false,
|
||||
);
|
||||
block_on(scanner.run());
|
||||
assert_eq!(1, 1);
|
||||
}
|
||||
#[test]
|
||||
fn infer_ulimit_lowering_no_panic() {
|
||||
// Test behaviour on MacOS where ulimit is not automatically lowered
|
||||
let addrs = vec!["8.8.8.8".parse::<IpAddr>().unwrap()];
|
||||
|
||||
// mac should have this automatically scaled down
|
||||
let range = PortRange {
|
||||
start: 400,
|
||||
end: 600,
|
||||
};
|
||||
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
|
||||
let scanner = Scanner::new(
|
||||
&addrs,
|
||||
10,
|
||||
Duration::from_millis(100),
|
||||
1,
|
||||
true,
|
||||
strategy,
|
||||
true,
|
||||
vec![9000],
|
||||
false,
|
||||
);
|
||||
block_on(scanner.run());
|
||||
assert_eq!(1, 1);
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn udp_scan_runs() {
|
||||
// Makes sure the program still runs and doesn't panic
|
||||
let addrs = vec!["127.0.0.1".parse::<IpAddr>().unwrap()];
|
||||
let range = PortRange {
|
||||
start: 1,
|
||||
end: 1_000,
|
||||
};
|
||||
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
|
||||
let scanner = Scanner::new(
|
||||
&addrs,
|
||||
10,
|
||||
Duration::from_millis(100),
|
||||
1,
|
||||
true,
|
||||
strategy,
|
||||
true,
|
||||
vec![9000],
|
||||
true,
|
||||
);
|
||||
block_on(scanner.run());
|
||||
// if the scan fails, it wouldn't be able to assert_eq! as it panicked!
|
||||
assert_eq!(1, 1);
|
||||
fn library_scanner_is_quiet_by_default() {
|
||||
let scanner = test_scanner();
|
||||
|
||||
assert!(!scanner.print_open_ports);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn udp_ipv6_runs() {
|
||||
// Makes sure the program still runs and doesn't panic
|
||||
let addrs = vec!["::1".parse::<IpAddr>().unwrap()];
|
||||
let range = PortRange {
|
||||
start: 1,
|
||||
end: 1_000,
|
||||
};
|
||||
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
|
||||
let scanner = Scanner::new(
|
||||
&addrs,
|
||||
10,
|
||||
Duration::from_millis(100),
|
||||
1,
|
||||
true,
|
||||
strategy,
|
||||
true,
|
||||
vec![9000],
|
||||
true,
|
||||
);
|
||||
block_on(scanner.run());
|
||||
// if the scan fails, it wouldn't be able to assert_eq! as it panicked!
|
||||
assert_eq!(1, 1);
|
||||
fn cli_can_enable_open_port_output() {
|
||||
let scanner = test_scanner().with_open_port_output();
|
||||
|
||||
assert!(scanner.print_open_ports);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn udp_quad_zero_scanner_runs() {
|
||||
let addrs = vec!["0.0.0.0".parse::<IpAddr>().unwrap()];
|
||||
let range = PortRange {
|
||||
start: 1,
|
||||
end: 1_000,
|
||||
};
|
||||
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
|
||||
let scanner = Scanner::new(
|
||||
&addrs,
|
||||
10,
|
||||
Duration::from_millis(100),
|
||||
1,
|
||||
true,
|
||||
strategy,
|
||||
true,
|
||||
vec![9000],
|
||||
true,
|
||||
);
|
||||
block_on(scanner.run());
|
||||
assert_eq!(1, 1);
|
||||
fn closed_ports_are_not_reported_by_default() {
|
||||
assert!(!test_scanner().report_closed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn udp_google_dns_runs() {
|
||||
let addrs = vec!["8.8.8.8".parse::<IpAddr>().unwrap()];
|
||||
let range = PortRange {
|
||||
start: 100,
|
||||
end: 150,
|
||||
};
|
||||
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
|
||||
let scanner = Scanner::new(
|
||||
&addrs,
|
||||
10,
|
||||
Duration::from_millis(100),
|
||||
1,
|
||||
true,
|
||||
strategy,
|
||||
true,
|
||||
vec![9000],
|
||||
true,
|
||||
);
|
||||
block_on(scanner.run());
|
||||
assert_eq!(1, 1);
|
||||
fn closed_port_reporting_is_opt_in() {
|
||||
assert!(test_scanner().with_closed_ports().report_closed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_interval_by_default() {
|
||||
assert!(test_scanner().interval.is_zero());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_interval_sets_the_delay_between_ports() {
|
||||
let scanner = test_scanner().with_interval(Duration::from_millis(250));
|
||||
|
||||
assert_eq!(scanner.interval, Duration::from_millis(250));
|
||||
}
|
||||
|
||||
/// Regression test for https://github.com/bee-san/RustScan/issues/933:
|
||||
/// the SNMP public-walk probe must be the exact 33-byte BER packet, with
|
||||
/// the literal `public` community string intact. The old hexdigits-only
|
||||
/// decoding mangled it into a 28-byte probe that agents never answered.
|
||||
#[test]
|
||||
fn udp_snmp_probe_bytes_match_nmap() {
|
||||
let payload = get_parsed_data()
|
||||
.iter()
|
||||
.find(|(ports, _)| ports.contains(&161))
|
||||
.map(|(_, payload)| payload)
|
||||
.expect("no UDP payload registered for port 161");
|
||||
let expected: Vec<u8> = vec![
|
||||
0x30, 0x1f, 0x02, 0x01, 0x00, 0x04, 0x06, b'p', b'u', b'b', b'l', b'i', b'c', 0xa1,
|
||||
0x12, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00, 0x30, 0x07, 0x30, 0x05,
|
||||
0x06, 0x01, 0x00, 0x05, 0x00,
|
||||
];
|
||||
assert_eq!(*payload, expected);
|
||||
}
|
||||
|
||||
/// The SSDP probe mixes `\xNN` escapes, `\"` escapes and literal text
|
||||
/// across two quoted segments: segments must decode and concatenate
|
||||
/// with no separators.
|
||||
#[test]
|
||||
fn udp_ssdp_probe_decodes_escapes_and_literal_text() {
|
||||
let payload = get_parsed_data()
|
||||
.iter()
|
||||
.find(|(ports, _)| ports.contains(&1900))
|
||||
.map(|(_, payload)| payload)
|
||||
.expect("no UDP payload registered for port 1900");
|
||||
let expected =
|
||||
b"M-SEARCH * HTTP/1.1\r\nHost: 239.255.255.250:1900\r\nMan: \"ssdp:discover\"\r\nMX: 5\r\nST: ssdp:all\r\n\r\n"
|
||||
.to_vec();
|
||||
assert_eq!(*payload, expected);
|
||||
}
|
||||
}
|
||||
+102
-30
@@ -1,55 +1,78 @@
|
||||
use itertools::{iproduct, Product};
|
||||
use std::iter::FusedIterator;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::slice;
|
||||
|
||||
/// An iterator that receives a slice of IPs and ports and returns a
|
||||
/// [`SocketAddr`] for each IP/port pair until all combinations are exhausted.
|
||||
///
|
||||
/// The goal of this iterator is to walk every IP and port combination
|
||||
/// *without* allocating a big intermediate buffer — the alternative would be
|
||||
/// materialising a `Vec<SocketAddr>` of every pair up front.
|
||||
///
|
||||
/// # Ordering
|
||||
///
|
||||
/// The IP/port order is intentionally reversed inside `product_it`: we want
|
||||
/// `iproduct!` to iterate *all IPs for one port* before advancing to the next
|
||||
/// port ("hold the port, go through all the IPs, then advance the port").
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// `SocketIterator` lives in a private module, so this example is not run as
|
||||
/// a doctest; `goes_through_every_ip_port_combination` checks the same order.
|
||||
///
|
||||
/// ```ignore
|
||||
/// # use std::net::IpAddr;
|
||||
/// let ips = [
|
||||
/// "127.0.0.1".parse::<IpAddr>().unwrap(),
|
||||
/// "192.168.0.1".parse::<IpAddr>().unwrap(),
|
||||
/// ];
|
||||
/// let ports = [80u16, 443];
|
||||
///
|
||||
/// let mut it = SocketIterator::new(&ips, &ports);
|
||||
/// assert_eq!(it.next(), Some("127.0.0.1:80".parse().unwrap()));
|
||||
/// assert_eq!(it.next(), Some("192.168.0.1:80".parse().unwrap()));
|
||||
/// assert_eq!(it.next(), Some("127.0.0.1:443".parse().unwrap()));
|
||||
/// assert_eq!(it.next(), Some("192.168.0.1:443".parse().unwrap()));
|
||||
/// assert_eq!(it.next(), None);
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct SocketIterator<'s> {
|
||||
// product_it is a cartesian product iterator over
|
||||
// the slices of ports and IP addresses.
|
||||
//
|
||||
// The IP/port order is intentionally reversed here since we want
|
||||
// the itertools::iproduct! macro below to generate the pairs with
|
||||
// all the IPs for one port before moving on to the next one
|
||||
// ("hold the port, go through all the IPs, then advance the port...").
|
||||
// See also the comments in the iterator implementation for an example.
|
||||
product_it:
|
||||
Product<Box<std::slice::Iter<'s, u16>>, Box<std::slice::Iter<'s, std::net::IpAddr>>>,
|
||||
// No boxing: the iterator owns the concrete slice iterators directly.
|
||||
// This keeps `SocketIterator` allocation-free and makes `Clone` free too.
|
||||
product_it: Product<slice::Iter<'s, u16>, slice::Iter<'s, IpAddr>>,
|
||||
}
|
||||
|
||||
/// An iterator that receives a slice of IPs and ports and returns a Socket
|
||||
/// for each IP and port pair until all of these combinations are exhausted.
|
||||
/// The goal of this iterator is to go over every IP and port combination
|
||||
/// without generating a big memory footprint. The alternative would be
|
||||
/// generating a vector containing all these combinations.
|
||||
impl<'s> SocketIterator<'s> {
|
||||
pub fn new(ips: &'s [IpAddr], ports: &'s [u16]) -> Self {
|
||||
let ports_it = Box::new(ports.iter());
|
||||
let ips_it = Box::new(ips.iter());
|
||||
Self {
|
||||
product_it: iproduct!(ports_it, ips_it),
|
||||
// `iproduct!` calls `.into_iter()` on each argument; `slice::Iter`
|
||||
// is already an iterator, so this is a no-op (no allocation).
|
||||
product_it: iproduct!(ports.iter(), ips.iter()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::doc_link_with_quotes)]
|
||||
impl<'s> Iterator for SocketIterator<'s> {
|
||||
impl Iterator for SocketIterator<'_> {
|
||||
type Item = SocketAddr;
|
||||
|
||||
/// Returns a socket based on the combination of one of the provided
|
||||
/// IPs and ports or None when these combinations are exhausted. Every
|
||||
/// IP will have the same port until a port is incremented.
|
||||
///
|
||||
/// let it = SocketIterator::new(&["127.0.0.1", "192.168.0.1"], &[80, 443]);
|
||||
/// it.next(); // 127.0.0.1:80
|
||||
/// it.next(); // 192.168.0.1:80
|
||||
/// it.next(); // 127.0.0.1:443
|
||||
/// it.next(); // 192.168.0.1:443
|
||||
/// it.next(); // None
|
||||
/// Returns the next socket, or `None` when all combinations are exhausted.
|
||||
/// Every IP is paired with the same port until the port advances.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.product_it
|
||||
.next()
|
||||
.map(|(port, ip)| SocketAddr::new(*ip, *port))
|
||||
}
|
||||
|
||||
// Delegate the size hint so callers like `collect()` can pre-size buffers.
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.product_it.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
// `Product` and `slice::Iter` are both fused, so we are too.
|
||||
impl FusedIterator for SocketIterator<'_> {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::SocketIterator;
|
||||
@@ -72,4 +95,53 @@ mod tests {
|
||||
assert_eq!(Some(SocketAddr::new(addrs[1], ports[2])), it.next());
|
||||
assert_eq!(None, it.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_hint_is_exact() {
|
||||
let addrs = ["127.0.0.1".parse::<IpAddr>().unwrap()];
|
||||
let ports: Vec<u16> = vec![22, 80, 443];
|
||||
let mut it = SocketIterator::new(&addrs, &ports);
|
||||
|
||||
assert_eq!(it.size_hint(), (3, Some(3)));
|
||||
it.next();
|
||||
assert_eq!(it.size_hint(), (2, Some(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clone_resumes_independently() {
|
||||
let addrs = [
|
||||
"127.0.0.1".parse::<IpAddr>().unwrap(),
|
||||
"192.168.0.1".parse::<IpAddr>().unwrap(),
|
||||
];
|
||||
let ports: Vec<u16> = vec![22, 80];
|
||||
|
||||
let mut a = SocketIterator::new(&addrs, &ports);
|
||||
assert_eq!(a.next(), Some(SocketAddr::new(addrs[0], ports[0])));
|
||||
let b = a.clone(); // b starts where a is now
|
||||
|
||||
// 2 IPs x 2 ports = 4 sockets, one of which was consumed before cloning.
|
||||
let rest = [
|
||||
SocketAddr::new(addrs[1], ports[0]),
|
||||
SocketAddr::new(addrs[0], ports[1]),
|
||||
SocketAddr::new(addrs[1], ports[1]),
|
||||
];
|
||||
assert_eq!(a.collect::<Vec<_>>(), rest);
|
||||
assert_eq!(b.collect::<Vec<_>>(), rest);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_inputs_yield_nothing() {
|
||||
let addrs = ["127.0.0.1".parse::<IpAddr>().unwrap()];
|
||||
let ports = [22u16];
|
||||
|
||||
for mut it in [
|
||||
SocketIterator::new(&[], &ports),
|
||||
SocketIterator::new(&addrs, &[]),
|
||||
] {
|
||||
assert_eq!(it.size_hint(), (0, Some(0)));
|
||||
assert_eq!(it.next(), None);
|
||||
// Stays exhausted, as promised by the `FusedIterator` impl.
|
||||
assert_eq!(it.next(), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
+126
-122
@@ -46,12 +46,9 @@
|
||||
//!
|
||||
//! - `fixtures/test_rustscan_scripts.toml`
|
||||
//!
|
||||
//! Script file examples:
|
||||
//! Metadata-only test fixture:
|
||||
//!
|
||||
//! - `fixtures/test_script.py`
|
||||
//! - `fixtures/test_script.pl`
|
||||
//! - `fixtures/test_script.sh`
|
||||
//! - `fixtures/test_script.txt`
|
||||
//! - `fixtures/.rustscan_scripts/test_script.txt`
|
||||
//!
|
||||
//! `call_format` in script files can be of 2 variants:
|
||||
//!
|
||||
@@ -79,83 +76,80 @@ 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}} {{ip}}"
|
||||
call_format = "nmap -vvv -p {{port}} -{{ipversion}} {{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 => Ok(scripts_to_run),
|
||||
ScriptsRequired::None => {}
|
||||
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 Some(scripts_dir_base) = dirs::home_dir() else {
|
||||
return Err(anyhow!("Could not infer scripts path."));
|
||||
let script_config = ScriptConfig::read_config()?;
|
||||
debug!("Script config \n{script_config:?}");
|
||||
|
||||
let script_dir_base = if let Some(config_directory) = &script_config.directory {
|
||||
PathBuf::from(config_directory)
|
||||
} else {
|
||||
dirs::home_dir().ok_or_else(|| anyhow!("Could not infer scripts path."))?
|
||||
};
|
||||
let script_paths = match find_scripts(scripts_dir_base) {
|
||||
Ok(script_paths) => script_paths,
|
||||
Err(e) => return Err(anyhow!(e)),
|
||||
};
|
||||
debug!("Scripts paths \n{:?}", script_paths);
|
||||
|
||||
let script_paths = find_scripts(script_dir_base)?;
|
||||
debug!("Scripts paths \n{script_paths:?}");
|
||||
|
||||
let parsed_scripts = parse_scripts(script_paths);
|
||||
debug!("Scripts parsed \n{:?}", parsed_scripts);
|
||||
|
||||
let script_config = match ScriptConfig::read_config() {
|
||||
Ok(script_config) => script_config,
|
||||
Err(e) => return Err(anyhow!(e)),
|
||||
};
|
||||
debug!("Script config \n{:?}", script_config);
|
||||
debug!("Scripts parsed \n{parsed_scripts:?}");
|
||||
|
||||
// Only Scripts that contain all the tags found in ScriptConfig will be selected.
|
||||
if 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());
|
||||
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);
|
||||
} else {
|
||||
debug!(
|
||||
"\nScript tags does not match config tags {:?} {}",
|
||||
&script_hashset,
|
||||
script.path.clone().unwrap().display()
|
||||
script_hashset,
|
||||
script.path.unwrap().display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
debug!("\nScript(s) to run {:?}", scripts_to_run);
|
||||
Ok(scripts_to_run)
|
||||
debug!("\nScript(s) to run {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);
|
||||
}
|
||||
@@ -193,12 +187,14 @@ struct ExecPartsScript {
|
||||
script: String,
|
||||
ip: String,
|
||||
port: String,
|
||||
ipversion: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ExecParts {
|
||||
ip: String,
|
||||
port: String,
|
||||
ipversion: String,
|
||||
}
|
||||
|
||||
impl Script {
|
||||
@@ -225,7 +221,7 @@ impl Script {
|
||||
// Some variables get changed before read, and compiler throws warning on warn(unused_assignments)
|
||||
#[allow(unused_assignments)]
|
||||
pub fn run(self) -> Result<String> {
|
||||
debug!("run self {:?}", &self);
|
||||
debug!("run self {:?}", self);
|
||||
|
||||
let separator = self.ports_separator.unwrap_or_else(|| ",".into());
|
||||
|
||||
@@ -253,49 +249,77 @@ 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 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,
|
||||
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"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if es != 0 {
|
||||
return Err(anyhow!("Exit code = {}", es));
|
||||
}
|
||||
Ok(c.stdout_str())
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
}
|
||||
Err(error) => {
|
||||
debug!("Command error {}", error.to_string());
|
||||
debug!("Command error {error}",);
|
||||
Err(anyhow!(error.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_scripts(mut path: PathBuf) -> Result<Vec<PathBuf>> {
|
||||
path.push(".rustscan_scripts");
|
||||
pub fn find_scripts(path: PathBuf) -> Result<Vec<PathBuf>> {
|
||||
if path.is_dir() {
|
||||
debug!("Scripts folder found {}", &path.display());
|
||||
debug!("Scripts folder found {}", path.display());
|
||||
let mut files_vec: Vec<PathBuf> = Vec::new();
|
||||
for entry in fs::read_dir(path)? {
|
||||
let entry = entry?;
|
||||
@@ -333,20 +357,20 @@ impl ScriptFile {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("Failed to read file: {}", &real_path.display());
|
||||
debug!("Failed to read file: {}", real_path.display());
|
||||
return None;
|
||||
}
|
||||
debug!("ScriptFile {} lines\n{}", &real_path.display(), &lines_buf);
|
||||
debug!("ScriptFile {} lines\n{}", real_path.display(), lines_buf);
|
||||
|
||||
match toml::from_str::<ScriptFile>(&lines_buf) {
|
||||
Ok(mut parsed) => {
|
||||
debug!("Parsed ScriptFile{} \n{:?}", &real_path.display(), &parsed);
|
||||
debug!("Parsed ScriptFile{} \n{:?}", real_path.display(), parsed);
|
||||
parsed.path = Some(real_path);
|
||||
// parsed_scripts.push(parsed);
|
||||
Some(parsed)
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Failed to parse ScriptFile headers {}", e.to_string());
|
||||
debug!("Failed to parse ScriptFile headers {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -358,6 +382,7 @@ pub struct ScriptConfig {
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub ports: Option<Vec<String>>,
|
||||
pub developer: Option<Vec<String>>,
|
||||
pub directory: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
@@ -376,27 +401,13 @@ impl ScriptConfig {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{find_scripts, parse_scripts, Script, ScriptFile};
|
||||
|
||||
// Function for testing only, it inserts static values into ip and open_ports
|
||||
// Doesn't use impl in case it's implemented in the super module at some point
|
||||
fn into_script(script_f: ScriptFile) -> Script {
|
||||
Script::build(
|
||||
script_f.path,
|
||||
"127.0.0.1".parse().unwrap(),
|
||||
vec![80, 8080],
|
||||
script_f.port,
|
||||
script_f.ports_separator,
|
||||
script_f.tags,
|
||||
script_f.call_format,
|
||||
)
|
||||
}
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn find_and_parse_scripts() {
|
||||
let scripts = find_scripts("fixtures/".into()).unwrap();
|
||||
let scripts = find_scripts("fixtures/.rustscan_scripts".into()).unwrap();
|
||||
let scripts = parse_scripts(scripts);
|
||||
assert_eq!(scripts.len(), 4);
|
||||
assert_eq!(scripts.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -412,26 +423,6 @@ mod tests {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn open_script_file_invalid_call_format() {
|
||||
let mut script_f =
|
||||
ScriptFile::new("fixtures/.rustscan_scripts/test_script.txt".into()).unwrap();
|
||||
script_f.call_format = Some("qwertyuiop".to_string());
|
||||
let script: Script = into_script(script_f);
|
||||
let _output = script.run().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn open_script_file_missing_call_format() {
|
||||
let mut script_f =
|
||||
ScriptFile::new("fixtures/.rustscan_scripts/test_script.txt".into()).unwrap();
|
||||
script_f.call_format = None;
|
||||
let script: Script = into_script(script_f);
|
||||
let _output = script.run().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn open_nonexisting_script_file() {
|
||||
@@ -456,39 +447,52 @@ mod tests {
|
||||
assert_eq!(script_f.ports_separator, Some(",".to_string()));
|
||||
assert_eq!(
|
||||
script_f.call_format,
|
||||
Some("nmap -vvv -p {{port}} {{ip}}".to_string())
|
||||
Some("fixture {{ip}} {{port}}".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn run_bash_script() {
|
||||
let script_f = ScriptFile::new("fixtures/.rustscan_scripts/test_script.sh".into()).unwrap();
|
||||
let script: Script = into_script(script_f);
|
||||
let output = script.run().unwrap();
|
||||
// output has a newline at the end by default, .trim() trims it
|
||||
assert_eq!(output.trim(), "127.0.0.1 80,8080");
|
||||
}
|
||||
fn test_custom_directory_config() {
|
||||
// Create test config
|
||||
let config_str = r#"
|
||||
tags = ["core_approved", "example"]
|
||||
directory = "fixtures/.rustscan_scripts"
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn run_python_script() {
|
||||
let script_f = ScriptFile::new("fixtures/.rustscan_scripts/test_script.py".into()).unwrap();
|
||||
let script: Script = into_script(script_f);
|
||||
let output = script.run().unwrap();
|
||||
// output has a newline at the end by default, .trim() trims it
|
||||
let config: ScriptConfig = toml::from_str(config_str).unwrap();
|
||||
assert_eq!(
|
||||
output.trim(),
|
||||
"Python script ran with arguments ['fixtures/.rustscan_scripts/test_script.py', '127.0.0.1', '80,8080']"
|
||||
config.directory,
|
||||
Some("fixtures/.rustscan_scripts".to_string())
|
||||
);
|
||||
|
||||
// Test that the directory is actually used
|
||||
let script_dir_base = PathBuf::from(config.directory.unwrap());
|
||||
let scripts = find_scripts(script_dir_base).unwrap();
|
||||
|
||||
// Verify we found the test script
|
||||
assert!(scripts.iter().any(|p| p
|
||||
.file_name()
|
||||
.and_then(|f| f.to_str())
|
||||
.map(|s| s == "test_script.txt")
|
||||
.unwrap_or(false)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn run_perl_script() {
|
||||
let script_f = ScriptFile::new("fixtures/.rustscan_scripts/test_script.pl".into()).unwrap();
|
||||
let script: Script = into_script(script_f);
|
||||
let output = script.run().unwrap();
|
||||
// output has a newline at the end by default, .trim() trims it
|
||||
assert_eq!(output.trim(), "Total args passed to fixtures/.rustscan_scripts/test_script.pl : 2\nArg # 1 : 127.0.0.1\nArg # 2 : 80,8080");
|
||||
fn test_default_directory_fallback() {
|
||||
let config_str = r#"
|
||||
tags = ["core_approved", "example"]
|
||||
"#;
|
||||
|
||||
let config: ScriptConfig = toml::from_str(config_str).unwrap();
|
||||
assert_eq!(config.directory, None);
|
||||
|
||||
// Test fallback to home directory
|
||||
let script_dir_base = if let Some(config_directory) = &config.directory {
|
||||
PathBuf::from(config_directory)
|
||||
} else {
|
||||
dirs::home_dir().unwrap()
|
||||
};
|
||||
|
||||
assert_eq!(script_dir_base, dirs::home_dir().unwrap());
|
||||
}
|
||||
}
|
||||
+59
-15
@@ -1,20 +1,55 @@
|
||||
//! Utilities for terminal output during scanning.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
/// Prints a single line to stdout without ever panicking.
|
||||
///
|
||||
/// `println!` panics when writing to stdout fails, which is fatal under the
|
||||
/// `panic = "abort"` release profile. That includes the `BrokenPipe` error
|
||||
/// produced when a downstream consumer (e.g. `rustscan -g ... | head`)
|
||||
/// closes the pipe, so instead the process exits quietly when that happens.
|
||||
/// Other output errors are reported on stderr before exiting.
|
||||
///
|
||||
/// All user-facing output should go through this function (the `warning!`,
|
||||
/// `detail!`, `output!` and `funny_opening!` macros already do) instead of
|
||||
/// `println!`.
|
||||
pub fn println_safe(args: std::fmt::Arguments<'_>) {
|
||||
match writeln!(std::io::stdout().lock(), "{args}") {
|
||||
Ok(()) => {}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => {
|
||||
// the reader end of the pipe is gone; nothing left to print to
|
||||
std::process::exit(0);
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = writeln!(std::io::stderr(), "rustscan: failed writing to stdout: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Terminal User Interface Module for RustScan
|
||||
/// Defines macros to use
|
||||
#[macro_export]
|
||||
macro_rules! warning {
|
||||
($name:expr) => {
|
||||
println!("{} {}", ansi_term::Colour::Red.bold().paint("[!]"), $name);
|
||||
$crate::tui::println_safe(format_args!(
|
||||
"{} {}",
|
||||
ansi_term::Colour::Red.bold().paint("[!]"),
|
||||
$name
|
||||
));
|
||||
};
|
||||
($name:expr, $greppable:expr, $accessible:expr) => {
|
||||
// if not greppable then print, otherwise no else statement so do not print.
|
||||
if !$greppable {
|
||||
if $accessible {
|
||||
// Don't print the ascii art
|
||||
println!("{}", $name);
|
||||
$crate::tui::println_safe(format_args!("{}", $name));
|
||||
} else {
|
||||
println!("{} {}", ansi_term::Colour::Red.bold().paint("[!]"), $name);
|
||||
$crate::tui::println_safe(format_args!(
|
||||
"{} {}",
|
||||
ansi_term::Colour::Red.bold().paint("[!]"),
|
||||
$name
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -23,16 +58,24 @@ macro_rules! warning {
|
||||
#[macro_export]
|
||||
macro_rules! detail {
|
||||
($name:expr) => {
|
||||
println!("{} {}", ansi_term::Colour::Blue.bold().paint("[~]"), $name);
|
||||
$crate::tui::println_safe(format_args!(
|
||||
"{} {}",
|
||||
ansi_term::Colour::Blue.bold().paint("[~]"),
|
||||
$name
|
||||
));
|
||||
};
|
||||
($name:expr, $greppable:expr, $accessible:expr) => {
|
||||
// if not greppable then print, otherwise no else statement so do not print.
|
||||
if !$greppable {
|
||||
if $accessible {
|
||||
// Don't print the ascii art
|
||||
println!("{}", $name);
|
||||
$crate::tui::println_safe(format_args!("{}", $name));
|
||||
} else {
|
||||
println!("{} {}", ansi_term::Colour::Blue.bold().paint("[~]"), $name);
|
||||
$crate::tui::println_safe(format_args!(
|
||||
"{} {}",
|
||||
ansi_term::Colour::Blue.bold().paint("[~]"),
|
||||
$name
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -41,24 +84,24 @@ macro_rules! detail {
|
||||
#[macro_export]
|
||||
macro_rules! output {
|
||||
($name:expr) => {
|
||||
println!(
|
||||
$crate::tui::println_safe(format_args!(
|
||||
"{} {}",
|
||||
RGansi_term::Colour::RGB(0, 255, 9).bold().paint("[>]"),
|
||||
ansi_term::Colour::RGB(0, 255, 9).bold().paint("[>]"),
|
||||
$name
|
||||
);
|
||||
));
|
||||
};
|
||||
($name:expr, $greppable:expr, $accessible:expr) => {
|
||||
// if not greppable then print, otherwise no else statement so do not print.
|
||||
if !$greppable {
|
||||
if $accessible {
|
||||
// Don't print the ascii art
|
||||
println!("{}", $name);
|
||||
$crate::tui::println_safe(format_args!("{}", $name));
|
||||
} else {
|
||||
println!(
|
||||
$crate::tui::println_safe(format_args!(
|
||||
"{} {}",
|
||||
ansi_term::Colour::RGB(0, 255, 9).bold().paint("[>]"),
|
||||
$name
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -68,7 +111,7 @@ macro_rules! output {
|
||||
macro_rules! funny_opening {
|
||||
// prints a funny quote / opening
|
||||
() => {
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::seq::IndexedRandom;
|
||||
let quotes = vec![
|
||||
"Nmap? More like slowmap.🐢",
|
||||
"🌍HACK THE PLANET🌍",
|
||||
@@ -97,9 +140,10 @@ macro_rules! funny_opening {
|
||||
"RustScan: Exploring the digital landscape, one IP at a time.",
|
||||
"TreadStone was here 🚀",
|
||||
"With RustScan, I scan ports so fast, even my firewall gets whiplash 💨",
|
||||
"Scanning ports so fast, even the internet got a speeding ticket!",
|
||||
];
|
||||
let random_quote = quotes.choose(&mut rand::thread_rng()).unwrap();
|
||||
let random_quote = quotes.choose(&mut rand::rng()).unwrap();
|
||||
|
||||
println!("{}\n", random_quote);
|
||||
$crate::tui::println_safe(format_args!("{}\n", random_quote));
|
||||
};
|
||||
}
|
||||
@@ -1,279 +0,0 @@
|
||||
pub mod udp_payload {
|
||||
// const MAX_PAYLOAD_IPV4: usize = 65507; // 65,535 - 20 (IP header) - 8 (UDP header)
|
||||
// const MAX_PAYLOAD_IPV6: usize = 65527; // 65,535 - 40 (IPv6 header) - 8 (UDP header)
|
||||
|
||||
/// From [RFC 768](https://www.rfc-editor.org/rfc/rfc768.txt)
|
||||
/// We send blank UDP packets, unless the port is determined to be special.
|
||||
///
|
||||
/// ```text
|
||||
/// HEADER Handled by the OS
|
||||
/// 0 1 2 3
|
||||
/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/// | Source Port | Destination Port |
|
||||
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/// | Length | Checksum |
|
||||
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/// We append the data, based on the port the the header
|
||||
/// DATA
|
||||
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/// | Data (based on port) |
|
||||
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/// ```
|
||||
///
|
||||
/// # Fields
|
||||
///
|
||||
/// - **Source Port**: The source port number.
|
||||
/// - **Destination Port**: The destination port number.
|
||||
/// - **Length**: The length of the UDP header and data.
|
||||
/// - **Checksum**: The checksum for error-checking.
|
||||
/// - **Data**: The payload data, which can vary based on the port.
|
||||
#[allow(clippy::match_single_binding)]
|
||||
pub fn cust_payload(dst_prt: u16) -> Vec<u8> {
|
||||
match dst_prt {
|
||||
// 53 => craft_dns_query_packet(),
|
||||
// 67 => craft_dhcpc_packet(),
|
||||
// 123 => craft_ntp_packet(),
|
||||
// 161 => craft_snmp_packet(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn craft_snmptrap_packet() -> Vec<u16> {
|
||||
let version = vec![0x30, 0x82]; // SNMP version
|
||||
let community_string = vec![0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63]; // Community string "public"
|
||||
let pdu_type = vec![0xa4, 0x82]; // PDU type for SNMP Trap
|
||||
let enterprise_oid = vec![0x06, 0x09, 0x2b, 0x06, 0x01, 0x02, 0x01, 0x01, 0x01, 0x00]; // Enterprise OID
|
||||
|
||||
// Combine the components into a single SNMP Trap packet
|
||||
[version, community_string, pdu_type, enterprise_oid].concat()
|
||||
}
|
||||
|
||||
/// From [RFC 1592](https://www.rfc-editor.org/rfc/rfc1592.txt)
|
||||
/// Crafts an SNMP packet based on the structure from RFC 1592.
|
||||
///
|
||||
/// RFC 1592 defines the structure of an SNMP packet as follows
|
||||
/// - **ASN.1 header**: 0x30
|
||||
/// - **PDU length**: 37 + length of community name
|
||||
/// - **SNMP version**: 0x02 0x01 0x00 (integer, length=1, value=0)
|
||||
/// - **Community name**: A string of varying length
|
||||
/// - **SNMP GET request**: 0xa0 0x1c (request type=0xa0, length=0x1c)
|
||||
/// - **SNMP request ID**: 0x02 0x01 0x01 (integer, length=1, ID=1)
|
||||
/// - **SNMP error status**: 0x02 0x01 0x00 (integer, length=1, error=0)
|
||||
/// - **SNMP index**: 0x02 0x01 0x00 (integer, length=1, index=0)
|
||||
/// - **VarBind list**: 0x30 0x11 (length=0x11)
|
||||
/// - **VarBind**: 0x30 0x0f (length=0x0f)
|
||||
/// - **Object ID**: 0x06 0x0b (length=0x0b)
|
||||
/// - **Object instance**: 1.3.6.1.4.1.2.2.1.1.1.0
|
||||
/// - **Null value**: 0x05 0x00 (length=0)
|
||||
///
|
||||
/// The PDU length formula:
|
||||
/// ```text
|
||||
/// PDU_length = length of version field and string tag (4 bytes)
|
||||
/// + length of community length field (1 byte)
|
||||
/// + length of community name (depends...)
|
||||
/// + length of SNMP GET request (32 bytes)
|
||||
/// ```
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A `Vec<u8>` representing the crafted SNMP packet.
|
||||
pub fn craft_snmp_packet() -> Vec<u8> {
|
||||
// TODO all versions of snmp
|
||||
let v1 = vec![0x30, 0x29, 0x02, 0x01, 0x00];
|
||||
|
||||
let community_string = vec![0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63];
|
||||
let pdu_type = vec![0xa0, 0x1c];
|
||||
|
||||
let request_id = vec![0x02, 0x04, 0x7a, 0x69, 0x67, 0x71];
|
||||
let error_status = vec![0x02, 0x01, 0x00];
|
||||
let error_index = vec![0x02, 0x01, 0x00];
|
||||
let variable_bindings = vec![
|
||||
0x30, 0x0e, 0x30, 0x0c, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x02, 0x01, 0x01, 0x01, 0x00,
|
||||
0x05, 0x00,
|
||||
];
|
||||
|
||||
let _basic_snmp = [
|
||||
0x30, 0x26, 0x02, 0x01, 0x01, 0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0xa0,
|
||||
0x19, 0x02, 0x04, 0x71, 0x64, 0xfe, 0xf1, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00, 0x30,
|
||||
0x0b, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x06, 0x01, 0x02, 0x01, 0x05, 0x00,
|
||||
];
|
||||
|
||||
[
|
||||
v1,
|
||||
community_string,
|
||||
pdu_type,
|
||||
request_id,
|
||||
error_status,
|
||||
error_index,
|
||||
variable_bindings,
|
||||
]
|
||||
.concat()
|
||||
}
|
||||
|
||||
/// Constructs a DHCP packet based on the RFC 1541 Dynamic Host Configuration Protocol specification.
|
||||
///
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A `Vec<u8>` representing the DHCP packet.
|
||||
///
|
||||
/// This function crafts a DHCP Discover packet with the following fields:
|
||||
/// - **op**: BOOTREQUEST (1)
|
||||
/// - **htype**: Ethernet (1)
|
||||
/// - **hlen**: 6
|
||||
/// - **hops**: 0
|
||||
/// - **xid**: Random transaction ID
|
||||
/// - **secs**: 0
|
||||
/// - **flags**: 0x8000 (Broadcast)
|
||||
/// - **ciaddr**: 0.0.0.0
|
||||
/// - **yiaddr**: 0.0.0.0
|
||||
/// - **siaddr**: 0.0.0.0
|
||||
/// - **giaddr**: 0.0.0.0
|
||||
/// - **chaddr**: 00:00:00:00:00:00 (Client MAC address)
|
||||
/// - **sname**: Not given
|
||||
/// - **file**: Not given
|
||||
/// - **options**: DHCP options
|
||||
pub fn craft_dhcpc_packet() -> Vec<u8> {
|
||||
let mut packet = Vec::with_capacity(240);
|
||||
|
||||
// BOOTP message type: Boot Request (1)
|
||||
packet.push(0x01);
|
||||
|
||||
// Hardware type: Ethernet (10Mb) (1)
|
||||
packet.push(0x01);
|
||||
|
||||
// Hardware address length: 6
|
||||
packet.push(0x06);
|
||||
|
||||
// Hops: 0
|
||||
packet.push(0x00);
|
||||
|
||||
// Transaction ID: random (0x3903F326)
|
||||
packet.extend(&[0x39, 0x03, 0xF3, 0x26]);
|
||||
|
||||
// Seconds elapsed: 0
|
||||
packet.extend(&[0x00, 0x00]);
|
||||
|
||||
// Bootp flags: 0x8000 (Broadcast) (0x8000)
|
||||
packet.extend(&[0x80, 0x00]);
|
||||
|
||||
// Client IP address: 0.0.0.0
|
||||
packet.extend(&[0x00, 0x00, 0x00, 0x00]);
|
||||
|
||||
// Your (client) IP address: 0.0.0.0
|
||||
packet.extend(&[0x00, 0x00, 0x00, 0x00]);
|
||||
|
||||
// Next server IP address: 0.0.0.0
|
||||
packet.extend(&[0x00, 0x00, 0x00, 0x00]);
|
||||
|
||||
// Relay agent IP address: 0.0.0.0
|
||||
packet.extend(&[0x00, 0x00, 0x00, 0x00]);
|
||||
|
||||
// Client MAC address: 00:00:00:00:00:00
|
||||
packet.extend(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
|
||||
|
||||
// Client hardware address padding: 00000000000000000000
|
||||
packet.extend(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
|
||||
|
||||
// Server host name not given
|
||||
packet.extend(&[0x00; 64]);
|
||||
|
||||
// Boot file name not given
|
||||
packet.extend(&[0x00; 128]);
|
||||
|
||||
// Magic cookie: DHCP
|
||||
packet.extend(&[0x63, 0x82, 0x53, 0x63]);
|
||||
|
||||
// DHCP Message Type: DHCP Discover (1)
|
||||
packet.extend(&[0x35, 0x01, 0x01]);
|
||||
|
||||
// DHCP Option: Parameter Request List
|
||||
packet.extend(&[0x37, 0x04, 0x01, 0x03, 0x06, 0x2a]);
|
||||
|
||||
// End Option
|
||||
packet.push(0xff);
|
||||
|
||||
packet
|
||||
}
|
||||
|
||||
/// Constructs a DNS query packet.
|
||||
///
|
||||
/// The structure of the DNS packet is as follows:
|
||||
///
|
||||
/// ```text
|
||||
/// +---------------------+
|
||||
/// | Header |
|
||||
/// +---------------------+
|
||||
/// | Question | the question for the name server
|
||||
/// +---------------------+
|
||||
/// | Answer | RRs answering the question
|
||||
/// +---------------------+
|
||||
/// | Authority | RRs pointing toward an authority
|
||||
/// +---------------------+
|
||||
/// | Additional | RRs holding additional information
|
||||
/// +---------------------+
|
||||
/// ```
|
||||
///
|
||||
/// The function builds a DNS query packet with the following fields:
|
||||
/// - **Header**: Contains the transaction ID and flags.
|
||||
/// - **Question**: Specifies the query (e.g., the domain name to look up).
|
||||
/// - **Answer**: Resource records that answer the question (empty for a query).
|
||||
/// - **Authority**: Resource records that point toward an authority (empty for a query).
|
||||
/// - **Additional**: Resource records holding additional information (empty for a query).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A `Vec<u8>` representing the DNS query packet.
|
||||
pub fn craft_dns_query_packet() -> Vec<u8> {
|
||||
let mut packet = Vec::new();
|
||||
|
||||
// Transaction ID: Random
|
||||
packet.extend(&[0x12, 0x34]);
|
||||
|
||||
// Flags: Standard query
|
||||
packet.extend(&[0x01, 0x00]);
|
||||
// Header ^
|
||||
|
||||
// Questions: 1
|
||||
packet.extend(&[0x00, 0x01]);
|
||||
|
||||
// Answer RRs: 0
|
||||
packet.extend(&[0x00, 0x00]);
|
||||
|
||||
// Authority RRs: 0
|
||||
packet.extend(&[0x00, 0x00]);
|
||||
|
||||
// Additional RRs: 0
|
||||
packet.extend(&[0x00, 0x00]);
|
||||
|
||||
// Query: www.google.com
|
||||
let query_name = "www.google.com";
|
||||
for part in query_name.split('.') {
|
||||
packet.push(part.len() as u8);
|
||||
packet.extend(part.as_bytes());
|
||||
}
|
||||
packet.push(0); // End of query name
|
||||
|
||||
// Query Type: A (host address)
|
||||
packet.extend(&[0x00, 0x01]);
|
||||
|
||||
// Query Class: IN (internet)
|
||||
packet.extend(&[0x00, 0x01]);
|
||||
|
||||
packet
|
||||
}
|
||||
|
||||
/// Creates the simplest NTP packet possible
|
||||
/// 0x1b followed by 47 0s, (Version = 3, Mode = 3)
|
||||
pub fn craft_ntp_packet() -> Vec<u8> {
|
||||
let mut packet = vec![0u8; 48];
|
||||
packet[0] = 0x1b;
|
||||
packet
|
||||
}
|
||||
|
||||
// TODO add more packets for the top 1000 ports
|
||||
// `sort -r -k3 /usr/share/nmap/nmap-services | grep udp | head -n 1000`
|
||||
// There has to be a better way to structure this, maybe internal crate for packet payload
|
||||
// creation
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
/*
|
||||
* Test rustscan against different targets with a time limit.
|
||||
* The tests assumes target/debug/rustscan has already been built.
|
||||
*
|
||||
* The tests are #[ignore] to avoid running them during normal development.
|
||||
*
|
||||
* Their tests in the timelimits module are run by travis during CI.
|
||||
*/
|
||||
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use wait_timeout::ChildExt;
|
||||
|
||||
const TIMEOUT_MARGIN: u32 = 3;
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn run_rustscan_with_timeout(args: &[&str], timeout: Duration) {
|
||||
println!("Running: target/debug/rustscan: {}", args.join(" "));
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
let mut child = Command::new("target/debug/rustscan")
|
||||
.args(args)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
|
||||
let mut tries = TIMEOUT_MARGIN;
|
||||
loop {
|
||||
match child.wait_timeout(timeout).unwrap() {
|
||||
Some(_status) => break,
|
||||
None => {
|
||||
tries -= 1;
|
||||
if tries == 0 {
|
||||
// child hasn't exited yet
|
||||
child.kill().unwrap();
|
||||
child.wait().unwrap().code().unwrap();
|
||||
panic!("Timeout while running command");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let end = Instant::now();
|
||||
let duration = end.saturating_duration_since(start).as_secs_f32();
|
||||
|
||||
println!("time: {:1.1}s", duration);
|
||||
}
|
||||
|
||||
mod timelimits {
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn scan_localhost() {
|
||||
let timeout = super::Duration::from_secs(25);
|
||||
super::run_rustscan_with_timeout(&["--greppable", "--no-nmap", "127.0.0.1"], timeout);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn scan_google_com() {
|
||||
super::run_rustscan_with_timeout(
|
||||
&[
|
||||
"--greppable",
|
||||
"--no-nmap",
|
||||
"-u",
|
||||
"5000",
|
||||
"-b",
|
||||
"2500",
|
||||
"google.com",
|
||||
],
|
||||
super::Duration::from_secs(28),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn scan_example_com() {
|
||||
super::run_rustscan_with_timeout(
|
||||
&[
|
||||
"--greppable",
|
||||
"--no-nmap",
|
||||
"-u",
|
||||
"5000",
|
||||
"-b",
|
||||
"2500",
|
||||
"example.com",
|
||||
],
|
||||
super::Duration::from_secs(28),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn scan_rustscan_cmnatic_co_uk() {
|
||||
super::run_rustscan_with_timeout(
|
||||
&[
|
||||
"--greppable",
|
||||
"--no-nmap",
|
||||
"-u",
|
||||
"5000",
|
||||
"-b",
|
||||
"2500",
|
||||
"rustscan.cmnatic.co.uk",
|
||||
],
|
||||
super::Duration::from_secs(26),
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn udp_scan_localhost() {
|
||||
let timeout = super::Duration::from_secs(25);
|
||||
super::run_rustscan_with_timeout(&["--greppable", "127.0.0.1", "--udp"], timeout);
|
||||
}
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn udp_scan_google_com() {
|
||||
super::run_rustscan_with_timeout(
|
||||
&[
|
||||
"--udp",
|
||||
"--greppable",
|
||||
"-u",
|
||||
"5000",
|
||||
"-b",
|
||||
"2500",
|
||||
"google.com",
|
||||
],
|
||||
super::Duration::from_secs(28),
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn udp_scan_example_com() {
|
||||
super::run_rustscan_with_timeout(
|
||||
&[
|
||||
"--udp",
|
||||
"--greppable",
|
||||
"-u",
|
||||
"5000",
|
||||
"-b",
|
||||
"2500",
|
||||
"example.com",
|
||||
],
|
||||
super::Duration::from_secs(28),
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn udp_scan_rustscan_cmnatic_co_uk() {
|
||||
super::run_rustscan_with_timeout(
|
||||
&[
|
||||
"--udp",
|
||||
"--greppable",
|
||||
"-u",
|
||||
"5000",
|
||||
"-b",
|
||||
"2500",
|
||||
"rustscan.cmnatic.co.uk",
|
||||
],
|
||||
super::Duration::from_secs(26),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
use rustscan::generated::get_parsed_data;
|
||||
use rustscan::scanner::build_udp_payload_lookup;
|
||||
|
||||
#[test]
|
||||
fn udp_payload_lookup_contains_common_udp_ports() {
|
||||
let udp_map = get_parsed_data();
|
||||
let lookup = build_udp_payload_lookup(udp_map);
|
||||
|
||||
// These are common UDP services; the payload database should include them.
|
||||
assert!(
|
||||
lookup.contains_key(&53),
|
||||
"expected UDP payload for DNS (53)"
|
||||
);
|
||||
assert!(
|
||||
lookup.contains_key(&123),
|
||||
"expected UDP payload for NTP (123)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn udp_payload_lookup_payloads_are_non_empty_for_known_ports() {
|
||||
let udp_map = get_parsed_data();
|
||||
let lookup = build_udp_payload_lookup(udp_map);
|
||||
|
||||
// Don't assert exact bytes (the generated payload set may evolve),
|
||||
// but it should not be empty for these well-known protocols.
|
||||
let dns = lookup.get(&53).expect("missing DNS payload");
|
||||
assert!(!dns.is_empty(), "DNS payload should not be empty");
|
||||
|
||||
let ntp = lookup.get(&123).expect("missing NTP payload");
|
||||
assert!(!ntp.is_empty(), "NTP payload should not be empty");
|
||||
}
|
||||
Reference in new issue
Block a user