Advertisement
Open Source Projects by Phil Schwartz

Building a Python Port Scanner with Custom Banner Grabbing

Network reconnaissance remains a valuable skill for sysadmins and developers who need to audit their own infrastructure. From a small coffee shop WiFi setup in Brisbane to a multi-tenant data centre in Sydney, knowing what's exposed on a network helps teams tighten defences before someone else points out the gaps. A Python-based port scanner with banner grabbing gives you a fast, scriptable way to fingerprint running services without pulling in heavyweight tooling.

This guide walks through assembling a lightweight scanner from scratch. The goal isn't to compete with Nmap's full feature set but to understand each component — sockets, connection timeouts, regex parsing, and concurrent workers — so you can adapt the script for your own lab environment. Along the way, you'll see how to extract version strings from SSH, HTTP, and FTP daemons, and how to log the results in a way that's easy to filter later.

Scaffolding the scanner with core Python modules

Start with a clean virtual environment so dependencies don't bleed into other projects:

python3 -m venv scanner-env
source scanner-env/bin/activate
pip install argparse

The argparse module handles command-line flags for target hosts, port ranges, and timeout values. A typical interface might accept something like python3 scanner.py --target 192.168.1.0/24 --ports 22,80,443 --timeout 2.0.

When you're sweeping a /24 across the office subnet in Melbourne, that kind of flexibility makes the script practical for everyday audits. Keep the entry point minimal — a main() function that parses arguments, then delegates scanning to a worker module. This separation pays off when you eventually want to add CSV export or post results into a search backend without rewriting the top-level logic.

Building a connection layer with sockets

The socket module is the backbone of any homegrown scanner. For each port, you create an AF_INET, SOCK_STREAM connection, set a timeout, attempt connect_ex(), and close the socket. The return code tells you whether the port is open, closed, or filtered by a stateful firewall. Many Australian networks — particularly those routing through the NBN — present slightly higher latency than direct fibre links, so generous timeouts are worth tuning per environment rather than hard-coding.

A basic probe function looks like this:

def probe(host, port, timeout):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.settimeout(timeout)
        try:
            result = s.connect_ex((host, port))
            return result == 0
        except socket.error:
            return False

Wrapping the socket in a context manager guarantees file descriptors get released, especially important when scanning thousands of hosts at once. On Linux, ulimits can be raised with ulimit -n 65535 if you hit descriptor exhaustion — a common gotcha when developing scanners that fan out aggressively across a corporate VLAN.

Adding banner grabbing with regex parsing

Opening a socket is half the job. The other half is reading whatever greeting the service sends. SSH daemons typically announce themselves before any handshake, often with a string like SSH-2.0-OpenSSH_9.6p1. HTTP servers wait for a request, but a minimal HEAD / HTTP/1.0\r\n\r\n triggers headers you can capture. FTP services send a 220 response with the server name.

Implementing this looks roughly like:

def grab_banner(s, port, timeout):
    if port in (80, 443):
        s.sendall(b"HEAD / HTTP/1.0\r\n\r\n")
    data = s.recv(1024)
    return data.decode(errors='ignore').strip()

For services that don't speak first — MySQL, for instance — sending a probe packet triggers a handshake you can read. A regex like r"SSH-\d+\.\d+-(.*)" extracts the software version cleanly. You can extend the parser to flag outdated banners, such as OpenSSH builds affected by recent CVEs. Some defenders even feed these fingerprints into spreadsheets before submitting them through incident-reporting channels run by the ACSC's ReportCyber portal.

Multiplexing scans with concurrent workers

A single-threaded scanner feels glacial when sweeping a full subnet. The concurrent.futures module brings you thread pooling without much ceremony:

with ThreadPoolExecutor(max_workers=100) as pool:
    futures = [pool.submit(scan_one, host, p) for p in ports]
    for f in as_completed(futures):
        ...

Threading fits TCP scans well because most of the time is spent waiting for network I/O. Too many in-flight connections just queue at the kernel, so start with around 30 workers and tune from there. If you need even more speed, swapping in asyncio with open_connection() and a Semaphore gives finer control without the GIL contention you might worry about on heavy workloads.

Capturing exceptions from each future matters just as much as launching them. Wrap the scan call so connection resets and DNS failures are logged per host rather than silently dropped — a habit that pays off when a junior dev runs the script against a /16 and asks why only half the hosts returned data.

Running it safely and sharing the output

A scanner is a tool, and tools need guardrails. Always restrict targets to networks you own or have written permission to test. Many Australian firms bake this principle into their acceptable-use policies, partly to align with the Privacy Act obligations that govern how network data is collected and stored. Saving results to a SQLite database with timestamps makes audits reproducible without leaving sensitive scans in random text files on a shared drive.

For long-term reuse, package the script with a setup.cfg, document it on the project's README, and include a sample .gitignore. If your work overlaps with protecting public-facing SSH endpoints, the DenyHosts project offers a complementary approach to blocking repeated brute-force attempts. Pairing that kind of intrusion defence with a scanner you control gives you a solid picture of both your attack surface and your existing mitigations — exactly what you'd want before tucking the laptop away at the end of the workday.