rustscan.md (46824B)
1 --- 2 title: "RustScan" 3 description: "RustScan 2.4.1 in depth: how the connect sweep works, the batch/timeout/ulimit maths, the Nmap hand-off and its traps, config precedence, the scripting engine, and HTB/CPTS workflows. Every behaviour checked against the source and a live lab." 4 category: enumeration 5 tags: [enumeration, port-scanning, network, nmap, htb, cpts] 6 tools: [RustScan, Nmap] 7 difficulty: intermediate 8 updated: "2026-10-09" 9 references: 10 - name: "RustScan source, tag 2.4.1" 11 url: "https://github.com/bee-san/RustScan/tree/2.4.1" 12 author: "RustScan contributors" 13 relation: inspired 14 note: "Flag semantics, config merge order, batch-size inference, target parsing and the script-header parser on this page were read from src/main.rs, src/input.rs, src/address.rs, src/scanner and src/scripts at this tag, then reproduced live." 15 - name: "RustScan wiki" 16 url: "https://github.com/bee-san/RustScan/wiki" 17 author: "RustScan contributors" 18 relation: link-only 19 note: "Upstream usage docs. Several pages (trigger_port, tag matching, config value case, adaptive learning) describe behaviour the 2.4.1 code does not have; where they disagree, this page follows the code." 20 - name: "Nmap Reference Guide: Host Discovery" 21 url: "https://nmap.org/book/man-host-discovery.html" 22 author: "Gordon Lyon" 23 relation: link-only 24 note: "What Nmap sends to decide a host is up, and so why the Nmap stage needs -Pn." 25 - name: "Docker Hub: rustscan/rustscan" 26 url: "https://hub.docker.com/r/rustscan/rustscan" 27 relation: link-only 28 note: "Tag list checked 2026-10-09: the newest image is 2.3.0, amd64 only." 29 - name: "Kali package tracker: rustscan" 30 url: "https://pkg.kali.org/pkg/rustscan" 31 relation: link-only 32 note: "2.4.1-0kali1, built for amd64, arm64, armhf and i386." 33 --- 34 35 ## What RustScan is, and what it isn't 36 37 RustScan answers one question fast: which TCP ports on these addresses finish a handshake? It 38 asks with plain `connect()` calls, thousands in flight at once on an async runtime, so it needs no 39 root and no raw sockets. Then, host by host, it runs a script. The default script is Nmap, pointed 40 at exactly the ports that answered. That split is where the speed comes from. The 65,535-port sweep 41 is cheap, and Nmap's expensive work (`-sC`, `-sV`) only ever touches ports already known to be open. 42 43 Everything else you might expect from a scanner, it either hands to Nmap or doesn't do at all: 44 45 | | RustScan 2.4.1 | What that means for you | 46 |---|---|---| 47 | Scan type | TCP connect only (full handshake, then shutdown) | No root needed. Every open port gets a real connection, which the service may log | 48 | Port states | open, or not open | Closed (RST) and filtered (silence) look identical. Nmap is what tells them apart | 49 | Host discovery | none | Every address you give it gets every port, alive or not | 50 | Retransmits | none unless `--tries` is above 1 | One dropped SYN or SYN-ACK is one missed port, with no warning | 51 | Versions, OS, NSE | none, delegated | `-sV`, `-sC` and `-O` happen in the Nmap stage, after `--` | 52 | UDP | `--udp`: one probe per port, open only if something replies | See [UDP mode](#udp-mode) | 53 | Stealth | none: no SYN, decoys, fragmentation or source-port options | `--scan-order random` shuffles the port order, and that's all | 54 | "Adaptive learning" | batch size is sized against your open-file limit at startup | Nothing is learned or saved between runs. 2.4.1 writes no state files | 55 56 > [!info] **How this page was checked.** Written against **RustScan 2.4.1** (the current release, February 2025) and **Nmap 7.99** on macOS arm64, on 2026-10-09. Every flag, message and gotcha below was read from the 2.4.1 source and then reproduced against throwaway listeners on 127.0.0.1, with `HOME` pointed at an empty directory so no stray config could interfere. Output blocks are real captures from those runs. Upstream's wiki disagrees with the code in several places. Where they differ, this page follows the code and says so. 57 58 --- 59 60 ## Quick start 61 62 ```bash 63 mkdir -p recon # Nmap's -oA fails, and takes the whole run with it, if the directory is missing 64 65 # One box: every TCP port, then default scripts + versions on exactly those ports, saved 66 rustscan -a $IP -u 5000 -- -Pn -sCV -oA recon/$NAME 67 68 # Port list only, no Nmap. Prints one line per host: 10.10.11.5 -> [22,80,443] 69 rustscan -a $IP -u 5000 -g 70 71 # Lossy VPN or a Windows firewall: smaller window, longer timeout, one retry 72 rustscan -a $IP -u 5000 -b 1000 -t 3000 --tries 2 -- -Pn -sCV -oA recon/$NAME 73 ``` 74 75 What each piece is doing: 76 77 - **`-u 5000`** raises the open-file limit for this run. Without it, a stock macOS shell (soft limit 78 256) silently cuts the batch to 128 sockets, and a typical Debian/Kali shell (1024) cuts it to 512. 79 See [the batch maths](#how-rustscan-sizes-the-batch). 80 - **`--`** ends RustScan's flags. Everything after it is appended to the Nmap command line, which is 81 run through `sh -c`. That matters for quoting, see [quoting through `--`](#quoting-through---). 82 - **`-Pn`** stops Nmap from running its own ping and deciding the host is down. RustScan has already 83 proved it's up. See [why `-Pn`, every time](#-pn-every-time). 84 - **`-oA recon/$NAME`** is your record. RustScan holds Nmap's output back until Nmap exits, and if 85 Nmap fails its output is thrown away. The files survive either way. 86 87 --- 88 89 ## What happens when you press enter 90 91 <figure class="flow plate corners"> 92 <figcaption class="flow__cap"><span class="flow__kind">One RustScan run, start to exit</span><span class="flow__dir">TD</span></figcaption> 93 <div class="flow__body"> 94 <div class="flow__diagram" data-dir="td"> 95 <div class="flow-rank"><div class="flow-node is-entry">FLAGS + CONFIG<span class="sub">~/.rustscan.toml overrides the CLI</span></div></div> 96 <div class="flow-edge"></div> 97 <div class="flow-rank"><div class="flow-node">TARGETS<span class="sub">CIDR · first DNS answer · file · minus -x</span></div></div> 98 <div class="flow-edge"></div> 99 <div class="flow-rank"><div class="flow-node">BATCH SIZE<span class="sub">-b, capped by your soft ulimit</span></div></div> 100 <div class="flow-edge"></div> 101 <div class="flow-rank"><div class="flow-node">CONNECT SWEEP<span class="sub">B sockets in flight · -t per try · --tries</span></div></div> 102 <div class="flow-edge"></div> 103 <div class="flow-rank"><div class="flow-node is-decision">-g or --scripts none?</div></div> 104 <div class="flow-branches"> 105 <div class="flow-lane"><div class="flow-edge"><span class="flow-edge__label">yes</span></div><div class="flow-node">PRINT<span class="sub">ip -> [ports]</span></div></div> 106 <div class="flow-lane"><div class="flow-edge"><span class="flow-edge__label">no</span></div><div class="flow-node">SCRIPTS, HOST BY HOST<span class="sub">default: nmap -vvv -p PORTS + your -- args</span></div></div> 107 </div> 108 <div class="flow-join"></div> 109 <div class="flow-rank"><div class="flow-node is-goal">EXIT 0<span class="sub">even if nothing was open or Nmap failed</span></div></div> 110 </div> 111 </div> 112 </figure> 113 114 1. **Flags are merged with `~/.rustscan.toml`, and the file wins.** If the config sets `addresses`, 115 `batch_size`, `timeout`, `command` or most other keys, your command-line value is replaced. This is 116 the single most confusing behaviour in the tool. See [the config file](#the-config-file). 117 2. **Targets become a flat list of IPs.** A CIDR expands to every address in it, network and 118 broadcast included. A hostname becomes its *first* resolved address only. A filename is read 119 line by line. Duplicates and `-x` exclusions are dropped. 120 3. **The batch size is checked against your soft open-file limit**, and cut if it doesn't fit. 121 4. **Sockets are ordered port-major:** port 1 on every host, then port 2 on every host, and so on. 122 On a subnet the load spreads across hosts instead of piling onto one. 123 5. **A sliding window of B connects runs.** Each socket gets up to `--tries` attempts of `-t` 124 milliseconds each, and the moment one finishes the next one starts. A completed handshake means 125 open. Anything else (RST, timeout, unreachable) means not open, and the reason is only visible 126 with `RUST_LOG=debug`. 127 6. **Results are grouped per host.** Ports come out in the order they answered, not sorted. 128 7. **Scripts run per host, one after another, after the whole sweep has finished.** The default 129 script is `nmap -vvv -p <ports> -4|-6 <ip>` with your `--` arguments appended. Its output is 130 captured and printed only when it exits. 131 8. **The exit status is 0** whether anything was found or not, and whether Nmap worked or not. It's 132 only non-zero for a config parse error, no resolvable target, or a broken custom-scripts setup. 133 134 --- 135 136 ## Install 137 138 | Source | Version (2026-10-09) | Command | Notes | 139 |---|---|---|---| 140 | Kali repo | 2.4.1 | `sudo apt install rustscan` | amd64, arm64, armhf and i386, so it works on Apple Silicon Kali VMs | 141 | Homebrew | 2.4.1 | `brew install rustscan` | Pulls in `nmap` as a dependency | 142 | Arch (extra) | 2.4.1 | `sudo pacman -S rustscan` | Install `nmap` separately | 143 | Cargo | 2.4.1 | `cargo install rustscan` | The only method upstream officially supports. Install `nmap` separately | 144 | GitHub release | 2.4.1 | download `rustscan.deb.zip`, `unzip`, then `sudo dpkg -i rustscan_2.4.1-1_amd64.deb` | The 2.4.1 assets are zip-wrapped, and the `.deb` is amd64 only | 145 | Docker Hub | **2.3.0** | `docker run -it --rm rustscan/rustscan:2.3.0 -a $IP` | Stale (last pushed September 2024) and amd64 only, so it's emulated on Apple Silicon and arm64 Kali. 2.4.1 ships no Dockerfile | 146 147 ```bash 148 rustscan --version # rustscan 2.4.1 149 nmap --version # the default script runs whatever `nmap` is first on PATH 150 ``` 151 152 > [!tip] **Skip the Docker image.** Older guides call Docker "recommended" because the container's open-file limit is high. A native package with `-u 5000` solves the same problem, runs the current version, and doesn't add an emulation layer or a NAT hop between you and the target. If you do use the container, remember that `--rm` deletes anything Nmap wrote inside it, so mount a directory for `-oA`. 153 154 --- 155 156 ## Flag reference (2.4.1, complete) 157 158 | Flag | Default | What it really does | 159 |---|---|---| 160 | `-a, --addresses <list>` | (required) | Comma list of IPs, CIDRs and hostnames, or a path to a file. See [Targets](#targets) | 161 | `-p, --ports <list>` | | Comma list only. Ranges are rejected (`-p 80-90` errors). Can't be combined with `-r` | 162 | `-r, --range <start-end>` | `1-65535` | Used automatically when neither `-p` nor `-r` is given | 163 | `-e, --exclude-ports <list>` | | Removed from whatever `-p`, `-r` or `--top` produced | 164 | `-x, --exclude-addresses <list>` | | IPs, CIDRs or hostnames, removed after expansion | 165 | `--top` | off | Uses the `[ports]` table from your config. **With no table, it silently scans all 65,535.** See [`--top`](#--top-does-nothing-without-a-config) | 166 | `-b, --batch-size <n>` | 4500 | Maximum sockets in flight. Capped by your soft ulimit | 167 | `-t, --timeout <ms>` | 1500 | Per attempt, not per port | 168 | `--tries <n>` | 1 | Attempts per port. `0` is corrected to `1` | 169 | `-u, --ulimit <n>` | | Sets the soft and hard open-file limit for this process before the batch is sized | 170 | `--scan-order serial\|random` | serial | Order of the sweep. Changes nothing about what's found | 171 | `--scripts none\|default\|custom` | default | `default` runs Nmap. `none` prints `ip -> [ports]`. `custom` uses `~/.rustscan_scripts/` | 172 | `--udp` | off | UDP probe mode. See [UDP mode](#udp-mode) | 173 | `-g, --greppable` | off | Prints only `ip -> [ports]`. No Nmap, no scripts, no warnings | 174 | `--accessible` | off | No banner, no colour, plain `Open ip:port` lines. Good for logs too | 175 | `--no-banner` | off | Hides the ASCII banner only | 176 | `-c, --config-path <file>` | `~/.rustscan.toml` | Read a different config | 177 | `-n, --no-config` | off | Ignore the config file completely | 178 | `--resolver <list\|file>` | system | DNS servers used **only if the system resolver fails** the name | 179 | `-- <args>` | | Appended to every script's command line, then re-parsed by `sh -c` | 180 | `RUST_LOG=info\|debug` (env var) | | `info` adds a timing summary. `debug` dumps merged options, batch size and socket errors | 181 182 > [!danger] **There is no `-q`.** Plenty of guides, including the previous version of this page, show `rustscan -q` for "ports only". 2.4.1 has no such flag: 183 > ```text 184 > error: unexpected argument '-q' found 185 > 186 > tip: to pass '-q' as a value, use '-- -q' 187 > ``` 188 > The flag you want is `-g`. 189 190 --- 191 192 ## Targets 193 194 ```bash 195 rustscan -a 10.10.11.5 196 rustscan -a 10.10.11.5,10.10.11.6,dc01.corp.htb 197 rustscan -a 10.10.110.0/24 -x 10.10.110.1,10.10.110.254 198 rustscan -a 2001:db8::15 # IPv6 works; the Nmap stage gets -6 automatically 199 rustscan -a scope.txt # one IP, CIDR or hostname per line 200 ``` 201 202 What bites: 203 204 - **A hostname becomes one address.** RustScan takes the first answer the system resolver gives 205 back. In the lab, `-a localhost` scanned `::1` only, so a service bound to `127.0.0.1` came back as 206 "no open ports". Dual-stack names and round-robin DNS hide hosts this way. Resolve them yourself 207 (`dig +short`, `getent ahosts`) and pass IPs. 208 - **The system resolver goes first.** Your `/etc/hosts` entries work, which is what you want for 209 `*.htb` names. `--resolver` is a fallback for names the system can't resolve, not an override. 210 - **A CIDR includes the network and broadcast addresses.** A `/30` is 4 targets, not 2. 211 - **Bad lines in a target file are skipped without a word.** A `# comment` or a typo just fails to 212 resolve. Only a top-level `-a` value that can't be resolved prints `Host "x" could not be resolved.` 213 If nothing resolves at all, you get `No IPs could be resolved, aborting scan.` and exit status 1. 214 - **Nmap is handed the IP, not the name.** HTTP scripts in the Nmap stage send the IP as the Host 215 header. On a vhost box that's useful, because `http-title` reports the redirect target 216 (`Did not follow redirect to http://box.htb/`) and gives you the name. Once you have it, point 217 web tools at the name. 218 - **There is no host discovery.** Every address gets every port. On a dead address, each probe burns 219 the full timeout, so a mostly-empty /24 is very slow (see the [runtime table](#the-only-formula-you-need)). 220 Find live hosts first: 221 222 ```bash 223 # Live hosts first. Unprivileged -sn only tries TCP 80/443, so run it as root, 224 # and add -PS22,445,3389 when Windows firewalls swallow ping. 225 sudo nmap -sn -PE -PS22,80,443,445,3389 10.10.110.0/24 -oG - | awk '/Up$/{print $2}' > live.txt 226 rustscan -a live.txt -u 5000 -g > open.txt 227 ``` 228 229 --- 230 231 ## Ports 232 233 ```bash 234 rustscan -a $IP # 1-65535 (the default range) 235 rustscan -a $IP -r 1-10000 236 rustscan -a $IP -p 22,80,443,445,3389,5985 237 rustscan -a $IP -e 21,23 # whatever the rules of engagement put off-limits 238 ``` 239 240 ### `--top` does nothing without a config 241 242 `--top` reads the top-1000 list from the `[ports]` table of your config file. Upstream ships that 243 table in its example `config.toml`. Homebrew, apt and cargo don't install it. With no table, `--top` 244 is ignored and you get the full range: 245 246 ```text 247 $ RUST_LOG=debug rustscan -a 127.0.0.1 --top -g # no ~/.rustscan.toml 248 Number of ports 65535 249 $ RUST_LOG=debug rustscan -a 127.0.0.1 --top -g # with the [ports] table installed 250 Number of ports 1000 251 ``` 252 253 Two ways to get a real top-N: 254 255 ```bash 256 # 1. Install upstream's [ports] table. If you already have a ~/.rustscan.toml, append instead of overwriting. 257 curl -fsSL https://raw.githubusercontent.com/bee-san/RustScan/2.4.1/config.toml \ 258 | sed -n '/^\[ports\]/,$p' > ~/.rustscan.toml 259 rustscan -a $IP --top 260 261 # 2. No config: build Nmap's current top-N from nmap-services and pass it with -p. 262 # Kali: /usr/share/nmap/nmap-services Homebrew: "$(brew --prefix)/share/nmap/nmap-services" 263 top=$(awk '$2 ~ /\/tcp$/ {print $3, $2}' /usr/share/nmap/nmap-services \ 264 | sort -rn | head -1000 | cut -d' ' -f2 | cut -d/ -f1 | paste -sd, -) 265 rustscan -a $IP -p "$top" 266 ``` 267 268 The `[ports]` table only ever applies when `--top` is passed. A normal run still scans the full 269 range, so it's safe to leave in place. 270 271 ### `--scan-order random` 272 273 This shuffles the port list (a shuffle for `-p` lists, a linear congruential sequence for ranges). 274 The set of ports found doesn't change, and the target still sees tens of thousands of connections in 275 seconds. It's not evasion. If you need to be gentle, lower `-b`. 276 277 --- 278 279 ## Speed, accuracy and the batch maths 280 281 ### The only formula you need 282 283 Each socket in the window either finishes quickly (open, or RST for closed) or burns its whole 284 timeout (filtered, or a dead host). In the worst case, where nothing answers: 285 286 ```text 287 runtime ≈ (hosts × ports × tries ÷ batch) × timeout 288 ``` 289 290 | Case (nothing answers) | Settings | Worst case | 291 |---|---|---| 292 | 1 host, all ports | `-b 4500 -t 1500` (defaults) | ~22 s | 293 | 1 host, all ports | `-b 4500 -t 1500 --tries 2` | ~44 s | 294 | 1 host, all ports | `-b 1000 -t 3000` | ~3.3 min | 295 | 1 host, all ports | `-b 500 -t 3000` | ~6.6 min | 296 | 1 host, all ports, stock macOS shell | batch silently cut to 128, `-t 1500` | ~12.8 min | 297 | /24, all ports | `-b 4500 -t 1500` | ~93 min | 298 299 The floor is set by RSTs. 65,535 ports on loopback took **2.5 s** in the lab at `-b 500`, `4500` 300 and `10000` alike, because every closed port refused instantly. A real target lands somewhere 301 between that floor and the table, depending on how much of it is filtered. 302 303 ### Why a big batch misses ports 304 305 RustScan sends one SYN per try and never retransmits. With thousands of handshakes in flight, 306 anything that drops packets under load loses some SYN-ACKs, and those ports quietly report as not 307 open: 308 309 - **your side:** VM NAT engines (VMware, VirtualBox), the VPN client (OpenVPN on HTB and OffSec labs), 310 home-router connection tables 311 - **their side:** SYN-flood protection, per-source rate limits, host firewalls that drop rather than 312 reject 313 314 The fix costs less than the problem: lower `-b`, raise `-t`, add `--tries 2`. A retry only costs 315 extra time on ports that don't answer. When it matters, run a gentle pass and diff it against the 316 fast one: 317 318 ```bash 319 fast=$(rustscan -a $IP -u 5000 -g | sed -E 's/.*\[(.*)\]/\1/' | tr , '\n' | sort -n) 320 slow=$(rustscan -a $IP -u 5000 -b 1000 -t 3000 --tries 2 -g | sed -E 's/.*\[(.*)\]/\1/' | tr , '\n' | sort -n) 321 diff <(echo "$fast") <(echo "$slow") && echo "same ports both runs" 322 ``` 323 324 ### How RustScan sizes the batch 325 326 Before the sweep starts, RustScan compares the batch you asked for (B, default 4500) with your soft 327 open-file limit (U, from `ulimit -Sn`, or the value you gave `-u`): 328 329 | Condition | Batch actually used | Seen in the lab | 330 |---|---|---| 331 | U ≥ B | B, unchanged | U=5000, B=4500 → 4500 | 332 | U < B and U < 3000 | U ÷ 2 | U=256 (macOS) → **128**; U=1024 (common on Kali) → **512** | 333 | U < B and 3000 ≤ U ≤ 8000 | U − 100 | U=4000, B=4500 → 3900 | 334 | U < B and U > 8000 | 3000 | U=10000, `-b 65535` → **3000** | 335 336 The last row is the trap. Asking for more than your limit when the limit is already high gets you 337 3000, not "as many as fit". If you want 9,900, ask for 9,900. 338 339 What a cut batch looks like: 340 341 ```text 342 $ ulimit -n 256; rustscan -a 127.0.0.1 -r 18000-18500 --no-banner --scripts none 343 [!] File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers 344 [!] Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'. 345 ``` 346 347 When your limit is comfortably above the batch, you get a hint instead, and it can be ignored: 348 `[~] File limit higher than batch size. Can increase speed by increasing batch size '-b 1048476'.` 349 350 ### Raising the limit 351 352 ```bash 353 ulimit -Sn; ulimit -Hn # soft and hard limits for this shell 354 rustscan -a $IP -u 5000 # per run: the simplest fix, no system changes 355 ulimit -n 5000 # per shell, if you'd rather set it once (put it in ~/.zshrc or ~/.bashrc) 356 ``` 357 358 `-u` sets both limits for RustScan and the Nmap it spawns. It works without root as long as the 359 value is at or below your hard limit. If the kernel refuses, the source prints 360 `[!] ERROR. Failed to set ulimit value.` and carries on with the old limit and a smaller batch. 361 362 ### Profiles 363 364 | Situation | Flags | Why | 365 |---|---|---| 366 | Loopback, or a lab VM on the same host | defaults + `-u 5000` | RSTs come back instantly | 367 | HTB / OffSec VPN, one box | `-u 5000`, then compare against a gentler pass if anything looks thin | The VPN is usually what drops packets | 368 | Windows box with the firewall on | `-u 5000 -b 1000 -t 3000 --tries 2 -- -Pn` | Filtered ports eat full timeouts, and drops are common | 369 | Through a ligolo-ng tunnel | `-b 200 -t 3000 --tries 2` | The tunnel's userland TCP stack is the bottleneck | 370 | Fragile or production kit (printers, OT, old appliances) | `-b 100 -t 3000` | Gentle, not stealthy. The tool's own help warns these may not cope | 371 | A subnet | host discovery first, then `-a live.txt` | Dead addresses cost full timeouts on every port | 372 373 --- 374 375 ## The Nmap hand-off 376 377 ### What actually runs 378 379 The built-in default script is one line: 380 381 ```text 382 nmap -vvv -p {{port}} -{{ipversion}} {{ip}} 383 ``` 384 385 Your `--` arguments are appended to the end, `{{port}}` becomes the comma-joined open ports, and 386 `{{ipversion}}` becomes `4` or `6`. Captured from the lab, with `-- -Pn -sV`: 387 388 ```text 389 Open 127.0.0.1:18021 390 Open 127.0.0.1:18022 391 Open 127.0.0.1:18080 392 Open 127.0.0.1:18445 393 [~] Starting Script(s) 394 [>] Running script "nmap -vvv -p {{port}} -{{ipversion}} {{ip}} -Pn -sV" on ip 127.0.0.1 395 Depending on the complexity of the script, results may take some time to appear. 396 [~] Starting Nmap 7.991 ( https://nmap.org ) at 2026-10-09 12:59 +0100 397 ... 398 PORT STATE SERVICE REASON VERSION 399 18021/tcp open ftp syn-ack 400 18022/tcp open ssh syn-ack (protocol 2.0) 401 ``` 402 403 The `Running script` line prints the template with its placeholders unfilled, and it only appears 404 when you passed `--` arguments. What follows from that one line: 405 406 - **`-vvv` is always on.** Only a custom script can remove it. 407 - **One Nmap per host, one after another**, and only after the whole sweep. A /24 with 40 live hosts 408 means 40 sequential Nmap runs. 409 - **Output is held until Nmap exits.** On a domain controller with `-sC`, that can be minutes of 410 silence. Use `-oA`, or run Nmap yourself (see [two-stage](#two-stage-when-you-want-control)). 411 - **Nmap's stderr is discarded, and a non-zero exit throws its stdout away too.** You get 412 `[!] Error Exit code = 1` and nothing else. 413 - **Nmap missing from PATH** shows up as `[!] Error Exit code = 127`. 414 415 ### `-Pn`, every time 416 417 Given a port list, Nmap still runs host discovery first. Unprivileged, that's a TCP connect to ports 418 80 and 443. As root, it's an ICMP echo, a SYN to 443, an ACK to 80 and an ICMP timestamp request. 419 If none of those get an answer, which is normal for a Windows box with its firewall on and no web 420 server, Nmap marks the host down and scans nothing, even though RustScan just found it open ports. 421 `-Pn` skips that check. RustScan has already done the proving. 422 423 ### Root-only Nmap flags fail quietly 424 425 RustScan runs Nmap as you. Tested as a normal user: 426 427 | `--` arguments | Result | 428 |---|---| 429 | `-sS`, `-O`, `-sU` | Nmap refuses to start. RustScan prints `[!] Error Exit code = 1` and that's all you get | 430 | `-A` | Runs, but unprivileged Nmap skips OS detection without any warning (it does the same when you run it directly) | 431 | `-sCV`, `--script ...`, `-oA` | Fine | 432 433 You can `sudo rustscan ...`, but then RustScan reads the config and scripts from root's home, and 434 your output files belong to root. The cleaner fix is to only give Nmap root: 435 [two-stage](#two-stage-when-you-want-control). 436 437 ### Quoting through `--` 438 439 RustScan joins your `--` arguments with spaces and hands the result to `sh -c`. The shell splits it 440 again, so any argument containing a space falls apart. This is a scope problem, not just a typo: 441 442 ```text 443 $ rustscan -a 127.0.0.1 -p 18080 --accessible -- -Pn --script 'banner or http-title' 444 Running script "nmap -vvv -p {{port}} -{{ipversion}} {{ip}} -Pn --script banner or http-title" on ip 127.0.0.1 445 NSE: Loaded 1 scripts for scanning. 446 Performing system-dns for 2 domain names that were deferred 447 Nmap scan report for localhost (127.0.0.1) 448 Nmap scan report for or (0.0.0.0) 449 ``` 450 451 Nmap ran one script, then treated `or` and `http-title` as extra **targets** and resolved them. 452 With a DNS search domain configured, a word like that can resolve to a real host you were never 453 authorised to scan. Wrap the inner value a second time: 454 455 ```bash 456 rustscan -a $IP -- -Pn --script "'banner or http-title'" 457 rustscan -a $IP -- -Pn --script "'(default or vuln) and not intrusive'" 458 ``` 459 460 The same goes for `--script-args` values with spaces in them. Don't pass `-p` after `--` either: 461 the hand-off already sets `-p`, and Nmap quits with `Only 1 -p option allowed`. The same `sh -c` also expands `$(...)`, backticks, `;`, `|` and globs inside your `--` arguments. 462 Treat everything after `--` as a shell command line. If an Nmap argument needs quotes, it's usually 463 easier to run Nmap yourself. 464 465 ### Two-stage: when you want control 466 467 ```bash 468 mkdir -p recon 469 ports=$(rustscan -n -a $IP -u 5000 -g | sed -E 's/.*\[(.*)\]/\1/') 470 echo "$ports" 471 sudo nmap -Pn -sS -sCV -O -p "$ports" -oA recon/$NAME $IP 472 ``` 473 474 You get live Nmap output, root-only flags, a real exit code, normal quoting, and `-n` keeps a stray 475 config from changing the sweep. Worth wrapping in a function (tested in bash and zsh): 476 477 ```bash 478 # rs <ip> [name]: every TCP port with RustScan, then a live nmap -sCV on exactly those ports. 479 rs() { 480 local ip=$1 name=${2:-$1} ports v= 481 case $ip in *:*) v=-6 ;; esac # Nmap needs -6 for IPv6 targets 482 mkdir -p recon 483 ports=$(rustscan -n -a "$ip" -u 5000 -g | sed -E 's/.*\[(.*)\]/\1/' | tr , '\n' | sort -n | paste -sd, -) 484 [ -n "$ports" ] || { echo "rs: no open TCP ports on $ip" >&2; return 1; } 485 echo "rs: $ip -> $ports" 486 nmap $v -Pn -sCV -p "$ports" -oA "recon/$name" "$ip" 487 } 488 ``` 489 490 --- 491 492 ## Output, parsing and logging 493 494 ### Normal output 495 496 ```text 497 [~] File limit higher than batch size. Can increase speed by increasing batch size '-b 1048476'. 498 Open 127.0.0.1:18021 499 Open 127.0.0.1:18022 500 [~] Starting Script(s) 501 [>] Running script "nmap -vvv -p {{port}} -{{ipversion}} {{ip}} -Pn" on ip 127.0.0.1 502 ``` 503 504 `[~]` is information, `[>]` is an action, `[!]` is a warning. `Open ip:port` lines print live as 505 ports answer, so you can start reading the attack surface before the sweep finishes. The banner (and 506 its random quote) also prints the config path it looked for: 507 `[~] The config file is expected to be at "/home/kali/.rustscan.toml"`. 508 509 ### Greppable rules 510 511 - One line per host **that had open ports**: `10.10.11.5 -> [22,80,443]`. Hosts with nothing open 512 print nothing. 513 - With `-g`, no Nmap or scripts run, even if you passed `--` arguments. 514 - Ports are listed in the order they answered, and hosts in no particular order. Sort before you diff. 515 - Warnings are suppressed too. An empty result can mean "nothing open" or "something's wrong". When in 516 doubt, run once without `-g`. 517 518 ```bash 519 # Comma list of ports for one host (portable: BSD and GNU sed, no grep -P) 520 rustscan -a $IP -g | sed -E 's/.*\[(.*)\]/\1/' 521 522 # Sorted, one per line 523 rustscan -a $IP -g | sed -E 's/.*\[(.*)\]/\1/' | tr , '\n' | sort -n 524 525 # Many hosts: save the sweep, then run Nmap per host, 4 at a time 526 mkdir -p recon 527 rustscan -a live.txt -u 5000 -g > open.txt 528 sed -E 's/^(.*) -> \[(.*)\]$/\1 \2/' open.txt \ 529 | xargs -P 4 -n 2 sh -c 'case $0 in *:*) v=-6 ;; *) v= ;; esac; nmap $v -Pn -sCV -p "$1" -oA "recon/$0" "$0"' 530 ``` 531 532 ### Exit codes 533 534 | Situation | Exit status | 535 |---|---| 536 | Ports found, Nmap succeeded | 0 | 537 | No open ports at all | 0 | 538 | Nmap failed (root-only flag, bad argument, missing binary) | 0 | 539 | TOML error in the config | 1 | 540 | No target could be resolved | 1 | 541 | `--scripts custom` without `~/.rustscan_scripts.toml` | 1 | 542 543 So `rustscan ... && next-step` tells you nothing about the scan. Test the output instead, as `rs` 544 does above. 545 546 ### Logging 547 548 RustScan has no `-v`. Verbosity comes from the `RUST_LOG` environment variable: 549 550 ```bash 551 RUST_LOG=info rustscan -a $IP --scripts none # adds a timing summary 552 RUST_LOG=debug rustscan -a $IP -p 22 -g 2>&1 | less # everything below 553 ``` 554 555 ```text 556 RustScan Benchmark Summary 557 Portscan | 2.475484 s 558 Scripts | 0.000007959s 559 RustScan | 2.5862627 s 560 ``` 561 562 `debug` is the fastest way to answer "why did it do that?": 563 564 - `Main() opts arguments are Opts { ... }` shows the final settings after the config merge. Check it 565 first whenever a flag seems to be ignored. 566 - `Batch size 128` shows the batch actually used, after the ulimit check. 567 - `Typical socket connection errors {"Connection refused (os error 61) ::1"}` lists the distinct 568 errors from the sweep. Refused means closed. Timeouts mean filtered or dead. Errors like 569 "Network is unreachable" mean the problem is your routing or VPN, not the target. 570 571 --- 572 573 ## UDP mode 574 575 `--udp` connects a UDP socket to each port, sends one probe per try, and waits `-t` for any reply. For 576 well-known services (DNS, NTP, SNMP, NetBIOS name service, IKE, IPMI, SSDP, mDNS, memcached, CLDAP and 577 others, 63 entries in total) the probe is Nmap's own payload from `nmap-payloads`, compiled into the 578 binary. Every other port gets an empty datagram. 579 580 - **A reply means open.** An ICMP port-unreachable means closed. **Silence isn't reported at all**, so 581 open services that ignore an empty or unexpected probe simply vanish. 582 - **It's slow and lossy over big ranges.** Linux rate-limits ICMP errors by default, so most closed 583 ports look silent and burn the full timeout. Keep `--udp` to short lists. 584 - **The default Nmap stage scans those port numbers over TCP**, not UDP. From the lab: 585 586 ```text 587 $ rustscan --udp -a 127.0.0.1 -p 15353 --accessible -- -Pn 588 Open 127.0.0.1:15353 589 ... 590 PORT STATE SERVICE REASON 591 15353/tcp closed unknown conn-refused 592 ``` 593 594 `-- -sU` fixes that, but needs root. The clean pattern is to find candidates with RustScan, then 595 version them with a root Nmap: 596 597 ```bash 598 # Ports with built-in probes that matter on HTB / CPTS style boxes 599 udp=$(rustscan -n -a $IP --udp -p 53,69,111,123,137,161,389,500,623,1900,5353,11211 \ 600 -t 2000 --tries 2 -g | sed -E 's/.*\[(.*)\]/\1/') 601 [ -n "$udp" ] && sudo nmap -Pn -sU -sV -p "$udp" $IP 602 603 # The broader alternative: let Nmap do UDP properly 604 sudo nmap -Pn -sU --top-ports 100 -sV --version-intensity 0 $IP 605 ``` 606 607 --- 608 609 ## The config file 610 611 RustScan reads `~/.rustscan.toml` unless you pass `-c <file>` (use another file) or `-n` (use none). 612 If the file exists but doesn't parse, the run stops with `Found <error> in configuration file. 613 Aborting scan.` and exit status 1. 614 615 ### The file overrides your command line 616 617 Most guides present the config as defaults, but it doesn't behave like defaults. Any key it sets 618 **replaces** the matching command-line flag. Tested with a config that set 619 `addresses = ["127.0.0.1"]`, `batch_size = 50`, `timeout = 900`, `greppable = false` and 620 `command = ["-sV"]`: 621 622 ```text 623 $ RUST_LOG=debug rustscan -c cfg.toml -a 127.0.0.2 -b 2000 -g ... 624 addresses: ["127.0.0.1"] batch_size: 50 timeout: 900 greppable: false command: ["-sV"] 625 626 $ RUST_LOG=debug rustscan -n -c cfg.toml -a 127.0.0.2 -b 2000 -g ... 627 addresses: ["127.0.0.2"] batch_size: 2000 greppable: true command: [] 628 ``` 629 630 With that file in place, every scan goes to 127.0.0.1 whatever you type after `-a`, ignores `-b`, 631 and ignores your `--` arguments. Configs copied from older guides (this page's previous version 632 included) set `addresses = ["127.0.0.1"]` for exactly this reason. Delete that line. 633 634 | Key | Type | Notes | 635 |---|---|---| 636 | `addresses` | array of strings | Replaces `-a`. **Never put this in a config** | 637 | `batch_size`, `timeout`, `tries` | integers | Replace `-b`, `-t`, `--tries` | 638 | `ulimit` | integer | Replaces `-u`. The one key worth setting | 639 | `greppable`, `accessible`, `udp` | booleans | Replace `-g`, `--accessible`, `--udp`. `greppable = false` beats `-g` | 640 | `scan_order` | `"Serial"` or `"Random"` | **Capitalised.** `"random"` is a parse error that aborts every scan | 641 | `scripts` | `"None"`, `"Default"` or `"Custom"` | **Capitalised**, same rule | 642 | `command` | array of strings | Replaces your `--` arguments entirely | 643 | `range` | `{ start = 1, end = 1000 }` | Replaces `-r` (`-p` still wins) | 644 | `exclude_ports`, `exclude_addresses` | arrays | Replace `-e`, `-x` | 645 | `resolver` | string | Replaces `--resolver` | 646 | `[ports]` table | `80 = 1` lines | Only read by `--top`; doesn't affect normal scans | 647 648 Keys you don't set leave the command line alone. Unknown keys are ignored, so the `ip = "127.0.0.1"` 649 line at the top of upstream's example file does nothing. 650 651 ### A config that helps instead of hurting 652 653 ```toml 654 # ~/.rustscan.toml: only settings you'd never want to change per scan 655 ulimit = 5000 656 657 # Paste upstream's [ports] table below this line if you want --top to work (see Ports). 658 ``` 659 660 Everything that changes per target (`-b`, `-t`, `--tries`, the Nmap arguments) belongs on the command 661 line or in a shell function like `rs`, where you can see it. 662 663 --- 664 665 ## The scripting engine 666 667 `--scripts custom` swaps the built-in Nmap call for your own scripts. It's powerful, but the parser 668 is strict, and almost every mistake fails silently. Everything below was tested by planting scripts 669 in a sandboxed home directory. 670 671 ### Where things live 672 673 | Path | Purpose | 674 |---|---| 675 | `~/.rustscan_scripts.toml` | **Required.** Holds `tags = [...]`. If it's missing: `[!] Initiating scripts failed!` and exit status 1 | 676 | `~/.rustscan_scripts/` | Script files. Every file directly inside is parsed, subdirectories are not | 677 678 Custom mode **replaces** the default Nmap run. If you still want Nmap, ship it as a script (example 1 679 below). 680 681 ### Which scripts get picked 682 683 A script runs only if **every tag in the script also appears in `~/.rustscan_scripts.toml`**. The 684 config is an allow-list. A script tagged `["htb", "web"]` doesn't run when the config says 685 `tags = ["htb"]`, and it does once the config says `tags = ["htb", "web"]`. Upstream's docs describe 686 the opposite rule. The code does this one. 687 688 ### The header format 689 690 ```bash 691 #!/usr/bin/env bash 692 #tags = ["htb"] 693 #developer = ["you"] 694 #ports_separator = " " 695 #call_format = "bash {{script}} {{ip}} {{port}}" 696 697 echo "the script body starts after the first line that isn't a # line" 698 ``` 699 700 How RustScan reads it: 701 702 1. **Line 1 is skipped** (the shebang, or anything else). 703 2. **From line 2, every consecutive line starting with `#` is collected.** All `#` characters are 704 stripped out of it and the result is parsed as TOML. 705 3. **The block ends at the first line that doesn't start with `#`.** Normally that's a blank line. 706 707 What goes wrong: 708 709 - **A normal comment directly under the header kills the script.** `# enumerate SMB` becomes the TOML 710 line `enumerate SMB`, the parse fails, and the script is dropped with no message (visible only with 711 `RUST_LOG=debug`). Always leave a blank line after the header. 712 - **`#` inside `call_format` is deleted**, because every `#` on the line goes. 713 - **`tags` must be an array.** `#tags = "htb"` fails to parse and the script is dropped. 714 715 | Field | Effect | 716 |---|---| 717 | `tags` | Matched against the config's allow-list. A script without tags never runs | 718 | `call_format` | The command, run with `sh -c` after the placeholders are filled. Without it: `Error Failed to parse execution format.` | 719 | `ports_separator` | Joins the open ports for `{{port}}`. Default `,`. Use `" "` to get them as separate arguments | 720 | `port` | **Replaces `{{port}}` with this literal value for every host that had any open port.** It doesn't check that the port was open | 721 | `developer` | Parsed and ignored | 722 | `trigger_port` | **Not a field in 2.4.1.** It's silently ignored and the script receives every open port. Upstream's docs and example scripts still use it | 723 724 Placeholders: `{{script}}` (the script's full path), `{{ip}}`, `{{port}}` (the joined open ports, or 725 the `port` value), `{{ipversion}}` (`4` or `6`). 726 727 ### How scripts run 728 729 - **One at a time, per host, after the sweep.** The order is directory order, not alphabetical, so 730 numbering files `10-`, `20-` doesn't sequence them. 731 - **`--` arguments are appended to every script's command line.** 732 - **Only stdout is shown, and only when the script exits.** stderr is discarded. Add `2>&1` inside 733 the script if you want it. 734 - **A non-zero exit throws the output away** and prints `[!] Error Exit code = N`. End scripts with 735 `exit 0` unless you really want that. 736 737 ### Three scripts that work 738 739 All three were run together against the lab listeners. 740 741 **1. Nmap with your defaults, saved per host.** This replaces the built-in call. 742 743 ```text 744 #!nmap 745 #tags = ["htb"] 746 #ports_separator = "," 747 #call_format = "nmap -vvv -Pn -sCV -p {{port}} -{{ipversion}} -oA recon/rs_{{ip}} {{ip}}" 748 ``` 749 750 Save it as `~/.rustscan_scripts/10-nmap.txt`. It needs no interpreter, because `call_format` calls 751 `nmap` directly. Run it from a directory that contains `recon/`. 752 753 **2. Find every web service, whatever the port.** Since `port` isn't a trigger, the script tests 754 each open port itself: 755 756 ```bash 757 #!/usr/bin/env bash 758 #tags = ["htb"] 759 #ports_separator = " " 760 #call_format = "bash {{script}} {{ip}} {{port}}" 761 762 # Every open port gets one HTTP and one HTTPS probe; anything that answers is a web service. 763 ip=$1; shift 764 for p in "$@"; do 765 for scheme in http https; do 766 out=$(curl -sk -m 4 -o /dev/null -w '%{http_code} %{redirect_url}' "$scheme://$ip:$p/" 2>/dev/null) 767 case $out in 768 000*) ;; # nothing spoke HTTP here 769 *) echo "[web] $scheme://$ip:$p -> $out" ;; 770 esac 771 done 772 done 773 exit 0 774 ``` 775 776 The redirect URL in the output is often the vhost you need for `/etc/hosts`. 777 778 **3. Gate on a port yourself.** This is how to get the "only if 445 is open" behaviour people expect 779 from `trigger_port`: 780 781 ```bash 782 #!/usr/bin/env bash 783 #tags = ["htb"] 784 #ports_separator = " " 785 #call_format = "bash {{script}} {{ip}} {{port}}" 786 787 # RustScan's `port` field is not a trigger, so the script gates itself. 788 ip=$1; shift 789 case " $* " in 790 *" 445 "*) nxc smb "$ip" -u '' -p '' --shares 2>&1; nxc smb "$ip" -u guest -p '' --shares 2>&1 ;; 791 *) echo "[smb] 445 closed on $ip, skipping" ;; 792 esac 793 exit 0 794 ``` 795 796 ```bash 797 printf 'tags = ["htb"]\n' > ~/.rustscan_scripts.toml 798 mkdir -p recon && rustscan -a $IP -u 5000 --scripts custom 799 ``` 800 801 See [NetExec](/sheets/active-directory/netexec) for what to do with the SMB result. 802 803 --- 804 805 ## Workflows 806 807 ### One HTB / CPTS box 808 809 ```bash 810 export IP=10.10.11.5 NAME=box 811 mkdir -p recon 812 813 # 1. Fast full sweep with versions and default scripts on the hits 814 rustscan -a $IP -u 5000 -- -Pn -sCV -oA recon/$NAME 815 816 # 2. If it's a Windows box, or anything looks thin, sweep again gently and compare 817 rustscan -a $IP -u 5000 -b 1000 -t 3000 --tries 2 -g 818 819 # 3. Quick UDP pass on the ports that matter (the Nmap half needs root) 820 udp=$(rustscan -n -a $IP --udp -p 53,69,111,123,137,161,389,500,623,1900,5353,11211 -t 2000 --tries 2 -g | sed -E 's/.*\[(.*)\]/\1/') 821 [ -n "$udp" ] && sudo nmap -Pn -sU -sV -p "$udp" -oA recon/${NAME}_udp $IP 822 ``` 823 824 Next steps by port are in [Service Enumeration](/sheets/pentest-workflow/service-enumeration) and 825 [Common Ports and Services](/sheets/enumeration/common-ports-and-services). This scan is stage 1 of 826 [Recon and Host Discovery](/sheets/pentest-workflow/recon-and-host-discovery). 827 828 ### A Windows domain controller 829 830 A DC typically answers on 53, 88, 135, 139, 389, 445, 464, 593, 636, 3268, 3269, 5985 and 9389, plus a 831 run of RPC ports in the 49152-65535 range. That's 20-30 ports, and `-sC` against all of them can take 832 minutes, during which RustScan shows nothing because Nmap's output is held back. Use the two-stage 833 form, or `rs`, so you can watch Nmap work: 834 835 ```bash 836 rs $IP dc01 837 ``` 838 839 ### An internal subnet 840 841 ```bash 842 mkdir -p recon 843 sudo nmap -sn -PE -PS22,80,443,445,3389 10.10.110.0/24 -oG - | awk '/Up$/{print $2}' > live.txt 844 rustscan -a live.txt -u 5000 -g > open.txt 845 sed -E 's/^(.*) -> \[(.*)\]$/\1 \2/' open.txt \ 846 | xargs -P 4 -n 2 sh -c 'case $0 in *:*) v=-6 ;; *) v= ;; esac; nmap $v -Pn -sCV -p "$1" -oA "recon/$0" "$0"' 847 ``` 848 849 Host discovery first, because RustScan has none. Then one sweep across every live host, then Nmap 850 in parallel instead of RustScan's one-host-at-a-time default. 851 852 ### Through a pivot 853 854 - **ligolo-ng, or any tun-style pivot:** RustScan works unchanged, because to it the internal network 855 is just a route. The tunnel's userland TCP stack is the bottleneck, so drop `-b` to a few hundred 856 and raise `-t` (see [ligolo-ng](/sheets/tunneling-pivoting/ligolo-ng)). 857 - **A SOCKS proxy (chisel, `ssh -D`) with proxychains:** RustScan has no proxy support, and thousands 858 of concurrent connects through one SOCKS proxy are slow and unreliable. Scan a short port list with 859 `proxychains -q nmap -sT -Pn -n` instead. 860 861 --- 862 863 ## Troubleshooting by symptom 864 865 | You see | Cause | Fix | 866 |---|---|---| 867 | `Looks like I didn't find any open ports for X. This is usually caused by a high batch size.` | Really nothing open, packet loss, a dead host, or the wrong address (a hostname that resolved to `::1`) | Pass an IP, try `-b 1000 -t 3000 --tries 2`, check `RUST_LOG=debug` socket errors | 868 | Different ports on each run | Drops under load: VPN, VM NAT, rate limits | Lower `-b`, raise `-t`, `--tries 2`, diff two runs | 869 | `[!] Your file limit is very small...` | Soft ulimit under 3000, batch halved | `-u 5000` | 870 | `-b 65535` scans slower than expected | Limit above 8000 but below B, so the batch dropped to 3000 | Ask for a B below your limit, for example `-b 9900` | 871 | `[!] Error Exit code = 1` after `Running script` | Nmap refused: root-only flag (`-sS`, `-O`, `-sU`), bad argument, or the `-oA` directory is missing | Two-stage with `sudo nmap`, `mkdir -p recon` | 872 | `[!] Error Exit code = 127` | `nmap` isn't on PATH | Install Nmap, or check PATH under sudo | 873 | Nmap says the host seems down, or `0 hosts up` | Nmap's own discovery failed | `-- -Pn` | 874 | Nmap scanned hosts called `or`, `and`, `not` | A space in an `--` argument got split | `-- --script "'a or b'"` | 875 | Your `-a`, `-b` or `--` args are ignored | The config file overrides them | `-n`, then fix `~/.rustscan.toml` | 876 | `Found TOML parse error at line 1, column 14` / `Aborting scan.` | Lowercase `scan_order` or `scripts` value, or other bad TOML | `"Random"`, `"Custom"` (capitalised) | 877 | `--top` takes as long as a full scan | No `[ports]` table in the config | See [`--top`](#--top-does-nothing-without-a-config) | 878 | UDP hit, but Nmap shows `/tcp closed` | The default Nmap stage scans TCP | `sudo nmap -sU` on the found ports | 879 | `Initiating scripts failed! No such file or directory (os error 2)` | `--scripts custom` without `~/.rustscan_scripts.toml` | Create it with a `tags` array | 880 | A custom script never runs | Tags not all allowed, a comment in the header block, or `tags` not an array | Fix the header, check `RUST_LOG=debug` | 881 | A port-specific script runs on every host | `port` overrides `{{port}}`, it isn't a trigger, and `trigger_port` doesn't exist | Gate inside the script (example 3) | 882 | A custom script's output vanished | Non-zero exit, or it wrote to stderr | `exit 0`, `2>&1` | 883 | A panic: `Too many open files. Please reduce batch size...` | Batch above what the OS actually allows | Lower `-b`, raise `-u` | 884 885 --- 886 887 ## Noise, safety and scope 888 889 - **Nothing about RustScan is quiet.** Every open port gets a complete TCP connection that the service 890 accepts and then sees closed. SSH daemons log it, many web servers log it, and any IDS sees tens of 891 thousands of connections from one source in seconds. Random order and a small batch only make that 892 take longer. 893 - **Fragile targets can fall over.** RustScan's own help text warns that a server may not handle this 894 many simultaneous connections. Printers, embedded and OT devices, old appliances and small VPSs get 895 `-b 100` or less. 896 - **Scope:** use `-x` for out-of-scope addresses inside an in-scope range. A CIDR includes the network 897 and broadcast addresses. A hostname resolves to one address, which for a CDN-fronted name may not 898 be the client's. And an unquoted `--` argument can add targets of its own. 899 900 --- 901 902 ## RustScan vs the alternatives 903 904 | | RustScan | `nmap -p- --min-rate` | masscan | naabu | 905 |---|---|---|---|---| 906 | How it scans | async `connect()` window | SYN as root (connect otherwise), with retransmits and adaptive timing | raw SYN from its own TCP stack, at a fixed packet rate | SYN as root, or connect | 907 | Root needed | no | for SYN | yes | for SYN | 908 | Host discovery | no | yes | no | yes | 909 | Reports closed ports | no | yes, and filtered separately | only with `--show closed` | no | 910 | Nmap hand-off | built in | not needed | manual | built in (`-nmap-cli`) | 911 | Best at | one box, every port, fast, no root | accuracy, the final word on a port | very large ranges | ProjectDiscovery pipelines | 912 913 Whatever finds the ports, Nmap owns the service and version truth. The plain-Nmap two-stage pattern 914 is in [Recon and Host Discovery](/sheets/pentest-workflow/recon-and-host-discovery), Nmap itself is in 915 [Nmap](/sheets/enumeration/nmap) and [NSE Guide](/sheets/enumeration/nse-guide), and parsing Nmap 916 output is in [Awesome Nmap Grep](/sheets/enumeration/awesome-nmap-grep). 917 918 --- 919 920 ## Quick reference 921 922 ```bash 923 rustscan -a $IP -u 5000 -- -Pn -sCV -oA recon/$NAME # the default first scan 924 rustscan -a $IP -u 5000 -g # ports only: ip -> [p,p,p] 925 rustscan -a $IP -u 5000 -b 1000 -t 3000 --tries 2 -- -Pn -sCV # lossy link or Windows firewall 926 rustscan -a $IP -p 22,80,443,445 # specific ports (no ranges in -p) 927 rustscan -a $IP -r 1-10000 # a range 928 rustscan -a $IP -e 21,23 # exclude ports 929 rustscan -a 10.10.110.0/24 -x 10.10.110.1 # subnet minus an address 930 rustscan -a live.txt -g > open.txt # file of targets 931 rustscan -n -a $IP -g # ignore ~/.rustscan.toml 932 rustscan -a $IP -- -Pn --script "'default or vuln'" # NSE expressions: double-quote them 933 rustscan -n -a $IP --udp -p 53,123,161,500 -t 2000 --tries 2 -g # UDP candidates (then sudo nmap -sU) 934 rustscan -a $IP --scripts custom # run ~/.rustscan_scripts/* 935 RUST_LOG=debug rustscan -a $IP -p 22 -g # see merged options, batch, errors 936 ```