
How to Build a Network Scanner with Python
Building network scanners is one of those automation tasks that separates hobbyists from professionals. Whether you’re managing infrastructure, conducting security assessments, or monitoring network health, having a custom-built scanner gives you control, flexibility, and deep insight into your environment. In this article, we’ll build production-ready network scanning tools from scratch, leveraging the libraries and techniques we’ve explored in earlier parts of this series.
Table of Contents
- Understanding Network Scanning Fundamentals
- Building a Basic Port Scanner
- Advanced Scanning Techniques with Scapy
- Asynchronous Scanning for Performance
- Handling Responses and Error States
- Production Considerations and Best Practices
Understanding Network Scanning Fundamentals
Network scanning involves probing hosts and ports to determine what’s active, what services are running, and how systems respond to different types of requests. At its core, scanning relies on TCP/IP behavior: when you send a SYN packet to an open port, you get a SYN-ACK back. Closed ports return RST packets. Filtered ports might not respond at all.
Python gives us multiple approaches to network scanning. The socket library provides low-level network primitives, scapy offers packet manipulation capabilities, and asyncio enables concurrent scanning without threading overhead. Each has its place depending on your requirements.
Legal and Ethical Considerations
Before we dive into code, understand that scanning networks you don’t own or have explicit permission to scan is illegal in most jurisdictions. Use these tools only on your own infrastructure, lab environments, or with written authorization. Network scanning generates traffic that security systems will detect and potentially flag.
Building a Basic Port Scanner
Let’s start with a foundational port scanner using Python’s socket library. This scanner checks whether specific ports are open on a target host by attempting TCP connections:
import socket
import sys
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed
class PortScanner:
def __init__(self, target, timeout=1, max_workers=100):
self.target = target
self.timeout = timeout
self.max_workers = max_workers
self.open_ports = []
def scan_port(self, port):
"""Attempt to connect to a single port"""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(self.timeout)
result = sock.connect_ex((self.target, port))
sock.close()
if result == 0:
try:
service = socket.getservbyport(port, 'tcp')
except OSError:
service = 'unknown'
return {'port': port, 'state': 'open', 'service': service}
return None
except socket.gaierror:
print(f"Hostname could not be resolved: {self.target}")
return None
except socket.error as e:
print(f"Connection error on port {port}: {e}")
return None
def scan_range(self, start_port=1, end_port=1024):
"""Scan a range of ports using thread pool"""
print(f"Scanning {self.target} from port {start_port} to {end_port}")
print(f"Started at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
futures = {executor.submit(self.scan_port, port): port
for port in range(start_port, end_port + 1)}
for future in as_completed(futures):
result = future.result()
if result:
self.open_ports.append(result)
print(f"Port {result['port']}/tcp open - {result['service']}")
print(f"\nScan completed at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Found {len(self.open_ports)} open ports")
return self.open_ports
# Usage
if __name__ == "__main__":
scanner = PortScanner("192.168.1.1", timeout=0.5)
results = scanner.scan_range(1, 1024)
This scanner uses ThreadPoolExecutor to scan multiple ports concurrently, significantly improving performance over sequential scanning. The connect_ex method returns an error indicator instead of raising exceptions, making it cleaner for scanning logic. We also attempt to resolve service names using getservbyport for better readability.
For those looking to deepen their understanding of network protocols and security fundamentals before building advanced scanners, Coursera offers comprehensive courses on network security and Python programming that complement hands-on practice.
Advanced Scanning Techniques with Scapy
While socket-based scanning works well for basic TCP connection attempts, scapy gives us packet-level control for more sophisticated scanning techniques like SYN scanning, UDP scanning, and OS fingerprinting. Here’s an implementation of a SYN scanner (also called half-open scanning) that’s stealthier than full TCP connections:
from scapy.all import *
import sys
class SynScanner:
def __init__(self, target, timeout=2, verbose=False):
self.target = target
self.timeout = timeout
self.verbose = verbose
conf.verb = 0 # Suppress scapy output
def syn_scan(self, port_range):
"""Perform SYN scan on specified ports"""
open_ports = []
filtered_ports = []
# Resolve target to IP
try:
dst_ip = socket.gethostbyname(self.target)
except socket.gaierror:
print(f"Cannot resolve {self.target}")
return None
print(f"SYN scanning {dst_ip} ({self.target})")
print(f"Ports: {port_range[0]}-{port_range[-1]}\n")
# Create SYN packets for all ports
for port in port_range:
# Build SYN packet
src_port = RandShort()
syn_packet = IP(dst=dst_ip)/TCP(sport=src_port, dport=port, flags='S')
# Send and wait for response
response = sr1(syn_packet, timeout=self.timeout, verbose=0)
if response is None:
filtered_ports.append(port)
if self.verbose:
print(f"Port {port}: Filtered (no response)")
elif response.haslayer(TCP):
if response[TCP].flags == 0x12: # SYN-ACK
open_ports.append(port)
print(f"Port {port}/tcp: OPEN")
# Send RST to close connection gracefully
rst_packet = IP(dst=dst_ip)/TCP(sport=src_port, dport=port, flags='R')
send(rst_packet, verbose=0)
elif response[TCP].flags == 0x14: # RST-ACK
if self.verbose:
print(f"Port {port}/tcp: Closed")
elif response.haslayer(ICMP):
if int(response[ICMP].type) == 3 and int(response[ICMP].code) in [1, 2, 3, 9, 10, 13]:
filtered_ports.append(port)
if self.verbose:
print(f"Port {port}: Filtered (ICMP unreachable)")
print(f"\nScan complete: {len(open_ports)} open, {len(filtered_ports)} filtered")
return {'open': open_ports, 'filtered': filtered_ports}
def udp_scan(self, ports):
"""Simple UDP scan implementation"""
dst_ip = socket.gethostbyname(self.target)
open_or_filtered = []
print(f"UDP scanning {dst_ip} on {len(ports)} ports\n")
for port in ports:
udp_packet = IP(dst=dst_ip)/UDP(dport=port)
response = sr1(udp_packet, timeout=self.timeout, verbose=0)
if response is None:
open_or_filtered.append(port)
print(f"Port {port}/udp: Open|Filtered")
elif response.haslayer(ICMP):
if int(response[ICMP].type) == 3 and int(response[ICMP].code) == 3:
if self.verbose:
print(f"Port {port}/udp: Closed")
return open_or_filtered
# Usage
if __name__ == "__main__":
scanner = SynScanner("192.168.1.1", timeout=1, verbose=False)
results = scanner.syn_scan(range(20, 100))
SYN scanning is more sophisticated than basic TCP connect scans. It sends only SYN packets and analyzes responses without completing the three-way handshake, making it harder to log and detect. The scanner interprets different TCP flags (SYN-ACK for open, RST for closed) and ICMP responses (for filtered ports).
Many network automation professionals enhance their packet analysis skills through interactive exercises on platforms like DataCamp, where you can practice packet manipulation in controlled environments.
Asynchronous Scanning for Performance
For scanning large networks or comprehensive port ranges, asynchronous I/O provides better performance than threading. Python’s asyncio library enables thousands of concurrent connections with minimal overhead. While we can’t use scapy directly with asyncio, we can use asyncio for socket-based scanning.
Building an Async Network Scanner
Asyncio shines when you need to scan hundreds of hosts or thousands of ports. The event loop manages all connections efficiently without the context-switching overhead of threads. This approach is particularly valuable when building network monitoring tools that need to continuously scan large infrastructure.
Handling Responses and Error States
Production network scanners must handle numerous edge cases: timeouts, connection refused, network unreachable, DNS resolution failures, and firewall-induced packet drops. Proper error handling differentiates reliable tools from fragile scripts.
Response Interpretation
Different responses indicate different port states. An RST packet means the port is closed but the host is reachable. No response could mean the port is filtered by a firewall, the host is down, or packets are being dropped. ICMP unreachable messages provide additional context about why connections fail. Your scanner should differentiate between these states and report them accurately.
Rate Limiting and Evasion
Aggressive scanning triggers intrusion detection systems and can overwhelm target systems. Implement rate limiting by adding delays between requests, randomizing scan order, and spreading scans over time. The timeout parameter balances scan speed against accuracy—too short and you’ll miss slow-responding services; too long and scans take forever.
Production Considerations and Best Practices
Moving from a working script to a production tool requires additional considerations:
Logging and Reporting
Implement comprehensive logging using Python’s logging module. Record scan parameters, timestamps, results, and errors. Export results in machine-readable formats like JSON or CSV for integration with other tools. Consider storing results in databases for historical comparison and change detection.
Configuration Management
Hard-coded values make tools inflexible. Use configuration files (YAML, JSON, or INI) to define scan targets, port ranges, timeouts, and output preferences. Environment variables work well for credentials and sensitive parameters. The configparser and PyYAML libraries simplify configuration management.
Privilege Requirements
SYN scanning and raw packet creation require root/administrator privileges because they involve raw socket access. Socket-based connect scans work with normal user privileges. Design your tools to gracefully degrade or clearly communicate when elevated privileges are needed.
Network Impact
Scanning generates significant network traffic. On production networks, coordinate with network teams, scan during maintenance windows, and implement bandwidth throttling. Some organizations require change requests before any scanning activity. Document your scanning activities and maintain audit trails.
Integration Points
Network scanners rarely operate in isolation. Build integration hooks for vulnerability databases, asset management systems, and security orchestration platforms. REST APIs, webhooks, and message queues enable your scanner to participate in larger automation workflows.
Testing and Validation
Test scanners against known environments where you control the infrastructure. Set up test networks with VirtualBox or Docker containers running various services. Verify that your scanner correctly identifies open, closed, and filtered ports. False positives and false negatives both undermine scanner credibility.