dpkt
- 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:
- Processing large pcap files where speed and memory matter
- Writing analysis scripts that iterate over millions of packets
- You do not need live capture or packet crafting
- You want a library with no external C dependencies beyond libpcap
Use Scapy when:
- You need to craft, modify, or send packets
- You need a protocol that dpkt does not support
- You want an interactive shell for exploration
- You need automatic checksum/length computation
For many real analysis workflows the two are complementary: capture with Scapy or tcpdump, analyse with dpkt.