Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Building a Robust Port Scanner

Hello! Welcome back to our course on architecting security products.

In the previous lesson, we established a first-principles understanding of the core port scanning techniques—TCP Connect, TCP SYN, and UDP scans. We analyzed them at the packet level, understanding what they do and the trade-offs between them in terms of speed, stealth, and required privileges.

Now, we move from the what to the how. A scanner that checks one port at a time is trivial to write but useless in practice. The real engineering challenge lies in building an engine that can execute these scans against thousands of targets and ports efficiently, reliably, and at scale. This lesson directly addresses the learning outcome: Design a modular port scanning engine with parallelism, state management, and timeout handling.

By the end of this lesson, you will understand the architectural components and design patterns required to build a high-performance, robust port scanning engine, using the world-class Nmap scanner as our primary case study.

1. From Theory to Architecture: The Three Pillars

A professional-grade port scanning engine must be built on three architectural pillars:

  1. Modularity: The engine must separate concerns. The logic for generating a SYN probe should be distinct from the logic for sending it, which should be distinct from the logic that tracks its state. This allows the engine to be extended with new scan types and easily maintained.
  2. Parallelism: To scan large networks quickly, the engine must perform many operations concurrently. This involves sending probes to multiple hosts or multiple ports simultaneously, which introduces challenges in managing network resources and avoiding self-congestion.
  3. State Management & Robustness: The network is unreliable. Packets get dropped, hosts go offline, and firewalls interfere. The engine must be a sophisticated state machine, tracking every probe sent, managing timeouts, handling retransmissions, and gracefully handling unresponsive targets without getting stuck.

To understand how these pillars are implemented in a real-world system, we will dive into the architecture of Nmap, one of the most sophisticated open-source security tools ever created.

Nmap Security Scanner - Augment Code

This resource provides a high-level overview of Nmap's source code structure. It's a great starting point to appreciate its modular design before we go deeper.

Read the 'Overview' section, which covers main.cc, nmap.h, and nmap.cc. Notice the clear separation of concerns: the entry point is minimal, delegating all the complex work to the main logic, which in turn orchestrates the various scanning capabilities. This exemplifies the modularity we're aiming for.

2. The Heart of the Engine: ultra_scan

At the core of Nmap's port scanning capability is a function called ultra_scan(). This function is the master orchestrator for all raw-packet port scans (like SYN and UDP scans). Understanding its internal structure gives us a complete blueprint for designing our own engine.

The document below is an analysis of Nmap's source code. It's dense, but it contains the exact first-principles details you appreciate.

Nmap's Core Scanning Engine Architecture

This is our deepest dive. We'll explore the core components of Nmap's scanning engine. This will form the basis of our own architectural design.

Read the section 'Architecture & Core Scanning Engine'. As you read, focus on these key concepts, which directly map to our architectural pillars: ultra_scan(): The main function that manages the entire scan lifecycle. Key Data Structures: Pay close attention to UltraProbe, HostScanStats, and GroupScanStats. These are the heart of the state management system. Think about what each one tracks and why that information is necessary. Probe Transmission Pipeline: This is the engine's main event loop. It's a beautifully designed state machine for handling new probes, retransmissions, and keep-alives. This is the core of the engine's robustness. Response Collection: Notice the different methods for raw scans (libpcap) vs. connect scans (select()). This is a key modularity point, abstracting the I/O mechanism. Timing & Congestion Control: This is the brain behind the engine's parallelism and performance. Note the concepts of RTT estimation, congestion window (cwnd), and rate limiting.

Let's distill that information into an architectural blueprint. A robust scanning engine needs:

  • A Probe Scheduler/Loop: Like ultra_scan, this loop continuously decides what to do next: send a new probe, retransmit a timed-out probe, or wait for responses.
  • State Tracking Structures:
    • A structure per-probe to track its type, target, port, and transmission status (e.g., number of retries).
    • A structure per-host to track its specific network characteristics like Round-Trip Time (RTT) and the number of probes currently "in-flight" to it.
    • A global structure to manage the overall scan progress and congestion control.
  • An Asynchronous I/O Layer: The engine cannot afford to block while waiting for a response. It must use non-blocking I/O mechanisms to send and receive packets. The Nmap source analysis mentions nsock, a library Nmap uses to abstract over platform-specific high-performance I/O APIs like epoll (Linux) and kqueue (BSD/macOS). For your background in software engineering, you can think of this as being analogous to using libraries like libuv or tokio.

3. Controlling the Engine: Parallelism and Performance Tuning

Now that we have a conceptual model for the engine's internals, let's look at how we expose control over its performance. A well-designed engine should allow the user to balance speed against stealth and network load.

The following video provides a practical look at the "knobs" Nmap provides to tune its scanning engine. Each of these knobs corresponds to a parameter in the internal structures we just studied (like HostScanStats and GroupScanStats).

Nmap - Scan Timing And Performance

This video from HackerSploit demonstrates the key performance-tuning options in Nmap. As you watch, think about how you would implement these features in the engine we're designing.

Watch the following three segments: Timing Templates (T0-T5) (starts at 02:22): Understand that these are simply presets for various underlying performance parameters. Parallelism (starts at 09:02): This covers --min-parallelism and --max-parallelism. This directly controls the size of the 'congestion window' (cwnd) we read about earlier—how many probes can be in-flight at once. Host Group Sizes (starts at 14:08): This covers --min-hostgroup and --max-hostgroup, which dictates how many hosts are scanned concurrently. This is a higher level of parallelism, managed by the orchestration engine we discussed in the first lesson.

These controls are the API to our engine's parallelism logic. By adjusting them, a user can make the scan:

  • Aggressive (-T4, -T5): High parallelism, large host groups, and low delays. This maximizes speed but is noisy and can overwhelm networks.
  • Stealthy (-T0, -T1): Low parallelism, serialized probes, and long delays. This is for evading Intrusion Detection Systems (IDS) but is extremely slow.

4. Ensuring Robustness: Timeout and Error Handling

Performance is useless if the scanner is not robust. The most critical aspect of robustness is handling timeouts correctly. What happens when a probe is sent and no response comes back?

Nmap - Scan Timing And Performance

Let's continue with the same HackerSploit video to see how Nmap handles timeouts and delays. These features are essential for a scanner that works in the real world.

Watch these two segments: Host Timeout (starts at 18:22): The --host-timeout option is a crucial fallback. It tells the engine to give up on a host that appears to be offline, preventing the entire scan from stalling. Scan Delays (starts at 23:01): The --scan-delay option injects a fixed delay between probes. This is a simple but effective form of rate-limiting, often used for stealth.

While fixed delays and timeouts are useful, a truly advanced engine uses adaptive timeouts. As we saw in the augmentcode reading, Nmap's engine constantly measures the Round-Trip Time (RTT) for each host. It then calculates a dynamic timeout for new probes to that host (typically RTT + a variance margin).

This adaptive approach is far more efficient:

  • For low-latency targets, timeouts are short, allowing the engine to quickly identify filtered ports or retransmit lost packets.
  • For high-latency targets (e.g., across the globe), timeouts are longer, preventing the engine from prematurely retransmitting packets that are simply taking a while to return.

This brings our state management full circle. The engine's state (HostScanStats) is not just for tracking completion; it's a feedback loop that constantly tunes the engine's performance based on observed network conditions.

5. Synthesizing the Architecture

We've covered the core engine loop, state management, parallelism controls, and timeout handling. Let's visualize how these components fit together. The following diagram, while designed for a web crawler, provides an excellent high-level abstraction for our port scanning engine.

Distributed Request Management System with Host Politeness
This diagram illustrates a generic, high-performance dispatch system. We can map its components directly to our port scanner architecture: 'Scheduler Output' is the list of targets/ports to scan; the 'Host Politeness Manager' represents our timing, parallelism, and rate-limiting logic; 'Host Queues' are our per-host state tracking structures; the 'Fetcher Dispatcher' is our main `ultra_scan` loop; and the 'Fetchers' are the asynchronous I/O workers sending and receiving packets.

By combining these concepts, we arrive at the design of a modular, high-performance port scanning engine:

  1. Input: A list of targets and ports from the main orchestration engine.
  2. Dispatcher: The main loop that manages a collection of host-specific queues. It populates these queues with probes.
  3. Host Manager: A component responsible for enforcing politeness and performance policies for each host (rate limits, parallelism caps, adaptive timeouts based on that host's RTT).
  4. I/O Workers: A pool of workers that pull probes from the queues.
    • They use a non-blocking, event-driven I/O model (e.g., epoll via a library like nsock) to send probes and receive responses.
    • For raw packet scans (SYN), they use libpcap to sniff for replies.
  5. State Updater: When a response is received or a probe times out, the worker notifies the dispatcher, which updates the state of the relevant probe and host (e.g., mark port as open, update RTT, increment retry counter).
  6. Output: As ports are confirmed open, closed, or filtered, the results are sent to a findings management system for storage and analysis.

Finally, at internet-scale, as the Censys paper (LINK) describes, this entire engine is run continuously. Its "two-phase" scanning approach—L4 discovery (our engine's job) feeding candidates to an L7 interrogation system—is a perfect example of this modular design in practice. The paper's discussion of "Service Refresh and Eviction" also highlights the importance of the timeout and state management logic we've designed for maintaining data accuracy over time.

Conclusion

In this lesson, we transitioned from the theory of port scanning to the architectural design of a modern scanning engine. We deconstructed the design of Nmap to understand how it achieves high performance and robustness.

Key Takeaways:

  • A modular scanning engine separates probe generation, I/O, and state management.
  • The core is an event-driven loop (like Nmap's ultra_scan) that orchestrates the sending of new probes and the retransmission of timed-out ones.
  • State is managed at multiple levels: per-probe, per-host, and globally. Per-host state is crucial for implementing adaptive timeouts based on RTT.
  • Parallelism is controlled through tunable parameters like host group sizes and the number of concurrent probes, which are often bundled into user-friendly timing templates.
  • Robustness is achieved through a combination of adaptive timeouts, retransmission logic, and a master host-timeout to prevent stalling on unresponsive targets.

Preview of the Next Lesson:

We have now designed an engine that can efficiently tell us which ports are open on a target. But an open port is just a door; we don't know what's behind it. In our next lesson, "Design a service detection and version identification engine using banner grabbing and probe-response matching," we will architect the component that connects to these open ports to figure out exactly what software is running.

Can't find a good explanation? Sign up and we'll make it for you

Sign up