Build a Network Packet Analyzer

Capture and dissect live network traffic with Python, Scapy, and Rich.

Introduction

30 Second Summary

Every time you open a website or stream a video, your computer sends and receives thousands of data packets across the network. Understanding what is inside those packets is a core skill tested on the CCNA.

In this project, you will build a real-time network packet analyzer with Python and Scapy. You will capture live traffic on your machine, dissect each packet layer by layer, and display everything in a live terminal dashboard.

What You'll Build

Run a single command and watch a live terminal dashboard light up with every packet flowing through your network, showing protocol mix, top talkers, and TCP handshakes updating four times per second.

By the end of this project, you'll have:

  • A working Python packet analyzer that captures live network traffic using Scapy and dissects each packet down to its protocol headers, extracting TCP flags, port numbers, TTL values, and DNS queries.
  • A live Rich terminal dashboard with three panels: a scrolling packet stream, top talkers ranked by traffic volume, and real-time telemetry showing packets per second and protocol mix.
  • Hands-on experience detecting TCP three-way handshakes (SYN, SYN-ACK, ACK) in real traffic, directly reinforcing CCNA exam concepts around the TCP/IP stack.
  • Secret Mission: Add a threat watchlist panel that detects port scan patterns and flags suspicious traffic in real time.

Do I need networking experience?

Basic Python knowledge is enough to get started. Familiarity with networking fundamentals like TCP/IP, ports, and protocols will help, but the project explains every concept as it comes up.

Before We Start

Before you start installing tools and writing code, take a moment to lock in what you are about to build and why it matters.

Set Up Your Packet Capture Environment

Python on Windows cannot capture raw network packets out of the box. The operating system restricts direct access to network interfaces, so your scripts need a low-level packet capture driver before they can see any traffic.

In this step, you'll install Npcap (the driver that unlocks raw packet access on Windows), then the Python libraries Scapy and Rich. By the end, you'll have a verified setup that can see every network interface on your machine.

In this step, get ready to:
  • Install Npcap to enable raw packet capture on Windows.
  • Install the Scapy and Rich Python packages.
  • Create the project folder, verify Scapy can see your network interfaces.
Download and install Npcap

Npcap is a Windows packet capture driver that sits between your network card and the operating system. It gives programs like Scapy the ability to read raw network traffic.

  • Open your browser and go to npcap.com.
  • Click the download link to download the latest Npcap installer.
  • Run the downloaded installer file.

Seeing a User Account Control prompt?

Windows will ask if you want to allow the installer to make changes to your device. Click Yes to proceed. Npcap needs system-level access to install its packet capture driver.

  • On the installer options screen, confirm the checkbox labeled Install Npcap in WinPcap API-compatible Mode is checked. It should be checked by default.
  • Click Install and wait for the installation to complete.
  • Click Finish to close the installer.

What is Npcap?

Npcap is a packet capture driver originally developed for the Nmap security scanner. Without it, Windows blocks programs from reading individual network packets for security reasons.

The WinPcap API-compatible mode lets Scapy communicate with Npcap using the older WinPcap interface that Scapy expects. Think of it as a translation layer between Scapy and Npcap.

Install Scapy and Rich

With the packet capture driver in place, you need two Python packages. Scapy handles packet capture and protocol dissection. Rich builds the live terminal dashboard you'll create in a later step.

  • Press the Windows key to open your search bar.
  • Type PowerShell and press Enter to open it.
  • Install both packages by running this command:
pip install scapy==2.7.0 rich==15.0.0

You should see output ending with a Successfully installed message listing both packages and their dependencies.

What do these packages do?

  • Scapy (2.7.0) is a packet manipulation library that can capture, decode, and forge network packets at every layer of the TCP/IP stack.
  • Rich (15.0.0) is a terminal formatting library. You'll use its Live display and Layout classes to build a real-time dashboard that updates four times per second.

pip command not working?

If your system does not recognize pip, try using Python's module flag instead:

python -m pip install scapy==2.7.0 rich==15.0.0

If that also fails, make sure Python 3.13 is added to your system PATH. You can verify by running python --version first.

Help me fix my pip installation on Windows.

Create your project and verify the setup

Packet capture requires Administrator privileges on Windows. You need to run VS Code as Administrator so its integrated terminal inherits those permissions.

  • Press the Windows key to open your search bar.
  • Type Visual Studio Code.
  • Right-click on Visual Studio Code and select Run as administrator.
  • If a User Account Control prompt appears, click Yes.

Why does packet capture need Administrator?

Reading raw network packets means intercepting data before the operating system processes it. Windows restricts this to Administrator accounts to prevent unauthorized programs from snooping on network traffic.

  • Open the integrated terminal by pressing Ctrl+` (backtick).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir packet-analyzer
cd packet-analyzer

PowerShell prints the details of the new folder. You're now inside packet-analyzer on your Desktop.

  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the packet-analyzer folder, and click Select Folder.

VS Code reloads with packet-analyzer as the active workspace. You should see the folder name in the Explorer sidebar on the left.

  • In the Explorer sidebar, hover over the PACKET-ANALYZER heading and click the New File icon.
  • Type analyzer.py and press Enter.

The file appears in the Explorer sidebar and opens as an empty tab in the editor. This is where all your analyzer code will live.

  • Open a new terminal in VS Code by pressing Ctrl+` (backtick).

Time to test whether Npcap, Scapy, and your Administrator permissions are all working together. What do you expect to see when you ask Scapy to list your network interfaces?

  • Verify that Scapy can see your network interfaces by running this command:
python -c "from scapy.all import conf; print(conf.ifaces)"

✔️ I see a list of network interfaces

You should see output listing your network adapters, such as your Wi-Fi adapter and Ethernet adapter. Each entry shows the interface name and details like its index number and description.

This confirms three things at once: Npcap is installed correctly, Scapy can communicate with it, and your terminal has Administrator privileges.

ⓧ I see an error about a missing module

Scapy is not installed in the Python environment your terminal is using. Go back to the previous substep and run the pip install command again from this VS Code terminal.

Help me fix Scapy not being found by Python.

ⓧ I see a different error

  • If you see an empty list or a permissions error, VS Code might not be running as Administrator. Close VS Code entirely, then reopen it by right-clicking and selecting Run as administrator.
  • If you see an Npcap-related error, confirm Npcap installed successfully. Press the Windows key, type Apps & Features, and check that Npcap appears in the installed programs list.
  • If Npcap is missing from the list, re-download and run the installer from npcap.com.

Help me troubleshoot my Scapy and Npcap setup.

Your packet capture environment is ready. Next up, you'll write your first sniffer and watch real packets flow through your machine.

Capture Your First Packets

Your capture environment is ready with Npcap and Scapy installed. Now it is time to capture real packets flowing through your network.

Before you can analyze traffic patterns or dissect protocol headers, you need to prove your setup actually works. In this step, you will build a basic sniffer and then enhance it to show structured details about each packet it captures.

In this step, get ready to:
  • Capture live network packets using Scapy's sniff() function.
  • Extract structured fields from each packet including IPs and protocols.
  • Number and count every packet your analyzer captures.
Write a basic packet sniffer

Scapy's sniff() function captures live packets from your network interface. It accepts a prn callback that runs on every captured packet.

Your first version will simply print each packet's built-in summary to prove the capture pipeline works.

  • Open analyzer.py in VS Code and add the following code:
from scapy.all import sniff

# Capture 20 packets and print a summary of each
print('Starting capture...')
sniff(prn=lambda pkt: print(pkt.summary()), count=20)
print('Capture complete.')

What does this code do?

  • sniff() uses Npcap to capture raw packets from your default network interface.
  • prn=lambda pkt: print(pkt.summary()) runs on every packet and prints a one-line description using Scapy's built-in summary format.
  • count=20 stops the capture after 20 packets so the script does not run forever.
  • Save analyzer.py.
  • Run the script in your Administrator terminal:
python analyzer.py
  • Open your browser and visit any website to generate network traffic.

You should see 20 lines of raw packet summaries scrolling in your terminal, followed by "Capture complete." Each line shows addresses and protocol names in Scapy's internal format.

Not seeing any packets?

  • Make sure your terminal is running as Administrator. Right-click Command Prompt or PowerShell and select Run as administrator.
  • Confirm Npcap is installed by opening Apps & Features in Windows Settings and looking for Npcap. If it is missing, reinstall it from https://npcap.com/.
  • If you see a permission error mentioning sockets or access denied, your terminal is not running with elevated privileges. Close it and reopen as Administrator.
  • Why isn't my Scapy sniff() capturing any packets on Windows?

Why do I need Administrator privileges?

Packet capture operates at a level below normal applications. Npcap needs Administrator access to read raw network frames directly from your network adapter. Without it, sniff() either silently captures nothing or throws a permission error.

Extract structured packet information

The raw summaries prove your sniffer works, but they cram everything into one line of Scapy's internal format. To build a real analyzer, you need to pull out individual fields: source IP, destination IP, protocol type, and packet size.

  • Replace all the code in analyzer.py with this enhanced version:
from scapy.all import IP, TCP, UDP, ICMP, DNS, DNSQR, sniff
from datetime import datetime

def process_packet(packet):
    # Skip non-IP packets (ARP, etc.)
    if not packet.haslayer(IP):
        return
    # Extract IP header fields
    src_ip = packet[IP].src
    dst_ip = packet[IP].dst
    pkt_len = len(packet)
    timestamp = datetime.now().strftime('%H:%M:%S')

    # Identify the transport protocol
    protocol = 'Other'
    if packet.haslayer(TCP):
        protocol = 'TCP'
    elif packet.haslayer(UDP):
        protocol = 'UDP'
    elif packet.haslayer(ICMP):
        protocol = 'ICMP'

    print(f'{timestamp} | {src_ip} -> {dst_ip} | {protocol} | {pkt_len} bytes')

print('Starting capture...')
sniff(prn=process_packet, count=20)
print(f'Capture complete.')

What does this code do?

  • packet.haslayer(IP) filters out non-IP traffic like ARP broadcasts so you only see Internet Protocol packets.
  • packet[IP].src and packet[IP].dst pull the source and destination IP addresses directly from the IP header.
  • The haslayer() checks for TCP, UDP, and ICMP identify which transport protocol each packet uses.
  • DNS and DNSQR are imported now but used in a later step when you dissect DNS queries.
  • Save analyzer.py.
  • Run the script again:
python analyzer.py
  • Open your browser and visit any website to generate traffic.

Each packet now shows a clean, structured line with the timestamp, source IP, destination IP, protocol type, and size in bytes.

Seeing 'Other' for every packet?

  • Some background traffic (ARP, IPv6 neighbor discovery) does not carry a TCP, UDP, or ICMP layer. The haslayer(IP) check filters most of these out. If every single packet still shows "Other", make sure you are generating web traffic by visiting a website.
  • Why are all my packets showing as 'Other' protocol in my Scapy analyzer?
Add a packet counter

Your analyzer shows protocol details for each packet, but you cannot tell at a glance how many packets you have captured. A simple counter numbers each packet and reports the total when capture ends.

  • In analyzer.py, find the line from datetime import datetime. Add a blank line below it, then add this global variable:
packet_count = 0
  • Inside the process_packet function, add global packet_count as the very first line of the function body, before the if not packet.haslayer(IP) check.
  • Right after the return line (where non-IP packets are skipped), add packet_count += 1 to increment the counter for each IP packet.

Now update the two print lines to include the count.

  • Find this line inside process_packet:
    print(f'{timestamp} | {src_ip} -> {dst_ip} | {protocol} | {pkt_len} bytes')
  • Add [{packet_count}] at the start of the f-string so the line looks like this:
    print(f'[{packet_count}] {timestamp} | {src_ip} -> {dst_ip} | {protocol} | {pkt_len} bytes')
  • Find the last print line at the bottom of the file:
print(f'Capture complete.')
  • Replace it with this line that includes the total count:
print(f'Capture complete. Total packets: {packet_count}')

What does the counter do?

  • packet_count is a global variable that starts at zero. Each time process_packet sees an IP packet, it increments by one.
  • The global keyword is needed because process_packet modifies a variable defined outside its scope. Without it, Python would create a local variable instead.
  • Save analyzer.py.

Before you run this, think about what the output will look like now. Will every packet get a number, or only some of them?

  • Run the script one more time:
python analyzer.py
  • Open your browser and visit any website to generate traffic.

Only IP packets get numbered, because your haslayer(IP) check skips everything else before the counter runs. You should see numbered, structured lines like [1] 14:32:07 | 192.168.1.5 -> 142.250.80.46 | TCP | 66 bytes, followed by a final "Capture complete" line showing the total count.

Packet numbers not showing up?

  • Make sure global packet_count is the very first line inside process_packet, before any other code. If it is missing, Python treats packet_count as a local variable and throws an error.
  • Check that packet_count += 1 appears after the return line, not before it. The counter should only increment for packets that pass the IP check.
  • Why is my packet counter not incrementing in my Scapy analyzer script?

✔️ Awesome, I've got everything!

Great. Double check you have saved analyzer.py before moving on.

ⓧ I'd like to double check the full code

Here is the complete analyzer.py at this point. Compare it against yours line by line.

from scapy.all import IP, TCP, UDP, ICMP, DNS, DNSQR, sniff
from datetime import datetime

packet_count = 0

def process_packet(packet):
    global packet_count
    if not packet.haslayer(IP):
        return
    packet_count += 1
    src_ip = packet[IP].src
    dst_ip = packet[IP].dst
    pkt_len = len(packet)
    timestamp = datetime.now().strftime('%H:%M:%S')

    protocol = 'Other'
    if packet.haslayer(TCP):
        protocol = 'TCP'
    elif packet.haslayer(UDP):
        protocol = 'UDP'
    elif packet.haslayer(ICMP):
        protocol = 'ICMP'

    print(f'[{packet_count}] {timestamp} | {src_ip} -> {dst_ip} | {protocol} | {pkt_len} bytes')

print('Starting capture...')
sniff(prn=process_packet, count=20)
print(f'Capture complete. Total packets: {packet_count}')

Your analyzer now captures live packets, extracts structured fields from each one, and numbers them as they arrive. Next up, you will dig deeper into each packet's protocol layers to extract the header fields that CCNA questions test.

Dissect Protocol Layers

Your analyzer captures live packets and prints structured summaries. You can see where traffic is flowing and which protocol each packet uses. But those summaries only scratch the surface of what each packet contains.

CCNA questions test your knowledge of specific protocol header fields. TTL values, TCP flags, port numbers, DNS query names. Your current output hides all of these behind a single word like TCP. In this step, you will dig into each protocol layer to extract the exact fields that the CCNA exam tests.

In this step, get ready to:
  • Build a dissection function that extracts detailed fields from TCP, UDP, ICMP, and DNS packets.
  • Format output to show each packet's layer stack and IP header values like TTL.
  • Identify real protocol patterns in your own network traffic.
Build a protocol dissection function

Every packet that crosses your network is a stack of protocol layers. An Ethernet frame wraps an IP packet, which wraps a TCP or UDP segment. Each layer carries specific header fields that control how the packet is routed, acknowledged, and delivered.

  • In analyzer.py, add the following dissect_packet function directly above your existing process_packet function:
def dissect_packet(packet):
    # Skip non-IP packets
    if not packet.haslayer(IP):
        return

    # Extract IP header fields
    ip = packet[IP]
    layer_stack = 'Ether > IP'
    details = f'TTL={ip.ttl} Proto={ip.proto}'

    # Check protocol layers from most specific to least
    if packet.haslayer(DNS) and packet.haslayer(DNSQR):
        layer_stack += ' > UDP > DNS'
        qname = packet[DNSQR].qname.decode(errors='ignore').rstrip('.')
        details += f' | DNS Query: {qname}'
    elif packet.haslayer(TCP):
        tcp = packet[TCP]
        layer_stack += ' > TCP'
        details += f' | {ip.src}:{tcp.sport} -> {ip.dst}:{tcp.dport} | Flags={tcp.flags} Seq={tcp.seq} Win={tcp.window}'
    elif packet.haslayer(UDP):
        udp = packet[UDP]
        layer_stack += ' > UDP'
        details += f' | {ip.src}:{udp.sport} -> {ip.dst}:{udp.dport}'
    elif packet.haslayer(ICMP):
        icmp = packet[ICMP]
        layer_stack += ' > ICMP'
        details += f' | Type={icmp.type} Code={icmp.code}'

    return f'[{layer_stack}] {details}'

What does this function do?

This function peels apart each packet layer by layer, just like the TCP/IP model the CCNA tests.

  • ip.ttl extracts the Time To Live value from the IP header. Each router that forwards the packet decrements this by one. It also reveals the sender's operating system, since different OSes start with different TTL defaults (Windows uses 128, Linux uses 64).
  • ip.proto is the protocol number in the IP header (6 for TCP, 17 for UDP, 1 for ICMP).
  • DNS is checked first because DNS rides on top of UDP. Without this ordering, a DNS query would be classified as plain UDP instead.
  • tcp.flags shows single-letter flags like S for SYN, A for ACK, and SA for SYN-ACK. These are the exact flags you will see in CCNA study materials for the TCP three-way handshake.
  • tcp.seq and tcp.window show the sequence number and window size. These control flow management in TCP.
  • Save analyzer.py.

Seeing a red underline under DNSQR?

Make sure your import line at the top of the file includes DNSQR. It should read from scapy.all import IP, TCP, UDP, ICMP, DNS, DNSQR, sniff. This import was already included in Step 2, so scroll up and confirm it is there.

help me fix my Scapy imports for DNS dissection

Update your packet processor to use dissection

Your process_packet function currently handles protocol detection on its own. Now that dissect_packet does that job with much more detail, you can simplify process_packet to just call the new function.

  • In analyzer.py, find your existing process_packet function. It currently looks like this:
def process_packet(packet):
    global packet_count
    if not packet.haslayer(IP):
        return
    packet_count += 1
    src_ip = packet[IP].src
    dst_ip = packet[IP].dst
    pkt_len = len(packet)
    timestamp = datetime.now().strftime('%H:%M:%S')

    protocol = 'Other'
    if packet.haslayer(TCP):
        protocol = 'TCP'
    elif packet.haslayer(UDP):
        protocol = 'UDP'
    elif packet.haslayer(ICMP):
        protocol = 'ICMP'

    print(f'[{packet_count}] {timestamp} | {src_ip} -> {dst_ip} | {protocol} | {pkt_len} bytes')
  • Replace the entire process_packet function with this simplified version:
def process_packet(packet):
    global packet_count
    # Skip non-IP packets
    if not packet.haslayer(IP):
        return
    packet_count += 1
    timestamp = datetime.now().strftime('%H:%M:%S')
    # Run full protocol dissection
    dissection = dissect_packet(packet)
    print(f'[{packet_count}] {timestamp} | {dissection}')

What changed here?

The old version extracted IP addresses, determined the protocol, and formatted a basic summary all in one place. The new version delegates all dissection to dissect_packet and only handles packet counting and timestamps. This separation keeps your code clean as you add more analysis features in later steps.

  • Save analyzer.py.
  • Run the analyzer as Administrator to confirm the new dissection output works:
python analyzer.py
  • Open a browser tab and visit any website to generate traffic while the capture runs.

You should see each packet printed with a layer stack like [Ether > IP > TCP] followed by header fields including TTL, Flags, port numbers, and sequence numbers.

Seeing None printed for some packets?

The dissect_packet function returns None for non-IP packets (like ARP broadcasts). This is expected. The process_packet function already filters these out with the if not packet.haslayer(IP) check. If you still see None, confirm that check is present in both functions.

help me debug why my packet dissection is printing None

✔️ Awesome, I've got everything!

Great. Confirm you have saved analyzer.py and the dissection output looks correct.

ⓧ I'd like to double check the full code

Your analyzer.py is over 30 lines at this point, so scroll through and confirm these sections are present in this order:

  • Imports: from scapy.all import IP, TCP, UDP, ICMP, DNS, DNSQR, sniff and from datetime import datetime.
  • Global counter: packet_count = 0.
  • dissect_packet function: Extracts IP header fields, builds a layer stack string, then branches into DNS, TCP, UDP, and ICMP to extract protocol-specific details. Returns a formatted string.
  • process_packet function: Increments packet_count, gets a timestamp, calls dissect_packet, and prints the result.
  • Main block: sniff(prn=process_packet, count=20) with start and end print statements.
Identify protocol patterns in your traffic

Now that your analyzer outputs detailed dissection, you can spot real protocol patterns in your own traffic. These are the same patterns CCNA questions describe in abstract terms.

  • Run the analyzer again as Administrator:
python analyzer.py
  • Open a new browser tab and navigate to any website you have not visited recently.

Before you look at the output, predict: what protocols do you expect to see when your browser loads a webpage? Will every packet be TCP?

Look through your 20 captured packets. You should see several distinct patterns.

What patterns should I see?

  • DNS queries on port 53: Before your browser can connect to a website, it needs to resolve the domain name to an IP address. Look for lines with DNS Query: followed by the domain you visited.
  • HTTPS traffic on port 443: Most TCP packets will show destination port 443 because modern websites use encrypted HTTPS connections.
  • TCP flags: Look for Flags=S (SYN), Flags=SA (SYN-ACK), and Flags=A (ACK). These are the three steps of the TCP three-way handshake. You will detect these automatically in the next step.
  • TTL values: Outgoing packets from your Windows machine start with a TTL of 128. Incoming packets from remote servers often show lower values (like 52 or 115) because each router along the path decremented the TTL by one.

Your analyzer now dissects packets layer by layer, showing the exact protocol header fields that CCNA questions test. Next up, you will add BPF filters to isolate specific traffic and build a handshake detector that catches the TCP three-way handshake in real time.

Filter Traffic and Catch a TCP Handshake

Your analyzer can now dissect every protocol layer in captured traffic. The problem is that it captures everything. DNS, UDP, ICMP, and TCP packets all scroll by in a wall of output where meaningful patterns are buried.

In this step, you will use BPF filters to isolate only the traffic you care about. Then you will build a handshake tracker that detects the TCP three-way handshake (SYN, SYN-ACK, ACK) in real time. This is one of the most heavily tested concepts on the CCNA exam, and you will watch it happen in your own traffic.

In this step, get ready to:
  • Filter captures to TCP-only traffic using BPF syntax.
  • Detect TCP three-way handshakes in real time.
  • Save captured packets to a .pcap file for later analysis.
Add a BPF filter to isolate TCP traffic

Right now your sniff() call captures every packet type. A BPF (Berkeley Packet Filter) tells the operating system to discard packets at the kernel level before they even reach Python.

  • In analyzer.py, scroll to the bottom of the file and find these two lines:
print('Starting capture...')
sniff(prn=process_packet, count=20)
  • Replace both lines so they now look like this:
print('Starting capture (filter: tcp)...')
sniff(prn=process_packet, filter='tcp', count=50)

What changed here?

The filter='tcp' argument passes a BPF filter string to Npcap at the kernel level. Only TCP packets reach your script now. This is the same filter syntax used in Wireshark and tcpdump.

The count is bumped from 20 to 50 because filtering removes most non-TCP traffic. You need more packets to see interesting patterns.

  • Save analyzer.py.
  • Run the script in your Administrator terminal:
python analyzer.py
  • Open a browser and visit any website to generate traffic.

Every packet in the output should show Ether > IP > TCP in its layer stack. No UDP, DNS, or ICMP packets should appear.

Still seeing non-TCP packets?

  • Make sure filter='tcp' is inside the sniff() call, not outside it as a separate variable.
  • Confirm your terminal is running as Administrator. BPF filters require the same elevated privileges as packet capture.
  • Help me debug my BPF filter issue.
Detect TCP three-way handshakes

Every TCP connection starts with a three-way handshake. The client sends a SYN, the server replies with SYN-ACK, and the client finishes with an ACK. Your analyzer will track this sequence across packets and announce when a full handshake completes.

  • In analyzer.py, find the line packet_count = 0 near the top. Add this new line directly below it:
handshake_tracker = {}
  • Now add the detect_handshake function below the dissect_packet function (above process_packet). Copy this code:
def detect_handshake(pkt):
    """Track TCP three-way handshakes."""
    if not pkt.haslayer(TCP) or not pkt.haslayer(IP):
        return None
    ip = pkt[IP]
    tcp = pkt[TCP]
    flags = tcp.flags
    # SYN: client initiates connection
    if flags == 'S':
        key = (ip.src, ip.dst, tcp.dport)
        handshake_tracker[key] = 'SYN_SENT'
    # SYN-ACK: server acknowledges
    elif flags == 'SA':
        reverse_key = (ip.dst, ip.src, tcp.sport)
        if handshake_tracker.get(reverse_key) == 'SYN_SENT':
            handshake_tracker[reverse_key] = 'SYN_ACK_RECEIVED'
    # ACK: client completes handshake
    elif flags == 'A':
        key = (ip.src, ip.dst, tcp.dport)
        if handshake_tracker.get(key) == 'SYN_ACK_RECEIVED':
            handshake_tracker[key] = 'ESTABLISHED'
            return f'TCP Handshake Complete: {key[0]} -> {key[1]}:{key[2]}'
    return None

What does this code do?

The function uses a dictionary to track each connection through three states. It keys connections by a tuple of (source IP, destination IP, destination port).

  • When flags == 'S' (SYN), the client is starting a new connection. The tracker records SYN_SENT.
  • When flags == 'SA' (SYN-ACK), the server is responding. The function checks the reverse direction to find the matching SYN and updates to SYN_ACK_RECEIVED.
  • When flags == 'A' (ACK), the client completes the handshake. If the state was SYN_ACK_RECEIVED, the connection is ESTABLISHED.

Scapy represents TCP flags as short strings. 'S' means the SYN bit is set, 'SA' means both SYN and ACK bits are set, and 'A' means only the ACK bit is set.

  • Now wire it into process_packet. Find the line:
    print(f'[{packet_count}] {timestamp} | {dissection}')
  • Add these three lines directly below it:
    handshake_msg = detect_handshake(packet)
    if handshake_msg:
        print(f'  *** {handshake_msg} ***')
  • Save analyzer.py.
  • Run the script again in your Administrator terminal:
python analyzer.py
  • Open a new browser tab and visit a website you have not visited recently (this forces new TCP connections).

You should see *** TCP Handshake Complete: ... *** messages appear between the regular packet lines. Each message shows the client IP, server IP, and destination port for a connection that just completed its SYN, SYN-ACK, ACK sequence.

Not seeing any handshake messages?

  • Visit a site you have not browsed recently. Your browser reuses existing connections to sites it has already connected to. A fresh site forces a new TCP handshake.
  • Make sure you added handshake_tracker = {} near the top of the file, below packet_count = 0. Without it, detect_handshake will raise a NameError.
  • Check that the three lines calling detect_handshake are indented inside process_packet, not at the module level.
  • Help me debug my handshake detection.
Save captured packets to a .pcap file

Your analyzer displays packets live but loses them when the script ends. Saving to a .pcap file lets you open the capture later in Wireshark for deeper analysis.

  • In analyzer.py, find the import line at the top of the file:
from scapy.all import IP, TCP, UDP, ICMP, DNS, DNSQR, sniff
  • Add wrpcap to the end of the import so it looks like this:
from scapy.all import IP, TCP, UDP, ICMP, DNS, DNSQR, sniff, wrpcap
  • Find packet_count = 0 near the top. Add a new list directly below it (above handshake_tracker):
all_packets = []
  • Inside the process_packet function, add this line at the very end of the function, after the if handshake_msg: block:
    all_packets.append(packet)
  • Scroll to the bottom of the file. Find this line:
print(f'Capture complete. Total packets: {packet_count}')
  • Add these lines directly below it to save all packets when the capture ends:
if all_packets:
    wrpcap('capture.pcap', all_packets)
    print(f'Saved {len(all_packets)} packets to capture.pcap')

What does wrpcap do?

The wrpcap() function writes a list of Scapy packets to a standard .pcap file. This is the same file format used by Wireshark, tcpdump, and every major network analysis tool.

The all_packets list collects every packet inside process_packet. When the script finishes, the list is saved in one call.

  • Save analyzer.py.

Before you run the script, what do you expect to see when capture finishes? Will the capture.pcap file be created?

  • Run the script in your Administrator terminal:
python analyzer.py
  • Open a new browser tab and visit a website you have not visited recently.
  • Wait for the capture to finish (50 packets).

You should see TCP handshake messages in the output followed by a final line like Saved 50 packets to capture.pcap.

  • Confirm the file was created by running:
dir capture.pcap

You should see capture.pcap listed with a non-zero file size.

Don't see capture.pcap?

  • Make sure all_packets.append(packet) is indented inside process_packet. If it sits at the module level, the list stays empty.
  • Check that your wrpcap block is below the sniff() call at the bottom of the file. The save happens after sniffing completes.
  • Confirm the import line includes wrpcap at the end.
  • Help me debug my pcap save issue.

✔️ Awesome, I've got everything!

Great. Double check you have saved analyzer.py.

ⓧ I'd like to double check the full code

Your analyzer.py should now contain these sections, in order:

  • Import line with IP, TCP, UDP, ICMP, DNS, DNSQR, sniff, wrpcap.
  • The datetime import.
  • Three globals: packet_count = 0, all_packets = [], handshake_tracker = {}.
  • The dissect_packet function (unchanged from the previous step).
  • The detect_handshake function that tracks SYN, SYN-ACK, and ACK flag sequences.
  • The process_packet function calling detect_handshake and appending to all_packets.
  • The sniff() call with filter='tcp' and count=50.
  • The wrpcap save block at the very end.

Scroll up through the chunks you added in this step and confirm each section is present and in order.

Your analyzer can now filter traffic, detect TCP handshakes, and save captures to disk. Next up, you will replace the scrolling terminal output with a live Rich dashboard that organizes all this data into panels updating in real time.

Build a Live Dashboard

Scrolling print output worked for debugging, but reading patterns in a wall of text is like reading raw binary on a wire. Which hosts generate the most traffic? What percentage is TCP versus UDP? You cannot answer those questions by scanning a scrolling terminal.

A live dashboard solves this by organizing packet data into panels that update in real time. To make that work, the sniffer has to run in a background thread while the UI renders in the foreground. In this step, you will restructure your entire script around threads, shared state, and a Rich Layout that refreshes 4 times per second.

In this step, get ready to:
  • Restructure the analyzer into a multi-threaded architecture with a background sniffer and rate calculator.
  • Build a Rich Layout with three live panels: packet stream, top talkers, and telemetry.
  • Wire up the Rich Live display and press q to stop capture cleanly.
Restructure your analyzer for multi-threaded capture

The dashboard needs a background sniffer thread, a rate-calculation thread, and a main thread for the UI. This means rewriting analyzer.py from the ground up. Every piece of shared state must be guarded by a lock so threads do not corrupt each other's data.

  • In VS Code, open analyzer.py and select all the existing code.
  • Delete everything. You are rebuilding the script from scratch.
  • Paste the new imports and global state by copying the code below:
import csv
import sys
import threading
import time
from collections import Counter, defaultdict
from datetime import datetime

from rich.console import Console
from rich.layout import Layout
from rich.live import Live
from rich.panel import Panel
from rich.table import Table
from scapy.all import IP, TCP, UDP, ICMP, DNS, DNSQR, sniff, wrpcap


# --- Global state (protected by lock) ---
lock = threading.Lock()
packet_buffer = []
all_raw_packets = []
total_packets = 0
total_bytes = 0

# --- Analytics counters ---
ip_packets = Counter()
ip_bytes = Counter()
protocol_counts = Counter()
unique_hosts = set()

# --- Rate measurement ---
packet_count_last_sec = 0
current_packet_rate = 0

What changed in these imports?

You now import threading and time for background threads, Counter for tracking protocol and IP tallies, and the full suite of Rich display classes (Console, Layout, Live, Panel, Table).

The global variables are the shared state that all three threads read and write. The lock object is a threading.Lock() that prevents two threads from modifying these values at the same instant.

  • Save analyzer.py.
  • Verify the module loads by running this command in your Administrator terminal:
python -c "from analyzer import lock; print(type(lock))"

You should see <class '_thread.lock'> printed, confirming the imports and globals are valid.

Seeing an import error?

  • Make sure your terminal is open inside the packet-analyzer folder. Run cd to check your current directory.
  • Check that scapy and rich are installed. Reinstall with pip install scapy==2.7.0 rich==15.0.0 if needed.

Still stuck? Why is my analyzer.py failing to import the modules?

  • Add the handshake tracker, a services lookup dictionary, and a helper function below the globals by copying this code:
# --- TCP handshake tracker ---
handshake_tracker = {}

# --- Common services lookup ---
COMMON_SERVICES = {
    80: "HTTP", 443: "HTTPS", 53: "DNS", 22: "SSH",
    21: "FTP", 23: "Telnet", 123: "NTP", 3389: "RDP",
}


def resolve_service(sport, dport):
    """Translate port numbers to service names."""
    if dport in COMMON_SERVICES:
        return COMMON_SERVICES[dport]
    if sport in COMMON_SERVICES:
        return COMMON_SERVICES[sport]
    return f"Port {dport}" if dport else "Unknown"

What does resolve_service do?

Instead of showing raw port numbers like 443 in the dashboard, resolve_service() translates them to human-readable names like HTTPS. It checks the destination port first, then the source port, and falls back to the raw number.

  • Save analyzer.py and verify the helper works by running:
python -c "from analyzer import resolve_service; print(resolve_service(0, 443))"

You should see HTTPS printed.

  • Add the detect_handshake function below resolve_service:
def detect_handshake(pkt):
    """Track TCP three-way handshakes."""
    if not pkt.haslayer(TCP) or not pkt.haslayer(IP):
        return None
    ip = pkt[IP]
    tcp = pkt[TCP]
    flags = tcp.flags
    if flags == "S":
        key = (ip.src, ip.dst, tcp.dport)
        handshake_tracker[key] = "SYN_SENT"
    elif flags == "SA":
        reverse_key = (ip.dst, ip.src, tcp.sport)
        if handshake_tracker.get(reverse_key) == "SYN_SENT":
            handshake_tracker[reverse_key] = "SYN_ACK_RECEIVED"
    elif flags == "A":
        key = (ip.src, ip.dst, tcp.dport)
        if handshake_tracker.get(key) == "SYN_ACK_RECEIVED":
            handshake_tracker[key] = "ESTABLISHED"
            return f"Handshake: {key[0]} -> {key[1]}:{key[2]}"
    return None

How is this different from the old version?

The logic is identical to what you wrote in Step 4. The only change is a shorter return message format: "Handshake: ..." instead of the longer string. The dashboard will call this function from inside the packet processor, so it no longer needs to print anything on its own.

  • Now add the rewritten process_packet function. This is the new callback for every sniffed packet. It classifies the protocol and service. Copy this first half below detect_handshake:
def process_packet(packet):
    """Callback for each sniffed packet."""
    global total_packets, total_bytes, packet_count_last_sec

    if not packet.haslayer(IP):
        return

    src_ip = packet[IP].src
    dst_ip = packet[IP].dst
    pkt_len = len(packet)
    timestamp = datetime.now().strftime("%H:%M:%S")

    protocol = "Other"
    service = "-"
    if packet.haslayer(DNS):
        protocol = "DNS"
        if packet.haslayer(DNSQR):
            service = packet[DNSQR].qname.decode(errors="ignore").rstrip(".")
        else:
            service = "DNS Response"
    elif packet.haslayer(TCP):
        protocol = "TCP"
        service = resolve_service(packet[TCP].sport, packet[TCP].dport)
    elif packet.haslayer(UDP):
        protocol = "UDP"
        service = resolve_service(packet[UDP].sport, packet[UDP].dport)
    elif packet.haslayer(ICMP):
        protocol = "ICMP"
        service = "Ping / Control"

What does this classification block do?

The function checks DNS first because DNS packets ride on UDP. Without this check, every DNS query would be labelled generic UDP. For TCP and UDP, it calls resolve_service() to translate port numbers into human-readable names.

  • The function is not complete yet. Add the second half of process_packet directly below the last line you pasted. This part calculates packet size and writes all data into the shared state under the lock:
    size_str = f"{pkt_len} B" if pkt_len < 1024 else f"{pkt_len / 1024:.1f} KB"

    with lock:
        total_packets += 1
        total_bytes += pkt_len
        packet_count_last_sec += 1

        ip_packets[src_ip] += 1
        ip_bytes[src_ip] += pkt_len
        protocol_counts[protocol] += 1
        unique_hosts.add(src_ip)
        unique_hosts.add(dst_ip)

        packet_buffer.append(
            (timestamp, src_ip, dst_ip, protocol, service, size_str)
        )
        if len(packet_buffer) > 100:
            packet_buffer.pop(0)

        all_raw_packets.append(packet)

        detect_handshake(packet)

Why is everything inside with lock?

The with lock: block is a mutex. It guarantees that only one thread touches the shared counters, lists, and sets at a time. Without it, the sniffer thread and the UI thread could read and write the same variable simultaneously, causing corrupted counts or crashes.

The buffer is capped at 100 entries with packet_buffer.pop(0) so the dashboard only shows recent packets without consuming unlimited memory.

  • Add the two background thread functions below process_packet:
def calculate_rates():
    """Background thread: calculate packet throughput every second."""
    global packet_count_last_sec, current_packet_rate
    while True:
        time.sleep(1)
        with lock:
            current_packet_rate = packet_count_last_sec
            packet_count_last_sec = 0


def start_sniffer(bpf_filter=None):
    """Background thread: run the packet sniffer."""
    sniff_args = {"prn": process_packet, "store": 0}
    if bpf_filter:
        sniff_args["filter"] = bpf_filter
    sniff(**sniff_args)

What does store=0 do?

Setting store=0 tells Scapy not to keep its own copy of every packet in memory. Your process_packet callback already appends each packet to all_raw_packets, so Scapy's internal storage would be a redundant copy that wastes memory on long captures.

  • Save analyzer.py and verify the backend loads cleanly:
python -c "from analyzer import start_sniffer, calculate_rates; print('Backend ready')"

You should see Backend ready printed with no errors.

Seeing a SyntaxError or IndentationError?

  • Make sure the with lock: block inside process_packet is indented one level (4 spaces) inside the function, and the lines under with lock: are indented two levels (8 spaces).
  • Check that size_str is at the same indentation level as protocol = "Other" above it. Both should be inside process_packet but outside with lock:.

Need help? Help me fix an indentation or syntax error in my process_packet function

Build the dashboard layout

The generate_layout() function builds a Rich Layout with three panels every time the dashboard refreshes. The function is large, so you will build it in three parts. Paste each block in sequence, then save when all three are in place.

  • Start the generate_layout function by adding the layout skeleton and the top panel table below start_sniffer:
def generate_layout(console):
    """Build the Rich Layout with current telemetry data."""
    layout = Layout()
    layout.split_column(
        Layout(name="top", ratio=5),
        Layout(name="bottom", ratio=3),
        Layout(name="footer", size=3),
    )
    layout["bottom"].split_row(
        Layout(name="top_talkers", ratio=3),
        Layout(name="telemetry", ratio=2),
    )

    with lock:
        # --- Top panel: Live packet stream ---
        top_table = Table(expand=True, show_edge=False)
        top_table.add_column("Time", style="dim green", width=9)
        top_table.add_column("Source IP", style="green")
        top_table.add_column("Destination IP", style="green")
        top_table.add_column("Protocol", style="bold green", width=9)
        top_table.add_column("Service", style="bright_green")
        top_table.add_column("Size", style="dim green", justify="right", width=9)

What is this layout structure?

The Layout splits the terminal into a top row (the packet stream), a bottom row (split into two side-by-side panels), and a small footer for the quit instruction. The ratio values control how much vertical and horizontal space each panel gets.

  • Continue building generate_layout by adding the top panel rows and the top talkers panel. Paste this directly below the last add_column line, keeping the same indentation level (inside with lock:):
        for pkt in packet_buffer[-15:]:
            top_table.add_row(pkt[0], pkt[1], pkt[2], pkt[3], pkt[4], pkt[5])

        mb_transferred = total_bytes / (1024 * 1024)
        top_panel = Panel(
            top_table,
            title=(
                f"Live Packet Stream | Packets: {total_packets}"
                f" | Data: {mb_transferred:.2f} MB"
            ),
            border_style="green",
        )
        layout["top"].update(top_panel)

        # --- Bottom-left: Top talkers ---
        talkers_table = Table(expand=True, show_edge=False)
        talkers_table.add_column("IP Address", style="bright_green")
        talkers_table.add_column("Packets", style="green", justify="right")
        talkers_table.add_column("Data", style="dim green", justify="right")

        for ip, p_count in ip_packets.most_common(5):
            b_count = ip_bytes[ip]
            data_str = (
                f"{b_count / 1024:.1f} KB"
                if b_count < 1048576
                else f"{b_count / 1048576:.2f} MB"
            )
            talkers_table.add_row(ip, str(p_count), data_str)

        layout["top_talkers"].update(
            Panel(talkers_table, title="Top Network Generators", border_style="green")
        )
  • Add the telemetry panel. Paste this directly below the layout["top_talkers"].update(...) line, still inside with lock::
        # --- Bottom-right: Telemetry ---
        avg_pkt_size = (total_bytes // total_packets) if total_packets > 0 else 0
        tcp_pct = (
            int((protocol_counts["TCP"] / total_packets) * 100)
            if total_packets > 0 else 0
        )
        udp_pct = (
            int((protocol_counts["UDP"] / total_packets) * 100)
            if total_packets > 0 else 0
        )
        icmp_pct = (
            int((protocol_counts["ICMP"] / total_packets) * 100)
            if total_packets > 0 else 0
        )

        telemetry_text = (
            f"[bold green]Velocity:[/] {current_packet_rate} pkts/sec\n"
            f"[bold green]Unique Hosts:[/] {len(unique_hosts)}\n"
            f"[bold green]Avg Packet Size:[/] {avg_pkt_size} Bytes\n"
            f"[bold green]Protocol Mix:[/] TCP: {tcp_pct}%"
            f" | UDP: {udp_pct}% | ICMP: {icmp_pct}%\n"
        )

        layout["telemetry"].update(
            Panel(telemetry_text, title="Telemetry", border_style="green")
        )
  • Close the generate_layout function with the footer panel and its return statement. Then add a keyboard input helper below it. Paste this code after the telemetry panel (outdented from with lock:, back at the function body level):
    # --- Footer ---
    footer_grid = Table.grid(expand=True)
    footer_grid.add_column(justify="left")
    footer_grid.add_row(
        "[bold green]Press 'q' to stop capture and exit[/bold green]"
    )
    layout["footer"].update(Panel(footer_grid, border_style="green"))

    return layout


def check_keyboard_input():
    """Non-blocking keyboard check for Windows."""
    if sys.platform == "win32":
        import msvcrt
        if msvcrt.kbhit():
            return msvcrt.getch().decode("utf-8", errors="ignore").lower()
    return None

What is msvcrt?

Python's msvcrt module is a Windows-only library for console I/O. The kbhit() function checks if a key has been pressed without blocking, and getch() reads that keypress. Together they let the dashboard loop check for a q press on every iteration without freezing the UI.

  • Save analyzer.py and verify the layout function loads:
python -c "from analyzer import generate_layout; print('Layout loaded')"

You should see Layout loaded with no errors.

Seeing an IndentationError in generate_layout?

  • The trickiest part is the indentation levels. Lines inside with lock: should be at 8 spaces. The footer section (after the lock block closes) should be at 4 spaces. The return layout line should also be at 4 spaces.
  • If you pasted code that mixed tabs and spaces, select all text in VS Code and use Ctrl+Shift+P then type Convert Indentation to Spaces to fix it.

Still stuck? Help me fix indentation in my generate_layout function

Wire up the live display and run the dashboard

The last piece is the main() function that ties everything together: it parses an optional BPF filter from the command line, starts the background threads, and runs the Rich Live display loop.

  • Add the first half of the main function below check_keyboard_input. This sets up the console, launches the daemon threads, and runs the live dashboard loop:
def main():
    bpf_filter = None
    if len(sys.argv) > 1:
        bpf_filter = sys.argv[1]

    console = Console()
    console.clear()
    console.print("[bold green]Network Packet Analyzer[/bold green]")
    if bpf_filter:
        console.print(f"[green]Filter: {bpf_filter}[/green]")
    console.print("[green]Starting capture... (press 'q' in dashboard to stop)[/green]")
    time.sleep(1)
    console.clear()

    # Start background threads
    threading.Thread(target=start_sniffer, args=(bpf_filter,), daemon=True).start()
    threading.Thread(target=calculate_rates, daemon=True).start()

    # Run live dashboard
    try:
        with Live(generate_layout(console), refresh_per_second=4, console=console) as live:
            while True:
                time.sleep(0.1)
                key = check_keyboard_input()
                if key == "q":
                    break
                live.update(generate_layout(console))
    except KeyboardInterrupt:
        pass

What are daemon threads?

Setting daemon=True on a thread means it dies automatically when the main thread exits. Without this, the sniffer thread would keep your script alive forever after you press q, because sniff() runs in an infinite loop.

The Live context manager with refresh_per_second=4 redraws the dashboard 4 times per second. Inside the loop, live.update() regenerates the entire layout with fresh data on each tick.

  • Complete the main function by adding the post-capture cleanup and the entry point. Paste this directly below the except KeyboardInterrupt: block:
    # Post-capture
    console.clear()
    console.print("[bold green]Capture stopped.[/bold green]\n")

    with lock:
        if all_raw_packets:
            wrpcap("capture.pcap", all_raw_packets)
            console.print(
                f"[green]Saved {len(all_raw_packets)} packets"
                f" to capture.pcap[/green]"
            )

        console.print(f"\n[bold]Session Summary:[/bold]")
        console.print(f"  Total packets: {total_packets}")
        console.print(f"  Total data: {total_bytes / 1024:.1f} KB")
        console.print(f"  Unique hosts: {len(unique_hosts)}")
        for proto, count in protocol_counts.most_common():
            console.print(f"  {proto}: {count} packets")


if __name__ == "__main__":
    main()

What happens when you press q?

The Live context manager closes, the terminal clears, and the script saves all captured packets to capture.pcap using wrpcap(). Then it prints a session summary showing total packets, data transferred, unique hosts, and protocol breakdown.

  • Save analyzer.py.

Before you run the dashboard, think about what you expect to see. You have a sniffer running in the background, a rate calculator updating every second, and a Rich Layout refreshing 4 times per second. What will the terminal look like?

  • Run the dashboard as Administrator:
python analyzer.py

Open a browser and visit any website. Watch the dashboard come alive.

You should see a full-screen green terminal dashboard with three panels. The top panel shows a scrolling packet stream table with columns for time, source IP, destination IP, protocol, service, and size. The bottom-left shows the top talkers (most active IPs by packet count and data volume). The bottom-right shows live telemetry: packets per second, unique hosts, average packet size, and the protocol mix as TCP/UDP/ICMP percentages.

  • Press q to stop capture cleanly.
  • Confirm you see a session summary printed after the dashboard closes, and a capture.pcap file is saved.

Dashboard not appearing or crashing?

  • Make sure you are running the terminal as Administrator. Right-click your terminal and select Run as administrator. Without Administrator privileges, Scapy cannot access the Npcap driver.
  • If the dashboard flashes and disappears, check for a NameError or AttributeError in the traceback. This usually means a function or variable was misspelled. Compare your code against the full-file check below.
  • If pressing q does not stop the dashboard, use Ctrl+C instead. The except KeyboardInterrupt block catches that and exits gracefully.

Still stuck? Help me debug my live dashboard

✔️ Awesome, I've got everything!

Great. Double check you have saved analyzer.py and move on.

ⓧ I'd like to double check the full code

Your analyzer.py is long. Scroll through the file and confirm the following sections appear in this exact order:

  • Imports: csv, sys, threading, time, Counter, defaultdict, datetime, all Rich classes, and all Scapy layers.
  • Global state block: lock, packet_buffer, all_raw_packets, total_packets, total_bytes, analytics counters, rate measurement, and handshake_tracker.
  • COMMON_SERVICES dictionary and resolve_service() function.
  • detect_handshake() function.
  • process_packet() function with protocol classification and with lock: block.
  • calculate_rates() and start_sniffer() functions.
  • generate_layout() function with top panel, top talkers panel, telemetry panel, and footer.
  • check_keyboard_input() function.
  • main() function with thread starts, Live loop, and post-capture save.
  • if __name__ == "__main__": entry point calling main().

Your packet analyzer now has a real-time dashboard that turns raw network traffic into organized, readable panels. Next up, you will tackle the Secret Mission and extend the dashboard with threat detection.

Secret mission

Detect Port Scan Patterns

Your analyzer captures and displays traffic, but it does not flag anything suspicious. Port scans are one of the most common network reconnaissance techniques. In this mission, you will add a threat watchlist panel that detects port scan patterns in real time and exports a full session audit log on exit.

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 on your local machine, so there are no ongoing cloud costs.

Resources you used:

  • Npcap driver (installed system-wide).
  • Python packages: scapy and rich.
  • packet-analyzer folder on your Desktop (contains analyzer.py, capture.pcap, and any session export files).

Keep everything running

No action needed. Choose this if you plan to keep experimenting with packet capture or use Npcap with other tools like Wireshark.

  • Npcap stays installed and ready for any packet capture tool on your system.
  • Your scapy and rich packages remain available for future Python projects.
  • Your analyzer.py script and any .pcap or .csv capture files remain in the packet-analyzer folder on your Desktop.

Pause - I'll come back to this later

The analyzer only captures traffic while you actively run it. There are no background processes or services to stop.

  • Your files, Python packages, and Npcap installation stay on your machine until you return.
  • To resume, open your packet-analyzer folder in VS Code and run analyzer.py as Administrator.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below.

Uninstall Npcap:

  • Press the Windows key to open your search bar.
  • Type Add or remove programs and press Enter.
  • Search for Npcap in the app list.
  • Click on Npcap, then click Uninstall.

Uninstall Python packages:

  • Remove the scapy and rich libraries by running this command:
pip uninstall scapy rich
  • Type y and press Enter to confirm each package removal.

Delete project files:

  • Delete the packet-analyzer folder from your Desktop by running this command:
rmdir /s /q "%USERPROFILE%\Desktop\packet-analyzer"
  • This removes analyzer.py, capture.pcap, and any .csv or .pcap session export files inside the folder.

Nice Work!

Nice Work!

You just built a real-time network packet analyzer from scratch using Python and Scapy. Every packet that crossed your network interface got captured, dissected, and visualized in a live dashboard you coded yourself.

You've learned how to:

  • Capture live network traffic with Scapy and Npcap, then dissect each packet layer by layer to extract IP headers, TCP/UDP ports, ICMP types, DNS queries, and TCP flags.
  • Filter traffic with BPF filters and detect TCP three-way handshakes (SYN, SYN-ACK, ACK) in real traffic, directly reinforcing the connection patterns tested on the CCNA exam.
  • Build a multi-threaded live terminal dashboard with Rich that displays real-time packet streams, top talkers ranked by volume, and protocol telemetry updating four times per second.
  • Secret Mission: Added port scan detection with a threat watchlist panel that flags suspicious IPs and exports session data to CSV and .pcap on exit.

Ready to quiz yourself?