pkttap
Cross-platform packet capture with pktbaffle filter expressions. Capture live traffic from a network interface or read packets from a .pcap / .pcapng file — through the same API, on Linux, macOS, and Windows.
Contents
- Installation
- Platform support
- Quick start
- Live capture
- File capture
- Writing pcap files
- The Packet type
- Error handling
- Platform notes
- The inspect example
- Filter expression language
Installation
[]
= "0.1"
Runtime dependencies by platform:
| Platform | Requirement |
|---|---|
| Linux | CAP_NET_RAW capability (or run as root) |
| macOS | Read permission on /dev/bpf* (or run as root) |
| Windows | Npcap installed |
Platform support
| Feature | Linux | macOS | Windows |
|---|---|---|---|
| Live capture | ✓ AF_PACKET | ✓ /dev/bpf | ✓ Npcap |
| Kernel-level BPF filter | ✓ SO_ATTACH_FILTER | ✓ BIOCSETF | ✓ pcap_setfilter |
| Promiscuous mode | ✓ | ✓ | ✓ |
| Snaplen | ✓ | ✓ | ✓ |
| pcap file read | ✓ | ✓ | ✓ |
| pcapng file read | ✓ | ✓ | ✓ |
| pcap file write | ✓ | ✓ | ✓ |
| pcapng file write | ✓ | ✓ | ✓ |
Quick start
use ;
// ── Live capture ──────────────────────────────────────────────────────────────
let mut cap = live
.promiscuous
.filter
.open?;
while let Some = cap.next?
// ── File capture ──────────────────────────────────────────────────────────────
let mut cap = from_file
.filter
.open?;
while let Some = cap.next?
// ── Write packets to a new pcap file ─────────────────────────────────────────
let mut dump = to_file
.link_type
.open?;
dump.write_packet?;
Live capture
Listing interfaces
Before starting a capture you can enumerate the available interfaces:
let interfaces = interfaces?;
for name in &interfaces
On Windows, friendly names are returned (e.g., "Wi-Fi", "Ethernet"). On Linux and macOS, system names are returned (e.g., "eth0", "en0").
From the command line:
$ cargo run --example inspect -- -l
eth0
lo
wlan0
Basic capture
use Capture;
let mut cap = live.open?;
loop
next() blocks until a packet arrives. It returns Ok(None) only for file captures at end-of-file; a live capture blocks indefinitely.
Promiscuous mode
By default the interface only delivers packets addressed to the host. Enable promiscuous mode to receive all traffic on the segment:
let mut cap = live
.promiscuous
.open?;
Promiscuous mode requires root or
CAP_NET_RAWon Linux and macOS, and administrator privileges on Windows.
Applying a filter
Filters are compiled with pktbaffle and applied in the kernel (or Npcap driver on Windows), so only matching packets ever reach your process. This is far more efficient than filtering in userspace.
filter() accepts a &str, an Option<&str>, or None. Passing None (or an Option containing None) is a no-op — equivalent to not calling .filter() at all. This lets you pass an optional filter from a variable without a conditional branch:
// Always filter — pass a &str directly
let mut cap = live.filter.open?;
// Conditionally filter — pass Option<&str> from e.g. a CLI argument
let expr: = args.get.map;
let mut cap = live.filter.open?;
// Explicitly no filter
let mut cap = live.filter.open?;
Further examples:
// HTTP or HTTPS, excluding internal traffic
let mut cap = live
.filter
.open?;
// DNS queries
let mut cap = live.filter.open?;
// All ICMP
let mut cap = live.filter.open?;
If filter() is not called (or None is passed), all packets are captured.
See the filter expression language section for the full syntax.
Snaplen
The snapshot length limits how many bytes of each packet are captured. Bytes beyond the snaplen are truncated. The default is 65535 (capture the entire packet).
// Capture only the first 128 bytes of each packet (headers only for most traffic)
let mut cap = live
.snaplen
.open?;
// Check whether a packet was truncated
if pkt.is_truncated
Pre-compiled filters
If you already have a compiled pktbaffle::bpf::Program, attach it directly:
use ;
use Capture;
let prog = compile?;
let cbpf = match prog ;
let mut cap = live
.filter_program
.open?;
This is useful when you want to compile the filter once and reuse it across multiple captures, or when you build the bpf::Program by hand.
File capture
pkttap reads both classic pcap (.pcap) and next-generation pcap (.pcapng) files. The format is detected automatically from the file's magic bytes, not the extension.
Reading a pcap file
use Capture;
let mut cap = from_file.open?;
while let Some = cap.next?
next() returns Ok(None) at end-of-file. Use ? to propagate I/O or parse errors.
Reading a pcapng file
pcapng files are handled identically from the caller's perspective. Per-interface link types (from Interface Description Blocks) are tracked automatically, so each Packet carries the correct link_type even in multi-interface captures.
let mut cap = from_file.open?;
while let Some = cap.next?
Filtering while reading
Filters are applied in userspace via the pktbaffle software VM when reading files. Packets that do not match are skipped transparently.
let mut cap = from_file
.filter
.open?;
while let Some = cap.next?
// Count SYN packets in a capture
let mut cap = from_file
.filter
.open?;
let mut syn_count = 0u64;
while cap.next?.is_some
println!;
Link type detection
The link type is read from the pcap file header (classic) or from Interface Description Blocks (pcapng). Capture::link_type() returns the type of the first interface:
let cap = from_file.open?;
println!; // e.g. Ethernet
Writing pcap files
The Dump type writes packets to a .pcap or .pcapng file. The format is selected automatically by file extension.
Streaming writes
use ;
let mut cap = live
.filter
.promiscuous
.open?;
// Write captured packets to a pcap file
let mut dump = to_file
.link_type // inherit from capture source
.open?;
let mut count = 0u32;
while count < 1000
// Dump flushes and closes automatically when dropped
Dump must be told the link type before open() is called — it uses this to write the correct header. Use cap.link_type() to inherit it from a live or file capture source.
flush() is a no-op (the underlying File has no Rust-level buffer), but it is available for uniform checkpoint-style code. The file is closed when Dump is dropped.
One-shot convenience function
For writing a collected set of packets in one call:
use ;
let packets: = collect_packets?;
dump_packets?;
pcapng output
Use a .pcapng extension to write next-generation format. pkttap writes an Interface Description Block followed by Enhanced Packet Blocks — fully compatible with Wireshark and tcpdump.
let mut dump = to_file
.link_type
.open?;
for pkt in &packets
Choosing a format:
| Format | Extension | Use when |
|---|---|---|
| pcap | .pcap |
Maximum compatibility — works with every tool |
| pcapng | .pcapng |
You need nanosecond timestamps, interface names, or multi-interface captures |
The Packet type
Every packet returned by Capture::next() or passed to Dump::write_packet() is a Packet:
while let Some = cap.next?
LinkType determines the framing of pkt.data:
LinkType |
pkt.data starts with |
|---|---|
Ethernet |
14-byte Ethernet header (dst MAC, src MAC, EtherType) |
RawIp |
IP header directly (no link-layer header) |
LinuxSll |
16-byte Linux SLL header |
Error handling
All fallible operations return Result<T, pkttap::Error>:
use Error;
match live.open
Error variants at a glance:
| Variant | When it occurs |
|---|---|
Error::PermissionDenied |
Insufficient privileges for live capture |
Error::Filter(pktbaffle::Error) |
Filter expression is invalid |
Error::Platform(String) |
Interface not found, Npcap not installed, etc. |
Error::Io(std::io::Error) |
File open/read/write failure |
Error::Pcap(pcap_file::PcapError) |
Malformed pcap or pcapng file |
The Error type implements std::error::Error and Display, so it works with ?, anyhow, thiserror, and similar libraries.
Platform notes
Linux
pkttap uses AF_PACKET / SOCK_RAW sockets with SO_ATTACH_FILTER for kernel-level cBPF filtering. No C libraries are required.
Privileges: The process needs CAP_NET_RAW. Either run as root or grant the capability:
sudo setcap cap_net_raw=ep ./your_binary
Interface names: Use the names shown by ip link or ifconfig (e.g., eth0, enp3s0, wlan0, lo).
Link type: Determined automatically by reading /sys/class/net/<iface>/type. Ethernet NICs and the loopback interface both report Ethernet (the kernel prepends a synthetic Ethernet header on loopback for AF_PACKET).
// All interfaces (including loopback)
let mut cap = live.filter.open?;
macOS
pkttap uses /dev/bpf* character devices with BIOCSETF for kernel-level cBPF filtering. No C libraries are required.
Privileges: /dev/bpf* is typically root-only. Either run as root or use Wireshark's ChmodBPF helper to grant access to a group.
Interface names: Use names shown by ifconfig (e.g., en0, en1, lo0).
Link type: Determined by BIOCGDLT after opening the device — the authoritative DLT from the kernel.
let mut cap = live
.filter
.promiscuous
.open?;
Windows
pkttap uses Npcap via dynamically-loaded wpcap.dll. The binary compiles and starts without Npcap present; open() and interfaces() return a clear error if Npcap is not installed.
Prerequisites: Install Npcap (free for personal use). Enable "WinPcap API-compatible mode" or use the Npcap SDK header path during installation.
Privileges: Live capture requires Administrator privileges or membership in the NPF_Users group (Npcap can be configured to allow non-admin access during install).
Interface names: pkttap exposes Npcap's friendly interface descriptions (e.g., "Wi-Fi", "Ethernet", "Local Area Connection"). You can also pass the raw \Device\NPF_{GUID} device path directly.
// Friendly name (preferred)
let mut cap = live.filter.open?;
// Raw device path (escape hatch)
let mut cap = live.open?;
// List available interfaces
for name in interfaces?
DLL search order: pkttap looks for wpcap.dll in:
%SystemRoot%\System32\Npcap\(Npcap's default install path)%SystemRoot%\System32\(WinPcap legacy / manually placed)- Directories on
%PATH%
The inspect example
The inspect example reads from a live interface or a pcap/pcapng file and prints each packet as a hex + ASCII dump. It demonstrates all major pkttap features.
$ cargo run --example inspect -p pkttap -- --help
inspect — packet inspector
USAGE:
inspect <INTERFACE|FILE> [FILTER]
inspect -l | --list-interfaces
inspect -h | --help
...
Common uses:
# Capture all traffic on eth0
# Capture HTTPS traffic only
# Inspect a pcap file
# Inspect a pcap file with a filter
# List available interfaces
Sample output:
link type: Ethernet
[ 1] 1747123456.018432 Ethernet 74 bytes
0000 45 00 00 4a 1a 2b 40 00 40 06 c3 d4 c0 a8 01 0a |E..J.+@.@.......|
0010 8b fb 45 00 c4 f2 01 bb de ad be ef 00 00 00 00 |..E..............|
0020 a0 02 fa f0 12 34 00 00 02 04 05 b4 04 02 08 0a |.....4..........|
0030 ...
[ 2] 1747123456.019102 Ethernet 66 bytes
0000 45 00 00 28 1a 2c 40 00 40 06 c3 ed c0 a8 01 0a |E..(.,@.@.......|
...
2 packets captured
Each packet shows:
- Packet number and timestamp (seconds.microseconds since epoch)
- Link type and on-wire length (with
[truncated to N]if snaplen applies) - Hex dump: 16 bytes per row — offset, two groups of 8 hex bytes, then a
|printable ASCII|column
Filter expression language
pkttap uses pktbaffle to compile filter expressions. The same libpcap / tcpdump syntax applies:
tcp port 443 # HTTPS
udp port 53 # DNS
icmp # all ICMP
host 192.168.1.1 # to/from a specific host
src net 10.0.0.0/8 # from an entire network
tcp and port 22 # SSH
not port 22 # everything except SSH
(port 80 or port 443) and tcp # web traffic
vlan 100 # VLAN 100
tcp[tcpflags] & tcp-syn != 0 # SYN packets (new connections)
len > 1200 # large packets
ether host aa:bb:cc:dd:ee:ff # specific MAC address
See the pktbaffle README for the complete filter expression reference, including raw byte access, named constants, VLAN/MPLS, and all supported primitives.
License
Licensed under the MIT license.