eBPF-powered network flow capture & analysis, built on aya.
flowscope attaches an XDP program to a network interface, accounts traffic per
protocol and per flow entirely inside the kernel, and streams packet, DNS and
raw-frame events to user space over ring buffers for live analysis, Prometheus
metrics and pcap export. It is a pure-aya program: no
libbpf, no C toolchain at run time.
- XDP capture — an
XDP_PASSprogram reads every ingress frame on the chosen interface without copying or dropping traffic. - Per-protocol counters — TCP / UDP / ICMP / other packet and byte totals
kept in a per-CPU array (
PROTO_STATS). - 5-tuple flow table — per-flow packet/byte counts and SYN/FIN/RST tallies in a kernel hash map, with user-space idle eviction.
- Packet event stream — compact per-packet metadata delivered over a ring buffer for live inspection and metrics.
- IPv4 and IPv6 — a family-tagged flow key and 16-byte address fields cover both stacks through one code path.
- DNS query visibility — UDP/53 queries and responses are parsed in the kernel and the raw QNAME labels are forwarded for user-space decoding.
- 802.1Q VLAN — a single VLAN tag is transparently skipped before L3 parsing.
- Top talkers — source hosts ranked by bytes, aggregated in user space.
- Prometheus metrics — a built-in HTTP endpoint exposes counters in the standard exposition format.
- TOML config — every CLI option is overridable from a config file (precedence: CLI > file > built-in default).
- pcap export — optionally write sampled frames to a classic
pcapfile readable by Wireshark / tcpdump. - JSON or table output — periodic reports as a human table or machine JSON.
- Graceful shutdown —
Ctrl-Cprints a final summary and exits cleanly.
┌──────────────────────────── kernel (XDP) ───────────────────────────┐
ingress frame ──────▶│ parse eth [+ 802.1Q] → IPv4/IPv6 → TCP/UDP/ICMP │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ PROTO_STATS FLOWS EVENTS / DNS_EVENTS / CAPTURE │
│ (per-cpu) (hash 5-tuple) (ring buffers) ▲ │
│ CONFIG (Array) │
└──────────┬───────────┬───────────────┬───────────────────┬──────────┘
│ │ │ │ set at start
read each tick read each tick drained per wakeup │
│ │ │ │
┌──────────▼───────────▼───────────────▼───────────────────▼──────────┐
│ user space (tokio) │
│ flow table + idle eviction · analysis (top talkers) · dns decode │
│ metrics server (Prometheus) · pcap writer · output (table / JSON) │
└─────────────────────────────────────────────────────────────────────┘
- Kernel side counts and indexes; it never allocates and never drops.
- User side snapshots maps on a timer, evicts idle flows, drains the ring
buffers, decodes DNS names, serves metrics and writes
pcap.
See docs/architecture.md for the full design.
- Linux ≥ 5.8 — the BPF ring buffer (
BPF_MAP_TYPE_RINGBUF) landed in 5.8. - Privileges —
CAP_BPF+CAP_NET_ADMIN(or simply root) to load the program and attach XDP. - No libbpf — loading is pure aya; there is no C toolchain dependency at run time.
The user-space crate builds on stable Rust. The flowscope-ebpf crate is
compiled to BPF bytecode and needs the nightly toolchain with the
rust-src component plus bpf-linker:
rustup toolchain install nightly --component rust-src
cargo install bpf-linkerA normal release build then compiles the eBPF object automatically — the
flowscope crate's build.rs invokes aya-build, which builds
flowscope-ebpf for bpfel-unknown-none and embeds the result:
cargo build --release
# binary at target/release/flowscope# basic: attach to eth0, print a table every 5s
sudo ./flowscope --iface eth0
# machine-readable periodic reports
sudo ./flowscope --iface eth0 --format json
# expose Prometheus metrics on :9184
sudo ./flowscope --iface eth0 --metrics-addr 0.0.0.0:9184
# load options from a config file (CLI still wins)
sudo ./flowscope --config /etc/flowscope.toml
# capture every 64th frame to a pcap while running
sudo ./flowscope --iface eth0 --pcap capture.pcap --sample 64
# tune reporting and eviction
sudo ./flowscope --iface wlan0 --interval 10 --top 20 --idle-timeout 120All CLI options may be set in a TOML file; anything passed on the command line overrides the file, and the file overrides the built-in defaults:
# /etc/flowscope.toml
iface = "eth0"
interval = 5 # seconds between reports
top = 10 # number of top talkers to show
idle_timeout = 60 # seconds before an idle flow is evicted
format = "table"
metrics_addr = "0.0.0.0:9184"
pcap = "capture.pcap"
sample = 64 # write every Nth frame to the pcap
dns = true # parse and report DNS queriesWhen --metrics-addr is set, any GET returns a Prometheus exposition body:
| Series | Type | Description |
|---|---|---|
flowscope_packets_total{proto="…"} |
counter | packets seen per protocol |
flowscope_bytes_total{proto="…"} |
counter | bytes seen per protocol |
flowscope_flows |
gauge | live flows in the table |
flowscope_packet_events_total |
counter | packet events streamed |
flowscope_dns_queries_total |
counter | DNS queries observed |
.
├── Cargo.toml # workspace manifest
├── flowscope/ # user-space loader + analysis (binary and library)
│ ├── build.rs # compiles + embeds the eBPF object via aya-build
│ └── src/
│ ├── main.rs # thin binary entry point
│ ├── lib.rs # re-exports the modules below for tests
│ ├── cli.rs # clap argument parsing
│ ├── config.rs # TOML config + settings resolution
│ ├── flow.rs # FlowTable: snapshot + idle eviction
│ ├── analysis.rs # top talkers / top flows
│ ├── dns.rs # DNS QNAME decoding
│ ├── metrics.rs # Prometheus HTTP endpoint
│ ├── pcap.rs # classic pcap writer
│ └── output.rs # table / JSON rendering
├── flowscope-common/ # shared no_std types (maps keys, ring events)
├── flowscope-ebpf/ # the XDP program (compiled to BPF bytecode)
└── docs/architecture.md # design notes
- IPv4 options are not parsed; a 20-byte IPv4 header is assumed.
- DNS QNAMEs are forwarded raw and truncated to
DNS_NAME_CAP(192) bytes. pcapcapture is bounded toCAP_SNAPLEN(256) bytes per frame; longer frames are recorded with their trueorig_lenbut a truncated payload.- Only a single 802.1Q tag is skipped; stacked (QinQ) tags fall through to the
otherbucket.
Dual-licensed under either of Apache-2.0 or MIT at your option.