Creating a network scanner in Python involves building a script that can identify active hosts, discover open ports, and sometimes even detect services running on a target network or specific IP addresses. This powerful tool is fundamental for network administrators and security professionals to understand network topology and potential vulnerabilities.
What is a Python Network Scanner?
A Python network scanner is a program that uses network protocols to probe hosts and ports, reporting on their status. This can range from a simple script that checks if a host is alive to a sophisticated tool that maps out an entire network's services and operating systems.
Essential Tools and Libraries for Network Scanning
Python offers several built-in modules and third-party libraries that are crucial for developing network scanning capabilities:
socket
: The core Python module for low-level network communication. It allows you to create sockets, connect to hosts, and send/receive data, making it ideal for port scanning.subprocess
: Used to run external system commands, such asping
ornmap
, from within your Python script.argparse
: An excellent module for parsing command-line arguments, allowing users to specify target IPs, port ranges, and other options when running your scanner.Scapy
: A powerful third-party library for packet manipulation. It allows you to forge or decode packets of a large number of protocols, send them on the wire, capture them, match requests and replies, and much more. It's incredibly versatile for advanced scanning techniques like ARP scans or crafting custom packets.- Installation:
pip install scapy
- Installation:
datetime
/time
: Useful for timing the scanning process and for logging.
Step-by-Step Guide to Building a Basic Network Scanner
Developing a network scanner typically follows a structured approach, allowing for modularity and scalability. Here's how to build one in Python:
Step 1: Importing Necessary Modules
The first step is to import all the required modules. This sets up your script with the functionalities it will need.
import socket
import subprocess
import argparse
import sys
from datetime import datetime
import threading # For concurrent scanning
Step 2: Handling Command-Line Arguments
To make your scanner user-friendly and flexible, you'll want to allow users to pass arguments via the command line, such as the target IP address or hostname and the range of ports to scan. The argparse
module is perfect for this.
def get_arguments():
parser = argparse.ArgumentParser(description="A simple Python network scanner.")
parser.add_argument("-t", "--target", dest="target", help="Target IP address or hostname (e.g., 192.168.1.1 or example.com)")
parser.add_argument("-p", "--ports", dest="ports", help="Port range to scan (e.g., 1-1024 or 80,443,8080)")
options = parser.parse_args()
if not options.target:
parser.error("[-] Please specify a target, use --help for more info.")
return options
# Example usage:
# options = get_arguments()
# target = options.target
# port_range_str = options.ports
This code snippet defines arguments for the target and the ports. You'd then parse the port_range_str
to create a list of integers for scanning.
Step 3: Implementing Network Scanning Functionality
This is the core of your scanner. It involves writing functions to perform host discovery (e.g., ping) and port scanning.
-
Host Discovery (Ping Scan): Check if a host is active.
def is_host_up(ip_address): try: # -c 1 for 1 packet, -w 1 for 1 second timeout (Linux/macOS) # -n 1 for 1 packet, -w 1000 for 1000ms timeout (Windows) param = "-n" if sys.platform.startswith("win") else "-c" command = ["ping", param, "1", ip_address] response = subprocess.call(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return response == 0 except Exception: return False
-
Port Scanning: Attempt to connect to specific ports to determine if they are open.
def scan_port(ip_address, port): try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(1) # 1 second timeout result = sock.connect_ex((ip_address, port)) # Returns 0 if successful, otherwise an error code if result == 0: try: service = socket.getservbyport(port) except OSError: service = "Unknown" return True, service else: return False, None except socket.error as e: return False, None finally: sock.close() def perform_port_scan(target_ip, ports_to_scan): print(f"\n[*] Scanning ports on {target_ip}...") open_ports = [] for port in ports_to_scan: status, service = scan_port(target_ip, port) if status: open_ports.append({'port': port, 'service': service}) return open_ports
Step 4: Formatting and Printing Results
After scanning, you need to present the information clearly. This involves printing which hosts are up and which ports are open, along with any identified services.
def print_results(target, start_time, end_time, open_ports, host_is_up):
print("-" * 50)
print(f"Scanning Target: {target}")
print(f"Scan Started at: {start_time.strftime('%Y-%m-%d %H:%M:%S')}")
print("-" * 50)
if not host_is_up:
print(f"Host {target} appears to be down or unreachable.")
print(f"Scan Finished at: {end_time.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Scan Duration: {end_time - start_time}")
return
if open_ports:
print("Open Ports:")
# Display results in a table for clarity
print(f"{'Port':<8} {'Service':<15}")
print(f"{'----':<8} {'-------':<15}")
for p in open_ports:
print(f"{p['port']:<8} {p['service']:<15}")
else:
print("No open ports found within the specified range.")
print("-" * 50)
print(f"Scan Finished at: {end_time.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Scan Duration: {end_time - start_time}")
print("-" * 50)
Putting It All Together (Main Script Logic):
def main():
options = get_arguments()
target = options.target
port_range_str = options.ports if options.ports else "1-1024" # Default port range
# Resolve hostname to IP address
try:
target_ip = socket.gethostbyname(target)
except socket.gaierror:
print(f"[-] Hostname could not be resolved: {target}")
sys.exit()
ports_to_scan = []
if '-' in port_range_str:
start_port, end_port = map(int, port_range_str.split('-'))
ports_to_scan = range(start_port, end_port + 1)
else:
ports_to_scan = [int(p) for p in port_range_str.split(',')]
print(f"[*] Starting scan on {target_ip} ({target})...")
start_time = datetime.now()
host_up = is_host_up(target_ip)
open_ports = []
if host_up:
open_ports = perform_port_scan(target_ip, ports_to_scan)
end_time = datetime.now()
print_results(target, start_time, end_time, open_ports, host_up)
if __name__ == "__main__":
main()
Advanced Scanning with Scapy
For more sophisticated scanning, like ARP poisoning, crafting custom packets, or stealth scanning (SYN scans), Scapy
is invaluable.
from scapy.all import *
def arp_scan(ip_range):
"""
Performs an ARP scan to discover active hosts on a local network.
"""
print(f"\n[*] Performing ARP scan on {ip_range}...")
try:
ans, unans = srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst=ip_range), timeout=2, verbose=0)
active_hosts = []
for s, r in ans:
active_hosts.append({'IP': r.psrc, 'MAC': r.hwsrc})
return active_hosts
except Exception as e:
print(f"[-] Error during ARP scan: {e}")
return []
# Example usage:
# active_hosts = arp_scan("192.168.1.1/24")
# if active_hosts:
# print("\nActive Hosts (ARP Scan):")
# for host in active_hosts:
# print(f" IP: {host['IP']}, MAC: {host['MAC']}")
Best Practices and Considerations
- Permissions: Always ensure you have explicit permission before scanning any network you do not own or manage. Unauthorized scanning can be illegal and lead to severe consequences.
- Error Handling: Implement robust
try-except
blocks to gracefully handle network issues, timeouts, and invalid inputs. - Performance: For scanning large port ranges or multiple hosts, consider using
threading
orasyncio
to perform scans concurrently, significantly speeding up the process. - Stealth vs. Speed: Basic
socket.connect_ex
scans are "full connect" scans, which are easily detectable. More advanced scanners use SYN (half-open) scans, often requiring raw socket access (and thus root privileges on Linux) or libraries like Scapy, to be less conspicuous. - Resource Management: Always close sockets (
sock.close()
) after use to prevent resource leaks.
Comparing Basic Socket Scanning vs. Scapy
Feature | Basic Socket Scanner (e.g., socket module) |
Scapy |
---|---|---|
Ease of Use | Simpler for basic TCP/UDP connect scans. | Steeper learning curve, but powerful. |
Protocols | Primarily TCP/UDP connections. | Supports virtually all protocols (IP, ARP, ICMP, DNS, HTTP, etc.). |
Packet Crafting | Limited to standard connections. | Full control over packet fields. |
Scan Types | Full-connect TCP, basic UDP. | SYN scans, ARP scans, custom packet attacks, many more. |
Requirements | Standard user privileges (for connect scans). | Often requires root/admin privileges for raw socket access. |
Performance | Can be slow without concurrency. | Fast, especially for raw packet manipulation. |
Use Case | Simple port scanning, network status checks. | Network security research, penetration testing, network troubleshooting, advanced attacks. |
By following these steps and understanding the underlying principles, you can effectively build and utilize network scanners in Python for various purposes, from network monitoring to security auditing.