ENUM^:Enumeration

RustScan

Practical RustScan: scan all 65,535 TCP ports in seconds, hand only the open ones to Nmap, and tune batch/timeout so you don't miss ports over a VPN. Copy-paste recipes for HTB boxes, Windows/DCs, subnets, pivots and UDP, plus the gotchas that silently break scans.

beginnerupdated 2026-10-13RustScan · Nmap

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.

PieceWhat it does
-a $IPTarget: IP, hostname, CIDR, comma list, or a file of them
-r 1-65535All ports. This is already the default, but writing it out removes any doubt
-u 5000Raises 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:

FlagDefaultTurn it…When
-b batch4500down (1000, 500, 200)VPN, VM NAT, Windows firewall, pivots, fragile kit
-t timeout ms1500up (3000)High latency (HTB from far away, tunnels)
--tries12Any time accuracy matters more than 20 extra seconds
SituationFlags
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 / loopbackdefaults

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 BBatch actually used
U ≥ BB
U < B, U < 3000U ÷ 2 (macOS 256 → 128, Kali 1024 → 512)
U < B, 3000–8000U − 100
U < B, U > 80003000 (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 -->.

  1. 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.
  2. No root-only flags after -- (-sS, -O, -sU). Nmap refuses to start and you only see [!] Error Exit code = 1. -A runs but silently skips OS detection. Use the two-stage pattern with sudo nmap.
  3. Never put -p after --. RustScan already sets it, and Nmap quits with Only 1 -p option allowed.
  4. Double-quote anything with spaces. Args go through sh -c again, so --script 'default or vuln' becomes targets called or and vuln. Write --script "'default or vuln'".
  5. mkdir -p your output folder first. If Nmap fails, RustScan throws away its output, and only -oA files 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 -x for out-of-scope addresses.
  • Hostnames become one IP, the resolver’s first answer (localhost may scan ::1 only). 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. Use proxychains -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 with sudo nmap -sU yourself, 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

SymptomFix
Looks like I didn't find any open portsPass an IP not a name, retry with -b 1000 -t 3000 --tries 2, check VPN (ping, ip route)
Different ports each runPacket loss. Lower -b, raise -t, --tries 2, compare two -g runs
Nmap says host down / 0 hosts upAdd -- -Pn
[!] Error Exit code = 1 after Running scriptRoot-only Nmap flag, missing -oA folder, or -p after --
[!] Error Exit code = 127nmap not on PATH (check under sudo too)
Nmap scanning hosts called or / andSpace in an -- arg. Use "'a or b'"
-a, -b or -- args ignored~/.rustscan.toml overrides them. Use -n
--top as slow as a full scanNo [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 NmapDefault 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 random only shuffles the order.
  • Fragile targets can fall over. Printers, OT and small VPSs get -b 100 or less.
  • Stay in scope: -x excludes addresses, CIDRs include network and broadcast, and a hostname may resolve to a CDN rather than the client.

RustScan vs the alternatives

RustScannmap -p- --min-rate 5000masscannaabu
Rootnofor SYNyesfor SYN
Retriesno (--tries)yes, adaptivenoyes
Host discoverynoyesnoyes
Best atone box, all ports, fast, no rootaccuracyhuge rangesProjectDiscovery 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

FlagDefaultNotes
-a <targets>requiredIPs, CIDRs, hostnames, comma list, or a file
-r <a-b>1-65535Range. 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
--topNeeds a [ports] table in config, otherwise full range
-b <n>4500Sockets in flight. Capped by ulimit
-t <ms>1500Per attempt
--tries <n>1Attempts per port
-u <n>Raise open-file limit for this run
-gPorts only (ip -> [ports]), no Nmap
--udpUDP probe mode
--scripts none|default|customdefaultdefault = Nmap
--scan-order serial|randomserialDoesn’t change results
-n / -c <file>Ignore config / use another config
--accessible, --no-bannerPlain output / hide banner
--resolver <list>systemFallback DNS only
-- <args>Appended to the Nmap (or script) command line
RUST_LOG=info|debugEnv 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
On this page

☧ CPTS · ordered posts

Locked

Unlock with your Main_Key through local GPG.

Start the local GPG helper

In your cheatsheet checkout, run:

npm run unlock:helper

Open the link it prints, or paste its pairing code below. Keep the terminal open until GPG finishes.

Import an index you already decrypted

Download the encrypted index, then decrypt with GPG:

gpg --output report-index.json --decrypt report-index.gpg

Your private key stays in GPG. The decrypted index is saved in this browser so you can reopen it after navigation, refreshes and restarts. Lock this browser or clear this site's browser data to remove the saved index. Nothing is uploaded.