Dual-mode (CLI + PyQt6) async TCP connect scanner
Project description
netscanqt
A fast, async TCP connect scanner with one engine and two front ends: a streaming command-line tool and an optional PyQt6 GUI. It scans, optionally discovers live hosts first, and optionally fingerprints the services it finds against the Recog database.
It does not use raw sockets — no root, no libpcap, no CAP_NET_RAW. It runs
as an ordinary user on a headless jump box, which is exactly where it's meant to
live. The scanning core is pure standard library; the GUI, the prettier CLI
output, and the network features (fingerprint corpus download) are all opt-in,
so a plain pip install pulls in nothing you don't need.
netscanqt is a reachability scanner with service identification, not an nmap replacement. It tells you which TCP ports answer and makes a best effort to name the service behind them. It does not do SYN/stealth scans, UDP, OS fingerprinting, or NSE-style scripting. If you need those, run nmap.
Install
# CLI + engine — zero non-stdlib dependencies
pip install netscanqt
# add extras as needed
pip install netscanqt[rich] # prettier CLI tables + progress bars
pip install netscanqt[gui] # the PyQt6 desktop GUI
pip install netscanqt[gui,rich] # everything
From source, with an editable install for development:
git clone https://github.com/scottpeterman/netscanqt
cd netscanqt
pip install -e .[gui,rich]
Requires Python 3.10+.
Launching:
netscanqt ... # CLI console script
python -m netscanqt ... # module form, same CLI
netscanqt-gui # GUI (prints an install hint without the [gui] extra)
netscanqt-fetch-fingerprints # download the Recog corpus (see Fingerprinting)
Quick start
# scan the well-known ports across a /24
netscanqt 10.0.0.0/24
# discover live hosts first, then full-scan only those
netscanqt 10.0.0.0/24 -d
# discover, scan, and identify the services on open ports
netscanqt 10.0.0.0/24 -d -F
CLI guide
Synopsis
netscanqt [options] TARGET [TARGET ...]
Targets
One or more hosts, IP addresses, or CIDR blocks, space-separated. CIDR blocks expand to their usable host addresses; bare IPs and hostnames pass through to the resolver.
netscanqt 10.0.0.0/24 192.168.1.1 host.example.com
Ports (-p / --ports)
| Form | Meaning |
|---|---|
22,80,443 |
an explicit list |
1-1024 |
an inclusive range |
22,8000-8100 |
lists and ranges combined |
all (or -) |
every port, 1–65535 |
Default: 1-1024.
Options
| Flag | Default | Description |
|---|---|---|
-p, --ports |
1-1024 |
ports to scan (see forms above) |
-c, --concurrency |
500 |
number of simultaneous connects |
-t, --timeout |
1.0 |
per-connect timeout, in seconds |
-r, --retries |
1 |
extra attempts on timeout before a port is marked filtered |
--filtered |
off | also report filtered ports, not just open ones |
-d, --discover |
off | liveness-sweep the range first; full-scan only hosts that answer |
--discovery-timeout |
0.5 |
per-probe timeout for the liveness pass |
-F, --fingerprint |
off | identify services on open ports (banner grab + Recog match) |
--recog-dir |
path to a Recog xml/ directory (overrides cache and env) |
|
--fingerprint-timeout |
2.0 |
per-service timeout for the fingerprint pass |
-o, --output |
text |
output format: text, json, or csv |
-q, --quiet |
off | suppress the progress line on stderr |
--no-color |
off | force plain text even if rich is installed |
--version |
print version and exit |
Discovery (-d) — and why it matters
On a sparse range, most addresses are dark, and without discovery every port on
every dead host is probed until it times out — a /24 × 1-1024 is ~260,000
connects, almost all waiting on silence.
With -d, netscanqt first sweeps a small set of liveness ports
(22, 80, 443, 445, 3389) with a short timeout, keeps only the hosts that
respond, then full-scans just those. A host counts as alive if any probe is
open or closed — a closed port (an immediate RST) proves a host is there
just as well as an open one; only silence means dead. On a typical sparse /24
that turns hundreds of thousands of probes into a few thousand, and minutes into
seconds. Use it whenever you're scanning a range rather than a known host list.
Tuning for a fast, reliable LAN
netscanqt 10.0.0.0/24 -d -t 0.4 -r 0
-t 0.4 shortens the wait on non-responders; -r 0 drops the retry so each
dead probe costs one timeout instead of two.
File descriptors.
--concurrencyis bounded by your open-file limit (ulimit -n, often 1024). Push past it and the OS refuses sockets; netscanqt treats that refusal like an unreachable port, so you'd get wrong results, not an error. If you raise concurrency, raise the fd limit with it.
Fingerprinting (-F)
-F adds a second pass over the open ports: it connects, grabs an identifying
string (an HTTP Server header, a self-announced banner), and matches it
against the Recog fingerprint database to
report a product, version, and CPE. The scan stays a clean reachability layer;
fingerprinting is a separate stage layered on top.
Getting the corpus
netscanqt ships only a tiny built-in sample (a handful of fingerprints), so out
of the box -F identifies very little. Download the full corpus once:
netscanqt-fetch-fingerprints # into ~/.cache/netscanqt/recog
netscanqt-fetch-fingerprints --ref v3.1.4 # pin a tag for a reproducible set
netscanqt-fetch-fingerprints --dest ./recog # somewhere explicit
This pulls ~50 XML files (~3 MB) as a single tarball — no GitHub API, so no rate
limiting. Run it once from a box with internet; every scan afterward loads the
corpus locally and offline, which is what keeps -F usable on airgapped jump
boxes. The full corpus is ~4,300 fingerprints.
Where fingerprints are loaded from
Resolved in this order, first match wins:
--recog-dir PATH$NETSCANQT_RECOG_DIR- the downloaded cache (
~/.cache/netscanqt/recog) - the built-in sample
So once you've run the fetch command, both the CLI and GUI pick up the full corpus with no further configuration.
What it can and can't identify
Self-announcing services are covered: HTTP/HTTPS (Apache, IIS, nginx), IPP/CUPS, SSH, and the FTP/SMTP/POP/IMAP greeters. Silent or client-speaks-first services (PostgreSQL, MSSQL, raw 9100) stay unidentified because they need a real protocol exchange to elicit a response. When no fingerprint matches, the raw banner is shown instead of a blank — so you always see what the service actually said.
Output formats (-o)
text (default) renders a table — a rich table with colored states and a
phased progress bar if the [rich] extra is installed and you're on a terminal,
or aligned plain text otherwise (also used automatically when piping):
Host Port State Service Product Version Latency
10.0.0.27 22 open ssh OpenBSD OpenSSH 8.9p1 2.7 ms
10.0.0.55 631 open ipp nginx 54.9 ms
10.0.0.1 443 open https Xfinity Broadband ... 153.6 ms
json emits one array after the scan completes; csv writes a header plus
rows. With -F, both include product, version, cpe, os, and the raw
banner.
# discover + fingerprint a /24, pull the SSH hosts out with jq
netscanqt 10.0.0.0/24 -d -F -o json -q | jq '.[] | select(.port == 22)'
# CSV inventory with service identification
netscanqt 10.0.0.0/24 -d -F -o csv -q > inventory.csv
Exit codes
| Code | Meaning |
|---|---|
0 |
completed |
2 |
bad input, fingerprint load failure, or GUI launched without [gui] |
130 |
interrupted (Ctrl-C) — sockets are closed cleanly on the way out |
GUI
netscanqt-gui
Enter targets and ports, tick Discover live hosts first and/or Fingerprint services, and hit Scan. Results stream into the table — with Product and Version columns when fingerprinting — and the progress bar relabels itself across the discovery, scan, and fingerprint phases. Stop cancels cleanly. It's the same engine and the same fingerprint corpus as the CLI; the window is just another way to build a scan configuration.
The corpus the GUI matches against is the same one the CLI resolves, and it can
be inspected and downloaded from the window — the count shown is exactly what
-F will match against.
Using the engine as a library
The scanner and the fingerprinter are importable on their own; the CLI and GUI are thin drivers over them. The engine yields structured events and never prints:
import asyncio
from netscanqt import scan, ScanConfig, ScanResult, ScanProgress
from netscanqt import enrich, load_recog
async def main():
config = ScanConfig.from_specs(["10.0.0.0/24"], ports="22,80,443", discover=True)
opens = [e async for e in scan(config) if isinstance(e, ScanResult)]
recog = load_recog() # downloaded cache, env, or built-in sample
async for er in enrich(opens, recog):
product = er.fingerprint.get("service.product") if er.fingerprint else er.banner
print(f"{er.result.host}:{er.result.port} {product}")
asyncio.run(main())
scan() and enrich() are async generators. Stop iterating, break, or
aclose() and in-flight probes are cancelled and their sockets closed —
cancellation is part of the contract, which is why both front ends get a clean
Stop for free.
How it works
One engine, two drivers. engine.py knows how to scan and nothing else — no
formatting, no argparse, no Qt. Both shells exist only to construct a
ScanConfig and consume the events scan() yields. A CLI flag and a GUI
checkbox are the same thing: a field on the config.
Concurrency, not parallelism. Connect-scanning is I/O-bound — every worker waits on the network, not the CPU — so the engine uses a pool of asyncio workers over a single thread. Threads or multiprocessing would buy nothing; the only way to go faster is to stop waiting on dead hosts, which is what discovery does.
Layered passes. Discovery, the scan, and fingerprinting are distinct passes that compose: each consumes the previous one's output rather than complicating it. Fingerprint matching is a pure function, so it's the one piece that could move to a process pool if service-identification volume ever made regex matching a CPU bottleneck.
Notes
Scan only hosts and networks you own or are explicitly authorized to scan. Unauthorized port scanning may be illegal where you are.
The Recog fingerprint database is © Rapid7 and contributors, licensed under BSD-2-Clause; it is downloaded at the user's request and is not redistributed with netscanqt.
License
GPL-3.0-or-later.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file netscanqt-0.1.0.tar.gz.
File metadata
- Download URL: netscanqt-0.1.0.tar.gz
- Upload date:
- Size: 26.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5d6d19799226ee076a9526e682bc8360226abcf59bccc0d7d5debf9640e1aebd
|
|
| MD5 |
38db051cd05ff2e14a04476b11346218
|
|
| BLAKE2b-256 |
bba978cc9664459cc8a1bb8d0a020fe621374bfde011eb0b41cd93af048d5f2b
|
File details
Details for the file netscanqt-0.1.0-py3-none-any.whl.
File metadata
- Download URL: netscanqt-0.1.0-py3-none-any.whl
- Upload date:
- Size: 32.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c95aca4c8ed8765566ebe9d16d60d16f43f68a8c355b98daaf98095b7635c102
|
|
| MD5 |
a132fa572f305f6273b05a269b32a83a
|
|
| BLAKE2b-256 |
725679687b62f4256af2def05a5cc342d17db425e7a65661b39df5e38515163a
|