TL;DR: the one command
mkdir -p recon # -oA fails (and you lose the Nmap output) if the folder doesn't exist
rustscan -a $IP -r 1-65535 -u 5000 -- -Pn -sCV -oA recon/$NAME
That’s every TCP port, swept in seconds, then Nmap runs default scripts + version detection on
only the ports that were open, and saves .nmap, .gnmap and .xml. It’s the scan in your
history (rustscan -a 10.129.71.106 -r 1-65535 -u 5000 -- -sCV -oA ./recon/trickFirstScan) with
-Pn added, because without it Nmap can decide a firewalled box is “down” and scan nothing.
| Piece | What it does |
|---|---|
-a $IP | Target: IP, hostname, CIDR, comma list, or a file of them |
-r 1-65535 | All ports. This is already the default, but writing it out removes any doubt |
-u 5000 | Raises the open-file limit for this run so the batch isn’t silently cut (see Speed vs accuracy) |
-- | End of RustScan flags. Everything after it goes to Nmap |
-Pn -sCV -oA ... | Normal Nmap flags. Don’t put -p here, RustScan sets it for you |
RustScan finds open ports, and Nmap tells you what’s running on them. Use RustScan for the speed and leave the service detail to Nmap.
Scanning all ports
RustScan scans 1-65535 by default. Every one of these is a full-range TCP scan:
rustscan -a $IP # implicit: no -p, -r or --top means 1-65535
rustscan -a $IP -r 1-65535 # explicit, same thing
rustscan -a $IP --top # SURPRISE: also 1-65535 unless your config has a [ports] table
Check what it’s really scanning instead of trusting it:
RUST_LOG=debug rustscan -n -a $IP -g 2>&1 | grep -aE 'Number of ports|Batch size|->'
# Batch size 4500
# Number of ports 65535
# 10.10.11.5 -> [22,80,8080]
Other port selections:
rustscan -a $IP -r 1-10000 # a range (only -r takes ranges)
rustscan -a $IP -p 22,80,443,445,5985 # a list (-p 80-90 is an ERROR: no ranges in -p)
rustscan -a $IP -e 9100,515 # all ports except these (e.g. off-limits printers)
-p and -r can’t be combined. For “a range plus a few extras”, run two scans or use -p with
the full list.
Getting a real top-1000
--top reads the [ports] table from ~/.rustscan.toml, and the package doesn’t install one.
Build the list from Nmap’s own frequency data instead:
# Nmap's top 1000 TCP ports as a comma list (Kali path; on NixOS use $(dirname $(readlink -f $(which nmap)))/../share/nmap/nmap-services)
top=$(awk '$2 ~ /\/tcp$/ {print $3, $2}' /usr/share/nmap/nmap-services \
| sort -rn | head -1000 | cut -d' ' -f2 | cut -d/ -f1 | paste -sd, -)
rustscan -a $IP -p "$top" -- -Pn -sCV
On a single box there’s little reason to do this. A full sweep only takes a few seconds longer than a top-1000 scan, and it finds the port-49663 web app that the top-1000 list skips.
Recipes
# --- single box ----------------------------------------------------------------
rustscan -a $IP -r 1-65535 -u 5000 -- -Pn -sCV -oA recon/$NAME # the default first scan
rustscan -a $IP -u 5000 -g # ports only: 10.10.11.5 -> [22,80]
rustscan -a $IP -u 5000 -b 1000 -t 3000 --tries 2 -- -Pn -sCV # lossy VPN / Windows firewall
rustscan -a $IP -- -Pn -sCV --script "'default or vuln'" # NSE expression: quote TWICE
rustscan -a box.htb -- -Pn -sCV # /etc/hosts names work
# --- many hosts ----------------------------------------------------------------
rustscan -a 10.10.110.0/24 -x 10.10.110.1 -g > open.txt # subnet minus the gateway
rustscan -a live.txt -u 5000 -g > open.txt # file: one IP/CIDR/host per line
rustscan -a 10.10.11.5,10.10.11.6,dc01.corp.htb -g # comma list
# --- UDP (candidates only; confirm with sudo nmap -sU) ------------------------
rustscan -n -a $IP --udp -p 53,69,111,123,137,161,500,623 -t 2000 --tries 2 -g
# --- debugging -----------------------------------------------------------------
RUST_LOG=info rustscan -a $IP -g # adds timing summary
RUST_LOG=debug rustscan -a $IP -p 22 -g 2>&1 | less # merged options, batch, socket errors
rustscan -n -a $IP -g # ignore ~/.rustscan.toml
There is no -q flag (older guides use it). For ports-only output, use -g.
Workflow: one HTB / CPTS box
export IP=10.129.71.106 NAME=trick
mkdir -p recon
# 1. Full TCP sweep, Nmap scripts + versions on the hits, saved
rustscan -a $IP -r 1-65535 -u 5000 -- -Pn -sCV -oA recon/$NAME
# 2. Sanity pass: gentler settings, ports only. Compare with step 1.
# A port that only shows up here was dropped by the fast pass (VPN loss is common on HTB).
rustscan -a $IP -u 5000 -b 1000 -t 3000 --tries 2 -g
# 3. UDP on the ports that actually matter (SNMP, TFTP, IPMI, IKE, NTP, DNS...)
udp=$(rustscan -n -a $IP --udp -p 53,69,111,123,137,161,500,623,1900,5353,11211 \
-t 2000 --tries 2 -g | sed -E 's/.*\[(.*)\]/\1/')
[ -n "$udp" ] && sudo nmap -Pn -sU -sV -p "$udp" -oA recon/${NAME}_udp $IP
Then read recon/$NAME.nmap, add any vhost from http-title (Did not follow redirect to http://trick.htb/) to /etc/hosts, and go port by port with
Service Enumeration and
Common Ports and Services.
Two-stage: RustScan finds, you run Nmap
The -- hand-off is convenient, but it hides Nmap’s output until Nmap finishes, runs Nmap as
your user (so -sS, -O and -sU fail), and always exits 0. On a domain controller with 25 open
ports, -sC can sit silently for minutes. Splitting the two stages fixes all of that:
ports=$(rustscan -n -a $IP -u 5000 -g | sed -E 's/.*\[(.*)\]/\1/')
echo "$ports" # 53,88,135,139,389,445,...
sudo nmap -Pn -sS -sCV -O -p "$ports" -oA recon/$NAME $IP # live output, root flags allowed
Add this to ~/.zshrc and you have a one-word full scan:
# rs <ip> [name]: every TCP port with RustScan, then a live nmap -sCV on exactly those ports.
rs() {
local ip=$1 name=${2:-$1} ports v=
case $ip in *:*) v=-6 ;; esac # Nmap needs -6 for IPv6 targets
mkdir -p recon
ports=$(rustscan -n -a "$ip" -u 5000 -g | sed -E 's/.*\[(.*)\]/\1/' | tr , '\n' | sort -n | paste -sd, -)
[ -n "$ports" ] || { echo "rs: no open TCP ports on $ip" >&2; return 1; }
echo "rs: $ip -> $ports"
nmap $v -Pn -sCV -p "$ports" -oA "recon/$name" "$ip"
}
rs 10.129.71.106 trick
Speed vs accuracy
RustScan does a plain TCP connect() on thousands of ports at once (the batch, -b), waits
-t ms per attempt, and by default never retries. A dropped SYN-ACK means a missed port, and
nothing warns you. The three settings that matter:
| Flag | Default | Turn it… | When |
|---|---|---|---|
-b batch | 4500 | down (1000, 500, 200) | VPN, VM NAT, Windows firewall, pivots, fragile kit |
-t timeout ms | 1500 | up (3000) | High latency (HTB from far away, tunnels) |
--tries | 1 | 2 | Any time accuracy matters more than 20 extra seconds |
| Situation | Flags |
|---|---|
| HTB / OffSec VPN, one box | -u 5000 (defaults), then a -b 1000 -t 3000 --tries 2 -g pass to compare |
| Windows host, firewall on | -b 1000 -t 3000 --tries 2 -- -Pn |
| Through ligolo-ng | -b 200 -t 3000 --tries 2 |
| Printers, OT, old appliances | -b 100 -t 3000 (gentle, not stealthy) |
| Local lab VM / loopback | defaults |
Worst case runtime (every port filtered, so every socket waits its full timeout):
runtime ≈ hosts × ports × tries ÷ batch × timeout
1 host, 65535 ports, -b 4500 -t 1500 ≈ 22 s
1 host, 65535 ports, -b 1000 -t 3000 --tries 2 ≈ 6.5 min
/24, 65535 ports, defaults ≈ 93 min ← find live hosts first
Closed ports reply with RST instantly, so most real boxes finish well under the worst case.
The open-file limit (-u)
Every in-flight socket is an open file. If your soft limit (ulimit -n) is below the batch,
RustScan quietly shrinks the batch:
| Your limit U vs batch B | Batch actually used |
|---|---|
| U ≥ B | B |
| U < B, U < 3000 | U ÷ 2 (macOS 256 → 128, Kali 1024 → 512) |
| U < B, 3000–8000 | U − 100 |
| U < B, U > 8000 | 3000 (so -b 65535 with a 10000 limit gets you 3000) |
ulimit -n # check yours
rustscan -a $IP -u 5000 # per-run fix, no root needed if ≤ your hard limit
If ulimit -n already shows a big number (NixOS and many desktops default to 524288), -u does
nothing useful. Don’t confuse “the batch fits” with “a bigger batch is better”. Over a VPN, a
bigger batch just drops more packets.
The Nmap hand-off: five rules
What RustScan actually runs per host is
nmap -vvv -p <open ports> -4 <ip> <your args after -->.
- Always pass
-Pn. Nmap still does its own host discovery, and a Windows box with no web server and ICMP blocked looks “down”, so Nmap scans nothing. - No root-only flags after
--(-sS,-O,-sU). Nmap refuses to start and you only see[!] Error Exit code = 1.-Aruns but silently skips OS detection. Use the two-stage pattern withsudo nmap. - Never put
-pafter--. RustScan already sets it, and Nmap quits withOnly 1 -p option allowed. - Double-quote anything with spaces. Args go through
sh -cagain, so--script 'default or vuln'becomes targets calledorandvuln. Write--script "'default or vuln'". mkdir -pyour output folder first. If Nmap fails, RustScan throws away its output, and only-oAfiles survive.
One Nmap per host runs sequentially, after the whole sweep. On a subnet, run the Nmaps yourself in parallel (below).
Subnets and pivots
RustScan has no host discovery. Every address gets all 65,535 ports, and a dead address burns the full timeout on each one. Find live hosts first, sweep them, then fan out Nmap:
mkdir -p recon
sudo nmap -sn -PE -PS22,80,443,445,3389 10.10.110.0/24 -oG - | awk '/Up$/{print $2}' > live.txt
rustscan -a live.txt -u 5000 -g > open.txt
# open.txt lines look like: 10.10.110.15 -> [22,80,445]
# Nmap each host, 4 at a time
sed -E 's/^(.*) -> \[(.*)\]$/\1 \2/' open.txt \
| xargs -P 4 -n 2 sh -c 'nmap -Pn -sCV -p "$1" -oA "recon/$0" "$0"'
- CIDR includes network + broadcast. Use
-xfor out-of-scope addresses. - Hostnames become one IP, the resolver’s first answer (
localhostmay scan::1only). For round-robin or dual-stack names, resolve them yourself and pass the IPs. - ligolo-ng / tun pivots: works unchanged, but drop to
-b 200 -t 3000 --tries 2. See ligolo-ng. - SOCKS (chisel,
ssh -D) + proxychains: don’t. RustScan has no proxy support. Useproxychains -q nmap -sT -Pn -n -p <short list>.
UDP
--udp sends one probe per port and only reports ports that reply. Well-known services get
Nmap’s real payloads (DNS, SNMP, NTP, IKE, IPMI, NetBIOS, SSDP…), and everything else gets an empty
packet that most services ignore. Silence never shows up as a result.
- Keep it to short lists. Full-range UDP mostly waits on timeouts.
- The default Nmap stage scans the found ports over TCP, so the result is wrong
(
15353/tcp closed). Confirm withsudo nmap -sUyourself, as in the box workflow. - For broad UDP coverage just use Nmap:
sudo nmap -Pn -sU --top-ports 100 -sV --version-intensity 0 $IP.
Output and parsing
Open 10.10.11.5:22 ← printed live as ports answer, start reading before it finishes
Open 10.10.11.5:80
[~] Starting Script(s)
[>] Running script "nmap -vvv -p {{port}} -{{ipversion}} {{ip}} -Pn -sCV" on ip 10.10.11.5
-g (greppable) prints one line per host that had open ports, 10.10.11.5 -> [80,22], in the
order the ports answered. It also suppresses warnings and skips Nmap.
rustscan -a $IP -g | sed -E 's/.*\[(.*)\]/\1/' # 80,22
rustscan -a $IP -g | sed -E 's/.*\[(.*)\]/\1/' | tr , '\n' | sort -n # one per line, sorted
Exit status is 0 even when nothing was found or Nmap failed. rustscan ... && next proves
nothing, so test the output instead (as rs does).
Config file: be careful
~/.rustscan.toml doesn’t set defaults. It overrides your command line. A config with
addresses = ["127.0.0.1"] (copied from many old guides) makes every scan hit localhost no matter
what you pass to -a. Same for batch_size, timeout and command (which replaces your -- args).
# ~/.rustscan.toml: the only safe things to put here
ulimit = 5000
# [ports] table here if you want --top to mean top-1000
- Values are capitalised:
scan_order = "Random",scripts = "Custom". Lowercase aborts every scan. - Something seems ignored? Run with
-n(no config) and see if the problem goes away.
Custom scripts (optional)
--scripts custom replaces the built-in Nmap call with your own scripts from
~/.rustscan_scripts/. It only runs scripts whose tags are all listed in
~/.rustscan_scripts.toml (and if that file is missing, the run fails).
printf 'tags = ["htb"]\n' > ~/.rustscan_scripts.toml
mkdir -p ~/.rustscan_scripts
#!/usr/bin/env bash
#tags = ["htb"]
#ports_separator = " "
#call_format = "bash {{script}} {{ip}} {{port}}"
# ^ blank line above is REQUIRED: a comment right under the header breaks TOML parsing and the
# script is silently dropped. trigger_port does not exist; gate on ports yourself.
ip=$1; shift
for p in "$@"; do
for s in http https; do
out=$(curl -sk -m 4 -o /dev/null -w '%{http_code} %{redirect_url}' "$s://$ip:$p/" 2>/dev/null)
[ "${out%% *}" != 000 ] && echo "[web] $s://$ip:$p -> $out"
done
done
case " $* " in *" 445 "*) nxc smb "$ip" -u '' -p '' --shares 2>&1 ;; esac
exit 0 # non-zero exit = output thrown away
Save it as ~/.rustscan_scripts/htb.sh, then run rustscan -a $IP --scripts custom. Custom mode
drops the default Nmap run. If you still want it, add a second script whose call_format is
nmap -vvv -Pn -sCV -p {{port}} -oA recon/rs_{{ip}} {{ip}}. Placeholders: {{ip}}, {{port}},
{{ipversion}}, {{script}}. Only stdout is shown, so use 2>&1. See
NetExec for the SMB follow-up.
Troubleshooting
| Symptom | Fix |
|---|---|
Looks like I didn't find any open ports | Pass an IP not a name, retry with -b 1000 -t 3000 --tries 2, check VPN (ping, ip route) |
| Different ports each run | Packet loss. Lower -b, raise -t, --tries 2, compare two -g runs |
Nmap says host down / 0 hosts up | Add -- -Pn |
[!] Error Exit code = 1 after Running script | Root-only Nmap flag, missing -oA folder, or -p after -- |
[!] Error Exit code = 127 | nmap not on PATH (check under sudo too) |
Nmap scanning hosts called or / and | Space in an -- arg. Use "'a or b'" |
-a, -b or -- args ignored | ~/.rustscan.toml overrides them. Use -n |
--top as slow as a full scan | No [ports] table, so it’s scanning 65535 (fix) |
error: unexpected argument '-q' | No such flag. Use -g |
invalid value '80-90' for '--ports' | Ranges go in -r, not -p |
[!] Your file limit is very small | -u 5000 |
UDP hit shows /tcp closed in Nmap | Default stage is TCP. Use sudo nmap -sU -p <ports> |
Silence for minutes after Starting Script(s) | Nmap is running, and its output is held until it exits. Use two-stage |
Noise and scope
- It isn’t stealthy. Every open port gets a full TCP handshake that services log, and an IDS sees
tens of thousands of connects in seconds.
--scan-order randomonly shuffles the order. - Fragile targets can fall over. Printers, OT and small VPSs get
-b 100or less. - Stay in scope:
-xexcludes addresses, CIDRs include network and broadcast, and a hostname may resolve to a CDN rather than the client.
RustScan vs the alternatives
| RustScan | nmap -p- --min-rate 5000 | masscan | naabu | |
|---|---|---|---|---|
| Root | no | for SYN | yes | for SYN |
| Retries | no (--tries) | yes, adaptive | no | yes |
| Host discovery | no | yes | no | yes |
| Best at | one box, all ports, fast, no root | accuracy | huge ranges | ProjectDiscovery pipelines |
For the plain-Nmap equivalent, run sudo nmap -p- --min-rate 5000 -Pn $IP and then -sCV on the
results. See Nmap,
Recon and Host Discovery and
Awesome Nmap Grep.
Flag reference
| Flag | Default | Notes |
|---|---|---|
-a <targets> | required | IPs, CIDRs, hostnames, comma list, or a file |
-r <a-b> | 1-65535 | Range. Used automatically when no -p/-r/--top |
-p <list> | Comma list only, no ranges. Can’t mix with -r | |
-e <list> | Exclude ports | |
-x <list> | Exclude addresses | |
--top | Needs a [ports] table in config, otherwise full range | |
-b <n> | 4500 | Sockets in flight. Capped by ulimit |
-t <ms> | 1500 | Per attempt |
--tries <n> | 1 | Attempts per port |
-u <n> | Raise open-file limit for this run | |
-g | Ports only (ip -> [ports]), no Nmap | |
--udp | UDP probe mode | |
--scripts none|default|custom | default | default = Nmap |
--scan-order serial|random | serial | Doesn’t change results |
-n / -c <file> | Ignore config / use another config | |
--accessible, --no-banner | Plain output / hide banner | |
--resolver <list> | system | Fallback DNS only |
-- <args> | Appended to the Nmap (or script) command line | |
RUST_LOG=info|debug | Env var, the only verbosity control |
Checked against 2.3.0 (nixpkgs) and 2.4.1 (Kali, Homebrew, Arch, cargo). The flags are the same in
both. Install with sudo apt install rustscan, brew install rustscan, rustscan from nixpkgs
or cargo install rustscan, and make sure nmap is on your PATH.
Quick reference
rustscan -a $IP -r 1-65535 -u 5000 -- -Pn -sCV -oA recon/$NAME # ALL ports + nmap scripts/versions
rustscan -a $IP -u 5000 -g # all ports, list only
rustscan -a $IP -u 5000 -b 1000 -t 3000 --tries 2 -g # accurate pass (compare)
rs $IP $NAME # two-stage function (live nmap)
rustscan -a $IP -p 22,80,443,445 # specific ports
rustscan -a $IP -r 1-10000 -e 9100 # range minus a port
rustscan -a live.txt -u 5000 -g > open.txt # many hosts
rustscan -a $IP -- -Pn --script "'default or vuln'" # NSE expression (double quotes)
rustscan -n -a $IP --udp -p 53,123,161,500,623 --tries 2 -g # UDP candidates
RUST_LOG=debug rustscan -n -a $IP -g 2>&1 | grep 'Number of ports' # prove what's scanned