courses

dpkt

Introduction

dpkt is a Python package for fast, low-overhead creation and parsing of network packets. Where Scapy is a full interactive environment with support for packet crafting, live capture, and fuzzing, dpkt is a focused parsing library: it reads raw bytes and gives you structured Python objects. That narrower scope makes it significantly faster and leaner than Scapy for tasks that only need to decode packets already captured to disk.

Install:

pip install dpkt

dpkt vs. Scapy

Both libraries parse the same network protocols, but they are built around different philosophies.

Feature dpkt Scapy
Primary use Parsing / analysis of existing captures Crafting, sending, sniffing, and parsing
Live capture No (delegates to pcap/libpcap) Yes (sniff(), raw sockets)
Packet crafting Basic; no automatic field computation Full: checksums, lengths, random fields
Interactive shell No Yes (Scapy CLI)
Performance Fast — pure decode, minimal overhead Slower — rich object model, field inference
Protocol coverage ~40 common protocols 300+ protocols
Root required Only for live capture Yes for send/sniff
Learning curve Low — it is just Python classes Steeper — custom layer system, operators
Output Python objects with byte-level access Rich Packet objects with display methods

The short version: use dpkt when you have a pcap file and want to extract information quickly. Use Scapy when you need to craft or send packets, or when you need a protocol that dpkt doesn’t cover.

Core concepts

dpkt represents each protocol as a class that inherits from dpkt.Packet. Parsing is done by constructing an object from raw bytes; the class unpacks the header fields automatically. Each layer’s data attribute contains the next layer’s raw bytes, which you then decode by constructing the appropriate class from it.

import dpkt

# raw ethernet frame bytes → Ethernet object
eth = dpkt.ethernet.Ethernet(raw_bytes)

# eth.data is the raw IP bytes
ip = eth.data                           # already decoded for common types
tcp = ip.data

For common encapsulations (Ethernet → IP → TCP/UDP, etc.) dpkt decodes child layers automatically when it recognises the protocol number or ethertype. The result is a chain of objects rather than a flat list of layers.

Reading a pcap file

dpkt.pcap.Reader wraps a file handle and yields (timestamp, raw_packet_bytes) tuples. This is the main entry point for offline analysis.

import dpkt

with open('capture.pcap', 'rb') as f:
    pcap = dpkt.pcap.Reader(f)
    for ts, raw in pcap:
        eth = dpkt.ethernet.Ethernet(raw)
        print(f"{ts:.6f}  {eth.src.hex()}{eth.dst.hex()}")

The timestamp is a float (seconds since the Unix epoch, with microsecond resolution). raw is bytes.

Working with IP, TCP, and UDP

import dpkt
import socket

def ip_to_str(addr: bytes) -> str:
    return socket.inet_ntop(socket.AF_INET, addr)

with open('capture.pcap', 'rb') as f:
    for ts, raw in dpkt.pcap.Reader(f):
        eth = dpkt.ethernet.Ethernet(raw)

        # skip non-IP frames
        if not isinstance(eth.data, dpkt.ip.IP):
            continue
        ip = eth.data

        src = ip_to_str(ip.src)
        dst = ip_to_str(ip.dst)

        if isinstance(ip.data, dpkt.tcp.TCP):
            tcp = ip.data
            print(f"TCP  {src}:{tcp.sport}{dst}:{tcp.dport}  "
                  f"flags={tcp.flags:#04x}  len={ip.len}")

        elif isinstance(ip.data, dpkt.udp.UDP):
            udp = ip.data
            print(f"UDP  {src}:{udp.sport}{dst}:{udp.dport}  "
                  f"len={udp.ulen}")

isinstance is the idiomatic way to check the protocol at each layer. There is no haslayer() equivalent; you check the type of data directly.

TCP flag constants

Each TCP packet carries a flags byte where individual bits signal protocol control actions. Multiple flags can be set simultaneously — that’s why they are individual bits, not an enum.

Flag Bit (hex) Meaning Typical context
SYN 0x02 Synchronize sequence numbers Opening a connection (client → server, then server → client)
ACK 0x10 Acknowledgment field is valid Every packet after the initial SYN
FIN 0x01 Sender has finished sending Graceful connection close
RST 0x04 Reset / abort the connection Error or refused connection
PSH 0x08 Push buffered data to application now Final segment of a request/response
URG 0x20 Urgent pointer field is valid Rare; used by some legacy protocols

A normal TCP connection starts with a SYN (client), then SYN+ACK (server), then ACK (client) — the three-way handshake. From that point on every packet carries ACK.

dpkt.tcp exports named flag constants:

import dpkt

TH_SYN  = dpkt.tcp.TH_SYN   # 0x02
TH_ACK  = dpkt.tcp.TH_ACK   # 0x10
TH_FIN  = dpkt.tcp.TH_FIN   # 0x01
TH_RST  = dpkt.tcp.TH_RST   # 0x04
TH_PUSH = dpkt.tcp.TH_PUSH  # 0x08
TH_URG  = dpkt.tcp.TH_URG   # 0x20

# Check for a SYN-ACK
# Use bitwise AND (&) because multiple flags can be set simultaneously.
# == would fail on a SYN-ACK because tcp.flags is 0x12, not 0x02 or 0x10 alone.
if tcp.flags & (dpkt.tcp.TH_SYN | dpkt.tcp.TH_ACK) == (dpkt.tcp.TH_SYN | dpkt.tcp.TH_ACK):
    print("SYN-ACK")

# Check for RST (connection refused or aborted)
if tcp.flags & dpkt.tcp.TH_RST:
    print("RST — connection reset")

IPv6

dpkt handles IPv6 through dpkt.ip6.IP6. The ethertype check determines which decoder to use:

import dpkt, socket

with open('capture.pcap', 'rb') as f:
    for ts, raw in dpkt.pcap.Reader(f):
        eth = dpkt.ethernet.Ethernet(raw)

        if eth.type == dpkt.ethernet.ETH_TYPE_IP:
            ip = eth.data
            src = socket.inet_ntop(socket.AF_INET, ip.src)
        elif eth.type == dpkt.ethernet.ETH_TYPE_IP6:
            ip = eth.data
            src = socket.inet_ntop(socket.AF_INET6, ip.src)
        else:
            continue

        print(src)

DNS

DNS packets arrive over UDP (or TCP for responses larger than 512 bytes). Decode the UDP payload as dpkt.dns.DNS:

import dpkt, socket

with open('capture.pcap', 'rb') as f:
    for ts, raw in dpkt.pcap.Reader(f):
        eth = dpkt.ethernet.Ethernet(raw)
        if not isinstance(eth.data, dpkt.ip.IP):
            continue
        ip = eth.data
        if not isinstance(ip.data, dpkt.udp.UDP):
            continue
        udp = ip.data
        if udp.dport != 53 and udp.sport != 53:
            continue

        try:
            dns = dpkt.dns.DNS(udp.data)
        except dpkt.dpkt.NeedData:
            continue

        for q in dns.qd:    # questions
            print(f"query   {q.name}  type={q.type}")
        for a in dns.an:    # answers
            if a.type == dpkt.dns.DNS_A:
                print(f"answer  {a.name}{socket.inet_ntoa(a.rdata)}")

HTTP

HTTP is decoded from the TCP payload. Because HTTP/1.x is stream-oriented, dpkt’s HTTP parser works on a single request or response at a time — you are responsible for reassembling TCP streams if you need to handle pipelined or multi-packet messages.

import dpkt

with open('capture.pcap', 'rb') as f:
    for ts, raw in dpkt.pcap.Reader(f):
        eth = dpkt.ethernet.Ethernet(raw)
        if not isinstance(eth.data, dpkt.ip.IP):
            continue
        tcp = eth.data.data
        if not isinstance(tcp, dpkt.tcp.TCP):
            continue
        if not tcp.data:
            continue

        if tcp.dport == 80:
            try:
                req = dpkt.http.Request(tcp.data)
                print(f"→ {req.method} {req.uri}")
                print(f"   Host: {req.headers.get('host', '?')}")
            except (dpkt.dpkt.NeedData, dpkt.dpkt.UnpackError):
                pass

        elif tcp.sport == 80:
            try:
                res = dpkt.http.Response(tcp.data)
                print(f"← {res.status} {res.reason}  "
                      f"Content-Type: {res.headers.get('content-type', '?')}")
            except (dpkt.dpkt.NeedData, dpkt.dpkt.UnpackError):
                pass

Writing a pcap file

dpkt.pcap.Writer writes packets back out in pcap format. This is useful for filtering or transforming captures.

import dpkt

with open('input.pcap', 'rb') as fin, open('output.pcap', 'wb') as fout:
    reader = dpkt.pcap.Reader(fin)
    writer = dpkt.pcap.Writer(fout)

    for ts, raw in reader:
        eth = dpkt.ethernet.Ethernet(raw)
        # only write TCP packets
        if isinstance(eth.data, dpkt.ip.IP) and \
           isinstance(eth.data.data, dpkt.tcp.TCP):
            writer.writepkt(raw, ts=ts)

Accessing raw bytes

Every dpkt.Packet object can be re-serialised with bytes(). This is how you get the raw bytes back out after modifying fields — though note that dpkt does not recompute checksums automatically (unlike Scapy).

ip = dpkt.ip.IP(raw_ip_bytes)
ip.ttl = 1
# ip.sum is now wrong — you must recompute it yourself or zero it for the kernel
raw_modified = bytes(ip)

Comparison with Scapy for common tasks

Reading a pcap and printing summaries

Scapy:

from scapy.all import rdpcap
packets = rdpcap('capture.pcap')
for p in packets:
    print(p.summary())

dpkt:

import dpkt, socket

with open('capture.pcap', 'rb') as f:
    for ts, raw in dpkt.pcap.Reader(f):
        eth = dpkt.ethernet.Ethernet(raw)
        ip = eth.data
        if isinstance(ip, dpkt.ip.IP):
            proto = 'TCP' if isinstance(ip.data, dpkt.tcp.TCP) else \
                    'UDP' if isinstance(ip.data, dpkt.udp.UDP) else str(ip.p)
            print(f"{ts:.3f}  {socket.inet_ntoa(ip.src)} → "
                  f"{socket.inet_ntoa(ip.dst)}  {proto}")

Scapy’s version is shorter; dpkt’s is faster and uses much less memory on large captures.

Extracting all destination IPs from a capture

Scapy:

from scapy.all import rdpcap, IP
ips = {p[IP].dst for p in rdpcap('capture.pcap') if IP in p}

dpkt:

import dpkt, socket
with open('capture.pcap', 'rb') as f:
    ips = {socket.inet_ntoa(dpkt.ethernet.Ethernet(raw).data.dst)
           for _, raw in dpkt.pcap.Reader(f)
           if isinstance(dpkt.ethernet.Ethernet(raw).data, dpkt.ip.IP)}

Crafting and sending a packet

Scapy (dpkt has no equivalent):

from scapy.all import IP, TCP, send
send(IP(dst="10.0.0.1") / TCP(dport=80, flags="S"))

dpkt has no send(). If you need to transmit a packet you have to go through a raw socket yourself. For any task involving packet injection, Scapy is the right tool.

When to use each

Use dpkt when:

Use Scapy when:

For many real analysis workflows the two are complementary: capture with Scapy or tcpdump, analyse with dpkt.

Additional resources