How to Build a Network Scanner with Python

How to Build a Network Scanner with Python
Photo by Tima Miroshnichenko on Pexels

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

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.

Stay in the loop — join 125,000+ IT professionals following Networkyy: Instagram · Facebook · Threads · Medium
Scroll to Top