# RustScan Administrator & Developer Guide

This guide is the source-based reference for the **architecture, operational posture, security boundaries, configuration surface, and developer internals** of RustScan (crate `rustscan`, version **2.4.1**, GPL-3.0-only, Rust edition 2018). It is written for the people who deploy RustScan in environments other than "one analyst on a laptop":

- Security engineers wiring RustScan into pipelines and SIEMs.
- Pentest-platform admins who containerize it for a team.
- Rust developers building on top of the public `rustscan` library API.
- Anyone who needs to know *exactly* what RustScan does to a network before approving it for production use.

Refer to the User Guide for everyday CLI usage; this document does not duplicate that material.

---

## 1. Architecture Overview

RustScan is a single-binary CLI that performs three things in sequence:

1. **Resolve** the user's target list (IPs, CIDRs, hostnames, files) into a deduplicated `Vec<IpAddr>`.
2. **Generate** a port set from one of three strategies (Manual list, Serial range, Random range), then schedule async TCP `connect()` (or UDP) probes through a fixed-size pool.
3. **Hand off** the open-port set to a downstream "script" — by default, an Nmap invocation; optionally a custom shell command parameterised on the discovered IP/port pairs.

The compiled binary is statically linked, ships with an embedded UDP-payload database (built at compile time from `nmap-payloads`), and depends only on standard async I/O — no privileged operations are required for the default TCP connect mode.

### 1.1 Component diagram

```text
                           rustscan (single binary)
                                 │
        ┌────────────────────────┼────────────────────────┐
        │                        │                        │
   ┌────▼────┐            ┌──────▼──────┐          ┌──────▼──────┐
   │  Opts   │            │   Config    │          │  Scripts    │
   │ (CLAP)  │── merge ──▶│ (~/.toml)   │          │  registry   │
   └────┬────┘            └─────────────┘          └──────┬──────┘
        │                                                 │
        │                                                 │
        ▼                                                 │
  ┌─────────────┐    ┌─────────────────────┐              │
  │ address.rs  │───▶│  Vec<IpAddr>        │              │
  │ parse_*     │    │  (deduped, excluded)│              │
  └─────────────┘    └──────────┬──────────┘              │
                                │                         │
                     ┌──────────▼──────────┐              │
                     │  PortStrategy       │              │
                     │  Manual / Serial /  │              │
                     │  Random (LCG)       │              │
                     └──────────┬──────────┘              │
                                │                         │
                     ┌──────────▼──────────┐              │
                     │  SocketIterator     │              │
                     │  IP × Port product  │              │
                     └──────────┬──────────┘              │
                                │                         │
                     ┌──────────▼──────────┐              │
                     │  Scanner            │              │
                     │  FuturesUnordered   │              │
                     │  pool (batch_size)  │              │
                     │  TCP connect / UDP  │              │
                     └──────────┬──────────┘              │
                                │                         │
                     ┌──────────▼──────────┐    ┌─────────▼───────────┐
                     │ Vec<SocketAddr>     │───▶│ Default script      │
                     │ (open sockets)      │    │ → nmap -Pn -vvv -p X│
                     └─────────────────────┘    │ Custom script       │
                                                │ → user TOML defs    │
                                                │ None → just print   │
                                                └─────────────────────┘
```

> [!NOTE]
> The pool is asynchronous but not multithreaded by default. Concurrency is via `FuturesUnordered` over `async-std`'s single-runtime; the kernel does the heavy lifting through non-blocking sockets. That's why batch size is a function of file descriptors, not CPU cores.

### 1.2 Component inventory

| Component | Path | Role |
|---|---|---|
| Entry point | `src/main.rs` | `Opts::read()` → `Config::read()` → merge → `Scanner::new()` → `block_on(scanner.run())` → script handoff. |
| Address parser | `src/address.rs` | `parse_addresses` / `parse_address` / `parse_excluded_networks`; DNS via `hickory-resolver` with Cloudflare-DoT fallback. |
| Input model | `src/input.rs` | `Opts` (CLAP-derived), `Config` (TOML-deserialized), `ScanOrder`, `ScriptsRequired`, `PortRange`. |
| Port strategy | `src/port_strategy/mod.rs` | `PortStrategy::pick()` returns `Manual` / `Serial` / `Random`. |
| LCG iterator | `src/port_strategy/range_iterator.rs` | `RangeIterator` — Linear Congruential Generator with coprime step. |
| Scanner core | `src/scanner/mod.rs` | `Scanner::new`, `Scanner::run`, `scan_socket`, `scan_udp_socket`, `connect`, `udp_bind`, `udp_scan`, `fmt_ports`. |
| Socket iterator | `src/scanner/socket_iterator.rs` | Cartesian product over `(ports, ips)` with the inner loop on IPs. |
| Scripts | `src/scripts/mod.rs` | `init_scripts`, `Script::build`, `Script::run`, `ScriptFile`. |
| TUI macros | `src/tui.rs` | `warning!`, `detail!`, `output!`, `funny_opening!` — all greppable/accessible-aware. |
| Generated payloads | `src/generated.rs` (built) | Compile-time UDP payload `BTreeMap<Vec<u16>, Vec<u8>>`. |
| Build script | `build.rs` | Parses `nmap-payloads` and writes `src/generated.rs`. |
| Embedded top-1000 | `config.toml` | The default `[ports]` table activated by `--top`. |
| Bench harness | `benches/benchmark_portscan.rs` | Criterion harness; not shipped with the binary. |

### 1.3 Technology stack

| Layer | Choice | Why |
|---|---|---|
| Async runtime | `async-std 1.13.x`, `futures 0.3` | `block_on` + `FuturesUnordered` give a fixed-size, runtime-cheap concurrency pool. |
| CLI parsing | `clap 4.6.x` (derive + `wrap_help`) | Declarative; the `Opts` struct *is* the help screen. |
| TOML config | `toml 1.1.x` + `serde 1.0.x` (derive) | Strict deserialization — typos abort the run instead of being ignored. |
| DNS | `hickory-resolver 0.24.x` with `dns-over-rustls` | System-resolver first, Cloudflare-DoT fallback, custom resolver via `--resolver`. |
| File-descriptor mgmt | `rlimit 0.11.x` (Unix only) | `setrlimit(RLIMIT_NOFILE)` for `--ulimit`, soft-limit query for auto-tuning. |
| RNG | `rand 0.10.x` | LCG seeding, port shuffling, funny-quote selection. |
| Output color | `colored 3.1.x`, `colorful 0.3.x`, `ansi_term 0.12.x`, `anstream =1.0.0` | Cross-platform ANSI, including Windows post `enable_ansi_support()`. |
| CIDR math | `cidr-utils 0.6.x` | CIDR expansion and exclusion. |
| Iterator product | `itertools 0.14.x` | `iproduct!` macro powers `SocketIterator`. |
| GCD | `gcd 2.0.x` | LCG coprime step search. |
| Templating | `text_placeholder 0.5.x` | `{{ip}}`/`{{port}}` substitution in custom-script `call_format`. |
| Logging | `log 0.4.x` + `env_logger 0.11.x` | `RUST_LOG=debug` toggles per-socket diagnostics. |

### 1.4 Data flow for a single TCP scan

```text
$ rustscan -a 10.0.0.0/24 -p 80,443 -- -A
       │
       │  (1) Opts::read()  → CLAP parses argv
       │  (2) Config::read() → TOML at $XDG_CONFIG_HOME/.rustscan.toml
       │  (3) opts.merge(&config) → CLI wins, missing fields filled from TOML
       ▼
   parse_addresses(&opts)
       ├─▶ each value: IPAddr::from_str → IpInet::from_str → DNS → file fallback
       ├─▶ exclude_addresses: same parsing, kept as Vec<IpCidr>
       └─▶ dedup via BTreeSet, drop excluded
       ▼
   PortStrategy::pick(range, ports, scan_order)
       ├─▶ Manual(ports) if explicit -p
       ├─▶ Serial(SerialRange) if -r and Serial order
       └─▶ Random(RandomRange{LCG}) if -r and Random order
       ▼
   Scanner::new(...)
       ├─▶ batch_size auto-tuned via infer_batch_size(opts, ulimit) on Unix
       └─▶ tries clamped to NonZeroU8 (0 → 1)
       ▼
   block_on(scanner.run())
       ├─▶ ports = strategy.order().filter(|p| !exclude_ports.contains(p))
       ├─▶ SocketIterator::new(&ips, &ports)  (port outer loop, IP inner)
       ├─▶ FuturesUnordered with batch_size in-flight
       ├─▶ each future: connect(socket, timeout=opts.timeout) × tries
       └─▶ panics if "too many open files" hits the assert
       ▼
   Vec<SocketAddr> open_sockets
       │
       │  group by IP → HashMap<IpAddr, Vec<u16>>
       ▼
   For each IP:
       ├─▶ if greppable: println!("{ip} -> [{ports}]") and continue
       ├─▶ if scripts == None: same as greppable
       └─▶ for each ScriptFile in scripts_to_run:
                 Script::build → Script::run
                 (default = nmap -Pn -vvv -p $PORTS [user args])
       ▼
   benchmarks.summary()  printed with RUST_LOG=info
```

---

## 2. System Requirements

### 2.1 Compile-time

| Item | Minimum | Recommended |
|---|---|---|
| Rust toolchain | edition 2018 with crates.io access | Latest stable (`rustup default stable`) |
| Cargo | 1.x | Pinned via `rust-toolchain.toml` if you need reproducible builds |
| `nmap-payloads` file | Present at repo root for `build.rs` | Track upstream Nmap releases |
| Disk for build | ~250 MB target dir | 1 GB if you also build benches |

### 2.2 Run-time

| Item | Minimum | Recommended | Notes |
|---|---|---|---|
| OS | Linux x86_64 / macOS / Windows / BSD | Linux x86_64 | Windows skips ulimit auto-tuning; uses `AVERAGE_BATCH_SIZE = 3000` constant. |
| Kernel | Any with non-blocking sockets | Linux 5.x+ | `epoll` is preferred; `kqueue` works on macOS/BSD. |
| File descriptors (`nofile`) | ≥ 1024 | ≥ 8000 | The default batch size is `4500`; below `nofile=4500`, RustScan auto-shrinks the batch. |
| RAM | 50 MB | 200 MB | Memory grows linearly with `batch_size`. |
| Network | Outbound TCP to scan targets | Sufficient pps headroom | Keep an eye on conntrack tables on stateful firewalls. |
| Companion: Nmap | Required only if `--scripts default` (the default) | Latest stable from your distro | The `.deb` package declares it as a dependency. |

> [!NOTE]
> RustScan does **not** require root for default TCP `connect()` scans. Root is needed only if you want `--ulimit` to actually raise `RLIMIT_NOFILE` above the soft limit, or if you want Nmap (called by the default script) to do raw-packet operations like `-sS`.

### 2.3 Network-policy requirements

| Direction | Why | Notes |
|---|---|---|
| Outbound TCP to scan targets | Core scanning | One full TCP handshake per open port × `tries`. |
| Outbound UDP to scan targets | `--udp` mode only | Sends payloads from the embedded `nmap-payloads` map. |
| Outbound DNS (53/udp, 853/tcp for DoT) | Hostname resolution | Falls back to Cloudflare DoT if `/etc/resolv.conf` fails. |
| No inbound traffic required | RustScan is purely client-side | Safe to run behind a NAT. |

---

## 3. Configuration Reference

This is the complete, source-based configuration surface. Anything not in these tables is not a valid setting.

### 3.1 Files

| File | Format | Default location | Purpose |
|---|---|---|---|
| `~/.config/.rustscan.toml` (or `$XDG_CONFIG_HOME/.rustscan.toml`) | TOML | XDG-resolved | Default option overrides, deserialized into `struct Config`. |
| `~/.rustscan.toml` | TOML | `$HOME` | Backwards-compatible legacy path. Used if the XDG path doesn't exist. |
| `--config-path` CLI arg | TOML | user-supplied | Highest-precedence config-file location. |
| `~/.rustscan_scripts/*.toml` (and `.rustscan_scripts.toml`) | TOML | `$HOME` | Custom-script descriptors loaded when `--scripts custom`. |
| Build-time `config.toml` (in repo root) | TOML | embedded via `--top` | Defines the top-1000 port list activated by `--top`. |

### 3.2 CLI flags (complete)

Sourced from `src/input.rs::Opts` (CLAP derive macros). Defaults are the literal `default_value` strings in the source.

| Short | Long | Type | Default | Description |
|---|---|---|---|---|
| `-a` | `--addresses` | `Vec<String>` (comma-delimited) | `[]` | List or file of CIDRs / IPs / hosts. |
| `-p` | `--ports` | `Option<Vec<u16>>` (comma-delimited) | `None` | Explicit port list. |
| `-r` | `--range` | `Option<PortRange>` (`start-end`) | `None` (effective `1-65535`) | Inclusive port range. Conflicts with `--ports`. |
| `-n` | `--no-config` | `bool` | `false` | Ignore config file completely. |
|   | `--no-banner` | `bool` | `false` | Suppress ASCII banner. |
| `-c` | `--config-path` | `Option<PathBuf>` | XDG default | Path to a TOML config file. |
| `-g` | `--greppable` | `bool` | `false` | Print `IP -> [PORTS]` only. Skips Nmap. |
|   | `--accessible` | `bool` | `false` | Drop ASCII art / color (screen-reader mode). |
|   | `--resolver` | `Option<String>` | `None` | Comma-list of DNS-resolver IPs, or path to a file. |
| `-b` | `--batch-size` | `usize` | `4500` | Concurrent socket attempts. |
| `-t` | `--timeout` | `u32` (ms) | `1500` | Per-port wait. |
|   | `--tries` | `u8` | `1` | Retries per socket. `0` is corrected to `1`. |
| `-u` | `--ulimit` | `Option<usize>` | `None` | Auto-raise `RLIMIT_NOFILE`. |
|   | `--scan-order` | enum `serial` / `random` (case-insensitive) | `serial` | Port iteration order. |
|   | `--scripts` | enum `none` / `default` / `custom` (case-insensitive) | `default` | Script-engine mode. |
|   | `--top` | `bool` | `false` | Use the top-1000 ports from your config's `ports = [...]`. |
| (positional after `--`) | `command` | `Vec<String>` | `[]` | Extra args appended to the script call (e.g., Nmap flags). |
| `-e` | `--exclude-ports` | `Option<Vec<u16>>` (comma-delimited) | `None` | Ports to exclude. |
| `-x` | `--exclude-addresses` | `Option<Vec<String>>` (comma-delimited) | `None` | IPs/CIDRs/hosts to exclude. |
|   | `--udp` | `bool` | `false` | UDP scan instead of TCP connect. |

The `version` flag is auto-injected by CLAP and prints `rustscan 2.4.1` (sourced from `CARGO_PKG_VERSION`).

### 3.3 Config-file keys (complete)

Each key is the field name on the `Config` struct in `src/input.rs`. All are optional; missing keys leave the CLI default in place.

| Key | TOML type | CLI counterpart | Notes |
|---|---|---|---|
| `addresses` | `Array<String>` | `--addresses` | Default targets. |
| `ports` | `Array<u16>` | `--ports` | Activated by `--top` even if `-r` was passed. |
| `range` | `Inline table {start, end}` | `--range` | |
| `greppable` | `bool` | `--greppable` | |
| `accessible` | `bool` | `--accessible` | |
| `batch_size` | `usize` | `--batch-size` | |
| `timeout` | `u32` | `--timeout` | |
| `tries` | `u8` | `--tries` | |
| `ulimit` | `usize` | `--ulimit` | |
| `resolver` | `String` | `--resolver` | |
| `scan_order` | `String` (`"Serial"`/`"Random"`) | `--scan-order` | |
| `command` | `Array<String>` | trailing `--` args | |
| `scripts` | `String` (`"None"`/`"Default"`/`"Custom"`) | `--scripts` | |
| `exclude_ports` | `Array<u16>` | `--exclude-ports` | |
| `exclude_addresses` | `Array<String>` | `--exclude-addresses` | |
| `udp` | `bool` | `--udp` | |
| `no_banner` | `bool` | `--no-banner` | |

> [!IMPORTANT]
> Any TOML parse error aborts the run with `Found {error} in configuration file. Aborting scan.` This is intentional: silently dropping a typo'd flag would change scan semantics. Validate config files in CI with a linter such as `taplo`.

### 3.4 Merge semantics

`Opts::merge(&self, &Config)` is split into two phases:

- **`merge_required`** — for `addresses`, `greppable`, `accessible`, `batch_size`, `timeout`, `tries`, `scan_order`, `scripts`, `command`, `udp`, `no_banner`: if the config's value is `Some`, it overrides the CLI value. (The CLAP defaults are applied first, so the CLI baseline is always non-`None`.)
- **`merge_optional`** — for `range`, `resolver`, `ulimit`, `exclude_ports`, `exclude_addresses`: only applied when the CLI didn't set them and the config did. The exception is `ports`, which is only filled from the config when `--top` was passed on the CLI.

`--no-config` short-circuits both phases.

### 3.5 Environment variables

| Variable | Read by | Purpose |
|---|---|---|
| `RUST_LOG` | `env_logger` | Sets log level (`trace`/`debug`/`info`/`warn`/`error`). `RUST_LOG=info` enables the per-phase benchmark summary. |
| `XDG_CONFIG_HOME` | `dirs` crate | Overrides the default config-file directory. |
| `HOME` | `dirs` crate | Used for the legacy `~/.rustscan.toml` lookup. |
| `CARGO_PKG_VERSION` | `clap` macro | Compile-time only — embedded into the `--version` string. |

RustScan does not read any other environment variables.

---

## 4. Deployment & Scaling

### 4.1 Deployment patterns

| Pattern | When to use | Trade-offs |
|---|---|---|
| **One-shot CLI on an analyst laptop** | Manual recon, ad-hoc verification | Easiest; constrained by laptop's `nofile`. |
| **Docker on a jump-host** | Shared team environment, CI, hardened distros | High default ulimits; per-run isolation; bring-your-own-Nmap not needed. |
| **`systemd-run --scope` from cron / Ansible** | Scheduled internal scans | Re-uses host's network namespace; honour host policy. |
| **Network-namespace / container per scan** | Multi-tenant scanning service | Strong blast-radius isolation; pair with cgroup CPU/memory caps. |
| **Library mode (`rustscan` crate)** | Embedding into your own Rust tooling | You own the runtime, output format, error handling. |

### 4.2 Docker reference run

```bash
docker run --rm \
  --read-only --tmpfs /tmp \
  --network host \
  rustscan/rustscan:latest \
  -a 10.0.0.0/24 -r 1-1024 --no-banner -- -sV -oX /tmp/result.xml
```

`--network host` is needed if you want the container to scan over the host's interfaces and routes (which is usually what you want — RustScan is the scanner, not the target). For multi-tenant deployments, use a dedicated bridge network with deliberate egress rules instead.

> [!TIP]
> Mount a config volume so analysts share a common policy: `-v /etc/rustscan.toml:/root/.config/.rustscan.toml:ro`. The container will pick it up automatically.

### 4.3 Scaling levers

| Lever | Effect | Caveat |
|---|---|---|
| `--batch-size` | Concurrency per RustScan process | Bounded by `nofile`. |
| `--ulimit` | Raises `nofile` on Unix | Requires `CAP_SYS_RESOURCE` or root. |
| `--timeout` | Time the scan takes per closed port | Doesn't help with rate-limiting upstream. |
| Sharding by `-a` chunks | Parallel scans across hosts | Run each chunk in its own process / container. |
| Sharding by `-r` chunks | Parallel scans across port subranges | Avoids one fat batch saturating one host. |
| `--scan-order random` | Avoids stripe patterns IDS dashboards use | Slightly higher cache misses, negligible cost. |

For shard/parallel approaches, RustScan is stateless — there is no shared coordinator. Aggregate output via `-g` and any log shipper (Vector, Fluent Bit, journald + Loki, etc.).

### 4.4 Rolling out a new version

1. Build the image / binary with the new commit.
2. Smoke-test against `scanme.nmap.org` from staging (`docker run rustscan/rustscan:NEW_VER -a scanme.nmap.org -r 22-100 -g`).
3. Compare output against the previous version on the same target. Diffs in port list = behavioural change; investigate before promoting.
4. Promote in your registry, update Ansible/Helm references, restart consumer jobs.

There's no daemon and no on-disk state, so rollbacks are just swapping the image tag back.

---

## 5. Operations

RustScan is a stateless, short-lived process, so "operations" is mostly about running it predictably and getting useful telemetry out.

### 5.1 Logging

The binary uses the `log` crate gated by `env_logger`. Levels:

| Level | What you see |
|---|---|
| `error` | Nothing extra; serious panics are still printed by Rust runtime. |
| `warn` | Default. CLI warnings ("File limit is lower than default batch size") plus any user-facing `warning!` lines. |
| `info` | Adds the per-phase benchmark summary: `RustScan`, `Portscan`, `Scripts` timers. |
| `debug` | Adds `Main()` opts dump, scripts initialised list, scanner debug, batch-size / ulimit decisions, per-socket connection errors. |
| `trace` | Same as `debug`; no extra trace-level logs are emitted by RustScan itself. |

```bash
RUST_LOG=info rustscan -a 192.0.2.10 -- -sV
```

```text
[~] RustScan: 1.243s
[~] Portscan: 0.812s
[~] Scripts:  0.314s
```

> [!NOTE]
> The benchmark summary is the most useful operational signal. If `Portscan` time grows linearly across runs while ports stay similar, you've likely hit a network-side rate limit; reduce batch size or add a sleep between shards.

### 5.2 Output capture

| Mode | Behaviour | Best for |
|---|---|---|
| Default | Banner + colored "Open IP:PORT" lines + Nmap output | Interactive analysts. |
| `--accessible` | Banner-less, plaintext "Open IP:PORT" + Nmap | Screen readers / paste-into-tickets. |
| `-g` / `--greppable` | Single line per IP: `IP -> [PORT,PORT,...]`, no Nmap | Pipelines, dashboards, regex. |
| `-g --no-banner` | Same as above; banner suppressed for clean piping | CI, log shipping. |

The greppable format is stable in the sense that the source code structurally produces it ([`println!("{} -> [{}]", &ip, ports_str)`]) — but RustScan does not commit to it as machine-stable API. If you script around it, pin a version.

### 5.3 Health checks

There's nothing to liveness-probe — RustScan exits when it's done. For containerized service offerings, build your own health endpoint that runs `rustscan --version` and confirms the binary is callable.

### 5.4 Common runbooks

#### 5.4.1 "A scheduled scan went 10x slower yesterday"

1. Pull `RUST_LOG=info` summary from yesterday vs. today.
2. If `Portscan` time changed dramatically: network/firewall side. Re-run with `--tries 1 -t 800 -b 2000` to isolate timeout vs. throughput.
3. If `Scripts` time changed: the Nmap call is the variable. Run with `--scripts none` to confirm.

#### 5.4.2 "RustScan reports zero open ports but I know SSH is open"

1. Re-run with `RUST_LOG=debug` to see actual connection errors.
2. Bisect: `rustscan -a TARGET -p 22 --scripts none -t 5000` (single port, generous timeout).
3. If it still fails: routing / ACL issue, not RustScan.
4. If it succeeds: lower batch size in the original scan.

#### 5.4.3 "We need to onboard a new team to RustScan"

1. Stand up a Docker image in your registry from a known commit.
2. Drop a shared config TOML into a config volume (`addresses` left blank, opinionated `batch_size`, `tries`, `timeout`).
3. Document the legal-authorization process before granting access (see Section 7).

> [!TIP]
> Pre-populate `command = ["-Pn", "-vvv", "--script", "default"]` in the shared TOML so analysts can't accidentally escalate to noisy NSE scripts. Anything they pass after `--` extends, doesn't replace.

#### 5.4.4 "Trace a single port-scan to root cause"

```bash
RUST_LOG=debug rustscan -a TARGET -p 22 --scripts none -b 1 2>&1 \
  | tee scan.log
```

The `-b 1` forces fully serial execution so the debug log is human-readable. The log contains `Connection was successful, shutting down stream` (success), or the exact `io::Error` string.

---

## 6. Performance

### 6.1 Where time goes

Per `Benchmark::summary()` output, a typical scan splits into three phases:

| Phase | Dominant cost |
|---|---|
| `RustScan` (total) | Sum of all phases plus startup. |
| `Portscan` | TCP `connect()` × IP × port × tries, capped at `batch_size` concurrency. |
| `Scripts` | One Nmap invocation per IP, concurrent with the next IP's. |

`Portscan` is almost always the biggest phase. `Scripts` is unpredictable because it's whatever Nmap does — version detection (`-sV`) can take longer than the entire scan.

### 6.2 Batch-size math

The `infer_batch_size` function (Unix only, in `src/main.rs`) implements:

```text
let DEFAULT_FILE_DESCRIPTORS_LIMIT = 8000      // Ubuntu-typical
let AVERAGE_BATCH_SIZE             = 3000

if ulimit < batch_size:
    if ulimit < 3000:
        batch_size = ulimit / 2          # very tight; halve to leave headroom
    elif ulimit > 8000:
        batch_size = 3000                # generous ulimit, conservative batch
    else:
        batch_size = ulimit - 100        # ride right under the limit
else if ulimit + 2 > batch_size && opts.ulimit.is_none():
    log "[~] File limit higher than batch size. Can increase speed by '-b {ulimit-100}'."
```

On non-Unix, batch size is the constant `AVERAGE_BATCH_SIZE = 3000`.

> [!TIP]
> The "useful warning" path (`ulimit + 2 > batch_size && opts.ulimit.is_none()`) is the easiest free speedup: it tells you the exact `-b` value to use. Capture stderr in CI, grep for `'-b ` and feed the recommendation back into your runbook.

### 6.3 Memory profile

RustScan's memory grows with three things:

1. The IP list — `Vec<IpAddr>` (16 bytes each on 64-bit, plus overhead).
2. The port set — `Vec<u16>` (2 bytes each, up to 65,535 elements ~ 128 KB).
3. The in-flight pool — `FuturesUnordered` of size `batch_size`, where each entry is a `connect()` future plus a `BTreeMap<Vec<u16>, Vec<u8>>` clone for UDP scans.

A typical full-range scan of a /24 with `batch_size=4500` runs in ~30-80 MB RSS depending on platform. Worst case (large CIDR, full range, UDP) is hundreds of MB; size containers accordingly.

### 6.4 Throughput tuning checklist

- [ ] `nofile` is at least `batch_size + 200` (kernel keeps a few FDs for itself, stdin/stdout, etc.).
- [ ] `--timeout` is realistic — `1500` ms is generous on a LAN, tight on a transcontinental link.
- [ ] `--tries` is `1` unless you're on a known-lossy link; each retry is a full timeout.
- [ ] `conntrack_max` (Linux) > `batch_size × number_of_targets`; bumping `nf_conntrack_max` is sometimes the actual fix.
- [ ] Network MTU isn't dropping packets — TCP connect needs three packets; UDP probe is one.
- [ ] You're not double-NAT'ing through a slow appliance.

### 6.5 Benchmarks

`benches/benchmark_portscan.rs` uses the Criterion framework to micro-benchmark `Scanner::run` against `127.0.0.1`. Run with:

```bash
cargo bench --bench benchmark_portscan
```

These numbers are useful for regression detection on the scanner core; they are not a real-world throughput estimate (loopback has no network costs).

---

## 7. Security

RustScan is a vulnerability-discovery / reconnaissance tool. Operating it correctly means thinking about three threat models:

1. **You vs. the target** — scanning is regulated; getting authorization wrong is a crime in most jurisdictions.
2. **You vs. detection** — RustScan's defaults are noisy; understand what an IDS will see.
3. **The output vs. attackers** — your scan results are an attacker's wish list.

### 7.1 Legal and authorization posture

> [!CAUTION]
> Port scanning systems you do not own and have not been explicitly authorized to scan can violate the Computer Fraud and Abuse Act (US, 18 USC §1030), the Computer Misuse Act 1990 (UK), Article 202c StGB (Germany), and equivalent laws in most jurisdictions. RustScan ships no authorization framework — that responsibility is entirely yours.

Operational guidance:

- Maintain a written, signed statement of work (SOW) or Rules of Engagement (RoE) before scanning third-party infrastructure. Scope should specify CIDRs, port ranges, time windows, and contact persons.
- Internal scanning of your own infrastructure typically falls under a blanket security-team authorization; confirm with legal, especially in cloud (AWS / GCP / Azure all have AUPs about port scanning shared infrastructure).
- `scanme.nmap.org` (public) and your own loopback are the only universally safe targets.

### 7.2 Detection posture

RustScan's default behaviour is a fast, full-range TCP connect scan from a single source IP. Every reasonable IDS will flag it.

| Default | Why detectable | Mitigation if you have authorization |
|---|---|---|
| Connect (full handshake) scan | Visible at L4 even to non-stateful sensors | If you need stealth, use Nmap `-sS` after RustScan discovery. |
| Full 1-65535 range | Volume-based rules | `-r` to a tight range; document in your SOW. |
| Single source IP | Trivial to baseline | Distribute scans across egress IPs. |
| Serial port order | "Stripe" pattern | `--scan-order random`. |
| Nmap follow-up | Second-stage signature | `--scripts none`, run Nmap separately. |
| Burst at `batch_size` | Sudden FD spike | Lower `--batch-size` and let it run longer. |

> [!WARNING]
> "Stealth" scans against systems you're authorized to test are fine. "Stealth" scans against systems you're not authorized to test are still illegal — the legality is independent of the noise level.

### 7.3 Output handling

Scan output describes attack surface. Treat it the way you'd treat any other recon artifact:

- Scan logs go to a private bucket with a short retention window (30-90 days unless evidence is needed for an ongoing engagement).
- Encrypt at rest. AWS S3 server-side encryption / GCS CMEK / Azure SSE — pick one and enforce via bucket policy.
- Restrict access to the security team only. Audit log every read.
- Strip / mask before sharing with clients — internal IPs, internal hostnames, and user-agent fingerprints can leak more than you intended.
- For multi-client engagements, segregate output by client into separate prefixes / projects with hard IAM boundaries.

> [!IMPORTANT]
> Greppable output (`-g`) is structured but not stable across versions. Version-pin RustScan (or the Docker image) for any pipeline that parses its output as data.

### 7.4 Logging posture

What to log:

- Operator identity (who ran the scan).
- Authorization reference (SOW / RoE id).
- Target scope (`-a` and `-r` values).
- Time window (start, end, duration).
- Source IP and egress path.
- Tool version (`rustscan --version`).

What **not** to log persistently:

- Per-port success/failure timing — useful for debug only, not retained.
- Full Nmap NSE script output where it contains harvested credentials, version banners, or service-specific data — segregate to client-restricted storage.

### 7.5 Default rate limits and avoiding IDS lockouts

RustScan has no built-in rate limiter — `batch_size` is the only knob. The kernel's TCP stack provides natural backpressure when `nofile` is exhausted, and that's it.

To avoid being detected:

- Stay under whatever flow-rate threshold your target IDS uses (commonly 100 SYN/sec/source). With default `-b 4500 -t 1500`, RustScan can easily exceed that for a /24.
- Throttle by lowering `-b` and shard by host: scan one /28 at a time with `-b 500`.
- Combine with `--scan-order random` so the bursts aren't temporally clustered on adjacent ports.
- For long-running engagements, distribute scans across multiple egress points (different VPC / different VPN endpoint).

### 7.6 Supply-chain considerations

- `Cargo.lock` is committed; `lto = true` and `panic = 'abort'` in `[profile.release]` keep the binary small and the panic behaviour deterministic.
- Compile with `--locked` (`cargo install --locked rustscan`) to refuse silent dependency drift.
- The Docker image bundles Nmap; track Nmap CVEs, not just RustScan ones.
- The project carries a `.travis.yml` and CI history but the canonical CI configuration in newer revisions may live in `.github/workflows/`; verify against the source commit you're deploying.

### 7.7 Code-level safety properties

- The crate compiles under `#![deny(clippy::all)]` and `#![warn(clippy::pedantic)]`. New code that adds unsafe blocks or unhandled panics will fail CI.
- The "too many open files" assertion in `scan_socket` is intentional: rather than silently degrading, RustScan panics so an automated wrapper sees a non-zero exit and can re-tune. Do not "fix" this by removing the assert.
- `Opts::default()` initialises every field; tests rely on it. If you add a new option, update both the struct, `Default`, and `Config`.

---

## 8. Developer Internals

### 8.1 Repository layout

```text
RustScan/
├── Cargo.toml                       Crate manifest — pinned deps, package metadata
├── Cargo.lock                       Reproducible build (committed)
├── build.rs                         Compile-time generator → src/generated.rs
├── nmap-payloads                    UDP probe database (text)
├── config.toml                      Embedded top-1000 ports for --top
├── Makefile / justfile / release.sh Convenience scripts
├── contributing.Dockerfile          Dev container for contributors
├── src/
│   ├── main.rs                      Entry point, batch-size math, opening banner
│   ├── lib.rs                       Public API (mod declarations + doc example)
│   ├── input.rs                     Opts, Config, ScanOrder, ScriptsRequired, PortRange
│   ├── address.rs                   IP/CIDR/host/file parsing, exclusions, DNS
│   ├── tui.rs                       warning! / detail! / output! / funny_opening!
│   ├── benchmark/mod.rs             NamedTimer + Benchmark struct
│   ├── port_strategy/
│   │   ├── mod.rs                   PortStrategy enum + Serial/Random
│   │   └── range_iterator.rs        LCG RangeIterator
│   ├── scanner/
│   │   ├── mod.rs                   Scanner core, scan_socket, scan_udp_socket
│   │   └── socket_iterator.rs       (port × ip) cartesian product
│   └── scripts/mod.rs               Script registry, builder, runner
├── tests/timelimits.rs              Integration tests with timeouts (ignored by default)
├── benches/benchmark_portscan.rs    Criterion micro-benchmark
└── fixtures/                        Test data (hosts.txt, naughty_strings.txt, etc.)
```

> [!NOTE]
> `src/generated.rs` is **not** committed; it's produced by `build.rs` from `nmap-payloads` on first build. Check `.gitignore` to confirm.

### 8.2 Public library API

The `rustscan` crate is also a Rust library. The public surface (per `src/lib.rs` and the modules it re-exports):

```rust
use std::{net::IpAddr, time::Duration};
use async_std::task::block_on;

use rustscan::input::{PortRange, ScanOrder};
use rustscan::port_strategy::PortStrategy;
use rustscan::scanner::Scanner;

fn main() {
    let addrs = vec!["127.0.0.1".parse::<IpAddr>().unwrap()];
    let range = PortRange { start: 1, end: 1_000 };

    let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);

    let scanner = Scanner::new(
        &addrs,
        10,                              // batch_size
        Duration::from_millis(100),      // timeout
        1,                               // tries
        true,                            // greppable
        strategy,
        true,                            // accessible
        vec![9000],                      // exclude_ports
        false,                           // udp
    );

    let scan_result = block_on(scanner.run());
    println!("{:?}", scan_result);
}
```

Public items (selected) — all from `src/`:

- `rustscan::input::{Opts, Config, ScanOrder, ScriptsRequired, PortRange, default_config_path, old_default_config_path}`
- `rustscan::address::{parse_addresses, parse_address, parse_excluded_networks}`
- `rustscan::scanner::Scanner`
- `rustscan::port_strategy::PortStrategy`
- `rustscan::scripts::{init_scripts, Script, ScriptFile}`
- `rustscan::benchmark::{Benchmark, NamedTimer}`
- Macros (re-exported): `warning!`, `detail!`, `output!`, `funny_opening!`

### 8.3 The async port-scanning pool

`Scanner::run` is the heart of RustScan. The pseudocode:

```rust
pub async fn run(&self) -> Vec<SocketAddr> {
    let ports = self.port_strategy.order()
        .into_iter()
        .filter(|p| !self.exclude_ports.contains(p))
        .collect::<Vec<u16>>();

    let mut socket_iterator = SocketIterator::new(&self.ips, &ports);
    let mut futures        = FuturesUnordered::new();
    let mut open_sockets   = Vec::new();

    // Prime the pool
    for _ in 0..self.batch_size {
        if let Some(s) = socket_iterator.next() {
            futures.push(self.scan_socket(s, udp_map.clone()));
        } else {
            break;
        }
    }

    // Drain + refill: each completed future is replaced by one fresh socket
    while let Some(result) = futures.next().await {
        if let Some(s) = socket_iterator.next() {
            futures.push(self.scan_socket(s, udp_map.clone()));
        }
        if let Ok(socket) = result {
            open_sockets.push(socket);
        }
    }

    open_sockets
}
```

Two design choices to know:

1. **Fixed-size pool.** Concurrency is at most `batch_size` in-flight sockets. This is the only knob that controls pressure on the network and the local FD table.
2. **Cartesian iteration order.** `SocketIterator` yields `(port, ip)` in the form "all IPs for port 22, then all IPs for port 23, ..." — visible in the test in `src/scanner/socket_iterator.rs`. This distributes load instead of pounding one host at a time.

### 8.4 Per-socket scan path

`scan_socket` (TCP):

1. Try `connect(socket)` with a `self.timeout` deadline.
2. On `Ok`: shutdown the stream both ways, format and print "Open IP:PORT" (unless greppable), return `Ok(socket)`.
3. On `Err` containing "too many open files" (case-insensitive): assert-fail with a remediation message — by design.
4. On any other error: retry up to `self.tries` times, then return `Err(io::Error::other(error_string))` with the IP appended.

`scan_udp_socket` (UDP):

1. Look up the port in the embedded payload `BTreeMap`.
2. Bind a local UDP socket, `send` the payload, `recv` with timeout.
3. On a non-empty response: success.
4. On timeout: not open (continue with next try).
5. After all tries: `Err`.

### 8.5 The Linear Congruential Generator (random scan order)

Random order isn't a `Vec::shuffle` — it's an LCG that produces a non-repeating permutation of the range without storing the full vector first:

```text
For range [start, end], normalize to [0, end - start]:
    step = pick_random_coprime(end - start + 1)
    first_pick = rng.random_range(0..normalized_end)
    next(N) = (current + step) % normalized_end
    iteration ends when next == first_pick
```

`pick_random_coprime` searches up to 10 candidates in the middle half of the range (`end/4..end - end/4`); if none are coprime, it falls back to `end - 1` (always coprime with `end`).

Why care: the iterator avoids cluster-of-adjacent-ports patterns even compared to a pre-shuffled vector, because a coprime step deliberately makes consecutive picks far apart.

### 8.6 Address parsing pipeline

`parse_addresses` (in `src/address.rs`) handles four input shapes for each token in `-a`:

1. `IpAddr::from_str` — bare IPv4 / IPv6 → single-element `Vec<IpAddr>`.
2. `IpInet::from_str` — CIDR → expand via `cidr_utils` to all host addresses.
3. `to_socket_addrs("{addr}:80")` — system DNS lookup → first resolved IP.
4. `backup_resolver.lookup_ip` — fallback DNS-over-TLS resolver if (3) fails.
5. If still empty, treat the token as a file path: read line-by-line and recurse.

Final dedup is via `BTreeSet<IpAddr>`; exclusions (from `-x`) are subtracted with `IpCidr::contains`.

> [!NOTE]
> The DNS fallback uses `ResolverConfig::cloudflare_tls()` (1.1.1.1 over DoT) when `/etc/resolv.conf` can't be parsed. If you can't tolerate that egress, pass `--resolver` with your own list (or `127.0.0.1` to force a local resolver).

### 8.7 Scripting engine (Rhai-style descriptors)

> [!NOTE]
> Scripts in RustScan 2.4.1 are TOML descriptors that parameterize an external shell command — not in-process Rhai/Lua execution. The `src/scripts/mod.rs` module loads `~/.rustscan_scripts/*.toml` and `.rustscan_scripts.toml`, validates required fields, and produces a `ScriptFile` per descriptor. At scan time, each `ScriptFile` is rendered via the `text_placeholder` crate (`{{ip}}`, `{{port}}`) and executed with `std::process::Command`.

Descriptor fields the project recognizes (per `Script::build`):

| Field | Required | Purpose |
|---|---|---|
| `path` | yes | Path to the script binary or interpreter command. |
| `tags` | yes | Tags used to match scripts to ports. |
| `developer` | recommended | Author attribution. |
| `port` | optional | Specific port the script applies to. |
| `ports_separator` | optional | Separator used when joining ports for the call. |
| `call_format` | yes | Templated command line with `{{ip}}` / `{{port}}` placeholders. |

The default-mode script is the well-known Nmap handoff — its `call_format` is something like:

```text
nmap -Pn -vvv -p {{port}} {{ip}}
```

…with `command` from `Opts` appended at runtime, which is how user `-- -A -sC` ends up at the end of the Nmap call.

`--scripts none` short-circuits the loop: RustScan still resolves and scans, then prints `IP -> [PORTS]` and exits. `--scripts custom` loads only descriptors matched against tags from `~/.rustscan_scripts.toml`.

### 8.8 Code patterns and conventions

- **`#![deny(clippy::all)]`** + **`#![warn(clippy::pedantic)]`** in `src/main.rs` enforce a strict lint profile. New code that fails clippy fails CI.
- **`#[cfg(not(tarpaulin_include))]`** marks blocks excluded from coverage measurement (typically functions whose effect is observable only in `main`).
- **`#[cfg(unix)]`** gates `rlimit`-based logic; the `#[cfg(not(unix))]` branch sets a fixed `AVERAGE_BATCH_SIZE = 3000`.
- **TUI macros** in `src/tui.rs` are dual-form: `warning!("msg")` is the simple form; `warning!("msg", greppable, accessible)` honours operator-mode flags. Always prefer the three-arg form in new code so `--accessible` / `-g` stay correct.
- **Tests live next to source** in `#[cfg(test)] mod tests`, plus integration tests in `tests/timelimits.rs` and benches in `benches/`.
- **Default-implementations are explicit.** `Opts::default()` is hand-written (not `#[derive(Default)]`) because some defaults differ from the type defaults (e.g., `greppable: true` in `Default`, `false` in CLAP — that's deliberate; the Default is for tests).

### 8.9 Testing strategy

| Layer | Where | What it covers |
|---|---|---|
| Unit | `src/**/mod.rs::tests` | Arithmetic on batch size; CIDR / file parsing; LCG range coverage; PortStrategy variants. |
| Network | `src/scanner/mod.rs::tests` | Real connect to localhost / Google DNS / IPv6 loopback; ensures no panic. |
| Integration | `tests/timelimits.rs` | End-to-end timing constraints; ignored by default (`-- --ignored`). |
| Bench | `benches/benchmark_portscan.rs` | Criterion harness (regression-only, not absolute throughput). |
| CLI | `src/input.rs::tests::verify_cli` | `Opts::command().debug_assert()` catches malformed CLAP attributes. |

Run the full local test sweep with:

```bash
ulimit -n 5000 && cargo test && cargo test timelimits:: -- --ignored --test-threads=1 --show-output
```

(That's the literal command from `.travis.yml`.)

### 8.10 Potential improvements (open features in upstream)

| Issue | Description | Where to start |
|---|---|---|
| #780 / #835 | Combined port range syntax `1-22,8080,8443` | Extend `parse_range` and the `ports`/`range` mutually-exclusive constraint in `src/input.rs`. |
| #825 | Proxy support | New `--proxy` flag → wrap `TcpStream::connect` in a SOCKS5 dialer. |
| #829 | Per-host TCP/UDP suffix syntax | Extend `parse_address` to recognise `/t` and `/u` suffixes; route to TCP or UDP scanning per host. |
| #683 | ARM CI coverage | Update CI matrix; verify Docker manifest list publishes ARM tags. |
| #741 | XDG config-path edge cases | Already mostly fixed via `dirs::config_dir()`; remaining work is the legacy fallback strategy. |
| #800 | Version reporting drift | Cargo.toml version of record vs. embedded `CARGO_PKG_VERSION` when releases skip a tag. |

### 8.11 Contributing workflow

1. Fork → branch from `master`.
2. Implement the change. New crates go in `Cargo.toml` with explicit version pins.
3. `cargo fmt --all` (Travis enforces this).
4. `cargo test` and `cargo clippy --all-targets -- -D warnings`.
5. Update tests next to source; if it touches scanning behaviour, add an integration test in `tests/`.
6. PR with a clear before/after — the clippy + format gate catches the rest.

---

## 9. Integration Points

RustScan is deliberately small; integration is mostly about the seam at the command-line, the seam at Nmap, and the seam at the public Rust crate.

### 9.1 With Nmap

The default `--scripts default` mode invokes Nmap with at minimum `-Pn -vvv -p $PORTS $IP`, plus anything you appended after `--`. Practical implications:

- Nmap is mandatory unless you set `--scripts none`. The `.deb` package declares it as a dependency; the Docker image ships it.
- All Nmap behaviour — version detection, OS fingerprint, NSE — is configured via the trailing `--`. You don't need a wrapper script to get Nmap features.
- Nmap output appears on stdout interleaved with RustScan's lines. If your downstream parser is brittle, run with `-g --scripts none` to get only RustScan's output, then call Nmap separately.

### 9.2 Shell pipelines

Greppable mode (`-g`) makes RustScan a first-class citizen in any tool chain that reads `IP -> [PORTS]`:

```bash
rustscan -g --no-banner -a 10.0.0.0/24 -r 1-1024 \
  | tee scans/$(date +%F).txt \
  | awk -F' -> ' '$2 != "[]"' \
  | mail -s "Open ports today" [email removed]
```

For structured downstream tools, write your own greppable parser:

```python
import re, subprocess
out = subprocess.check_output(["rustscan", "-g", "-a", "10.0.0.0/24"], text=True)
for line in out.splitlines():
    m = re.match(r'(\S+) -> \[(.*)\]', line)
    if not m: continue
    ip, ports = m.group(1), [int(p) for p in m.group(2).split(',') if p]
    yield ip, ports
```

### 9.3 As a Rust library

Add to your `Cargo.toml`:

```toml
[dependencies]
rustscan = "2.4"
async-std = "1.13"
futures = "0.3"
```

Drive the scanner directly from your code (see Section 8.2 for the canonical example). The library doesn't pull in CLAP unless you depend on it through `rustscan::input::Opts::parse()`, but the main scanning path (`Scanner` + `PortStrategy` + `parse_addresses`) is dependency-light.

### 9.4 In CI

```yaml
# GitHub Actions
- uses: docker://rustscan/rustscan:latest
  with:
    args: >-
      -g --no-banner --scripts none
      -a ${{ secrets.SCAN_TARGET }}
      -r 1-1024
```

The container starts, scans, prints, exits. Pair with `actions/upload-artifact` to retain results for the security team.

### 9.5 SIEM ingestion

Greppable output ships well into:

- Splunk / Elastic: line-based `IP -> [PORTS]` with timestamp from the log shipper.
- Loki: same, with `job=rustscan` label.
- Datadog Logs: parse with a custom grok pattern `(?<ip>%{IP}) -> \[(?<ports>[^\]]*)\]`.

Don't ingest the full default output (banner + Nmap interleaved) — it's interactive-shaped, not log-shaped.

### 9.6 Container orchestration

RustScan exits when done; there is no idle process to keep alive. For Kubernetes:

- Run as a `Job` (or `CronJob` for scheduled scans).
- `restartPolicy: OnFailure` so a transient DNS hiccup retries.
- `securityContext.capabilities.add: ["SYS_RESOURCE"]` if you want `--ulimit` to actually raise FDs.
- Bind the config TOML with a `ConfigMap` volume if your team shares a policy.

### 9.7 What RustScan does NOT integrate with

To keep the negative-knowledge surface explicit:

- **No daemon mode.** RustScan does not run as a long-lived service. There is no socket, no REST endpoint, no metrics scrape target.
- **No webhook / event push.** It writes to stdout and exits. Hook your own.
- **No proxy support.** Tracked in #825. Use `proxychains` or namespace routing.
- **No raw SYN scanning.** Connect-only. Use Nmap `-sS` after the fact for that.
- **No IPv4-mapped IPv6 special casing.** Whatever the resolver returns is what RustScan scans.
- **No persistence.** No database, no on-disk cache. Each run is independent.

---

_Generated April 2026 from bee-san/RustScan at b033798. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
