Network Recon Tool with Python
Build a Python tool that maps your network, scans ports, and detects ARP spoofing.
Introduction
30 Second Summary
Every device on your home network is constantly exchanging data with every other device. Your router, phone, and laptop broadcast packets thousands of times a second, but all of this activity is completely invisible without the right tools.
In this project, you will build a Python-powered network reconnaissance tool using Scapy that discovers every device on your network, captures live traffic, scans for open ports, and detects attacks in real time.
What You'll Build
Run a single command and watch your tool map every device on your network, decode the protocols flowing between them, and alert you the instant someone tries to tamper with your traffic.
By the end of this project, you'll have:
- A live network map that shows every connected device's IP and MAC address, discovered using ARP broadcasts you craft by hand.
- A real-time packet sniffer that captures traffic and decodes each packet by protocol (TCP, UDP, DNS, ICMP) so you can see exactly what is happening on your network.
- An automated recon command that port scans every discovered host, detects ARP spoofing attacks, and saves a full network report to a file.
- Secret Mission: Add DNS query logging to see every website any device on your network is trying to reach.
Are there any prerequisites?
You need basic command line familiarity (cd, ls, running scripts) and programming experience in any language. You also need a Linux Mint machine with Python 3 already installed.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters to you.
Set Up Your Recon Lab
Normal Python can send HTTP requests and read files, but it cannot craft raw network packets or listen to traffic at the wire level.
To build a tool that discovers devices, sniffs traffic, and detects attacks, you need two things: a packet capture library (libpcap) and a Python framework that lets you construct and decode packets layer by layer (Scapy). In this step, you will install both.
In this step, get ready to:
- Install the libpcap packet capture library.
- Install Scapy 2.7.0 via pip.
- Verify everything works by launching Scapy's interactive shell.
Install the packet capture library
Scapy uses libpcap under the hood to apply BPF (Berkeley Packet Filter) expressions when sniffing traffic. Without it, Scapy's filter parameter will not work.
- Install libpcap-dev by running this command:
sudo apt-get install libpcap-dev
What does this command do?
apt-get install downloads and installs system packages on Debian-based Linux distributions like Linux Mint.
libpcap-dev provides the C headers and compiled libraries that Scapy needs to compile BPF filter expressions at runtime.
- When prompted to confirm the installation, type y and press Enter.
- You should see output ending in a line that confirms the package was set up successfully.
Seeing a permission error?
Make sure you included sudo at the start of the command. Installing system packages requires root privileges on Linux.
help me fix a permission error when running apt-get install
Install Scapy
Scapy is a Python library that lets you forge, send, capture, and decode network packets at every protocol layer. You will use it to build every feature in your reconnaissance tool.
- Install Scapy by running this command:
pip install scapy
Why Scapy instead of Nmap or raw sockets?
Scapy gives you raw access to construct packets layer by layer. You build an Ethernet frame, stack an ARP header on top, and send it yourself. This teaches you how protocols actually work at the byte level.
Tools like Nmap hide these details behind a CLI. Raw sockets require pages of byte-manipulation boilerplate. Scapy sits in the sweet spot: explicit protocol layers without drowning in low-level code.
- You should see pip download and install the package, ending with a success message that includes the version number 2.7.0.
Pip command not found?
Try pip3 install scapy instead. On some Linux Mint systems the Python 3 pip binary is named pip3.
help me fix pip not being found on Linux Mint
Verify the installation
Scapy includes an interactive shell that loads all of its packet-crafting tools into a Python REPL. Launching it with sudo confirms both that Scapy installed correctly and that it can access the network interfaces it needs.
Before you run the next command, what do you think you will see if both libpcap and Scapy are installed correctly?
- Launch the Scapy interactive shell by running:
sudo scapy
You should see a welcome banner that includes the Scapy version (2.7.0) followed by an interactive >>> prompt.
- Exit the Scapy shell by typing this command at the >>> prompt:
exit()
- Confirm you are back at your normal terminal prompt.
✔️ I see the Scapy welcome banner
Your recon lab is ready. Scapy and libpcap are installed and working together.
ⓧ I see an error about missing libpcap
This means the libpcap-dev package did not install properly. Run the install command again:
sudo apt-get install libpcap-dev
- After it completes, try sudo scapy again.
help me fix a libpcap error when launching Scapy
ⓧ Command not found when running sudo scapy
This means Scapy did not install into a path that sudo can see. This happens when pip installs to your user's local bin but sudo uses the system path.
- Reinstall Scapy so it is available system-wide by running:
sudo pip install scapy
- Then try sudo scapy again.
help me fix scapy command not found
Your recon lab is locked and loaded. Next up, you will write your first Python script that uses Scapy to discover every device on your network.
Discover Your Network with ARP
Your recon lab is ready. Now it is time to use it.
Before you can monitor traffic or scan for vulnerabilities, you need to know what devices exist on your network. ARP (Address Resolution Protocol) is how every device on a local network discovers its neighbours. It maps IP addresses to physical MAC addresses. Your first tool sends an ARP request to every possible address on your subnet and listens for replies.
In this step, get ready to:
- Create a Python script that crafts ARP broadcast packets with Scapy.
- Send the packet to your local subnet and collect responses.
- Display every discovered device with its IP and MAC address.
Create the scanner script with imports
Every capability you build in this project lives in a single file. You will add functions to it step by step.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create a project folder and move into it by running these commands:
mkdir network-recon
cd network-recon
- Create scanner.py with the imports your tool needs by running this command:
#!/usr/bin/env python3
"""Network Reconnaissance and Intrusion Detection Tool"""
import argparse
import sys
import time
from datetime import datetime
from collections import defaultdict
from scapy.all import (
ARP, Ether, IP, TCP, UDP, ICMP, DNS, DNSQR,
srp, sr, sniff, send, conf
)
What do these imports do?
The standard library imports (argparse, sys, time, datetime, defaultdict) handle command-line arguments, timing, and data structures you will use in later steps.
The scapy.all import pulls in the protocol layers (ARP, Ether, IP, TCP, etc.) and the send/receive functions (srp, sr, sniff) that let you craft and capture raw packets.
- Save scanner.py.
- Confirm the file exists by running:
ls
You should see scanner.py listed in the output.
Build the ARP discovery function
ARP works by broadcasting a "who has this IP?" message to every device on the network segment. Each device that recognises the IP replies with its MAC address. Your function crafts that broadcast and collects the replies.
- In scanner.py, add the following function below your imports:
def discover_network(target_subnet):
"""Discover devices on the local network using ARP."""
print(f"\n[*] Scanning {target_subnet}...")
# Create an ARP request targeting the subnet
arp_request = ARP(pdst=target_subnet)
# Wrap it in an Ethernet frame addressed to broadcast
broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
# Combine layers into one packet
packet = broadcast / arp_request
# Send at layer 2 and collect responses
answered, _ = srp(packet, timeout=2, verbose=False)
# Extract IP and MAC from each response
devices = []
for sent, received in answered:
devices.append({
"ip": received.psrc,
"mac": received.hwsrc
})
# Print a formatted table
print(f"\n[+] Discovered {len(devices)} device(s):\n")
print(f"{'IP Address':<18}{'MAC Address':<20}")
print("-" * 38)
for device in devices:
print(f"{device['ip']:<18}{device['mac']:<20}")
return devices
What does this function do?
- ARP(pdst=target_subnet) crafts an ARP "who-has" request for every IP in the given range.
- Ether(dst="ff:ff:ff:ff:ff:ff") wraps it in a broadcast Ethernet frame so every device on the segment receives it.
- srp() sends the packet at layer 2 (Ethernet level) and waits 2 seconds for replies. It returns two lists: answered and unanswered.
- received.psrc is the responding device's IP address. received.hwsrc is its MAC address.
- Save scanner.py.
Wire up the command-line interface and run it
Your function exists, but you need a way to call it from the terminal with arguments. Python's argparse module lets you define flags like --discover and --iface that control which capability runs.
- Below discover_network(), add the main() function and the entry point:
def main():
parser = argparse.ArgumentParser(
description="Network Reconnaissance and Intrusion Detection Tool"
)
parser.add_argument(
"--discover", metavar="SUBNET",
help="Discover devices on a subnet (e.g., 192.168.1.0/24)"
)
parser.add_argument(
"--iface", metavar="INTERFACE",
help="Network interface to use (e.g., wlan0)"
)
args = parser.parse_args()
# Set custom interface if provided
if args.iface:
conf.iface = args.iface
# Route to the correct function based on flags
if args.discover:
discover_network(args.discover)
else:
parser.print_help()
sys.exit(1)
if __name__ == "__main__":
main()
What does main() do?
- argparse.ArgumentParser defines the command-line interface for your tool. Each add_argument call registers a flag the user can pass.
- conf.iface is Scapy's global config for the default network interface. Setting it means all Scapy functions use that interface automatically.
- The if __name__ == "__main__" block ensures main() only runs when you execute the script directly.
- Save scanner.py.
Now you need to find your local subnet so you can tell the scanner where to look.
- Find your subnet by running:
ip route
How do I read this output?
Look for a line that starts with an IP range ending in /24 (for example 192.168.1.0/24 or 192.168.0.0/24). That is your local subnet. Copy it for the next command.
Before you run the scanner, what do you think it will print? How many devices do you expect to find on your network?
- Run the scanner against your subnet using sudo (required for raw packet access):
sudo python3 scanner.py --discover [[SUBNET="your subnet, e.g. 192.168.1.0/24"]]
You should see a table of devices with their IP and MAC addresses. At minimum, your router (gateway) will appear. If your phone or other devices are on the same network, they show up too.
Not seeing any devices?
- Make sure you included sudo before the command. Scapy needs root privileges to send raw packets.
- Double-check your subnet matches the one from ip route. A typo means you are scanning a range with no devices.
- If you are on a VPN, disconnect it first. VPNs route traffic away from your local network.
Still stuck? Help me troubleshoot why my ARP scan returns no devices.
✔️ Awesome, I've got everything!
Great. Double check you have saved scanner.py and that your output shows at least one device.
ⓧ I'd like to double check the full code
Your scanner.py should contain three sections in order:
- The shebang line, docstring, and imports (scroll up to the "Create the scanner script with imports" chunk).
- The discover_network() function (scroll up to the "Build the ARP discovery function" chunk).
- The main() function and if __name__ == "__main__" block (scroll up to the "Wire up the command-line interface" chunk).
Confirm each section is present in that order with no extra blank lines between them beyond a single separator line.
You have a working network scanner. With a single command, you can map every device on your local network. Next up, you will add live traffic sniffing to see what those devices are actually doing.
Sniff Live Traffic
Your ARP scanner tells you who is on the network. But it says nothing about what those devices are actually doing.
To understand what is happening on your network, you need to see the packets flowing across the wire in real time. In this step, you will build a live traffic sniffer that captures each packet, identifies its protocol, and decodes the key fields into plain English.
In this step, get ready to:
- Add a sniffer function using Scapy's sniff() with a real-time packet callback.
- Decode protocol layers (ARP, TCP, UDP, DNS, ICMP) and display relevant fields.
- Print a traffic summary showing packet counts by protocol.
Build the packet callback
Scapy's sniff() function captures packets from your network interface. You pass it a callback via the prn parameter, and Scapy calls that function once for every packet it sees.
- In scanner.py, add the following function below discover_network(). This creates the sniffer function and its inner callback that handles ARP and TCP packets:
def sniff_traffic(interface=None, count=50):
"""Sniff live traffic and decode protocol layers."""
protocol_counts = defaultdict(int)
def process_packet(packet):
"""Callback to process each captured packet."""
proto = "OTHER"
info = ""
if packet.haslayer(ARP):
proto = "ARP"
arp = packet[ARP]
if arp.op == 1:
info = f"{arp.psrc} is asking about {arp.pdst}"
elif arp.op == 2:
info = f"{arp.psrc} is at {arp.hwsrc}"
elif packet.haslayer(TCP):
proto = "TCP"
ip = packet[IP]
tcp = packet[TCP]
info = f"{ip.src}:{tcp.sport} -> {ip.dst}:{tcp.dport} [Flags: {tcp.flags}]"
What does this code do?
protocol_counts is a defaultdict that automatically starts at zero for any new key. It tallies how many packets of each type the sniffer sees.
process_packet() is a nested function that Scapy calls for every captured packet. It uses haslayer() to identify the protocol, then extracts relevant fields. For ARP, op == 1 is a request ("who has this IP?") and op == 2 is a reply ("I'm at this MAC").
Add remaining protocol handlers and launch the sniffer
The callback currently handles ARP and TCP. Now you will add UDP, DNS, and ICMP decoding, then call sniff() to start capturing.
- Still inside scanner.py, add the following code directly below the TCP elif block you just wrote (still indented inside process_packet):
elif packet.haslayer(UDP):
if packet.haslayer(DNS):
proto = "DNS"
ip = packet[IP]
if packet.haslayer(DNSQR) and packet[DNS].qr == 0:
qname = packet[DNSQR].qname.decode()
info = f"{ip.src} -> {ip.dst} Query: {qname}"
else:
info = f"{ip.src} -> {ip.dst} Response"
else:
proto = "UDP"
ip = packet[IP]
udp = packet[UDP]
info = f"{ip.src}:{udp.sport} -> {ip.dst}:{udp.dport}"
elif packet.haslayer(ICMP):
proto = "ICMP"
ip = packet[IP]
icmp = packet[ICMP]
info = f"{ip.src} -> {ip.dst} Type: {icmp.type}"
protocol_counts[proto] += 1
print(f" [{proto:<5}] {info}")
What does the DNS check do?
DNS rides on top of UDP, so the code first checks for UDP, then looks inside for a DNS layer. The field packet[DNS].qr == 0 means it is a query (not a response). packet[DNSQR].qname gives the raw domain name being requested.
The last two lines run for every packet regardless of type. They increment the protocol counter and print a formatted line showing the protocol label and decoded info.
- Now add the sniff call and traffic summary. Place this code below the process_packet function definition, still inside sniff_traffic (indented one level under the outer function):
print(f"\n[*] Sniffing {count} packets (Ctrl+C to stop early)...")
print("-" * 60)
sniff(
iface=interface,
prn=process_packet,
count=count,
store=False
)
print("\n" + "-" * 60)
print("\n[+] Traffic Summary:")
for proto, count_val in sorted(protocol_counts.items()):
print(f" {proto:<8}: {count_val} packets")
print(f" {'TOTAL':<8}: {sum(protocol_counts.values())} packets")
return protocol_counts
What does this section do?
sniff() is Scapy's main capture function. The prn parameter receives your callback. count limits how many packets to capture (default 50). store=False tells Scapy not to keep packets in memory since you are processing them on the fly.
After sniffing completes, the summary loop prints how many packets of each protocol type were captured.
Wire the sniffer into your command-line interface
The function exists, but your script does not know when to call it. You need to add two new flags to the argument parser and connect them.
- In scanner.py, find the --discover argument block inside main(). Add these two new arguments directly below it (before the --iface argument):
parser.add_argument(
"--sniff", action="store_true",
help="Sniff live network traffic"
)
parser.add_argument(
"--iface", metavar="INTERFACE",
help="Network interface to use (e.g., wlan0)"
)
parser.add_argument(
"--count", type=int, default=50,
help="Number of packets to capture when sniffing (default: 50)"
)
- Find the if args.discover: block at the bottom of main(). Replace the else: branch so it now includes an elif for sniffing:
if args.discover:
discover_network(args.discover)
elif args.sniff:
sniff_traffic(interface=args.iface, count=args.count)
else:
parser.print_help()
sys.exit(1)
- Save scanner.py.
Before you run the sniffer, what do you think you will see? Will the output be neatly labelled packets, or raw byte dumps?
- Run the sniffer to capture 20 packets by running this command:
sudo python3 scanner.py --sniff --count 20
While it is running, generate some traffic. Open a website on your phone or another device on the same network. You should see lines scrolling by with protocol labels like [TCP ], [DNS ], and [ARP ], followed by decoded source IPs, destination IPs, and ports. After 20 packets, a traffic summary prints the count per protocol.
Not seeing any packets?
- Make sure you are running with sudo. Packet sniffing requires root privileges on Linux.
- If you are on Wi-Fi, try specifying your interface explicitly with --iface wlan0 (or whatever ip link show reports as your active interface).
- If the script hangs waiting for packets, try browsing a website or pinging your gateway to generate traffic.
Still stuck? Why is my Scapy sniffer not capturing any packets on Linux Mint?
✔️ Awesome, I've got everything!
Great. Double check you have saved scanner.py and that both --discover and --sniff work when you run the script.
ⓧ I'd like to double check the full code
Here is the complete scanner.py as it should look at the end of this step. Scroll through and confirm each section is present in order.
Your file should contain these sections in order:
- The shebang line and imports (lines 1-13).
- The discover_network() function (unchanged from Step 2).
- The new sniff_traffic() function with process_packet handling ARP, TCP, UDP/DNS, and ICMP.
- The sniff call with iface, prn, count, and store=False, followed by the traffic summary loop and return protocol_counts.
- The main() function with --discover, --sniff, --iface, and --count arguments, plus the if/elif/else dispatch at the bottom.
You can now see exactly what every device on your network is doing at the packet level. Next up, you will use this visibility to probe devices for open ports and running services.
Scan for Open Ports
Your tool can discover devices and watch traffic flow across the wire. But you still have no idea what services those devices are running.
A TCP SYN scan probes specific ports on a target to find which ones respond, revealing exposed services that could be entry points for an attacker. This is the core reconnaissance technique used in penetration testing. In this step, you will build it from scratch.
In this step, get ready to:
- Build a TCP SYN port scanner that crafts raw packets for 18 common ports.
- Detect open ports by analysing SYN-ACK responses and cleanly close half-open connections.
- Wire the scanner into your CLI and scan your gateway for exposed services.
Add the port scanning function
A TCP SYN scan works by sending a SYN packet (the first step of the TCP handshake) to each port. If the port is open, the target replies with a SYN-ACK. If it is closed, the target sends a RST or no response at all.
- In scanner.py, find the end of the sniff_traffic() function (the return protocol_counts line).
- Add the following scan_ports function below it, with a blank line separating the two functions:
def scan_ports(target_ip, ports=None):
"""Perform a TCP SYN scan on a target."""
if ports is None:
ports = [21, 22, 23, 25, 53, 80, 110, 135, 139, 443, 445, 993, 995, 3306, 3389, 5900, 8080, 8443]
print(f"\n[*] Scanning {target_ip} for open ports...")
print(f"[*] Checking {len(ports)} common ports...\n")
open_ports = []
# Craft a SYN packet for each port and send them all at once
packets = [IP(dst=target_ip) / TCP(dport=port, flags="S") for port in ports]
answered, _ = sr(packets, timeout=2, verbose=False)
# Check each response for SYN-ACK (flags 0x12 = open port)
for sent, received in answered:
if received.haslayer(TCP) and received[TCP].flags == 0x12:
open_ports.append(sent[TCP].dport)
# Send RST to cleanly close the half-open connection
send(IP(dst=target_ip) / TCP(dport=sent[TCP].dport, flags="R"), verbose=False)
if open_ports:
print(f"[+] Open ports on {target_ip}:")
for port in sorted(open_ports):
print(f" Port {port} - OPEN")
else:
print(f"[-] No open ports found on {target_ip}")
return open_ports
What does this code do?
- The ports list contains 18 well-known ports (SSH, HTTP, HTTPS, DNS, MySQL, RDP, etc.). Each one represents a service an attacker would look for.
- IP(dst=target_ip) / TCP(dport=port, flags="S") builds a raw TCP SYN packet aimed at one port on the target. The / operator stacks protocol layers together.
- sr() sends all 18 packets at layer 3 and waits for responses. It returns matched (answered) and unmatched pairs.
- Flags 0x12 means SYN+ACK. That is the target saying "yes, this port is open, let's connect." A RST or no response means the port is closed.
- After finding an open port, send(... flags="R") sends a RST to tear down the half-open connection. Without this, the target's connection table fills up with stale entries.
Connect the scanner to your CLI
The function exists but your CLI does not know about it yet. You need a --portscan flag that accepts a target IP address.
- In the main() function, find the --sniff argument definition. Add the following new argument directly after it (before the --iface argument):
parser.add_argument(
"--portscan", metavar="TARGET",
help="Scan a target IP for open ports"
)
- Now scroll down to the if/elif chain at the bottom of main(). Find this line:
elif args.sniff:
sniff_traffic(interface=args.iface, count=args.count)
- Add this new elif branch directly below it:
elif args.portscan:
scan_ports(args.portscan)
What does this wiring do?
The --portscan flag stores the target IP address the learner passes on the command line. The elif branch calls scan_ports() with that IP, kicking off the SYN scan.
- Save scanner.py.
Not sure where to place the new code?
The scan_ports function goes between sniff_traffic() and main(). The new argument goes alongside the other parser.add_argument calls, and the new elif goes after the elif args.sniff: block.
help me place the scan_ports function and --portscan flag in my scanner.py
✔️ Awesome, I've got everything!
Make sure you have saved scanner.py with both the new function and the updated main().
ⓧ I'd like to double check the full code
Your scanner.py is now over 30 lines, so confirm these sections are present in this order:
- Imports at the top (argparse, sys, time, datetime, defaultdict, scapy.all).
- discover_network() function (unchanged from Step 2).
- sniff_traffic() function (unchanged from Step 3).
- scan_ports() function (the new code from this step).
- main() with --discover, --sniff, --portscan, --iface, and --count arguments.
- The if/elif chain dispatches discover_network, sniff_traffic, then scan_ports in that order.
Run your first port scan
You need to target a device on your network. Your router (gateway) is the best first target because it almost always has at least one port open (typically port 53 for DNS or port 80 for its admin interface).
- Find your gateway IP by running:
ip route | grep default
Look for the IP address after via in the output. This is typically something like 192.168.1.1 or 192.168.0.1.
Before you run the scan, predict: do you think your router will have any open ports? Which ones might be open and why?
- Run the port scan against your gateway by running this command (replacing the IP with your gateway):
sudo python3 scanner.py --portscan [[GATEWAY_IP="your gateway IP, e.g. 192.168.1.1"]]
You should see at least one open port reported. Most routers expose port 53 (DNS) or port 80 (admin web interface).
What is a half-open connection?
A normal TCP connection requires a three-way handshake: SYN, SYN-ACK, ACK. Your scanner only sends the first SYN and listens for the SYN-ACK. It never completes the handshake, leaving the connection "half-open."
Sending a RST after detecting an open port tears down that half-open state on the target. This is polite scanning. Without it, the target accumulates stale entries in its connection table.
No open ports found?
If you see [-] No open ports found, your target may have a firewall dropping all SYN packets silently. Try scanning a different device discovered in Step 2, or try your own machine's IP (127.0.0.1).
Make sure you are running with sudo. Without root privileges, Scapy cannot send raw packets.
my port scan is not finding any open ports on my network
Your recon tool can now discover devices, decode live traffic, and probe for open services. Next up, you will add the ability to detect when someone is attacking your network with ARP spoofing.
Detect ARP Spoofing
Your scanner can now discover devices, sniff traffic, and scan ports. But it has no way of knowing if a device on the network is lying about its identity.
ARP has no built-in authentication. Any device can broadcast "I am 192.168.1.1" and the rest of the network just believes it. This is exactly how man-in-the-middle attacks work. In this step, you will build a detection system that compares live ARP replies against a known device table and raises an alert whenever the MAC address for a known IP changes.
In this step, get ready to:
- Build an ARP spoof detection function that compares live ARP replies to known device mappings.
- Wire up a --monitor flag that discovers devices first, then watches for spoofing.
- Verify the monitor reports no alerts on a clean network.
Build the ARP spoof detection function
The detection strategy is straightforward. You already have a list of devices from your ARP scan. Each device has a known IP-to-MAC mapping. If a new ARP reply arrives claiming a known IP belongs to a different MAC address, something is wrong.
- In scanner.py, add the following function below scan_ports. This is the first part of the function, containing the ARP table and the detection callback:
def detect_arp_spoof(known_devices, interface=None, duration=30):
"""Monitor ARP traffic and detect spoofing attempts."""
arp_table = {device["ip"]: device["mac"] for device in known_devices}
alerts = []
def check_arp(packet):
"""Check ARP replies against known mappings."""
if packet.haslayer(ARP) and packet[ARP].op == 2:
sender_ip = packet[ARP].psrc
sender_mac = packet[ARP].hwsrc
if sender_ip in arp_table:
if arp_table[sender_ip] != sender_mac:
alert_msg = (
f"[CRITICAL] ARP Spoofing Detected!\n"
f" IP: {sender_ip}\n"
f" Known MAC: {arp_table[sender_ip]}\n"
f" Attacker MAC: {sender_mac}\n"
f" Time: {datetime.now().strftime('%H:%M:%S')}"
)
print(f"\n{alert_msg}")
alerts.append(alert_msg)
- Directly below the code you just added, paste the rest of the function. This adds the actual sniff call and the summary output:
print(f"\n[*] Monitoring ARP traffic for {duration} seconds...")
print(f"[*] Watching {len(arp_table)} known device(s) for spoofing...")
print("[*] Press Ctrl+C to stop early.\n")
sniff(
iface=interface,
prn=check_arp,
filter="arp",
timeout=duration,
store=False
)
if not alerts:
print("\n[+] No ARP spoofing detected during monitoring period.")
else:
print(f"\n[!] {len(alerts)} spoofing alert(s) detected!")
return alerts
What does this function do?
- arp_table is a dictionary that maps each known IP to its legitimate MAC address, built from the devices list your ARP scan returned.
- check_arp is a callback that inspects every ARP reply (op == 2). If a reply claims a known IP belongs to a MAC that does not match the table, it fires a CRITICAL alert.
- sniff() with filter="arp" uses a BPF filter so only ARP packets reach the callback. The timeout parameter stops monitoring after the specified duration.
- Save scanner.py.
The function is complete but not callable from the command line yet. You will wire it up next.
Wire up the --monitor flag
The monitor mode needs to do two things in sequence: first discover all devices (to build the known ARP table), then sniff ARP traffic and compare it against that table.
- In scanner.py, scroll down to the main() function.
- Find this argument definition:
parser.add_argument(
"--portscan", metavar="TARGET",
help="Scan a target IP for open ports"
)
- Add this new argument directly below it:
parser.add_argument(
"--monitor", metavar="SUBNET",
help="Monitor ARP traffic for spoofing on a subnet"
)
- Now find the elif args.portscan: handler near the bottom of main():
elif args.portscan:
scan_ports(args.portscan)
- Add this handler directly below it:
elif args.monitor:
devices = discover_network(args.monitor)
detect_arp_spoof(devices, interface=args.iface)
Why does --monitor run discover first?
The detection function needs a baseline of known IP-to-MAC mappings to compare against. By running discover_network() first, you build that baseline from the current state of your network. Then detect_arp_spoof() watches for any reply that contradicts it.
- Save scanner.py.
Seeing a syntax error when you save?
- Make sure the detect_arp_spoof function is at the same indentation level as scan_ports (top-level, no extra indentation).
- Check that the elif args.monitor: block sits at the same indentation as the other elif blocks in main().
- Confirm you have the closing parenthesis on the f-string in alert_msg.
Help me debug a syntax error in my detect_arp_spoof function.
Test the ARP monitor
Before you run this, think about what you expect to see. No one is attacking your network right now, so what should the monitor report?
- Run the monitor by executing this command (replace the subnet with your own):
sudo python3 scanner.py --monitor 192.168.1.0/24
You will see the discovery phase run first, listing your devices. Then the monitor starts and watches ARP traffic for 30 seconds. Since no attack is happening, it ends with:
[+] No ARP spoofing detected during monitoring period.
What would a real alert look like?
If an attacker on your network sent a forged ARP reply claiming your router's IP belongs to their MAC address, you would see a CRITICAL alert with the conflicting details. The known MAC (your router's real address) would be shown alongside the attacker's MAC. This is passive detection only. Your tool watches and reports. It does not perform any attacks.
Monitor exits immediately with no output?
- Make sure you are running with sudo. Sniffing with a BPF filter requires root privileges.
- Verify libpcap-dev is installed. The filter="arp" parameter requires libpcap for BPF compilation.
- Check that your subnet matches your actual network. Run ip addr to confirm your interface and subnet.
Help me troubleshoot why my ARP monitor is not capturing any traffic.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete scanner.py at this point. Scroll up and confirm your file contains each section in this order:
- Imports and shebang at the top.
- discover_network() function.
- sniff_traffic() function with process_packet callback.
- scan_ports() function.
- detect_arp_spoof() function with check_arp callback.
- main() with --discover, --sniff, --portscan, --monitor, --iface, and --count flags.
Your tool now has four capabilities: discovery, sniffing, port scanning, and ARP spoof detection. Next up, you will tie all of these together into a single automated reconnaissance command that runs every phase and produces a full network report.
Tie It All Together
Your script has four separate capabilities: network discovery, traffic sniffing, port scanning, and ARP spoof detection.
A real penetration testing tool runs all of these in one pass and produces a clean report you can hand to someone. In this step, you will tie everything into a single automated reconnaissance workflow that executes all phases and saves the findings to a file.
In this step, get ready to:
- Add a --recon mode that runs all phases in sequence.
- Generate a structured text report with your network findings.
- Save the report to a timestamped file on disk.
Add the automated recon mode
Each of your four functions already prints its own output. A recon function calls them in order: discover devices first, then port scan each one, then monitor for ARP spoofing.
- In scanner.py, find the detect_arp_spoof function and scroll past it to the blank space before def main():.
- Add the following run_recon function above main():
def run_recon(target_subnet, interface=None):
"""Run full automated reconnaissance."""
print("=" * 60)
print(" NETWORK RECONNAISSANCE REPORT")
print(f" Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("=" * 60)
# Phase 1: Discovery
devices = discover_network(target_subnet)
# Phase 2: Port scan all discovered hosts
scan_results = {}
for device in devices:
open_ports = scan_ports(device["ip"])
if open_ports:
scan_results[device["ip"]] = open_ports
# Phase 3: ARP monitoring
alerts = detect_arp_spoof(devices, interface=interface, duration=30)
What does this code do?
This function orchestrates all three reconnaissance phases in the order a professional tool would run them.
- Phase 1 calls discover_network() to find every device on the subnet.
- Phase 2 loops through each discovered device and port scans it, storing results in a dictionary keyed by IP.
- Phase 3 feeds the known devices into detect_arp_spoof() to watch for spoofing for 30 seconds.
Now wire this function into the CLI so you can trigger it with a flag.
- Scroll down to the main() function.
- Find the --monitor argument block. Add this new argument directly below it:
parser.add_argument(
"--recon", metavar="SUBNET",
help="Run full automated reconnaissance on a subnet"
)
- Now find the elif args.monitor: branch in the argument handling section.
- Add this branch directly below the --monitor block (before the else: that prints help):
elif args.recon:
run_recon(args.recon, interface=args.iface)
- Save scanner.py.
- Run the recon mode to confirm the three phases execute by running:
sudo python3 scanner.py --recon 192.168.1.0/24
You should see the discovery phase print a device table, the port scan check each device, and the ARP monitor run for 30 seconds before finishing.
Seeing a NameError for run_recon?
- Make sure the run_recon function is defined above main(). Python reads top to bottom, so it must exist before main() calls it.
- Check that the elif args.recon: branch sits between the elif args.monitor: branch and the final else:.
Help me debug a NameError when running my recon mode.
Generate a structured text report
The three phases print live output, but a professional tool also produces a saved report you can review later or share with a team.
- In scanner.py, find the last line of run_recon (the alerts = detect_arp_spoof(...) line).
- Add the following report-building code directly below it:
# Generate report
report_lines = []
report_lines.append("=" * 60)
report_lines.append(" NETWORK RECONNAISSANCE REPORT")
report_lines.append(f" Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
report_lines.append(f" Target: {target_subnet}")
report_lines.append("=" * 60)
report_lines.append("\n--- DISCOVERED DEVICES ---\n")
for device in devices:
report_lines.append(f" {device['ip']:<18}{device['mac']}")
report_lines.append(f"\n Total: {len(devices)} device(s)")
report_lines.append("\n--- PORT SCAN RESULTS ---\n")
if scan_results:
for ip, ports in scan_results.items():
report_lines.append(f" {ip}: ports {', '.join(map(str, ports))}")
else:
report_lines.append(" No open ports found on any host.")
What does this code do?
This builds a list of formatted strings that will become your report file. Each section mirrors a phase of the recon.
- The header records the timestamp and target subnet for context.
- The discovered devices section lists every IP and MAC pair found during Phase 1.
- The port scan section shows open ports per host, or a clean message if none were found.
Save the report to a file
The report still needs an ARP security section, a footer, and code to write everything to disk.
- Directly below the code you just added, add the remaining report code:
report_lines.append("\n--- ARP SECURITY ---\n")
if alerts:
for alert in alerts:
report_lines.append(f" {alert}")
else:
report_lines.append(" No ARP spoofing detected.")
report_lines.append("\n" + "=" * 60)
report_lines.append(" END OF REPORT")
report_lines.append("=" * 60)
report_text = "\n".join(report_lines)
filename = f"recon_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
with open(filename, "w") as f:
f.write(report_text)
print(f"\n[+] Report saved to {filename}")
return filename
What does this code do?
- The ARP security section records any spoofing alerts that fired during Phase 3.
- All the report lines are joined into a single string and written to a timestamped file like recon_report_20260810_143022.txt.
- The function prints the filename so the learner knows where to find it, then returns it for programmatic use.
- Save scanner.py.
Before you run the full recon, predict: what will you see in your terminal, and what file will appear on disk?
- Run the full automated reconnaissance by running:
sudo python3 scanner.py --recon 192.168.1.0/24
You should see the full sequence execute: a device table from discovery, port scan results for each host, 30 seconds of ARP monitoring, and finally a line confirming the report was saved.
- Confirm the report file exists by running:
ls recon_report_*.txt
You should see a file named something like recon_report_20260810_143022.txt in the output.
- View the report contents by running:
cat recon_report_*.txt
You should see a formatted report with sections for discovered devices, port scan results, and ARP security status.
Report file not appearing?
- Check that the report-building code is indented inside run_recon at the same level as the phase calls (4 spaces in).
- Make sure the return filename line is the last line of the function. If it appears too early, the file-writing code below it never executes.
- If you see a PermissionError, make sure you are running with sudo. The script needs root for packet operations, and the file is written to the current directory.
Help me figure out why my recon report file is not being created.
✔️ Awesome, I've got everything!
Double check you have saved scanner.py and confirmed the report file on disk.
ⓧ I'd like to double check the full code
Your scanner.py should now contain these sections in order. Scroll through your file and confirm each is present:
- Imports (argparse, sys, time, datetime, defaultdict, scapy.all).
- discover_network() function.
- sniff_traffic() function with process_packet() nested inside.
- scan_ports() function.
- detect_arp_spoof() function with check_arp() nested inside.
- run_recon() function with Phase 1 (discovery), Phase 2 (port scan loop), Phase 3 (ARP monitoring), report generation, and file writing.
- main() with --discover, --sniff, --portscan, --monitor, --recon, --iface, and --count arguments.
- The if __name__ == "__main__": block at the very end.
Your tool now runs a complete automated reconnaissance workflow with a single command. Next up, the Secret Mission challenges you to add DNS query logging so you can see every website any device on your network is trying to reach.
Secret mission
Log DNS Queries
Your sniffer already sees DNS packets flow by, but it does not record which domains devices are reaching out to. Every website visit, every app ping, every background sync starts with a DNS query. Add a dedicated DNS logger that captures and saves every domain any device on your network tries to reach.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources on your machine, or delete them entirely.
Resources you used:
- scanner.py (your main recon tool script).
- dns_log.txt (DNS query log created by the Secret Mission).
- recon_report_*.txt (any report files generated by --recon runs).
- Scapy 2.7.0 Python package (installed via pip).
- libpcap-dev system package.
Keep everything running
No action needed. Choose this if you plan to keep using your recon tool to monitor your network.
- Your scanner.py script stays on your machine ready to run anytime with sudo.
- Scapy and libpcap-dev remain installed for future networking projects.
- Report and log files stay in your working directory for reference.
Pause - I'll come back to this later
Nothing to pause. Everything runs locally with no background processes or services. Your files and packages stay on disk until you decide to remove them.
- No servers or daemons are running from this project.
- Pick up exactly where you left off by running sudo python3 scanner.py --help in your terminal.
Delete - I don't want to use this again
Remove all project files and uninstall the packages you installed during this project.
- Delete your project files by running:
rm scanner.py dns_log.txt recon_report_*.txt
- Uninstall Scapy by running:
pip uninstall scapy
- Remove libpcap-dev by running:
sudo apt-get remove libpcap-dev
- Confirm the files are gone by running ls in the directory where you built the project. You should no longer see scanner.py or dns_log.txt.
Nice Work!
Nice Work!
You just built a fully functional network reconnaissance and intrusion detection tool from scratch using Python and Scapy. You can now see, scan, and defend your home network at the packet level.
You've learned how to:
- Craft and send ARP broadcast packets to discover every device on your local network, mapping IPs to MAC addresses.
- Sniff live traffic and decode packets across multiple protocol layers (TCP, UDP, DNS, ICMP) in real time.
- Perform TCP SYN port scans to identify open services and detect ARP spoofing attacks by monitoring IP-to-MAC mapping changes.
- Automated the full reconnaissance workflow into a single --recon command that generates a saved network report.
- Secret Mission: Added DNS query logging to see every website any device on your network tries to reach.
Ready to quiz yourself?