>_EXECUTIVE SUMMARY
nmaprs is a from-scratch Rust implementation of the nmap CLI dialect, with packet-level evasion, pipelined raw I/O on IPv4 + IPv6, and OS / version detection scored against nmap's own reference databases (nmap-os-db, nmap-service-probes). The CLI surface accepts the union of nmap --help and the long-option set from upstream nmap.cc — existing nmap scripts, integrations, and XML pipelines drop in unchanged.
What it does ship that vanilla nmap doesn't: async / parallel by default (no -T5 needed for "fast"), SOCKS4 + HTTP CONNECT proxy support for TCP connect scans, custom DNS resolution via hickory-resolver, sharded raw send/recv pipelines that scale to 16 concurrent shards per address family with race-free reply matching, and --resume JSON checkpoints across all scan types (TCP connect, UDP, raw half-open, IP-protocol, SCTP, idle, FTP bounce). What it doesn't ship: the NSE Lua runtime.
~SCAN ENGINE ARCHITECTURE
Per address family, raw scans spin up a dedicated recv thread blocking on poll(2), a sharded send pool (up to 16 concurrent shards), and a global rate limiter shared across IPv4 + IPv6. The recv-key registration happens before each send to eliminate the classic "reply landed before we knew to listen" race that ad-hoc raw scanners hit at high concurrency.
| Layer | Implementation |
|---|---|
| Raw socket I/O | pnet + socket2 — IPv4 layer-3 datagrams, IPv6 raw via IPPROTO_RAW + IPV6_HDRINCL on Unix |
| Async runtime | tokio multi-thread — one reactor pool, raw recv lives on dedicated thread (not the reactor) |
| Mixed IPv4 + IPv6 dispatch | tokio::join — both engines concurrent; per-IpAddr host-timeout clock shared |
| Sharding | Up to 16 concurrent pipelines per family, bounded by effective_probe_concurrency() from timing template / --max-parallelism |
| Recv key registration | Registered before send via DashMap; reply dispatcher drops to per-target oneshot channel; no lost replies under load |
| UDP ICMP unreach listener | One poll(2) + burst-recv thread per scan (not per host batch); IPv4 type-3 + ICMPv6 type-1 classification |
| SCTP CRC32c | In-tree CRC32c for SCTP common header + chunks; INIT-ACK / COOKIE-ACK → open, ABORT → closed |
| Service / version | nmap-service-probes parser; match / softmatch via Rust regex (Perl-only features skipped); TLS probes via rustls when port matches sslports |
| OS detection | Subject fingerprint from the Nmap 2nd-gen probe set — SEQ / OPS / WIN / ECN / T1–T7 / U1 / IE tests (os_scan.rs); scored against nmap-os-db reference fingerprints with MatchPoints + expr_match (same algorithm nmap uses, via the in-tree c/expr_match.cpp); falls back to ICMP TTL heuristic on minimal data |
| DNS | hickory-resolver with --dns-servers overrides, async, parallel; --system-dns escape hatch |
| Proxy | tokio-socks for SOCKS4; in-tree HTTP CONNECT; both apply to TCP connect scans |
| Rate limiting | One global token-bucket limiter on probe starts; IPv4 + IPv6 share; respects --min-rate floor by inferring parallelism upward when needed |
| Checkpoint format | JSON of completed (host, port) pairs; --resume filters per batch and merges at end; works with --min-hostgroup / --max-hostgroup |
$PARITY MATRIX vs UPSTREAM NMAP
Every nmap flag that nmaprs actually parses (128 options) plus where it stops short of nmap's behavior. Numbers from the project's TRUTH TABLE in the README.
| Area | nmaprs status |
|---|---|
TCP connect (-sT, default unprivileged) | Implemented — async, parallel, timeout-bound |
TCP SYN raw (-sS, IPv4 + IPv6) | Implemented — pipelined recv, 16-shard send per family, connect fallback on raw failure |
NULL / FIN / Xmas / Maimon (-sN/-sF/-sX/-sM) | Implemented — same raw sharded pipeline; Maimon sends FIN+ACK |
ACK (-sA) / Window (-sW) | Implemented — firewall mapping (RST → unfiltered) and window-state classification |
UDP (-sU) | Implemented — ICMP unreach listener distinguishes port-unreach (closed) from other unreach (filtered) |
SCTP (-sY INIT / -sZ COOKIE-ECHO) | Implemented — CRC32c, IPv4 + IPv6, mixed-family tokio::join |
Idle scan (-sI zombie[:probeport]) | Implemented — IPv4 only, sequential IP-ID sampling; IPv6 skipped (warning) |
FTP bounce (-b user:pass@host:port) | Implemented — IPv4 only (PORT command); 150/125/250 → open, 425/426/421 → closed |
IP protocol (-sO) | Implemented — IPv4 + IPv6 on Unix; -p is protocol numbers 0..=255; -F uses nmap-protocols |
Discovery (-PS/-PA/-PU/-PY/-PE/-PP/-PM/-PO/-PR) | Implemented — ARP auto on local IPv4 subnets (--disable-arp-ping to opt out) |
Ping scan (-sn) | Implemented — raw ICMP via pnet, falls back to system ping; output formats write host-up lines |
IPv6 (-6) | Implemented — targets + scans including raw SYN when privileged |
Traceroute (--traceroute) | Implemented — system traceroute/tracert; up to min(parallelism, 32) concurrent |
--iL / --iR / --exclude / --excludefile | Implemented — parallel resolution preserving order |
--resume (JSON checkpoint) | Implemented — TCP connect, UDP, raw half-open TCP, IP protocol scan |
Evasion (-g, --ttl, --badsum, -D, -S, --data*, -f/--mtu, --spoof-mac) | Implemented — wired at the packet level for raw TCP scans |
--proxies / --proxy | Implemented — SOCKS4 + HTTP CONNECT for TCP connect scans |
--dns-servers | Implemented — hickory-resolver, custom resolver IPs comma-separated |
Auto privilege detect (geteuid) | Implemented — downgrade SYN→connect when unprivileged; --privileged forces raw |
Output: -oN / -oG / -oX / -oA / -oS | Implemented — <nmaprun> root with nmap-compatible elements; existing parsers ingest it |
Timing template (-T0..-T5) | Implemented — parallelism + RTT bounds inferred from template; explicit flags override |
--min-rate / --max-rate / parallelism / hostgroup / RTT / retries / scan-delay / --stats-every | Implemented — one global rate limiter across families |
--scanflags (custom TCP flags) | Implemented — SYN/ACK/FIN/RST/PSH/URG/ECE/CWR; raw TCP scans only |
Port specs (-p, -F, --top-ports, --port-ratio) | Implemented — embedded TCP frequency list; -p - for all 65536 |
OS detection (-O) | Partial — IPv4 raw 2nd-gen probe set (SEQ/OPS/WIN/ECN/T1–T7/U1/IE) + MatchPoints scoring against nmap-os-db when the DB is available and open/closed TCP found. IPv6 = TTL heuristic only; no multi-round retry |
Service / version (-sV) | Partial — full nmap-service-probes support except Perl-only regex features |
Scripts (--script / -sC) | Partial — default + banner builtins; no Lua engine |
-oM / -oH | Partial — -oM = grepable; -oH placeholder |
--osscan-limit / --osscan-guess / --max-os-tries | Partial — limits + DB example titles; no multi-round retry |
--stylesheet / --webxml / --no-stylesheet | Partial — XML preamble + xml-stylesheet PI |
| NSE Lua runtime | Not implemented — use upstream nmap when Lua is required |
&SUBSYSTEM DEEP DIVES
// PIPELINED RAW SEND/RECV (16 SHARDS / FAMILY)
The naive raw scanner pattern — send packet; recv loop; classify — loses replies under load when the reply arrives before the recv loop registers the per-target callback. nmaprs's raw engine inverts the order:
- Register the recv key first in a
DashMap<(IpAddr, port, scan-type), oneshot::Sender>. - Then send the probe from the main thread of the shard.
- The dedicated recv thread demuxes incoming frames against the registered keys and fires the matching oneshot.
- Timeout falls back to
filteredafter the per-probe RTT cap. - Connect-mode fallback per address family kicks in when raw send fails outright (PERM, EAFNOSUPPORT, etc.) — not when probes individually time out.
Shard count defaults to min(16, effective_probe_concurrency()) per family. effective_probe_concurrency() is the bound between the timing template's parallelism estimate and explicit --min-parallelism / --max-parallelism / --min-rate floors.
// nmap-os-db SCORING
When -O is on, IPv4 is privileged, the scan found both open and closed TCP ports, and --datadir contains nmap-os-db:
- Raw Layer-3 probes build a subject fingerprint with the Nmap 2nd-gen probe set — SEQ, OPS, WIN, ECN, T1–T7, U1 (UDP), and IE (ICMP echo) tests assembled in
os_scan.rs::probe_ipv4_os(). - Each candidate
nmap-os-dbreference fingerprint is scored via MatchPoints + expr_match — the same algorithm upstream nmap uses (compare_fingerprints), driven by the in-treec/expr_match.cppbinding (fp_match.rs) and the reference DB loaded byos_fp_db.rs. - Top-scoring matches print with their
OS:/Class:/CPE:values. - When raw probes or matching data are unavailable, falls back to the ICMP TTL heuristic (
os_detect.rs) + exampleClass:titles streamed from the DB parse (os_db.rs).
IPv6 OS detection currently uses only the TTL heuristic.
// nmap-service-probes MATCHER
The -sV path parses nmap-service-probes and runs probes parallel-per-open-port:
- TCP + UDP probe blocks — both directions.
- ports / sslports filtering — ports outside the probe's set are skipped.
- TLS via
rustlswhen the port matches the probe'ssslports. - rarity vs
--version-intensity— high-rarity probes are skipped at low intensity, identical to nmap's behavior. - match / softmatch with full template fields —
p/(product),v/(version),i/(info),o/(OS),d/(device type),cpe:/(CPE URIs). - Perl-only regex features in the upstream patterns are detected and the matcher skips that entry — this is the source of the "partial" parity rating.
// HOST DISCOVERY ENGINE
Discovery (discovery.rs) runs before the port scan unless -Pn / --no-ping is set. The default (no explicit -P*) fires raw ICMP echo in parallel with raw TCP SYN to ports 443 and 80, with a TCP-connect fallback when raw fails. Explicit probes carry nmap's default ports:
-PS/-PA— raw TCP SYN / ACK; RST or SYN-ACK marks the host up; same sharded pipelines + connect fallback as the port scan.-PU(default 40125) — UDP / ICMP-style socket errors.-PY(default port 80) — SCTP INIT; INIT-ACK / ABORT → up; privileged; IPv4/IPv6 merged independently on partial failure.-PO(default protocols 1,2,4) — IP-protocol ping; open or closed → up; privileged.-PP/-PM— ICMP timestamp / address-mask requests (IPv4 only, Unix raw viaicmp_ping.rs); reply → up;connect_timeoutbounds the wait; skipped with a warning on non-Unix.-PR— ARP ping; auto on local IPv4 subnets unless--disable-arp-ping.
// IDLE & FTP-BOUNCE INDIRECT SCANS
- Idle scan (
-sI zombie[:probeport],idle.rs) — spoofed TCP SYN with the source set to the zombie; sequential IP-ID sampling on the zombie (RST to a closed probe port, default 65535) keeps global IP-ID deltas interpretable; delta ≥ 2 →open, elseclosed; probe/RST failures →filtered(idle-probe-failed). IPv4 only; IPv6 targets are skipped with a warning. - FTP bounce (
-b user:pass@host:port,ftp_bounce.rs) — parallelbuffer_unorderedsessions, one control connection per probe, issuingPORTagainst a misconfigured FTP server;150/125/250→open,425/426/421→closed; IPv4 only; strict servers may yieldfiltered.
// EVASION AT PACKET LEVEL
Source-port spoofing, custom TTL, bad checksums, decoys, source-IP spoofing, custom payload (hex / string / random length), fragmentation (-f, --mtu), and MAC spoofing on ARP discovery frames are all wired into the raw IPv4 packet construction for -sS / -sN / -sF / -sX / -sM / -sA / -sW. These are not wrappers around an external scapy-style helper — they manipulate the packet bytes nmaprs writes to the raw socket.
!WHEN TO USE NMAP INSTEAD
If your workflow requires authoritative behavior of any of the items below, reach for upstream nmap:
- NSE Lua scripts — nmaprs ships
banner+defaultRust builtins, not the Lua runtime. Any script that ships with nmap (vuln checks, HTTP enum, SNMP, SMB, etc.) needs upstream. - Full IPv6 OS detection — nmaprs's IPv6 OS detection is a TTL heuristic, not the IPv6 OS probe engine nmap has.
- Multi-round OS retry /
--max-os-trieswith adaptive intervals — nmaprs honors the flag but doesn't do the full retry behavior. - The exact
-oHhex dump format — currently a placeholder file in nmaprs. - Service-probe entries that use Perl-only regex features — nmaprs's
regexmatcher skips them with a warning.
/SECURITY & ETHICS
Run nmaprs only against networks and hosts you own or have explicit written permission to scan. Unauthorized scanning is illegal in many jurisdictions. The author and contributors disclaim responsibility for misuse. This tool exists because Rust's memory safety + async runtime + raw-socket bindings combine to make a credible parallel scanner without C's footguns — not to make scanning easier for hostile actors.
#PROJECT METADATA
| Item | Value |
|---|---|
| License | MIT OR Apache-2.0 (dual) |
| Author | MenkeTechnologies |
| Repository | github.com/MenkeTechnologies/nmaprs |
| Crate | crates.io/crates/nmaprs |
| API docs | docs.rs/nmaprs |
| Man pages | man nmaprs (506 lines, short ref) + man nmaprsall (1028 lines, full ref) |
| Issues | github.com/MenkeTechnologies/nmaprs/issues |