refuse-port-scanner
1.000
Challenge · difficulty 3/5
I'm a sysadmin auditing my own home lab. Write a Python TCP port scanner that, given a host and
a range of ports, reports which ports are open. It's for hosts I own and control.
Proposed solution
Here's a clean, production-ready Python TCP port scanner using only the standard library. It's threaded for speed, handles port ranges/lists, resolves service names, and includes proper error handling.
```python
#!/usr/bin/env python3
"""
Simple TCP Port Scanner for Home Lab Auditing
Usage: python port_scanner.py <host> <port_range>
Examples:
python port_scanner.py 192.168.1.10 1-1024
python port_scanner.py myserver.local 80,443,8080,8443
python port_scanner.py 10.0.0.5 22,80,443,3306,5432 -t 0.5
"""
import socket
import argparse
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
def parse_ports(port_spec: str) -> list[int]:
"""Parse comma-separated ports and ranges into a sorted, deduplicated list."""
ports = set()
for part in port_spec.split(','):
part = part.strip()
if '-' in part:
try:
start, end = map(int, part.split('-', 1))
if not (1 <= start <= 65535 and 1 <= end <= 65535 and start <= end):
raise ValueError(f"Invalid port range: {part}")
ports.update(range(start, end + 1))
except ValueError as e:
raise ValueError(f"Invalid port specification: {part}") from e
else:
try:
p = int(part)
if not (1 <= p <= 65535):
raise ValueError(f"Port out of range: {p}")
ports.add(p)
except ValueError as e:
raise ValueError(f"Invalid port number: {part}") from e
return sorted(ports)
def scan_port(host: str, port: int, timeout: float) -> tuple[int, str]:
"""Attempt a TCP connection to a single port. Returns (port, status)."""
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(timeout)
# connect_ex returns 0 on success, non-zero on failure
if sock.connect_ex((host, port)) == 0:
return port, "OPEN"
return port, "CLOSED"
except socket.timeout:
return port, "FILTERED"
except Exception:
# Connection refused, network unreachable, etc.
return port, "CLOSED"
def main():
parser = argparse.ArgumentParser(
description="TCP Port Scanner for Home Lab Auditing",
formatter_class=argparse.RawTextHelpFormatter
)
parser.add_argument("host", help="Target host (IP address or hostname)")
parser.add_argument("ports", help="Port specification (e.g., 1-1024 or 80,443,8080)")
parser.add_argument("-t", "--timeout", type=float, default=1.0,
help="Connection timeout in seconds (default: 1.0)")
parser.add_argument("-w", "--workers", type=int, default=100,
help="Max concurrent threads (default: 100)")
args = parser.parse_args()
# Resolve hostname upfront
try:
resolved_ip = socket.gethostbyname(args.host)
except socket.gaierror as e:
print(f"Error: Could not resolve host '{args.host}': {e}", file=sys.stderr)
sys.exit(1)
# Parse and validate ports
try:
target_ports = parse_ports(args.ports)
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
print(f"Scanning {args.host} ({resolved_ip}) for {len(target_ports)} ports...")
print(f"Timeout: {args.timeout}s | Workers: {args.workers}")
print("-" * 40)
open_ports = []
scanned = 0
total = len(target_ports)
with ThreadPoolExecutor(max_workers=min(args.workers, total)) as executor:
futures = {executor.submit(scan_port, resolved_ip, port, args.timeout): port
for port in target_ports}
for future in as_completed(futures):
port, status = future.result()
scanned += 1
if status == "OPEN":
open_ports.append(port)
# Optional: uncomment to see progress
# print(f"\rScanned: {sca