Snowlock's Blog

From 30 Seconds to 2 Seconds: Building a Concurrent TCP Port Scanner in Python

I have been wanting to dive hands-on into cybersecurity for a while, and after some research, I thought the best first project was to build a basic TCP port scanner in python.

Using python's inbuilt socket library, I had a working prototype ready.

The Benchmark

The next obvious step was to benchmark my project. Testing it on localhost(127.0.0.1) would tend to be extremely fast, and so I decided to give a try to scanme.nmap.org, a dedicated site provided by the Nmap team, a target generously provided specifically for testing scanners safely and legally. (but please don't spam it!)

The version 0.01 of my script took ~30s (!!) to scan only a few hundred ports. It performed a full TCP Connect handshake on a single port, waited for a response or timeout, and only then moved on to the next. Sitting idle while waiting for network timeouts was killing performance, which was very slow and so in a attempt to improve it, I decided to check out how concurrency would work in python.

Concurrency

Using concurrent.futures I was able to whip up a quick refactor (v0.02) of my script, so that there were (by default) 100 workers, each would handle a port, and it was all concurrent.

# main concurrency logic
with ThreadPoolExecutor(max_workers=max_workers) as executor:
    results = executor.map(lambda p: scan_port(target_host, p), ports_to_scan)

This is the main part of the rewrite. Instead of idling while waiting for a single port to time out, the script kept all 100 threads busy, bringing the scan time down from half a minute to under two seconds.

Metric Sequential (v0.01) Multithreaded (v0.02)
Execution Time ~30.0s <2.0s
Concurrency Strategy Single-threaded 100 Worker Threads
Relative Speedup Baseline (1x) ~15x Faster

Key Takeaways & Trade-offs

The speed improvement was massive. That said, using full TCP connect scans with basic threading isn't without its drawbacks. Some drawbacks were:

Next Plans

This current version is a great start, but there is still plenty of room to improve both performance and stealth. Here is what I am planning to tackle in the next few iteration:

  1. Stealthier Scans: Swapping full TCP connections for raw SYN (half-open) packet crafting using scapy to reduce network noise.

  2. Banner Grabbing: Probing open ports to read service headers and automatically identify running software (like SSH, HTTP, or FTP).

  3. asyncio Refactor: Benchmarking Python's single-threaded asyncio event loop against ThreadPoolExecutor to see which handles high concurrency better.

  4. CLI & Export Tools: Adding argparse support for custom port ranges and outputting scan results directly to JSON.

Source Code & Repository

You can check out the full source code, test it out, or follow along with future updates over on GitHub:

github.com/snowlock-dev/pyScan