Network Traffic Analyzer with Scapy
Capture live packets, detect ARP spoofing, and generate a security report.
Introduction
30 Second Summary
Every time you open a website or stream a video, your computer sends and receives hundreds of tiny data packets across the network. Most people never see this invisible traffic, but anyone on your network could be reading your unencrypted messages or impersonating your router.
In this project, you will build a command-line network traffic analyzer using Python and Scapy. It captures live packets flowing through your machine, classifies them by protocol, detects security threats, and generates a formatted security report.
What You'll Build
You run the analyzer while browsing the web and watch it reveal every DNS lookup your machine makes, every TCP connection it opens, and every unencrypted payload anyone on your network could read.
By the end of this project, you'll have:
- A live packet capture showing DNS queries, TCP connections, UDP packets, ICMP pings, and ARP requests as they flow through your machine in real time.
- A threat detection system that flags ARP spoofing attempts, SYN flood attacks, and unencrypted HTTP traffic that anyone on your network can read.
- A security analysis report with protocol breakdowns, DNS query logs, top talkers ranked by packet count, and a saved PCAP file you can open in Wireshark.
- Secret Mission: Add port scan detection that flags sources sending SYN packets to many different ports.
Are there any prerequisites?
This project runs on a Linux machine and requires sudo access for capturing network packets. You should have basic Python knowledge and Python version 3.7 or later installed.
Before We Start
Before we dive in, take a moment to lock in what you're about to build and why it matters.
Set Up Scapy
Every packet flowing through your machine carries data across multiple protocol layers. To capture and inspect these packets, your analyzer needs direct access to the network interface through raw sockets.
Scapy is the Python library built for this. Raw sockets require root privileges on Linux, so you need to verify Scapy works under sudo before writing any capture code.
In this step, get ready to:
- Create a project directory with a Python virtual environment.
- Install Scapy and verify it works with sudo privileges.
Create the project and install Scapy
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir traffic-analyzer
cd traffic-analyzer
- Confirm you are in the right folder by running:
pwd
You should see a path ending in traffic-analyzer.
- Create a Python virtual environment and activate it by running these commands:
python3 -m venv venv
source venv/bin/activate
Your terminal prompt now shows (venv) at the start of the line. This confirms the virtual environment is active.
Seeing an ensurepip error?
Some Linux distributions do not ship the venv module by default. If you see a message about ensurepip not being available, install the python3-venv package first:
sudo apt-get install python3-venv
Then re-run the python3 -m venv venv and source commands above.
Still stuck?
- Install Scapy by running:
pip install scapy==2.7.0
You should see pip downloading the package, ending with Successfully installed scapy-2.7.0.
Why Scapy?
Scapy gives you direct Python access to every packet layer and field. Tools like tshark are great for CLI analysis, but they do not let you build custom detection logic in Python.
Scapy is the standard tool for security professionals and penetration testers. It also has no external dependencies on Linux, so it works out of the box.
Pip install failed?
Make sure you see (venv) in your prompt before running pip. If the virtual environment is not active, run source venv/bin/activate first.
If you see a network error, check your internet connection and try again.
Help me fix my pip install
Verify Scapy works with sudo
Scapy captures packets using raw sockets, which require root privileges. You run Scapy with sudo, but sudo does not inherit your virtual environment's PATH.
Before you run the next command, do you think the import will succeed? Think about where sudo looks for Python versus where your virtual environment installed Scapy.
- Test that Scapy imports correctly under sudo by running:
sudo venv/bin/python3 -c "from scapy.all import sniff, IP, TCP, UDP, DNS, ICMP, ARP, Ether, Raw, wrpcap; print('Scapy ready')"
You may be prompted for your password. After entering it, you should see Scapy ready printed without errors.
Why sudo venv/bin/python3 instead of sudo python3?
When you run sudo python3, sudo uses the system Python, which does not have Scapy installed. By specifying venv/bin/python3 directly, you tell sudo to use the Python inside your virtual environment where Scapy lives.
Every time you run your analyzer in this project, you will use sudo venv/bin/python3 for this reason.
Seeing ModuleNotFoundError?
If the error says No module named 'scapy', you likely ran sudo python3 instead of sudo venv/bin/python3. Make sure you include the full path to the virtual environment's Python binary.
Also confirm you are running the command from inside the traffic-analyzer directory, where the venv folder is located.
Help me fix my Scapy import under sudo
Your environment is ready. Next up, you'll write your first packet capture and see live network traffic flowing through your machine.
Capture Live Network Traffic
Your environment is ready. Scapy is installed, and you've confirmed it can capture packets with sudo privileges.
Now it's time to put Scapy to work. In this step, you'll write a short script that captures live packets flowing through your network interface and prints what it finds.
In this step, get ready to:
- Write a packet capture script that extracts IP addresses from live traffic.
- Run the analyzer and observe live packet output.
Create analyzer.py with basic capture
Every captured packet passes through a callback function. Your callback will extract the source and destination IP addresses and print them alongside Scapy's built-in summary.
- In your traffic-analyzer directory, create a file called analyzer.py and add the packet processing function:
import sys
from scapy.all import IP, sniff
# Callback function that runs once for every captured packet
def process_packet(packet):
# Check if this packet has an IP layer
if packet.haslayer(IP):
src = packet[IP].src
dst = packet[IP].dst
# Print source, destination, and Scapy's built-in summary
print(f"{src} -> {dst} | {packet.summary()}")
What does this code do?
- packet.haslayer(IP) checks whether the captured packet contains an IP layer. Packets without one (like some low-level ARP frames) are silently skipped.
- packet[IP].src and packet[IP].dst extract the source and destination IP addresses from the packet.
- packet.summary() is a built-in Scapy method that returns a one-line description of the packet's layers.
- Save analyzer.py.
- Confirm the file was created by running:
ls
You should see analyzer.py alongside the venv directory.
- In analyzer.py, add the main function below process_packet:
def main():
# Default to capturing 20 packets
packet_count = 20
# Accept a custom count from the command line
if len(sys.argv) > 1:
packet_count = int(sys.argv[1])
print(f"Capturing {packet_count} packets... (Ctrl+C to stop early)")
print("-" * 60)
# Capture packets, calling process_packet for each one
try:
sniff(count=packet_count, prn=process_packet, store=0)
except KeyboardInterrupt:
print("\nCapture stopped.")
if __name__ == "__main__":
main()
How does sniff() work?
- count=packet_count tells Scapy to stop after capturing that many packets.
- prn=process_packet tells Scapy to call your process_packet function once for every captured packet.
- store=0 tells Scapy not to keep packets in memory after processing them. This keeps memory usage low.
- The try/except KeyboardInterrupt block lets you stop early with Ctrl+C without crashing.
- Save analyzer.py.
✔️ Awesome, I've got everything!
Great. Double check you've saved analyzer.py.
ⓧ I'd like to double check the full code
Here is the complete analyzer.py file so far. Make sure your version matches:
import sys
from scapy.all import IP, sniff
def process_packet(packet):
if packet.haslayer(IP):
src = packet[IP].src
dst = packet[IP].dst
print(f"{src} -> {dst} | {packet.summary()}")
def main():
packet_count = 20
if len(sys.argv) > 1:
packet_count = int(sys.argv[1])
print(f"Capturing {packet_count} packets... (Ctrl+C to stop early)")
print("-" * 60)
try:
sniff(count=packet_count, prn=process_packet, store=0)
except KeyboardInterrupt:
print("\nCapture stopped.")
if __name__ == "__main__":
main()
Run the analyzer and generate traffic
Before running the analyzer, make a prediction. When you capture 20 packets of mixed traffic, do you think you'll be able to tell which are DNS lookups, which are web requests, and which are pings?
- Start the analyzer by running this command:
sudo venv/bin/python3 analyzer.py
The analyzer is now listening for packets on your network interface. It will print each packet as it arrives and stop after 20.
- Open a second terminal window. You can press Ctrl+Shift+T to open a new tab in most Linux terminals.
- Generate some traffic by running these commands:
ping -c 3 8.8.8.8
curl -s http://example.com > /dev/null
- Switch back to the terminal running the analyzer.
You should see lines scrolling past as packets are captured. Each line shows a source IP, a destination IP, and a raw summary string.
Now look closely at the output. DNS queries, TCP connections, ICMP pings, and HTTP requests all blend together into the same format.
What is the problem here?
Your analyzer captures real packets, but the output is an undifferentiated stream. Without protocol labels, you cannot tell a DNS lookup from a TCP connection from a ping reply.
This is the core challenge of network analysis. Raw capture is noise. Protocol classification is signal. The next step solves this.
Not seeing any output?
- If you see Operation not permitted, make sure you're running with sudo at the start of the command.
- If you see ModuleNotFoundError, use the full venv path: sudo venv/bin/python3, not sudo python3. Sudo does not inherit your virtual environment.
- If the analyzer runs but no packets appear, make sure you ran the traffic commands in a separate terminal while the analyzer was still running.
- Why isn't my packet capture working?
You've captured live traffic and seen real packets flowing through your machine. But the output is a wall of noise with no structure. In the next step, you'll classify each packet by protocol and transform this stream into readable, labeled output.
Classify Packets by Protocol
In Step 2, you captured live packets and saw them scroll by. Every packet printed a source IP, a destination IP, and a raw summary. The problem is clear: DNS lookups, TCP connections, and ICMP pings all look the same in that wall of text.
In this step, you will classify each packet by its protocol. Scapy can inspect every layer of a packet, so you will use it to tag each line as [DNS], [TCP], [UDP], [ICMP], or [ARP] and print a protocol statistics summary at the end.
In this step, get ready to:
- Replace analyzer.py with protocol classification logic that tags every packet.
- Run the analyzer while generating traffic and compare the structured output to Step 2's raw firehose.
Replace analyzer.py with protocol classification
The existing analyzer.py prints every packet the same way. You need to rebuild it so each packet gets inspected layer by layer and printed with a protocol tag.
Why check ARP before IP?
ARP packets operate at layer 2 of the network stack. They do not contain an IP layer at all. If you check for IP first and skip non-IP packets, you would silently discard every ARP packet and never see who-has or is-at messages on your local network.
By handling ARP first and returning early, the rest of the function can safely assume every remaining packet has an IP layer.
- Clear the entire contents of analyzer.py and paste the following code. This sets up the imports, a protocol counter, and the first part of process_packet with ARP handling:
import sys
from collections import defaultdict
from scapy.all import ARP, DNS, ICMP, IP, TCP, UDP, sniff
# Track packet counts per protocol
stats = {"TCP": 0, "UDP": 0, "ICMP": 0, "DNS": 0, "ARP": 0, "Other": 0}
def process_packet(packet):
# Handle ARP packets (layer 2, no IP layer)
if packet.haslayer(ARP):
stats["ARP"] += 1
arp = packet[ARP]
op_label = "who-has" if arp.op == 1 else "is-at"
print(f"[ARP] {arp.psrc} ({arp.hwsrc}) | {op_label} {arp.pdst}")
return
# Skip non-IP packets
if not packet.haslayer(IP):
return
src = packet[IP].src
dst = packet[IP].dst
What does this code do?
- stats is a dictionary that counts how many packets of each protocol you capture. It starts with every count at zero.
- packet.haslayer(ARP) asks Scapy whether this packet contains an ARP layer. If it does, the code reads the operation type (who-has or is-at), the sender's IP and MAC, and prints a tagged line. Then it returns early so the packet does not fall through to the IP classification.
- The if not packet.haslayer(IP) guard skips any remaining non-IP traffic so the rest of the function can safely read packet[IP].src and packet[IP].dst.
- Directly below the dst = packet[IP].dst line, paste the IP protocol classification. This continues the process_packet function:
# Classify by protocol layer
if packet.haslayer(DNS) and packet.haslayer(UDP):
stats["DNS"] += 1
if packet[DNS].qd:
query = packet[DNS].qd.qname.decode(errors="ignore").rstrip(".")
print(f"[DNS] {src} -> {dst} | Query: {query}")
elif packet.haslayer(TCP):
stats["TCP"] += 1
sport = packet[TCP].sport
dport = packet[TCP].dport
flags = str(packet[TCP].flags)
print(f"[TCP] {src}:{sport} -> {dst}:{dport} | Flags: {flags}")
elif packet.haslayer(UDP):
stats["UDP"] += 1
print(f"[UDP] {src} -> {dst}")
elif packet.haslayer(ICMP):
stats["ICMP"] += 1
icmp_type = packet[ICMP].type
label = {0: "Reply", 8: "Request"}.get(icmp_type, f"Type {icmp_type}")
print(f"[ICMP] {src} -> {dst} | {label}")
else:
stats["Other"] += 1
What does each protocol branch do?
- DNS: DNS rides on top of UDP, so the code checks for both layers. packet[DNS].qd.qname extracts the domain being queried (e.g. example.com).
- TCP: Prints the source port, destination port, and flags like S (SYN), SA (SYN-ACK), and A (ACK). These flags reveal the TCP handshake in action.
- UDP: Non-DNS UDP traffic gets a simple source and destination tag.
- ICMP: Distinguishes between Echo Request (type 8, outgoing ping) and Echo Reply (type 0, incoming response).
- Save analyzer.py.
- Verify the function loads without syntax errors by running:
sudo venv/bin/python3 -c "from analyzer import process_packet; print('process_packet loaded')"
- You should see process_packet loaded printed with no errors. If you see a SyntaxError, check that the indentation on every line inside process_packet uses four spaces.
- Now add the statistics summary and the main function. Paste the following code below process_packet at the bottom of analyzer.py:
# Print protocol breakdown after capture
def print_stats():
total = sum(stats.values())
print("\n" + "=" * 60)
print(" Protocol Statistics")
print("=" * 60)
for proto, count in sorted(stats.items(), key=lambda x: -x[1]):
if count > 0:
pct = count / total * 100
print(f" {proto:6s} {count:5d} ({pct:5.1f}%)")
print(f"\n Total: {total} packets")
print("=" * 60)
def main():
packet_count = 20
if len(sys.argv) > 1:
packet_count = int(sys.argv[1])
print(f"Capturing {packet_count} packets... (Ctrl+C to stop early)")
print("-" * 60)
try:
sniff(count=packet_count, prn=process_packet, store=0)
except KeyboardInterrupt:
print("\nCapture stopped.")
# Print summary after capture ends
print_stats()
if __name__ == "__main__":
main()
What does print_stats do?
- print_stats() reads the stats dictionary and prints each protocol sorted by packet count (highest first). It calculates the percentage of total traffic each protocol represents.
- main() now calls print_stats() after the capture finishes, so you always get a summary at the end.
- Save analyzer.py.
Seeing an IndentationError?
- Make sure print_stats and main are top-level functions (no leading spaces before def). They should not be indented inside process_packet.
- Check that the protocol classification block (the if packet.haslayer(DNS) chain) is indented at the same level as the ARP check above it. Both are inside process_packet.
- Still stuck? Help me fix an IndentationError in my analyzer.py. I have three functions: process_packet, print_stats, and main. The protocol classification if/elif chain is inside process_packet.
✔️ Awesome, I've got everything!
Make sure you have saved analyzer.py. Your file should have three functions: process_packet, print_stats, and main.
ⓧ I'd like to double check the full code
Scroll up and confirm your analyzer.py contains these sections in order:
- Imports: sys, defaultdict from collections, and ARP, DNS, ICMP, IP, TCP, UDP, sniff from scapy.all.
- stats dictionary: A module-level dict with keys for TCP, UDP, ICMP, DNS, ARP, and Other, all starting at 0.
- process_packet function: Handles ARP first (with early return), then guards against non-IP packets, then classifies DNS, TCP, UDP, ICMP with an else for Other.
- print_stats function: Reads the stats dict and prints a sorted protocol breakdown with counts and percentages.
- main function: Calls sniff() with prn=process_packet, then calls print_stats() after capture finishes.
- Entry point: if __name__ == "__main__": main() at the very bottom.
Run the analyzer and compare to Step 2
Your analyzer now classifies every packet by protocol. Time to see the difference.
Before you run the analyzer, think back to Step 2's output. What do you expect to see differently this time?
- Start the analyzer by running:
sudo venv/bin/python3 analyzer.py
- In another terminal, generate a mix of traffic by running:
ping -c 3 8.8.8.8
curl -s http://example.com > /dev/null
Each line is now tagged. [DNS] lines show the exact domain being queried. [TCP] lines show port numbers and SYN/ACK flags. [ICMP] lines distinguish between Request and Reply. [ARP] lines show who-has requests and is-at replies on your local network.
After the capture finishes, a statistics summary prints at the bottom showing the protocol breakdown at a glance. Compare this to the unreadable wall from Step 2.
Not seeing any tagged output?
- Make sure you are running with sudo venv/bin/python3 (not sudo python3). Without the virtual environment path, Python cannot find Scapy.
- If you see a NameError for ARP, DNS, or another protocol name, check that your import line includes all of them: ARP, DNS, ICMP, IP, TCP, UDP, sniff.
- If the capture runs but you only see [TCP] lines, generate DNS and ICMP traffic in the other terminal (ping creates ICMP, curl triggers DNS lookups).
- My analyzer runs but shows no output or errors I can identify.
Your analyzer now turns raw network noise into structured, readable output. Every packet is tagged and classified, and the statistics summary tells you the protocol breakdown at a glance. Next up, you will add security detection to catch threats hiding inside this traffic.
Detect Threats and Generate a Security Report
Your analyzer now classifies packets by protocol. You can see every DNS query, every TCP connection, and every ARP request on your network.
Knowing what is happening is only half the picture. The other half is knowing what is dangerous. In this step, you will add security detection that catches ARP spoofing, SYN floods, and unencrypted HTTP traffic. You will also save captures to PCAP files and generate a formatted report with top talkers.
In this step, get ready to:
- Detect ARP spoofing, SYN floods, and unencrypted HTTP traffic in live packets.
- Save captured packets to PCAP files you can open in Wireshark.
- Generate a formatted security analysis report with protocol breakdown, top talkers, DNS statistics, and threat detection.
Replace analyzer.py with the full security analyzer
The current analyzer.py uses module-level functions and a global stats dictionary. The security features you are adding need to track much more state: ARP tables, SYN counts, HTTP payloads, DNS logs, and every captured packet. You will rebuild the file around a TrafficAnalyzer class that holds all of this in one place.
- Select all code in analyzer.py and delete it.
- Paste the following imports and class constructor at the top of the now-empty file:
import sys
from collections import Counter, defaultdict
from datetime import datetime
from scapy.all import ARP, DNS, ICMP, IP, Ether, Raw, TCP, UDP, sniff, wrpcap
class TrafficAnalyzer:
def __init__(self):
self.stats = {
"TCP": 0, "UDP": 0, "ICMP": 0, "DNS": 0, "ARP": 0, "Other": 0,
}
self.dns_queries = []
self.connections = set()
self.http_payloads = []
self.arp_table = {}
self.arp_spoofing_alerts = []
self.syn_counter = defaultdict(int)
self.packet_counter = Counter()
self.all_packets = []
self.start_time = None
What are these new data structures?
- arp_table maps each IP address to the last MAC address seen in ARP replies. If an IP suddenly changes MAC, that is a spoofing indicator.
- syn_counter counts SYN packets per source IP. A source sending more than 50 SYNs during a capture gets flagged as a potential flood.
- http_payloads stores readable payloads from unencrypted HTTP traffic on port 80.
- all_packets collects every packet so the full capture can be saved to a PCAP file at the end.
- packet_counter uses Python's Counter to rank IPs by how many packets they sent, revealing the top talkers on your network.
- Below __init__, add the beginning of the process_packet method. This section handles timestamps, packet storage, and ARP spoofing detection:
def process_packet(self, packet):
if self.start_time is None:
self.start_time = datetime.now()
self.all_packets.append(packet)
if packet.haslayer(ARP):
self.stats["ARP"] += 1
arp = packet[ARP]
op_label = "who-has" if arp.op == 1 else "is-at"
print(f"[ARP] {arp.psrc} ({arp.hwsrc}) | {op_label} {arp.pdst}")
if arp.op == 2:
ip = arp.psrc
mac = arp.hwsrc
if ip in self.arp_table and self.arp_table[ip] != mac:
alert = (
f"IP {ip} changed MAC from"
f" {self.arp_table[ip]} to {mac}"
)
self.arp_spoofing_alerts.append(alert)
print(f"[!!] ARP SPOOFING: {alert}")
self.arp_table[ip] = mac
return
How does ARP spoofing detection work?
Every ARP reply (op == 2) tells the network which MAC address owns an IP. The code records each mapping in self.arp_table.
If an IP that was previously associated with one MAC suddenly appears with a different MAC, someone may be impersonating that device. This is the same technique used by tools like arpwatch. The return at the end ensures ARP packets skip the IP-layer handling below, because ARP operates at layer 2 and has no IP header.
- Save analyzer.py.
- Confirm the class is valid so far by running:
venv/bin/python3 -c "from analyzer import TrafficAnalyzer; a = TrafficAnalyzer(); print(a.stats)"
You should see the stats dictionary printed with all protocol counts at zero.
Seeing an ImportError or SyntaxError?
- Make sure you deleted all the old code before pasting. Leftover lines from Step 3 will cause conflicts.
- Check that the class TrafficAnalyzer: line and the def __init__(self): line are indented correctly. __init__ should be indented one level inside the class.
- Confirm you are running venv/bin/python3 (not python3) so the import finds Scapy.
- Why is my class not importing correctly?
- Continue building process_packet. Directly below the ARP return statement, add the IP-layer protocol handling with DNS logging, connection tracking, and SYN flood detection:
if not packet.haslayer(IP):
return
src = packet[IP].src
dst = packet[IP].dst
self.packet_counter[src] += 1
if packet.haslayer(DNS) and packet.haslayer(UDP):
self.stats["DNS"] += 1
if packet[DNS].qd:
query = packet[DNS].qd.qname.decode(errors="ignore").rstrip(".")
self.dns_queries.append(query)
print(f"[DNS] {src} -> {dst} | Query: {query}")
elif packet.haslayer(TCP):
self.stats["TCP"] += 1
sport = packet[TCP].sport
dport = packet[TCP].dport
flags = str(packet[TCP].flags)
self.connections.add((src, dst, dport))
if "S" in flags and "A" not in flags:
self.syn_counter[src] += 1
- Still inside the elif packet.haslayer(TCP) block, directly below the self.syn_counter line, add the HTTP detection and the remaining protocol handlers:
if dport == 80 or sport == 80:
if packet.haslayer(Raw):
payload = packet[Raw].load.decode(errors="ignore")
if any(m in payload for m in ("GET ", "POST ", "HTTP/")):
preview = payload[:80].replace("\r\n", " | ")
self.http_payloads.append(
{"src": src, "dst": dst, "preview": preview}
)
print(f"[HTTP] {src} -> {dst} | {preview}")
return
print(f"[TCP] {src}:{sport} -> {dst}:{dport} | Flags: {flags}")
elif packet.haslayer(UDP):
self.stats["UDP"] += 1
print(f"[UDP] {src} -> {dst}")
elif packet.haslayer(ICMP):
self.stats["ICMP"] += 1
icmp_type = packet[ICMP].type
label = {0: "Reply", 8: "Request"}.get(icmp_type, f"Type {icmp_type}")
print(f"[ICMP] {src} -> {dst} | {label}")
else:
self.stats["Other"] += 1
What is the HTTP detection doing?
When a TCP packet uses port 80 and carries a Raw payload, the code checks whether that payload contains GET, POST, or HTTP/ keywords. If it does, the payload is readable plain text. Anyone on the network could read it too.
HTTPS traffic (port 443) never matches because its payload is encrypted. The Raw layer is ciphertext, so the keyword check fails. This is exactly why HTTPS matters.
The SYN detection checks for TCP packets with the SYN flag set but not the ACK flag ("S" in flags and "A" not in flags). These are connection-initiation packets. A single source sending many of them could be running a SYN flood attack.
- Save analyzer.py.
- Confirm the full process_packet method is syntactically valid by running:
venv/bin/python3 -c "from analyzer import TrafficAnalyzer; print('Packet processor ready')"
You should see Packet processor ready with no errors.
- Now add the print_report method below process_packet. Start with the report header, PCAP save, protocol breakdown, and top talkers:
def print_report(self):
end_time = datetime.now()
duration = (
(end_time - self.start_time).total_seconds() if self.start_time else 0
)
total = sum(self.stats.values())
if total == 0:
print("\nNo packets captured.")
return
pcap_file = "capture.pcap"
wrpcap(pcap_file, self.all_packets)
print("\n" + "=" * 60)
print(" NETWORK TRAFFIC ANALYSIS REPORT")
print("=" * 60)
print(f"\n Duration: {duration:.1f} seconds")
print(f" Total packets analyzed: {total}")
print(f" Capture saved to: {pcap_file}")
print("\n Protocol Breakdown:")
for proto, count in sorted(self.stats.items(), key=lambda x: -x[1]):
if count > 0:
pct = count / total * 100
bar = "#" * int(pct / 2)
print(f" {proto:6s} {count:5d} ({pct:5.1f}%) {bar}")
What does wrpcap do?
wrpcap writes the list of captured packets to a PCAP file. This is the standard binary format that Wireshark, tshark, and other analysis tools read. Saving the capture means you can replay and investigate it later without needing to re-capture the traffic.
- Continue print_report directly below the protocol breakdown. Add the top talkers, DNS query statistics, HTTP warnings, and ARP spoofing alerts:
if self.packet_counter:
print("\n Top Talkers (by packet count):")
for ip, count in self.packet_counter.most_common(5):
print(f" {ip:20s} {count} packets")
if self.dns_queries:
unique = list(dict.fromkeys(self.dns_queries))
print(
f"\n DNS Queries ({len(self.dns_queries)} total,"
f" {len(unique)} unique):"
)
for domain in unique[:10]:
count = self.dns_queries.count(domain)
print(f" {domain} (x{count})")
if self.http_payloads:
print(
f"\n !! UNENCRYPTED HTTP TRAFFIC DETECTED"
f" ({len(self.http_payloads)} requests) !!"
)
print(" These requests are visible to anyone on the network:")
for entry in self.http_payloads[:5]:
print(
f" {entry['src']} -> {entry['dst']}:"
f" {entry['preview'][:60]}"
)
if self.arp_spoofing_alerts:
print(
f"\n !! ARP SPOOFING DETECTED"
f" ({len(self.arp_spoofing_alerts)} alerts) !!"
)
for alert in self.arp_spoofing_alerts:
print(f" {alert}")
- Save analyzer.py.
- Confirm the print_report method is registering by running:
venv/bin/python3 -c "from analyzer import TrafficAnalyzer; print('Report method ready')"
You should see Report method ready with no errors.
- Continue print_report directly below the ARP alerts. Add the SYN flood detection, connection summary, and the beginning of the Security Summary:
syn_flooders = {
ip: count for ip, count in self.syn_counter.items() if count > 50
}
if syn_flooders:
print("\n !! SYN FLOOD DETECTED !!")
for ip, count in syn_flooders.items():
print(f" {ip} sent {count} SYN packets")
print(f"\n Unique connections: {len(self.connections)}")
print(f" ARP table entries: {len(self.arp_table)}")
print("\n Security Summary:")
if self.http_payloads:
print(
f" [!!] {len(self.http_payloads)} unencrypted HTTP"
f" request(s) detected"
)
else:
print(" [OK] No unencrypted HTTP traffic detected")
if self.arp_spoofing_alerts:
print(
f" [!!] {len(self.arp_spoofing_alerts)} ARP spoofing"
f" alert(s)"
)
else:
print(" [OK] No ARP spoofing detected")
- Finish print_report and add the main function below the class. Paste this directly after the ARP spoofing else line:
if syn_flooders:
print(
f" [!!] {len(syn_flooders)} SYN flood source(s)"
f" detected"
)
else:
print(" [OK] No SYN flood activity detected")
print("\n" + "=" * 60)
def main():
packet_count = 50
if len(sys.argv) > 1:
packet_count = int(sys.argv[1])
analyzer = TrafficAnalyzer()
print(f"Capturing {packet_count} packets... (Ctrl+C to stop early)")
print("Generate traffic: browse websites, run 'ping 8.8.8.8', etc.")
print("-" * 60)
try:
sniff(count=packet_count, prn=analyzer.process_packet, store=0)
except KeyboardInterrupt:
print("\nCapture stopped.")
analyzer.print_report()
if __name__ == "__main__":
main()
What changed in main?
- The default packet count is now 50 instead of 20, giving the capture window enough time to catch varied traffic.
- main creates a TrafficAnalyzer instance and passes analyzer.process_packet as the callback to sniff. Each captured packet calls the method on that instance.
- After capture, analyzer.print_report() replaces the old print_stats() call. This generates the full security report and saves the PCAP file.
- Save analyzer.py.
✔️ Awesome, I've got everything!
Great. Confirm you have saved analyzer.py and move on to the test run below.
ⓧ I'd like to double check the full code
Your analyzer.py is over 30 lines, so scroll through and confirm each section is present in this order:
- Imports: sys, Counter, defaultdict, datetime, and the Scapy imports including Ether, Raw, and wrpcap.
- class TrafficAnalyzer with __init__ defining 10 instance attributes (stats through start_time).
- process_packet method starting with timestamp and packet storage, then ARP handling with spoofing detection and return, then the IP check, DNS, TCP (with SYN counter and HTTP detection), UDP, ICMP, and Other branches.
- print_report method starting with wrpcap save, then protocol breakdown with bars, top talkers, DNS queries, HTTP warnings, ARP alerts, SYN flood alerts, connection/ARP table counts, and Security Summary.
- main() function with default packet_count = 50, creating a TrafficAnalyzer instance and calling sniff then print_report.
- if __name__ == "__main__": guard at the very end.
Test with a mix of traffic
The analyzer is complete. Time to run it against real traffic and see what it catches.
Before you run this, think about what you expect to happen. You will visit http://example.com (HTTP, not HTTPS) and https://github.com (HTTPS). Which request do you think the analyzer will flag as a security concern, and why?
- Start the analyzer by running:
sudo venv/bin/python3 analyzer.py 50
- Open a second terminal and generate a mix of traffic by running:
ping -c 2 8.8.8.8
curl -s http://example.com > /dev/null
curl -s https://github.com > /dev/null
Watch the first terminal as packets scroll by. Each line is tagged by protocol. The http://example.com request should trigger [HTTP] lines showing the readable payload. The https://github.com request shows [DNS] and [TCP] lines, but no payload is visible because the traffic is encrypted.
Why is the HTTPS payload invisible?
HTTPS wraps the HTTP request in TLS encryption before sending it. Scapy sees the TCP packets, but the Raw layer contains ciphertext. The keyword check for GET and POST fails because those words are encrypted. This is exactly why HTTPS matters. Anyone capturing your network traffic can read HTTP, but HTTPS is opaque.
Not seeing any HTTP lines?
- Make sure you used http://example.com (no 's' in http). Some browsers and terminals auto-upgrade to HTTPS.
- Try running curl -s http://example.com from the terminal instead of a browser. Browsers are more aggressive about enforcing HTTPS.
- If the analyzer finished before your curl command, increase the packet count: sudo venv/bin/python3 analyzer.py 100.
- Why am I not seeing HTTP traffic in my analyzer?
Read the report and check the PCAP
After the capture completes (50 packets), the report prints automatically. Look for these sections in your output:
- Protocol Breakdown shows each protocol with a packet count, percentage, and a visual bar made of # characters.
- Top Talkers ranks the 5 most active source IPs by packet count. Your machine's IP should be near the top.
- DNS Queries lists every domain your machine contacted, with frequency counts. You should see example.com and github.com listed.
- UNENCRYPTED HTTP TRAFFIC DETECTED flags the http://example.com request and shows a preview of the readable payload.
- Security Summary gives a checklist of threat categories with [OK] or [!!] status for HTTP, ARP spoofing, and SYN floods.
A capture.pcap file has been saved in your project directory. Confirm it exists by running:
ls capture.pcap
You should see capture.pcap listed. This is the evidence artifact a SOC analyst would attach to an incident report. You can open it in Wireshark for visual analysis.
Read the PCAP with tshark
If you have tshark installed, you can preview the saved capture right from the terminal:
tshark -r capture.pcap | head -10
This shows the first 10 packets from your capture in tshark's familiar columnar format.
No capture.pcap file?
- The PCAP is written inside print_report. If the analyzer stopped before the report printed (for example, if you pressed Ctrl+C during capture), the file may not have been saved. Run the analyzer again and let it complete the full 50 packets.
- Check that wrpcap is in your import line at the top of analyzer.py.
- Why is my capture.pcap file not being created?
Secret mission
Detect Port Scans
Port scanning is one of the most common reconnaissance techniques attackers use to discover which services are running on a target. Your analyzer already detects SYN floods, but it misses the distinct pattern of a single source probing many different ports. Can you add port scan detection using data you are already collecting?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- traffic-analyzer/ project directory on your Desktop (contains analyzer.py, capture.pcap, and the venv/ virtual environment with Scapy installed).
Keep everything running
No action needed. Choose this if you are still actively building or want to experiment with capturing more traffic.
- Your traffic-analyzer/ directory stays on your Desktop with all files intact.
- The capture.pcap file remains available to open in Wireshark or replay with tshark.
- You can run the analyzer again any time with sudo venv/bin/python3 analyzer.py from inside the project directory.
Pause - I'll come back to this later
There are no running processes to stop. The analyzer only runs when you execute it manually, so there is nothing to pause.
- Your project files stay on disk and take up minimal space.
- To pick up where you left off, navigate to ~/Desktop/traffic-analyzer and activate your virtual environment with source venv/bin/activate.
Delete - I don't want to use this again
Remove all project files and start fresh. This deletes the analyzer script, the virtual environment, and any saved PCAP captures.
- If you are inside the virtual environment, deactivate it first by running:
deactivate
- Delete the entire project directory by running:
rm -rf ~/Desktop/traffic-analyzer
- Confirm the directory is gone by running:
ls ~/Desktop/traffic-analyzer
You should see a "No such file or directory" message. Everything is cleaned up.
Nice Work!
Nice Work!
Nice work. You just built a production-grade network traffic analyzer using Python and Scapy that captures live packets, classifies them by protocol, detects real security threats, and generates a formatted report. This is the kind of tool SOC analysts and security engineers build and use daily.
You've learned how to:
- Captured and classified live network traffic into DNS, TCP, UDP, ICMP, and ARP protocols with structured output showing domains, ports, flags, and ARP operations.
- Detected real security threats including ARP spoofing (IP-to-MAC mapping changes), SYN floods (burst connection attempts), and unencrypted HTTP traffic with readable payloads that prove why HTTPS matters.
- Generated a formatted security analysis report with protocol statistics, top talkers ranked by packet count, DNS query logs, and saved captures to PCAP files for later analysis in Wireshark.
- Secret Mission: Added port scan detection that flags sources sending SYN packets to many different ports.
Ready to quiz yourself?