Source Code Repository
Networking Intermediate 25 min read 7 reads

Hands-on Network Packet Analysis with Scapy & Wireshark

Lab Abstract & Architecture Objectives
Deep network packet dissection lab: capture, decode, and manipulate raw Ethernet, IPv4, TCP, and DNS frames using Python, Scapy, and Wireshark.
Fidel Eliud
Published on September 02, 2026

Network Packet Dissection & Protocol Analysis Lab

Deep network visibility is the foundation of network engineering and cyber security. Modern observability requires going beneath high-level HTTP abstractions down to raw Ethernet frames, IP packet headers, TCP window flags, and DNS payload structures.

In this hands-on lab, we build programmatic packet sniffers, dissect the TCP 3-way handshake at the byte level, and construct forensic filters to detect stealth port scans and DNS tunneling attacks.


1. The TCP/IP Protocol Stack & Encapsulation Model

TCP/IP Protocol Encapsulation & Packet Dissection Model Figure 1: Hierarchical frame encapsulation (Ethernet, IPv4, TCP), TCP 3-way handshake timeline, and Wireshark forensic inspection.

Every network transmission is encapsulated in hierarchical protocol layers. Each layer adds structured headers with specific offsets:

+-------------------------------------------------------------------------+
| Layer 2: Ethernet Frame Header (14 Bytes: Dst MAC, Src MAC, EtherType)  |
+-------------------------------------------------------------------------+
| Layer 3: IPv4 Header (20 Bytes: TTL, Protocol, Src IP, Dst IP, Checksum) |
+-------------------------------------------------------------------------+
| Layer 4: TCP Header (20 Bytes: Src/Dst Ports, Seq/Ack, Flags, Window)   |
+-------------------------------------------------------------------------+
| Layer 7: Application Payload (HTTP / DNS / TLS / Custom Raw Bytes)      |
+-------------------------------------------------------------------------+

Key TCP Header Control Flags

  • SYN (0x02): Synchronize sequence numbers to initiate a connection.
  • ACK (0x10): Acknowledgment field is significant.
  • FIN (0x01): No more data from sender; graceful teardown.
  • RST (0x04): Reset connection abruptly.
  • PSH (0x08): Push buffered data immediately to receiving application.
  • URG (0x20): Urgent pointer field is valid.

2. Laboratory Environment Setup

To capture raw network packets without running Python as root, grant socket permissions to the Python binary on Linux:

# 1. Install Scapy and Wireshark CLI tools
sudo apt update && sudo apt install -y wireshark tshark python3-scapy

# 2. Grant packet capture capabilities to Python binary (eliminates sudo requirement)
sudo setcap cap_net_raw,cap_net_admin=eip $(readlink -f $(which python3))

# 3. Verify socket permissions
getcap $(readlink -f $(which python3))

3. Programmatic Packet Sniffing with Berkeley Packet Filters (BPF)

Scapy allows high-performance kernel-level filtering using Berkeley Packet Filter (BPF) syntax:

#!/usr/bin/env python3
from scapy.all import sniff, IP, TCP, UDP, DNS, DNSQR

def packet_dissector(pkt):
    """Callback function executed for every captured frame matching BPF filter."""
    if pkt.haslayer(IP):
        src_ip = pkt[IP].src
        dst_ip = pkt[IP].dst
        ttl = pkt[IP].ttl

        if pkt.haslayer(TCP):
            tcp = pkt[TCP]
            flags = tcp.sprintf('%TCP.flags%')
            print(f"[TCP] {src_ip}:{tcp.sport} -> {dst_ip}:{tcp.dport} | Flags: {flags:<4} | Seq: {tcp.seq} Ack: {tcp.ack}")

        elif pkt.haslayer(UDP) and pkt.haslayer(DNS):
            dns = pkt[DNS]
            if dns.qr == 0 and dns.qd: # Query
                qname = dns.qd.qname.decode('utf-8', errors='ignore')
                print(f"[DNS QUERY] {src_ip} asked for: {qname}")

print("[*] Listening on active network interfaces (BPF filter: 'ip and (tcp or udp)')...")
sniff(filter="ip and (tcp or udp)", prn=packet_dissector, store=0)

4. Deconstructing the TCP 3-Way Handshake at the Byte Level

A standard TCP connection requires three distinct segments: 1. Client sends SYN (Seq = $X$) 2. Server responds with SYN-ACK (Seq = $Y$, Ack = $X + 1$) 3. Client completes with ACK (Seq = $X + 1$, Ack = $Y + 1$)

Crafting a Custom SYN Packet in Scapy

from scapy.all import IP, TCP, sr1

target_host = "127.0.0.1"
target_port = 8000

# Step 1: Craft and send SYN packet with random initial sequence number (ISN)
ip_layer = IP(dst=target_host)
tcp_layer = TCP(dport=target_port, sport=54321, flags="S", seq=1000)

print(f"[*] Dispatching SYN to {target_host}:{target_port}...")
syn_ack_response = sr1(ip_layer / tcp_layer, timeout=2, verbose=0)

if syn_ack_response and syn_ack_response.haslayer(TCP):
    resp_tcp = syn_ack_response[TCP]
    if resp_tcp.flags == 0x12: # SYN-ACK (0x02 | 0x10)
        print(f"[+] Received SYN-ACK! Server ISN={resp_tcp.seq}, ACK={resp_tcp.ack}")

        # Step 2: Complete handshake by sending final ACK
        ack_pkt = IP(dst=target_host) / TCP(
            dport=target_port,
            sport=54321,
            flags="A",
            seq=resp_tcp.ack,
            ack=resp_tcp.seq + 1
        )
        print("[+] Connection Established!")
    elif resp_tcp.flags == 0x14: # RST-ACK (Port Closed)
        print("[-] Port is closed (Received RST-ACK).")

5. Detecting Stealth Port Scans & Reconnaissance

Attackers use non-standard TCP flag combinations to evade stateful firewalls:

  • SYN Stealth Scan (-sS): Half-open scan where attacker sends RST immediately after receiving SYN-ACK.
  • XMAS Scan (-sX): Sets FIN, PSH, and URG flags simultaneously (looks like a lit Christmas tree). RFC 793 dictates closed ports reply with RST, while open ports drop the packet silently.
  • Null Scan (-sN): No flags set at all (flags=0).

Automated Scan Detector Script

from scapy.all import sniff, TCP, IP

def detect_anomalies(pkt):
    if pkt.haslayer(TCP):
        flags = pkt[TCP].flags

        # Check for XMAS scan (FIN + PSH + URG = 0x29)
        if flags == 0x29:
            print(f"[ALERT] XMAS Scan detected from {pkt[IP].src} targeting port {pkt[TCP].dport}")

        # Check for NULL scan (no flags set = 0x00)
        elif flags == 0:
            print(f"[ALERT] NULL Scan detected from {pkt[IP].src} targeting port {pkt[TCP].dport}")

print("[*] Anomaly detection engine active...")
sniff(filter="tcp", prn=detect_anomalies, store=0)

6. Forensics: Identifying DNS Data Exfiltration & Tunneling

Malicious actors encode stolen credentials into subdomains to exfiltrate data through recursive DNS resolvers: eHl6MTIz.malicious-domain.com.

Detection Metrics

  1. Domain Length: Standard queries average 15-25 characters. Exfiltration subdomains exceed 60 characters.
  2. Shannon Entropy: High randomness in query labels indicates encrypted or encoded payloads.
import math

def calculate_shannon_entropy(text: str) -> float:
    """Calculates the entropy (randomness) of a domain label string."""
    if not text:
        return 0.0
    entropy = 0.0
    for char in set(text):
        p_x = float(text.count(char)) / len(text)
        if p_x > 0:
            entropy += - p_x * math.log2(p_x)
    return entropy

# Normal domain: 'google.com' -> Entropy: ~2.3
# Encoded tunnel: 'u8f2a9x1m8zq918b7c' -> Entropy: ~4.1

7. Laboratory Verification & Summary

Export captured traffic to standard .pcap files for offline analysis in Wireshark:

# Capture 1000 packets and save to capture.pcap
tshark -i eth0 -c 1000 -w capture.pcap

# Inspect protocol breakdown
tshark -r capture.pcap -q -z io,phs
Topic Tags: #Networking #Scapy #Wireshark #TCP/IP #Forensics #Python
Back to top
Fidel Eliud
About the Author

Fidel Eliud

Chairperson of NexGen Group. Delegated by founders Kevin and Derrick. Dedicated to building world-class engineers through pure technical meritocracy.