libwireshark-backed pcap/pcapng reader via CFFI - full dissector stack, no subprocess
Project description
wiresharkffi
Python bindings for libwireshark - use Wireshark's dissectors from Python without spawning a subprocess.
If you've ever processed pcap files in Python you know the options aren't great: pyshark wraps tshark and requires spawning a subprocess, scapy is great - but it has its own dissectors and quirks. wiresharkffi takes a different approach and links directly against libwireshark via CFFI, so you get the exact same dissection output as Wireshark itself. Every protocol Wireshark supports, every field it extracts - available as a Python dict, per packet.
Requirements
libwireshark 4.2.x, 4.4.x, 4.6.x, or 4.7.x must be installed before building:
# Ubuntu / Debian
sudo apt install `libwireshark`-dev libglib2.0-dev pkg-config
# Arch Linux
sudo pacman -S wireshark-qt pkgconf
# macOS (Homebrew)
brew install wireshark
Installation
pip install wiresharkffi
From source:
git clone https://github.com/calligraf0/wiresharkffi
cd wiresharkffi
pip install -e .
The C extension is compiled at install time and links against the system libwireshark. No pre-built wheels.
Quick start
from wiresharkffi import PcapReader
with PcapReader("capture.pcapng") as pcap:
for pkt in pcap:
print(pkt["_num"], pkt.get("ip.src"), pkt.get("tcp.stream"))
Each packet is a plain dict with JSON-compatible values. Fields are keyed by Wireshark abbreviation (ip.src, tcp.stream, http.request.uri, etc.) - the same names you'd use in a Wireshark display filter.
Display filters
Pass any Wireshark display filter expression to skip non-matching packets:
with PcapReader("capture.pcapng", display_filter="tcp") as pcap:
for pkt in pcap:
print(pkt["tcp.stream"], pkt["tcp.srcport"])
with PcapReader("capture.pcapng", display_filter="http.response.code >= 400") as pcap:
for pkt in pcap:
print(pkt["http.response.code"])
The syntax is identical to tshark's -Y flag. An invalid filter raises ValueError at construction time, before any packets are read.
Field whitelist
If you only need a few fields, pass fields= to discard everything else:
with PcapReader("capture.pcapng", fields={"ip.src", "ip.dst", "tcp.stream"}) as pcap:
for pkt in pcap:
print(pkt.get("ip.src"), pkt.get("ip.dst"))
_num, _ts, _caplen, and _len are always included. _streams is included only when at least one of its source fields appears in fields. Note that libwireshark still fully dissects every packet - fields= filters the returned dict, it doesn't short-circuit the dissector.
Preferences
Set any Wireshark dissector preference before packets are read:
with PcapReader("capture.pcapng", prefs={"tcp.desegment_tcp_streams": "TRUE"}) as pcap:
for pkt in pcap:
...
Keys and values use the same format as tshark's -o flag. You can also change preferences after construction:
from wiresharkffi import set_preference
set_preference("tcp.desegment_tcp_streams", "FALSE")
set_preference() must be called after at least one PcapReader has been created. For initial setup use the prefs= dict instead.
TLS decryption
Wireshark's TLS dissector runs automatically. Point it at a key log file and decrypted fields appear in the packet dict like any other field:
with PcapReader("capture.pcapng",
prefs={"tls.keylog_file": "/path/to/sslkeylog.txt"}) as pcap:
for pkt in pcap:
print(pkt.get("http.request.uri"), pkt.get("http.response.code"))
Most TLS clients write a key log when SSLKEYLOGFILE is set in the environment. For RSA sessions (no forward secrecy) you can supply the private key instead:
prefs={"tls.keys_list": "192.168.1.1,443,http,/path/to/server.key"}
Async
import asyncio
from wiresharkffi import PcapReader
async def main():
async with PcapReader("capture.pcapng") as pcap:
async for pkt in pcap.async_packets():
print(pkt["_num"], pkt.get("ip.src"))
asyncio.run(main())
async_packets() dispatches each read to a thread-pool executor so it plays nicely with an event loop, but it does not parallelize dissection.
Packet dict format
{
"_num" : 1, # 1-based frame number
"_ts" : 1655239250.367184, # Unix timestamp, float seconds (~µs resolution)
"_caplen" : 98, # captured bytes
"_len" : 98, # on-wire bytes
"_streams": {"tcp": 3}, # stream IDs (omitted if none present)
"eth.src" : "00:0c:29:fa:a3:37",
"ip.src" : "192.168.1.1",
"ip.dst" : "8.8.8.8",
"tcp.stream": 3,
"tcp.srcport": 52431,
"frame.protocols": "eth:ethertype:ip:tcp:http",
# ... every field Wireshark dissects, keyed by abbreviation
}
Field types map to Python types based on the Wireshark field type:
| Wireshark type | Python type | Notes |
|---|---|---|
FT_UINT*, FT_INT* |
int |
|
FT_FLOAT, FT_DOUBLE |
float |
|
FT_STRING* |
str |
|
FT_BYTES, FT_UINT_BYTES |
bytes or str |
controlled by bytes_repr= |
| everything else (IPs, MACs, OIDs, …) | str |
Wireshark's human-readable label |
| repeated field (e.g. multiple TCP options) | list |
Capture metadata
reader.metadata returns pcapng block options as a dict. Plain .pcap files always return {} since pcapng blocks don't exist in that format. reader.file_type (e.g. 'pcapng') and reader.snaplen (per-packet capture byte limit, 0 = unlimited) are available for both formats.
with PcapReader("capture.pcapng") as pcap:
print(pcap.metadata)
# {
# 'shb_userappl': 'Dumpcap (Wireshark) 4.6.x',
# 'shb_os': 'Linux ...',
# 'interfaces': [{'name': 'eth0'}, {'name': 'wlan0'}]
# }
Concurrency
wiresharkffi is one reader per process - libwireshark uses a process-global memory scope that can only be entered once at a time. Creating two PcapReader instances simultaneously crashes with a GLib assertion. The context manager handles this correctly.
For parallel processing across multiple files, use separate processes:
from multiprocessing import Pool
from wiresharkffi import PcapReader
def count_tcp(path):
with PcapReader(path, display_filter="tcp") as pcap:
return sum(1 for _ in pcap)
with Pool() as pool:
counts = pool.map(count_tcp, ["a.pcapng", "b.pcapng", "c.pcapng"])
Threads are not safe - Wireshark dissectors are not thread-safe as the memory scope is process-global, not thread-local. In case you missed it: not thread safe.
Bytes representation
FT_BYTES / FT_UINT_BYTES fields (e.g. tcp.options, tcp.payload) are returned as raw
Python bytes by default. Use the bytes_repr parameter to change the representation:
| Value | Result type | Example |
|---|---|---|
'bytes' (default) |
bytes |
b'\x01\x01\x08\x0a' |
'hexstring' |
str |
'0101080a' |
'ascii' |
str |
'ab\x08\x0a' (printable as-is, non-printable as \xXX) |
'asciidump' |
str |
'ab..' (printable as-is, non-printable as .) |
# JSON-friendly output - no default= workaround needed
with PcapReader("capture.pcapng", bytes_repr="hexstring") as pcap:
for pkt in pcap:
print(json.dumps(pkt))
# Human-readable dump
with PcapReader("capture.pcapng", bytes_repr="asciidump") as pcap:
for pkt in pcap:
if "tcp.payload" in pkt:
print(pkt["tcp.payload"])
JSON output
import json
from wiresharkffi import PcapReader
# Option 1: use bytes_repr for fully JSON-serializable output
with PcapReader("capture.pcapng", bytes_repr="hexstring") as pcap:
for pkt in pcap:
print(json.dumps(pkt))
# Option 2: keep raw bytes and use default=str as a fallback
with PcapReader("capture.pcapng") as pcap:
for pkt in pcap:
print(json.dumps(pkt, default=str))
Architecture
src/wiresharkffi/
_constants.py - buffer sizes and stream-field table
_fields.py - translates the C ws_field_t array into Python dicts
_prefs.py - standalone set_preference() function
_version.py - version check at import time
_reader.py - PcapReader (sync iterator + async generator)
_ws_build.py - CFFI build script (cdef + path detection)
_ws_impl.c - C helper layer (abstracts WS 4.2 / 4.4 / 4.6 API differences)
See docs/internals.md for a detailed walkthrough of how the packet pipeline works.
Running tests
pip install -e .[dev]
pytest tests/ -v
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
File details
Details for the file wiresharkffi-0.1.0.tar.gz.
File metadata
- Download URL: wiresharkffi-0.1.0.tar.gz
- Upload date:
- Size: 3.0 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8d729e4707528f0a356cf5fc0e09984e3400a761b3f73418435ffe3c95927bc5
|
|
| MD5 |
0661b4a9dd527f75dc77f4c8d804968d
|
|
| BLAKE2b-256 |
72c79280e7d21ad25673720dab884d42c59d1cd935e797badb1672e72f3a53ce
|