October 17, 2026
Nmap Under the Hood How Network Discovery and Port Scanning Actually Work
Every security assessment I run and every Python scanner I build eventually reduces to a small set of questions: which hosts are aliveβ¦

By Akarsh_Β’haturvedi
9 min read
Every security assessment I run and every Python scanner I build eventually reduces to a small set of questions: which hosts are alive, which ports answer, what speaks on those ports, and what does that evidence actually prove? Nmap has been the reference tool for answering the first three questions for two decades, yet it is routinely used as a magic black box. This article opens the box: the documented architecture, the packet-level behavior behind each port state, the practical commands I use in authorized lab assessments, and the interpretation discipline that separates a finding from a guess. Implementation claims here are checked against the official Nmap reference guide and the public source repository; conceptual workflow stages are labeled as such and are not internal APIs.
2.1 Why Network Discovery Matters
You cannot assess what you cannot enumerate. Asset inventories are always stale the forgotten VM, the printer on the lab VLAN, the "temporary" service nobody decommissioned. Discovery is the reconciliation step between what an organization thinks it runs and what actually answers packets. For defenders it is the attack-surface baseline; for testers it is the map that makes every later phase efficient. Skipping it and scanning blindly wastes time and generates misleading results against rate-limited infrastructure.
2.2 What Nmap Does and Does Not Establish
Nmap establishes observed network behavior at a point in time: a host responded, a port accepted or refused a connection, a service banner matched a probe. It does not establish that a service is vulnerable (version strings lie, banners are configurable), that a silent host is absent (it may be filtered or dropping probes), or that an open port is a defect (business context decides that). Treating scan output as ground truth rather than evidence is the single most common interpretation error in either direction.
2.3 Architecture and Scan Stages
The official documentation and source describe a pipeline of responsibilities: command-line parsing and target specification, scan orchestration (nmap.cc, scan_engine.cc in the public repo), host discovery, port-scan techniques, response collection and state classification, service/version probing, OS fingerprinting via TCP/IP stack quirks, NSE script execution, and output generation. The diagram below shows these as conceptual stages deliberately not as internal classes, threads, or APIs, because Nmap's internals do not map one-to-one onto this tidy picture.
Mermaid Architecture Diagram
The same architecture expressed as Mermaid source (rendered copy embedded; source preserved in Appendix A):
flowchart TD
A[Command Line & Target Specification] β B[Scan Configuration & Orchestration]
B β C[Host Discovery]
C β D[Port Scanning Logic]
D β E[Probe Transmission & Response Collection]
E β F[Port State Classification]
F β G[Service & Version Detection]
F β H[OS Fingerprinting]
G β I[NSE Script Execution]
I β J[Output Generation: -oN / -oX / -oG]
H β J
J β K[Result Interpretation & Reporting]
2.4 Target Specification and Host Discovery
Targets are specified in CIDR, ranges, or lists from a file (-iL). Host discovery (-sn, formerly -sP; -PS/-PA/-PU for tailored probes) determines which addresses get port-scanned at all a skipped discovery phase means Nmap assumes hosts are up and scans everything, which is slow and noisy. In my fictional lab, discovery on the authorized 10.10.0.0/24 range:
$ nmap -sn 10.10.0.0/24
Starting Nmap 7.94 ( https://nmap.org )
Nmap scan report for 10.10.0.5
Host is up (0.00040s latency).
Nmap scan report for 10.10.0.10
Host is up (0.00087s latency).
Nmap scan report for 10.10.0.20
Host is up (0.0012s latency).
Flags and limits: -sn sends discovery probes (ICMP echo, TCP SYN to 443, TCP ACK to 80, ICMP timestamp the exact set depends on privileges) and skips port scanning. The results prove only that three hosts answered the chosen probes; a host with a firewall that silently drops all of them would be missed, which is why discovery evidence is labeled "responsive hosts," not "existing hosts."
2.5 Port States
Every scanned port is classified into one of five states, and each state is a statement about observed packets, not about intent:
The two ambiguous states deserve respect. open|filtered usually means a UDP port or a scan technique that cannot distinguish drop from open; closed|filtered appears in idle scan contexts. Both are invitations to gather more evidence a version probe, a different technique not to guess.
2.6 TCP SYN Scanning vs TCP Connect Scanning
A TCP SYN scan (-sS, the default when privileges allow) sends a bare SYN and interprets the reply: SYN/ACK means open, RST means closed, silence-after-retries means filtered. When the scanner sees SYN/ACK it records "open" and immediately sends RST, never completing the handshake which is why -sS historically drew less attention from naive logging. A TCP connect scan (-sT) performs the full three-way handshake through the operating system; it works without privileges but is slower and louder, and the OS itself completes connections the application never asked for. Same classification logic, different side effects: privilege requirements, speed, and what shows up in host logs.
These are simplified flows. Stateful firewalls, asymmetric routing, packet loss, rate limiting, middleboxes, and unusual TCP stacks all perturb the ideal exchange which is exactly why Nmap retransmits and why a "filtered" verdict is a statement about observed silence, not about a firewall's intent.
2.7 UDP Scanning and the Ambiguity of Missing Responses
UDP has no handshake. An open UDP port typically produces silence (nothing is listening to trigger a response unless the application answers), a closed port produces an ICMP port-unreachable, and a filtered port produces silence or an ICMP admin-prohibited. The painful consequence: open and filtered UDP ports are often indistinguishable in one pass, so Nmap reports open|filtered and slows down dramatically as it retransmits against the void. UDP scanning (-sU) selected on key ports is worth it DNS, SNMP, NTP, DHCP but budget the time and expect ambiguity, not certainty.
2.8 Filtering, Timeouts, Retransmissions, and Rate Limiting
Timeouts and retransmissions are the mechanism behind both accuracy and slowness. Nmap sends probes, waits, retransmits unacknowledged probes (the count scales with timing templates), and escalates only when evidence justifies it. Rate limiting on the target or an IDS deliberately tarpitting the scanner stretches every timeout. This is why -T4 is my lab default while -T5 on a fragile target manufactures packet loss and, with it, phantom "filtered" verdicts. Scan timing is a trade between evidence quality and courtesy; both matter in authorized engagements.
2.9 Service, Version, and OS Detection
Once ports are classified, -sV opens conversations: Nmap sends service-specific probes from its probe database and matches the banners or handshakes against known signatures, reporting service names, product, and version with confidence caveats. O fingerprints operating systems by measuring TCP/IP stack quirks initial TTL, window size, option ordering against a fingerprint database, and requires both open and closed ports to be available for comparison. Neither is proof: banners are trivially rewritable, and fingerprinting degrades behind NAT or with tuned kernels. Version output is a lead for investigation, never a vulnerability conclusion.
2.10 NSE and Safe Use
The Nmap Scripting Engine (nmap.org/book/nse.html) runs Lua scripts in categories β auth, broadcast, brute, default, discovery, dos, exploit, external, fuzzer, intrusive, malware, safe, version, vuln. The vuln category deserves special caution: scripts like vuln scan for known conditions but produce findings that require verification, and some scripts (brute, dos, exploit, intrusive) are inappropriate outside a lab you own. My default posture script "default,safe" or a small explicit list such as script http-title,ssl-cert,ssh-hostkey. Every script is a program the target executes paths for; "safe" means it was written to avoid harmful side effects, not that it is free of cost.
$ nmap -sV script "default,safe" -p 22,80,443 10.10.0.10
-sV: service/version detection
script "default,safe": curated non-intrusive scripts only
-p 22,80,443: restrict to selected ports on the authorized lab host
2.11 Output Formats, Automation, and Integration
Human-readable normal output (-oN), grepable output (-oG), and XML (-oX) serve different masters. Grepable output is officially deprecated in favor of XML and other maintained formats parse -oX, not -oG, in anything you intend to keep. XML is stable, self-describing, and consumed by every serious toolchain (Metasploit, vulnerability managers, CI pipelines). Saving all three from a single run costs nothing and future-proofs the evidence:
$ sudo nmap -sS -sV -O β script "default,safe" -p- -T4 \
-oN scan_main.txt -oX scan_main.xml -oG scan_main.gnmap \
10.10.0.10 10.10.0.20
Flags: -sS SYN scan (needs root/cap_net_raw), -p- all 65535 TCP ports, -T4 aggressive but sane timing, three output formats in one pass. What it does not prove: that unlisted ports are closed rather than filtered, or that any detected version is exploitable. This command is valid only because the targets are my own authorized lab hosts the same command against an arbitrary public address would be unauthorized scanning, full stop.
2.12 A Repeatable Fictional Lab Assessment
The complete assessment sequence for the topology in Figure 2.3, with every host and result illustrative:
Step 1 β Define the authorized range
$ nmap -sn 10.10.0.0/24 # discovery: responsive hosts only
Step 2 β TCP SYN scan with service detection
$ sudo nmap -sS -sV -p- -T4 -oX full_tcp.xml 10.10.0.10 10.10.0.20
Fictional excerpt:
10.10.0.10: 22/tcp open ssh OpenSSH 8.9p1
80/tcp open http nginx 1.18.0
10.10.0.20: 22/tcp open ssh OpenSSH 8.9p1
21/tcp filtered ftp
873/tcp open rsync (protocol version 31)
Step 3 β UDP on selected ports
$ sudo nmap -sU -p 53,123,161 top-ports 20 10.10.0.20
Fictional excerpt: 161/udp open|filtered snmp ambiguous by design
Step 4 β Interpret the ambiguity
161/udp open|filtered gets a targeted probe rather than a guess:
$ sudo nmap -sU -sV -p 161script snmp-info 10.10.0.20
Fictional result: SNMPv2c community "public" responds β now "open" with evidence
Step 5 β Prioritize by business and security context
The exposed rsync daemon on the backup host outranks the nginx banner: it offers unauthenticated file access in this fictional lab, while the SSH service merely needs version review against advisories. Prioritization is a judgment call grounded in what the service can do, not in scan order.
Step 6 β Defensible hardening recommendations
Β· Restrict rsync to localhost or require authenticated modules (fictional lab finding).
Β· Disable SNMPv1/v2c "public" community or migrate to SNMPv3 with auth/privacy.
Β· Confirm SSH configuration (key-only auth, current patch level); the banner alone proves nothing.
Β· Re-run after hardening and diff against the saved XML to verify exposure actually closed.
Step 7 β Save results for future comparison
The -oX file from Step 2 becomes the baseline; the same command after remediation produces the diff. That before/after pair is the audit trail that makes the assessment defensible.
Step 8 β Concise command reference
Basic host discovery (responsive hosts)
nmap -sn 10.10.0.0/24
Selected TCP ports, normal output
nmap -p 22,80,443 10.10.0.10 -oN basic.txt
TCP SYN scan (requires privileges)
sudo nmap -sS -p- 10.10.0.10
TCP connect scan (no privileges needed)
nmap -sT -p 22,80,443 10.10.0.10
UDP scan of selected ports (slow, expect ambiguity)
sudo nmap -sU -p 53,123,161 10.10.0.10
Service/version detection
sudo nmap -sV -p 22,80,443 10.10.0.10
OS detection (needs open + closed ports, privileges)
sudo nmap -O 10.10.0.10
XML output for automation (preferred over deprecated -oG)
sudo nmap -sS -sV -p- -oX scan.xml 10.10.0.10
2.13 Python XML Parser
The following script (also delivered as nmap_xml_report.py alongside this document) parses Nmap XML into a concise report. It treats banner/version strings strictly as reported attributes β never as proof of vulnerability:
#!/usr/bin/env python3
"""Parse Nmap XML output into a concise host/port/service report."""
import sys
import xml.etree.ElementTree as ET
def parse_nmap_xml(path: str) -> list[dict]:
try:
tree = ET.parse(path)
except FileNotFoundError:
sys.exit(f"error: file not found: {path}")
except ET.ParseError as exc:
sys.exit(f"error: malformed XML in {path}: {exc}")
root = tree.getroot()
if root.tag != "nmaprun":
sys.exit("error: unexpected root element; not an nmap XML file")
hosts = []
for host in root.iter("host"):
entry = {"addr": "unknown", "status": "unknown",
"ports": [], "os": []}
addr = host.find("address")
if addr is not None:
entry["addr"] = addr.get("addr", "unknown")
status = host.find("status")
if status is not None:
entry["status"] = status.get("state", "unknown")
for port in host.iter("port"):
svc = port.find("service")
entry["ports"].append({
"port": port.get("portid", "?"),
"proto": port.get("protocol", "?"),
"state": (port.find("state").get("state", "?")
if port.find("state") is not None else "?"),
"service": svc.get("name", "?") if svc is not None else "?",
"version": (f'{svc.get("product", "")} '
f'{svc.get("version", "")}'.strip()
if svc is not None else ""),
})
os_el = host.find("os")
if os_el is not None:
m = os_el.find("osmatch")
if m is not None:
entry["os"].append(m.get("name", "?"))
hosts.append(entry)
return hosts
def main() -> None:
if len(sys.argv) != 2:
sys.exit("usage: nmap_xml_report.py <nmap.xml>")
for h in parse_nmap_xml(sys.argv[1]):
os_guess = f" os: {', '.join(h['os'])}" if h["os"] else ""
print(f"host {h['addr']} status={h['status']}{os_guess}")
for p in h["ports"]:
ver = f" ({p['version']})" if p["version"] else ""
print(f" {p['proto']}/{p['port']} {p['state']:<14}"
f"{p['service']}{ver}")
print("\nnote: service banners/versions are reported attributes, "
"not evidence of vulnerability; verify separately.")
if name == "main":
main()
On the fictional lab XML, the script prints one block per host with status, each open port's protocol, state, service name, and reported version β the exact evidence set needed for triage, and nothing more. Error handling covers missing files, malformed XML, and non-nmap documents; the closing note encodes the discipline that a version string is a lead, never a conclusion.
2.14 Operator Mistakes and Responsible Scanning
Common operator mistakes: scanning without authorization (legally and ethically disqualifying, regardless of intent); assuming silence equals absence; trusting -sV banners as vulnerability proof; running -T5 or intrusive NSE categories against fragile infrastructure; scanning arbitrary public addresses "because the internet is public"; and archiving only human-readable output with no machine-readable baseline. Defensive considerations cut both ways: rate limiting and tarpits exist partly to manage scanners, and a responsible operator respects them by adjusting timing rather than overpowering them.
Responsible-Scanning Checklist
β Written authorization naming the exact ranges and window
β Targets restricted to the authorized scope lab or approved ranges only
β Discovery before port scanning; responsive-host list reviewed
β Timing template chosen for target fragility (-T3/-T4 default posture)
β NSE limited to default/safe or explicitly justified scripts
β XML output archived as the diff-able baseline
β Results interpreted as evidence, with ambiguity flagged not guessed
β No version string treated as a vulnerability without separate verification
2.15 Further Reading and Medium Metadata
Further Reading:
Β· Nmap Reference Guide β https://nmap.org/book/
Β· Official Nmap man page β https://nmap.org/book/man.html
Β· Nmap Scripting Engine documentation β https://nmap.org/book/nse.html
Β· Official Nmap source repository β https://github.com/nmap/nmap