hickory's synchronous resolver runs its own tokio runtime, and tokio panics
when a runtime is dropped inside an asynchronous context. That was already
true before, but library users now have to run the scanner on tokio, so
they are more likely to call `parse_addresses` from async code. Document
the two safe ways: call it before starting the runtime (as the binary does)
or through `spawn_blocking`.
What the first runs showed:
- Only answers that arrive while the probe is still being sent were
measured: on the loopback interface, refusals and ICMP errors are queued
before connect()/send() returns. A remote host answers a round trip
later, which is the path where tokio's UDP receive missed ICMP errors.
On Linux the workflow now also sets up a host 5 ms away (a network
namespace behind a netem-delayed veth pair, with ICMP rate limiting off,
running its own listeners), and the benchmark sweeps it over TCP and UDP.
- Windows retries a refused connection for about a second. With
`-t 500`, 8,191 concurrent attempts made even the base miss listeners.
Windows now uses the default timeout and sweeps 8,191 ports (2,047 for
the 500-socket batch).
- A build now fails the check only if it misses more listeners than the
base does (any miss at all if the base misses none). Misses by the base
are reported as a warning in the summary.
- The macOS runner has no `net.inet.icmp.icmplim`, which failed the
"describe" step, and caps descriptors at 10240, so the 65536 limit
could not be set. The describe step can no longer fail the job, and
10240 is tried as well.
A UDP probe that gets no answer straight away is handed to tokio to wait for
the reply. On Linux, an ICMP "port unreachable" that arrives later only
raises EPOLLERR on the socket. tokio does not count that as readable, so
`UdpSocket::recv` slept until the timeout instead of returning the refusal.
async-io treated EPOLLERR as readable.
The straight port (before the eager probe) showed the effect: the UDP sweep
of the local host took 4.1 s instead of 1.0 s, because every closed port
waited for the 500 ms timeout. With the eager probe it only hit closed ports
whose ICMP arrived after the first receive. That is rare on the local host
but the common case for any remote host, where it turned every closed port
into a timeout.
`recv_or_error` waits for `READABLE | ERROR` readiness and hands out the
queued error (`take_error`), so a refused probe ends as soon as the ICMP
arrives, as before.
The scan drives every socket from one future on a current-thread runtime,
so the runtime only polls for I/O and fires timers between two polls of
that future. A poll only ended once no socket was ready, which could take
a long time:
- starting a whole batch at once: 8,191 sockets took longer than a 500 ms
timeout on the Windows runner, so connections that had succeeded during
the fill were only seen after their timers fired. The benchmark found no
open port at all with `-b 10000 -t 500` there;
- UDP probes to closed ports on the local host finish straight away, so a
whole sweep ran in one poll, and the replies from open ports waited until
the end: the first open UDP port was reported 2.6x later than with
async-std.
`scan_sockets` now starts and finishes sockets in rounds of at most 128
(`WORK_PER_TURN`, both counted together) and calls `yield_now()` between
rounds, which defers its wake-up until the runtime has polled the OS
selector without blocking. Each round still keeps up to `batch_size`
sockets in flight; the batch now fills over several rounds instead of in
one go. The cost is one extra non-blocking `epoll_wait` (or equivalent)
per 128 sockets.
The test suite deliberately never opens sockets (see #942), so nothing in CI
showed whether a change to the scanner makes RustScan slower, hungrier or
wrong. The tokio port (#944) needs exactly that evidence.
The "Scan benchmark" workflow runs for pull requests that touch the scanner,
its runtime or its dependencies, and on demand. It builds the change and its
base in release mode on Linux x64/arm64, macOS and Windows runners. It then
runs .github/scan-benchmark/scan_bench.py, which:
- opens 315 TCP listeners and 32 UDP responders on 127.0.0.1 inside the
runner. Nothing outside the runner is scanned. Ports stay out of the
ephemeral range, so a scan can never connect a socket to itself;
- scans them with both builds, alternating the build order between runs:
full TCP sweeps with batch sizes 500/4500/10000, 8 loopback hosts, one
port, a UDP sweep, and on Linux ports that iptables drops (filtered TCP and
UDP, and a mixed sweep), so timeouts are measured too;
- fails unless every run finds every listener and no build reports ports the
base never found, or misses ports the base always found;
- reports medians of throughput (from RustScan's own scan timer), wall time,
time to the first open port, peak RSS, CPU time, and sampled peak
descriptors and threads. The table goes to the job summary, the raw data
to an artifact;
- fails if the change's median throughput or wall time is more than 15%
worse, or its peak RSS more than 25% worse, in any scenario.
contributing.md documents the workflow as the place where scanning is
measured, next to the rule that tests must not scan.
tokio's socket I/O is readiness based: `UdpSocket::send` and `recv` first
wait until the reactor has reported the socket ready and only then make the
system call. async-io, which async-std used, made the call first and only
waited after `WouldBlock`.
With a straight port, every UDP probe therefore made an extra trip through
the event loop before its send. Probes to closed ports on the local host
also no longer finished in their first poll: there the ICMP "port
unreachable" is usually queued before `send` returns, so async-std's first
`recv` already saw the refusal.
Bind a non-blocking std socket instead, send the probe and try the first
receive straight away. The socket is only handed to tokio, and registered
with its reactor, when there is really something to wait for. Errors and
timeouts are reported exactly as before.
The scan polls every in-flight socket from one future through a
`FuturesUnordered`. tokio's cooperative scheduling gives that future a budget
of 128 units per poll. Once it is spent, every tokio resource returns
`Pending` and defers its wake-up, but `FuturesUnordered` still polls all the
other ready sockets in that same poll. When thousands of connections finish
together (a burst of refusals, or a batch of timeouts firing at once), the
scan keeps re-polling sockets only to park them again, roughly 128 at a time.
async-std had no budget. `tokio::task::unconstrained` restores that
behaviour: one poll handles every ready socket. The future still returns
`Pending`, and so lets the runtime poll I/O and timers, as soon as no socket
is ready.
async-std is discontinued upstream (RUSTSEC-2025-0052). tokio is already in
the dependency tree through hickory-resolver, so switching adds no crate and
removes 28 from Cargo.lock (async-std, async-io, polling,
async-global-executor, blocking, futures-executor, ...).
This is a like-for-like port. The scanner keeps its architecture and the
library API keeps its signatures:
- a single `FuturesUnordered` keeps at most `batch_size` sockets in flight
from one future; nothing is spawned per socket;
- connect and UDP receive timeouts use `tokio::time::timeout` and report the
same `TimedOut` error that async-std's `io::timeout` did;
- open TCP sockets are still shut down in both directions before they are
closed (through `into_std()`, because tokio only has a write-side shutdown);
- `--interval` sleeps with `tokio::time::sleep`.
The binary runs the scan on a current-thread runtime that exists only for
the scan. Address resolution (blocking DNS lookups and file reads) runs
before the runtime is created and scripts run after it is dropped, so neither
can stall the scan, and hickory's internal runtime is never nested in ours.
Library users now have to poll the scan futures from a tokio runtime with the
I/O and time drivers enabled. The crate and `Scanner` docs say so, and the
crate example shows how.
Builds on the earlier port in #856 by @vrtgs.
Co-authored-by: Vrtgs <[email protected]>
* Refactor SocketIterator for allocation-free performance
Refactor SocketIterator to eliminate boxing and improve memory efficiency. Update documentation and tests to reflect changes.
* fix(socket_iterator): fix clone test off-by-one and skip private doc example
- clone_resumes_independently expected None after 3 of 4 sockets; assert
the exact remaining sequence for both the original and the clone.
- The struct doc example referenced a placeholder crate and a private
module, so it could never compile as a doctest; mark it ignore.
- Add a test for empty IP/port inputs covering size_hint and the
FusedIterator promise.
---------
Co-authored-by: Autumn Skerritt <[email protected]>
Small cleanups in main.rs:
- Iterate over the parsed scripts by reference and clone only the script being run, instead of cloning the whole list for every IP.
- Use `or_default()` for the per-IP port map.
- If the soft file-descriptor limit does not fit in a `usize` (32-bit targets), fall back to `DEFAULT_FILE_DESCRIPTORS_LIMIT` instead of `usize::MAX`, which bypassed all batch-size adjustments.
- Fix a typo.
The effective-batch-size warning fix originally proposed here landed via #921.
Co-authored-by: Autumn Skerritt <[email protected]>
New opt-in `--interval <MS>` option (also `interval` in the config file): after a port has been scanned on every address, wait that many milliseconds before scanning the next port, for slow, low-noise scans. Within a port `--batch-size` still limits concurrency; with the default of 0 scanning behaviour is unchanged.
Library: `Scanner::with_interval(Duration)` is added; `Scanner::new()` is unchanged.
Co-authored-by: Autumn Skerritt <[email protected]>
New `--closed` option (also `closed = true` in the config file) that additionally lists TCP ports that actively refused the connection: printed live as `Closed ip:port` and summarised under `closed ports:` at the end (also in greppable mode). Scripts are never run against closed ports, and without `--closed` scanning behaviour is unchanged.
Library: `Scanner::with_closed_ports()` and `Scanner::run_with_status() -> Vec<PortStatus>` are added; `Scanner::new()` and `run()` are unchanged.
Co-authored-by: Autumn Skerritt <[email protected]>
`-r/--range` now accepts several comma-separated ranges, e.g. `-r 1-100,200-300,5000-5100`. Overlapping/adjacent ranges are merged; random order uses the existing LCG walk extended over the merged ranges (every port visited exactly once), serial order follows the input order and skips duplicates. Reversed ranges (`300-200`) are now rejected instead of underflowing.
`range` in the config file keeps accepting the old `{ start = 1, end = 1000 }` table and additionally accepts `"1-500,1000-2500"` or `[[1, 500], [1000, 2500]]`.
Library API: `input::PortRange { start, end }` is replaced by `input::PortRanges(Vec<(u16, u16)>)`; `PortStrategy::pick` keeps its signature otherwise.
Fixes#835.
Co-authored-by: Autumn Skerritt <[email protected]>
Add regression tests for CIDR inputs whose host bits are set (e.g. `192.168.1.13/29` -> 192.168.1.8–15, `192.168.1.5/23`, `10.0.0.7/30`). The parsing itself was already fixed on master by 3bdac9c (IpInet accepts host bits), so the proposed normalize_cidr helper was dropped as unreachable.
On native Windows, use the configured `-b/--batch-size` instead of a hard-coded 3000, and report the effective batch size (not the raw CLI value) in the "no open ports" hint on all platforms. `--ulimit` is hidden from Windows help and rejected on Windows (exit code 2), including when it comes from the config file. Unix batch-size/ulimit inference is unchanged.
Effective-batch-size reporting fix originally identified by @0xxreacher in #905.
Co-authored-by: 0xxreacher <[email protected]>
Look for the config file at the XDG-style `$XDG_CONFIG_HOME/rustscan/config.toml` first (via `dirs::config_dir()`, so the platform equivalent on macOS/Windows), then fall back to the previous `$XDG_CONFIG_HOME/.rustscan.toml` and `~/.rustscan.toml` locations so existing configs keep working.
Fixes#741.
Build a port -> payload lookup once per UDP scan instead of cloning the whole nmap payload BTreeMap into every scanned socket and scanning it linearly. Payload selection keeps the previous "last match wins" semantics.
Adds a socket-free micro-benchmark (benches/benchmark_helpers.rs) and lookup tests (tests/udp_payload_lookup.rs).
`Scanner` no longer prints each open socket to stdout by default, so library users can embed it and use the returned sockets without unsolicited output. The CLI opts in via the new `Scanner::with_open_port_output()` builder. The UDP socket bind error is now logged at debug level instead of printed.
Fixes#706.
Rewrite the nmap-payloads decoder in build.rs to follow Nmap's C-style escape format: `\xNN` and single-character escapes are decoded, all other characters are kept literally, and multi-line quoted segments are concatenated. Literal protocol text (e.g. the SNMP `public` community string, NetBIOS names, SSDP headers) was previously dropped, producing malformed probes. Also strip CR line endings when reading nmap-payloads.
Adds regression tests pinning the SNMP (161) and SSDP (1900) probe bytes.
Fixes#933.
Route all user-facing stdout writes through tui::println_safe, which exits 0 on BrokenPipe (e.g. `rustscan -g ... | head`) instead of panicking (fatal under panic = "abort"). On Windows, skip hickory-resolver's system configuration and hosts file when SystemRoot is unset, falling back to the existing cloudflare_tls resolver.
Fixes#939, fixes#940.
Add the GitHub-hosted ubuntu-24.04-arm runner to the test matrix so the suite runs natively on arm64 Linux (macos-latest already covers arm64 macOS; build.yml already cross-builds armv7/aarch64 Linux binaries), and document ARM support in docs/arm-support.md.
Fixes#683.
Co-authored-by: Autumn Skerritt <[email protected]>
Build release archives with houseabsolute/actions-rust-release and publish them (plus the .deb and checksums) to the GitHub release when a tag is pushed. CD now runs on pushes to master and tags only.
Also: read-only default workflow token (publish job gets contents: write), third-party actions pinned to release SHAs, fail on unmatched release assets, and the disabled trigger-less dependabot auto-merge workflow is removed.
Fixes#700, fixes#801.
Co-authored-by: Autumn Skerritt <[email protected]>
Replace the deprecated actions-rs/toolchain action with actions-rust-lang/setup-rust-toolchain (pinned to v1.17.0, clippy + rustfmt components, built-in cache and default RUSTFLAGS disabled) and restrict the workflow token to contents: read.
Co-authored-by: Autumn Skerritt <[email protected]>
* Change batch_size and ulimit type from u16 and u64 to usize
* add error handling for overflow
* remove unnecessary conversions `clippy`
* fix missed
* chore: fix CI (unrelated)
The example installation for Arch Linux previously showed installation via yay, which is an unofficial package manager only required if installing from the AUR. Since rustscan is in the official extra package repository, change the instructions to use pacman.
While here, also fix link to full install guide.
as part of #651, IP addresses are deduplicated; as this uses a
`BTreeSet`, the original order is not necessarily retained.
use
[`retain()`](https://doc.rust-lang.org/std/vec/struct.Vec.html#method.retain)
to remove duplicated and/or excluded IPs, mutating the original list of
IPs but preserving the original order.
This CI will fail initially due to an issue fixed in another PR.
This runs the udp scan and tcp scan 100 times and gets an average measure.
Currently these are:
```
portscan tcp time: [38.941 ms 39.659 ms 40.397 ms]
change: [+62486512% +64310488% +66078003%] (p = 0.00 < 0.05)
Performance has regressed.
Found 1 outliers among 100 measurements (1.00%)
1 (1.00%) high mild
Benchmarking portscan udp: Warming up for 3.0000 s
Warning: Unable to complete 100 samples in 5.0s. You may wish to increase target time to 5.9s, or reduce sample count to 80.
portscan udp time: [58.419 ms 59.596 ms 60.775 ms]
change: [+93198493% +96139097% +98913211%] (p = 0.00 < 0.05)
Performance has regressed.
```
Notes:
* TCP scan takes 40ms on average
* UDP scan does not complete all 100 samples, as it's a bit slow. Takes 60ms on average.
* I ran these previously for testing so "performance has regressed" can be ignored
* We scan 1k ports because 64k ports over 100 samples is way too many.
Our library is sorely lacking docs. I read this https://docs.rs/rustscan/latest/rustscan/ and had no idea what the arguments are. These comments should hopefully show up :)
* Remove builds from CI and change to `just`
Per discussion in Discord, we are:
* Removing all official distributions to focus solely on RustScan itself
* Moving to `just` so developers can run 1 command to run all of our test suites. In some PRs it has turned into a cat and mouse game where they are not running all of our testsl
* make CI run
* update readme
* update readme
* try to fix just
* typo
* fixing just
* install nextest
---------
Co-authored-by: bee <[email protected]>
- prevent running of the release workflow on pull-request: releases should
only happen on explicit tagging.
- tests etc. still run as part of the build workflow.
* update the master branch to use nmap-payloads file
* code generation
* statically generate payloads, workingish
* actually puts the map in memory at compile time
* I am not sure this needs to be in the repo since its built on compilation
* actually ignores
* converting this in networking class counts as paying attention to the lecture right?
* see if if works on windows
* see
* I think i fixed CI
* fixes clippy
* ugh
* formats the gen code automagically
* UDP Service Probing
* UDP Service Probing
* Better comments
* fixes clippy
* fixes cargo fmt
- move to the latest versions of direct dependencies.
- update `rand` usage to use `Range` types.
- update `cidr_utils` to reflect underlying `cidr` crate.
* feat(cli): add custom dns resolver option
* fix: correct clippy lints
* refactor: update resolvers to be Option
use an `Option<String>` rather than a `String`, defaulting to `""`.
* feat(cli): include resolver in config.
* refactor: change resolver chain
- remove extraneous `std::net::` prefixes for already-imported `IpAddr`.
- move derivation to a function:
- parse input/file as normal;
- attempt to use the system DNS resolution;
- fall back to previous CloudFlare behaviour.
---------
Co-authored-by: Khanh Nguyen <[email protected]>
* refactor: make RustScan additionally a library
- add `lib.rs` (permitted as per
[_Packages and Crates_](https://doc.rust-lang.org/book/ch07-01-packages-and-crates.html)).
- refactor `main.rs` to reference the library.
- update module references as required.
* doc: correct documentation issues
- fix some failing doc-tests.
- amend formatting causing misinterpretation as code-samples.
- mark some examples as
[`compile_fail`](https://doc.rust-lang.org/rustdoc/write-documentation/documentation-tests.html#attributes)
as there's no immediate way to instantiate the required `struct`.
* docs: add basic module documentation
- add lib-level docs. including a minimal example.
- add minimal, headline information for each module.
* Update Dockerfile
Upgrade alpine to 3.13, add ca-certificates and bind-tools.
* Add github/docker to dependabot. Update some of the actions
* Skip Registry login for Pull Requests
* build: remove explicit strip steps
- update `Cargo.toml` to
[`strip`](https://doc.rust-lang.org/cargo/reference/profiles.html#strip)
as part of the release profile.
- remove explicit `strip` steps from the release workflow.
- update step name to better reflect `git config` changes.
* build: remove prefix from expected tags
- remove the `v` prefix from the triggering tag (all previous tags lack this).
- update tag-deriving steps to better reflect the expected tag pattern.
- update tag-deriving steps to be consistent in how `GITHUB_REF` is parsed.
* build: make build/OS checks generic
- update GitHub Actions conditions to use
[`contains`](https://docs.github.com/en/actions/learn-github-actions/expressions#contains)
to match _all_ related builds.
- update shell conditions to accommodate the same.
Changed this :
For things like --script '(safe and vuln)' enclose it in quotations marks \"'(safe and vuln)'\"")
to:
For things like --script '(safe and vuln)' enclose it in quotations marks \"'(safe and vuln)'\"
* First attempt enabling exclude port list capability
* disabling a not desired println! used for debugging purpose
* fixing small typo: dbg! by debug!
* adding emtpy commit to make CI/CD pass
* running cargo fmt and removing a TODO comment
* Actually running cargo fmt without check LoL
* fixed some clippy error
* fixing one more cargo clippy complain
---------
Co-authored-by: Wuelfhis Asuaje <[email protected]>
* fix port range to include end
* empty
Signed-off-by: Maximiliano Molina <[email protected]>
---------
Signed-off-by: Maximiliano Molina <[email protected]>
* Fix markdown-link-checker workflow
Added a configuration file that tells MLC to ignore the URLs in .github\workflows\mlc_config.json as MLC is falsely reporting these as dead
* Update markdown.yml
* Update README.md
Fixed the issue with the links
closes#516
* Update README.md
1. Fixed the arch Linux link. Closes#519
2. Fixed Arch badge so it's linked to the correct version
3. Cleaned up missing links throughout the document
4. Moved all links other than the contributor's section to the end of the document for easier maintenance
5. Fixed language so it was grammatically correct
Alpine Linux v3.12 is end of life about months, the builder base image
is alreayd Alpine v3.17, and Alpine v3.17 is the latest stable release
with about 2 more years support, should be good to ugprade to it.
Signed-off-by: Peter Dave Hello <[email protected]>
Signed-off-by: Peter Dave Hello <[email protected]>
* feat(docker): build and publish docker image in ci
Signed-off-by: leofvo <[email protected]>
* refactor(docker): updated dockerfile to build release version of rustscan
Signed-off-by: leofvo <[email protected]>
In the readme there are 2 links to docker & kali, 'Usage' and 'Read the install guide'. These links weren't working.
Co-authored-by: Bee <[email protected]>
Improve documentation for addresses CLI arg to make it
clear that file parsing of addresses is already supported
(#452)
Co-authored-by: Bee <[email protected]>
* Changed dns resolving library to to_socket_addrs and removed the usage of trust-dns
* Reverted automatic IDE spacing
* Ran cargo fmt on the code
* Made trust-dns a fallback dns resolver and reverted cargo toml and lock
* Reverted cargo.lock
* Ran cargo fmt
* Changed style of err checking
* Fixed errors
* Fixed merging errors
* Changed for CR
Co-authored-by: Bee <[email protected]>
A new Dockerfile "contributing.Dockerfile" was added.
This Dockerfile permits creating a "ready to develop" container image to ease
contributing to RustScan.
The instructions to use it are detailed in contributing.md.
By default the number of try is 1, it can be set on the command line by using
--tries option.
This should closeRustScan/RustScan#38.
Co-authored-by: Bee <[email protected]>
This has the benefit of making the input flexible and fixing an existing
bug that happens when a single port is specified, see #211.
Other benefits:
- We are now parsing only failures as a file, reducing the amount of
attempts to open a file.
- We check if a file exists before trying to open it.
Fixes#211
Co-authored-by: Bee <[email protected]>
Changing only a few lines does not give you maintainer. Also people might contact the person if they need to reach out about something which is probably not intended.
Co-authored-by: Bee <[email protected]>
* Add integration tests with timeout
* cargo fmt
* Fix travis command line
* Add extra margins for the timeout values
* Increase the time to scan
Output the time taken after each test
DockerHub now contains every major release from `v1.0.1` to `1.10.0`
Waiting for this README PR to approve before I remove the `v1.9.0` DockerHub tag as it no longer conforms with standards.
Co-authored-by: Bee <[email protected]>
At the moment the Dockerfile brings the entire current directory in as a context for the building container. We can encourage some caching here by only bringing forward the essentials:
`cargo.tml`, `cargo.lock` and the `src`
+ Added a maintainer label
v1.8.0 is the most recent published version of RustScan, the alpine tag quickly became depreciated - meaning people had to rely on `latest` to get any features made within the last month.
Version tags will build as per major realease i.e. `v1.9.0`
The goal of this iterator is to generate sockets based on combinations
of IPs and ports in a memory-bounded way, we only consume the next item
of this iterator when a socket has been processed by our scanner.
Before this change we were generating every combination of IP and port
and storing it in a VecDeque, and while this is fine for a small subset
of IPs and ports it can quickly get out of hand with multiple IPs and
a big port range.
When reading the config file '$CONFIG_DIR/.config/rustscan/config.toml'
should also be checked. If both '~/.rustscan.toml' and
'$CONFIG_DIR/.config/rustscan/config.toml' exist, then later will be
used.
This configuration file will be read from the home directory of the
user. Options set within this file will have priority over arguments
from the command line unless the `--no-config` option is passed to the
program.
By default, Cargo instructs compilation units to be compiled and optimized
in isolation. LTO instructs the linker to optimize at the link stage.
This can, for example, remove dead code and often times reduces binary size.
By default, when Rust code encounters a situation when it must call panic!(),
it unwinds the stack and produces a helpful backtrace. The unwinding code,
however, does require extra binary size. rustc can be instructed to abort
immediately rather than unwind, which removes the need for this extra unwinding code.
For more information see: https://github.com/johnthagen/min-sized-rust
Before this change we would always scan manually specified ports in
sequential way, this is not the case anymore. This is an important
change since we are planning to introduce a config file with the top 1K
ports and we would like to scan those in a randomized way.
These options are mutually exclusive, this means that when a user
selects manual ports we will ignore ranges and vice-versa.
Manually specifying ports won't allow scan randomization for now.
+ Added another step to clarify process for Docker as the build will fail if the user is not in the downloaded repo
+ DockerHub has CI with master branch and two images tags `latest` and `alpine` where `alpine` is tagged on feature releases (currently 1.7.1)
For now if we can establish a socket on port 80 we can infer the IP of the
host. This might change in the future if we start considering other
ports like 443.
User will be able to pick from:
- serial: in order to have their ports scanned in ascending order
- random: in order to have their ports scanned randomly
This is based on the Linear Congruential Generator algorithm which
guarantees we will iterate through an entire range in a pseudo random
manner.
This implementation should be bound in memory by the batch_size
specified by the user, which will be in average 1_000.
IPs should be provided in a comma separated list.
Right now every IP will be scanned sequentially and NMAP will run before
the next IP scan starts. This will change on the long term once we
provide randomized scans in order to not overwhelm hosts or trigger
detection systems in place.
RustScan should be able to run on systems with either 32 or 64 bits. In
order to achieve that we should rely on the `rlimit` library to get the
proper unsigned int type from the system.
We can let structopt parse the supplied IP for us and fail early when
that IP address is invalid. We can also let IpAddr let us know when an
IP is an ipv4 or ipv6 without relying on the user to supply this
information to us.
In certain OSs a process might become a zombie if we do not explicitly
wait for it to finish. This addresses the child not being killed when a
parent receives a CTRL+C.
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.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
![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]
</div>
# 🤔 What is this?
1. Find ports quickly using Rustscan (**8 seconds at its fastest**).
2. Automatically runs `nmap` on those ports.
3. Profit???
<table>
<tr>
<th>Name</th>
<th>⚡ Nmap <b>with RustScan</b> ⚡ </th>
<th>🐢 Nmap 🐢</th>
</tr>
<tr>
<th>Gif</th>
<td><img src="pictures/with_rustscan.gif" alt="The guy she tells you not to worry about"></td>
Why spend time running fast scans and manually copying the ports, or waiting for a 20 minute scan to finish when you can just do all 65k ports in less than a minute?

RustScan is a modern take on the port scanner. Sleek & fast. All while providing extensive extendability to you.
**RustScan running in 8 seconds and finding all open ports out of 65k.**
Not to mention RustScan uses Adaptive Learning to improve itself over time, making it the best port scanner for **you**.
| Realises it's not useful, and pipes the only useful data into the only useful port scanner | ✅ | ❌ | ❌ |
![fast][speed-1]
Speed is guaranteed via RustScan. However, if you want to run a slow scan due to stealth, that is possible too.
## 🙋 FAQ
> I think this would be a great port scanner on its own without Nmap!
Firstly, let's talk code.
No. If you want a fast port scanner, use Masscan.
> I have this great idea for a script to get information on ports / hosts
We have tests that check to see if RustScan is significantly slower than the previous version. If it is, the continuous integration fails, and we can't commit code to master unless we make it faster.
Great. Contribute it to Nmap! :D
> Not everyone has nmap installed....
[HyperFine][speed-2] is used to monitor RustScan's performance over time to answer the question, "Are we getting faster? Are we getting slower?".
If you're a pentester, then yes, you have Nmap installed.
Every pull request is reviewed by **one** person, but more often than not, **two** people review it. We test it manually and ensure the code doesn't negatively affect performance.
> I want to contribute!
[Read more here][speed-3].
Great! I'd love some help with this. Read the [contributing.md file](contributing.md) file for more information!
## ⚙️ Extensible
# 📖 Full Installation Guide
**You need Nmap**. If you have Kali Linux or Parrot OS installed, you already have Nmap. If not, [follow the nmap install guide](https://nmap.org/download.html).
![scripts][extensible-1]
The easiest way to install RustScan is to use one of the packages provided for your system, such as HomeBrew or Yay for Arch Linux.
### _RustScan piping results into the custom Python script_
The most universal way is to use `cargo`, Rust's built in package manager (think Pip but for Rust). [Follow this guide to installing Rust & Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html).
RustScan has a new scripting engine that allows anyone to write scripts in mostlanguages. Python, Lua, and Shell are all supported.
If you face any issues at all, please leave a GitHub issue. I have only tested this on Linux, so there may be issues for Mac OS or Windows.
Want to take your found ports and pipe them into Nmap for further analysis? That's possible. Want to run `smb-enum` if SMB is found open? Possible.
## 🖥️ Debian / Kali
The possibilities are endless -- and you can write scripts in whatever language you feel comfortable with.
Run the commpand `dpkg -i` on the file. Note: sometimes you can double click the file to achieve the same result.
[Read more here][extensible-2].
### 🥧 .deb file not working?
The .deb file only works on AMD64 CPUs. If yours is different (such as a Raspberry Pi) or the .deb file doesn't work, it is easy to build the .deb file yourself.
**Note**: It is easier to install Rust and install via Cargo, then it is to build the .deb file. But this is just in case!
## 🌊 Adaptive
1. Install Rust You can do this with `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` which I took from the Rust website https://www.rust-lang.org/tools/install
2.`cargo install rustscan` if you want the easiest method possible. Otherwise, to build the .deb file `cargo install cargo-deb`
3. Git clone this repo `git clone https://github.com/brandonskerritt/RustScan`
4. cd RustScan (into the git cloned repo) `cd RustScan`
5. Run `cargo deb`
6. Your .deb file is now located in `target/releases/Debian/`
![adaptive][adaptive-1]
## 🍺 HomeBrew
### _RustScan automatically fine-tunes itself to match the host OS_
Tap the brew:
RustScan has a cool set of features called "Adaptive Learning". These features "learn" about the environment you are scanning and how _you_ use RustScan to **improve itself over time**.
```
brew tap brandonskerritt/rustscan
```
We use this umbrella term for any feature that fits this criterion. The list constantly changes, so [check out our wiki for more information][adaptive-learning].
Install it:
## 👩🦯 Accessible
```
brew install rustscan
```
![fast][accessible-1]
## 🔧 Building it yourself
RustScan is one of the first penetration testing tools that aims to be entirely accessible.
1. Git clone the repo.
2. Install Rust. You can do this with `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` which I took from the Rust website https://www.rust-lang.org/tools/install
3. cd into the Git repo, and run `cargo build --release`
4. The binary is located at `target/release/rustscan`
5. Symlink to the binary or something. Whatever you want!
[Most penetration testing tools are not accessible][accessible-2], which negatively affects the whole industry.
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.
# 🤸 Usage
```
rustscan -h
```
We have 2 usage guides. [Basic Usage][usage-1] and [Things you may want to do][usage-2].
```
RustScan 1.2.0
Bee https://github.com/brandonskerritt
Fast Port Scanner built in Rust
We also have documentation about our config file [here][config-file-here].
USAGE:
rustscan [OPTIONS] <ip> [command]...
# 🎪 Community
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
[Contributing][community-1] Read this to learn how.
OPTIONS:
-T, --timeout <T> The timeout before a port is assumed to be close. In MS. [default: 1500]
-b, --batch <b> Increases speed of scanning. The batch size for port scanning. Depends on your open file limit
of OS. If you do 65535 it will do every port at the same time. Although, your OS may not
support this. [default: 4500]
ARGS:
<ip> The IP address to scan
<command>... The Nmap arguments to run. To use the argument -A, end RustScan's args with '-- -A'. To run
EXAMPLE: 'rustscan -T 1500 127.0.0.1 -- -A -sC'. This argument auto runs nmap {your commands}
-vvv -p $PORTS
```
The format is `rustscan -b 500 -T 1500 192.168.0.1` to scan 192.168.0.1 with 500 batch size with a timeout of 1500ms. The timeout is how long RustScan waits for a response until it assumes the port is closed.
The batch size determines how fast RustScan is. Set it to 65k, and it will scan all 65k ports at the same time. This means at at 65k batch size, RustScan will take TIMEOUT long to scan all ports. Essentially, if timeout is 1000ms, **RustScan can scan in 1 second**.
Your operating system may not support this, but it is worth it to play around and see where your open file limit is. Shortly I will be releasing a dockerised version with a much larger open file limit, so this will be possible.
## ⚠️ WARNING
This program, by default, scans 5000 ports at a time (5000 per second).
This may cause damage to a server, or may make it incredibly obvious you are scanning the server.
There are 2 ways to deal with this;
1. Decrease batch size
`rustscan -b 10` will run 10 port scans for 1 second, and then another 10 for 1 second and so on.
2. Increase timeout
`rustscan -T 5000` will mean RustScan waits 5 seconds until it scans the next ports.
You can also use both of these at the same time, to make it as slow or as fast as you want. A fun favourite is 65535 batch size with 1 second timeout. Theoretically, this scans all 65535 ports in 1 second.
**Please** do not use this tool against sensitive servers. It is designed mainly for Capture the Flag events, not real world servers with sensitive data.
## 🚨 Thread Paniced at Main: Too Many Open Files
This is the most common error found in RustScan.
The open file limit is how many open sockets you can have at any given time.
This limit changes from OS to OS.
RustScan does not automatically create defaults (other than 5000) like Nmap does with their -T1, -T2 system.
By figuring out for yourself the optimal batch size, you will know that RustScan is the most optimised port scanner for your system.
There are 2 things you can do:
1. Decrease batch size
2. Increase open file limit
Decreasing batch size slows down the program, so as long as it isn't too drastic, this is a good option.
Run these 3 commands:
```
ulimit -a
ulimit -Hn
ulimit -Sn
```
They will give you an idea on the open file limit of your OS.
If it says "250", run `rustscan -b 240` for a batch size of 240.
Increasing the open file limit increases speed, but poses danger. Although, **opening more file sockets on the specified IP address may damage it**.
To open more, set the ulimit to a higher number:
```
ulimit -n 5000
```
**Mac OS**
Mac OS has, from what I can tell, a naturally very low open file descriptor limit. The limit for Ubuntu is 8800. The limit for Mac OS is 255!
In this case, I would say it is safe to increase the open file limit. As most Linux based OS' have limits in the thousands.
Although, if this breaks anything, please don't blame me.
**Windows Subsystem for Linux**
Windows Subsystem for Linux does not support ulimt (see issue #39).
The best way is to use it on a host computer, in Docker, or in a VM that isn't WSL.
**Automatic Ulimit updating**
We are currently working on automatic Ulimit updating. If it is too high, it will lower itself. If it is too low, it will suggest a higher Ulimit. Watch [this issue](https://github.com/brandonskerritt/RustScan/issues/25) for more.
## 🔌 Nmap Custom Flags
To run your own nmap commands, end the RustScan command with `-- -A` where `--` indicates "end of RustScan flags, please do not parse anything further" and any flags after that will be entered into nmap.
RustScan automatically runs `nmap -vvv -p $PORTS $IP`. To make it run `-A`, execute the command `rustscan 127.0.0.1 -- -A`.
**Note**: due to how Nmap behaves, sometimes you cannot SIGINT ctrl+c to end the scan and it acts like it is running in the background, but printing to STD::OUT. There is nothing I can do about this, unless I create a TTY shell for the sole purpose of running Nmap. Sorry 😓 I'll try to fix in an upcoming release. If any Rust experts want to help, please do!
## 🎯 Increasing speed / accuracy
* Batch size
This increases speed, by allowing us to process more at once. Something experimental I am working on is changing the open file limit. You can do this manually with `ulimit -n 70000` and then running rustscan with `-b 65535`. This _should_ scan all 65535 ports at the exact same time. But this is extremely experimental.
For non-experimental speed increases, slowly increase the batch size until it no longer gets open ports, or it breaks.
* Accuracy (and some speed)
To increase accuracy, the easiest way is to increase the timeout. The default is 1.5 seconds, by setting it to 4 seconds (4000) we are telling RustScan "if we do not hear back from a port in 4 seconds, assume it is closed".
Decreasing accuracy gives some speed bonus, but my testing found that batch size dramatically changed the speed whereas timeout did, but not so much.
# 🎪 Contributing
Please read the [contributing.md file](contributing.md)
# 💻 Other Hacking Projects By This Author
## 🧮 Ciphey
Ciphey is an automated decryption tool using artifical intelligence & natural language processing.
[Check it out here!](https://github.com/ciphey/ciphey)
## Contributors ✨
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[usage-2]: https://github.com/RustScan/RustScan/wiki/Things-you-may-want-to-do-with-RustScan-but-don't-understand-how "Things you may want to do with rustscan but don't know how"
[community-1]: https://github.com/RustScan/RustScan/wiki/Contributing "Learn how to contribute"
You can now modify RustScan files with your favorite editor, once you want to compile and test your modifications, type the following in the container prompt:
```bash
root@container:/rustscan# cargo build
```
You can check the command-line interface without starting a scan:
```bash
root@container:/rustscan# cargo run -- --help
```
You can also format, lint with `clippy` and test the code with the following commands:
```bash
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.
Scanning behaviour and speed are measured separately, by the opt-in "Scan benchmark"
GitHub Actions workflow (`.github/workflows/scan-benchmark.yml`). It runs for pull
requests that touch the scanner, its runtime or its dependencies, and can be started
by hand from the Actions tab. It builds the change and its base in release mode,
scans listeners that it opens on 127.0.0.1 inside the runner, checks that both
builds find the same open ports, and adds a comparison table to the job summary. Its
script (`.github/scan-benchmark/scan_bench.py`) port-scans the loopback interface,
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.
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.
ثم نفس سقطت وبالتحديد،, جزيرتي باستخدام أن دنو. إذ هنا؟ الستار وتنصيب كان. أهّل ايطاليا، بريطانيا-فرنسا قد أخذ. سليمان، إتفاقية بين ما, يذكر الحدود أي بعد, معاملة بولندا، الإطلاق عل إيو.
//! The scanner is asynchronous and runs on [Tokio](https://docs.rs/tokio):
//! await [`Scanner::run`](crate::scanner::Scanner::run) from a Tokio runtime
//! with the I/O and time drivers enabled. It never spawns tasks, so a
//! current-thread runtime is enough (see [`Scanner`](crate::scanner::Scanner)
//! for details).
//!
//! This example is compiled but never executed by the test suite.
//!
//! ```no_run
//! use std::{net::IpAddr, time::Duration};
//!
//! 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 = PortRanges(vec![(1, 1_000)]);
//! let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random); // can be serial, random or manual https://github.com/RustScan/RustScan/blob/master/src/port_strategy/mod.rs
//! let scanner = Scanner::new(
//! &addrs, // the addresses to scan
//! 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?
//! );
//!
//! // Inside an async context (e.g. `#[tokio::main]`) this is simply
//! // `scanner.run().await`.
//! let runtime = tokio::runtime::Builder::new_current_thread()
//! .enable_all()
//! .build()
//! .unwrap();
//! let scan_result = runtime.block_on(scanner.run());
/// If you're looking for the actual scanning, check out the module Scanner
fnmain(){
letmatches=App::new("RustScan")
.author("Bee https://github.com/brandonskerritt")
.about("Fast Port Scanner built in Rust\nWARNING Do not use this program against sensitive infrastructure. The specified server may not be able to handle this many socket connections at once.")
.version("1.2.0")
.setting(AppSettings::TrailingVarArg)
// IP address is a required argument
.arg(Arg::with_name("ip")
.required(true)
.index(1)
.long("ip-address")
.help("The IP address to scan"))
.arg(Arg::with_name("b")
.short("b")
.long("batch")
.takes_value(true)
.default_value("4500")
.help("Increases speed of scanning. The batch size for port scanning. Depends on your open file limit of OS. If you do 65535 it will do every port at the same time. Although, your OS may not support this."))
.arg(Arg::with_name("T")
.short("T")
.long("timeout")
.takes_value(true)
.default_value("1500")
.help("The timeout before a port is assumed to be close. In MS."))
.arg(Arg::with_name("q")
.short("-q")
.long("quiet")
.takes_value(false)
.help("Quiet mode. Only output the ports. No Nmap. Useful for grep or outputting to a file."))
.arg(Arg::with_name("u")
.short("u")
.long("ulimit")
.help("Automatically ups the ULIMIT with the value you provided.")
.takes_value(true))
.arg(
Arg::with_name("command")
.help("The Nmap arguments to run. To use the argument -A, end RustScan's args with '-- -A'. To run EXAMPLE: 'rustscan -T 1500 127.0.0.1 -- -A -sC'. This argument auto runs nmap {your commands} -vvv -p $PORTS ")
println!("{}","WARNING: Your file description limit is lower than selected batch size. Please considering upping this (how to is on the README). NOTE: this may be dangerous and may cause harm to sensitive servers. Automatically reducing Batch Size to match your limit, this process isn't harmful but reduces speed.".red());
// TODO this is a joke please fix
// If we got here it means the IP was not found within the HashMap, this
// means the scan couldn't find any open ports for it.
// if the OS supports high file limits like 8000
// but the user selected a batch size higher than this
// reduce to a lower number
// basically, ubuntu is 8000
// but i can only get it to work on < 5k in testing
// 5k is default, so 3000 seems safe
ifx>8000{
batch_size=3000
letx=format!("Looks like I didn't find any open ports for {:?}. This is usually caused by a high batch size.
\n*I used {} batch size, consider lowering it with {} or a comfortable number for your system.
\n Alternatively, increase the timeout if your ping is high. Rustscan -t 2000 for 2000 milliseconds (2s) timeout.\n",
println!("Your file description limit is higher than the batch size. You can potentially increase the speed by increasing the batch size, but this may cause harm to sensitive servers. Your limit is {}, try batch size {}.",x,x-one);
// Closed ports are only listed; scripts are never run against them.
/// 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.
fneffective_batch_size(opts: &Opts)-> usize{
#[cfg(unix)]
{
infer_batch_size(opts,adjust_ulimit_size(opts))
}
// if no ports are found, suggest running with less
ifnmap_str_ports.is_empty(){
panic!("{} Looks like I didn't find any open ports. This is usually caused by a high batch size.
\n*I used {} threads, consider lowering to {} with {} or a comfortable number lfor your system.
\n Alternatively, increase the timeout if your ping is high. Rustscan -T 2000 for 2000 second timeout.","ERROR".red(),batch_size,(batch_size/2).to_string().green(),"'rustscan -b <batch_size> <ip address>'".green());
#[cfg(not(unix))]
{
opts.batch_size
}
}
// Tells the user we are now switching to Nmap
if!quiet{
println!(
"{}",
"Starting nmap.".blue(),
/// Runs the scan to completion on a current-thread Tokio runtime.
///
/// The scanner drives every socket from a single future and never spawns
/// tasks, so one thread is all it can use: a current-thread runtime runs the
/// scan, the I/O driver and the timers on the calling thread, without the
/// worker threads and cross-thread wake-ups of a multi-threaded runtime.
///
/// The runtime only exists for the duration of the scan. Address resolution
/// (blocking DNS lookups and file reads) happens before it is created, so it
/// can never stall the scan, and scripts run after it is gone.
// Adjust the batch size when the ulimit value is lower than the desired batch size
ifulimit<batch_size{
warning!("File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers",
opts.greppable,opts.accessible
);
// 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.
ifulimit<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);
info!("Halving batch_size because ulimit is smaller than average batch size");
batch_size=ulimit/2;
}elseifulimit>DEFAULT_FILE_DESCRIPTORS_LIMIT{
info!("Batch size is now average batch size");
batch_size=AVERAGE_BATCH_SIZE;
}else{
batch_size=ulimit-100;
}
}
// When the ulimit is higher than the batch size let the user know that the
// batch size can be increased unless they specified the ulimit themselves.
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.");
assert!(!ntp.is_empty(),"NTP payload should not be empty");
}
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.