daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

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 ```