A live XDP packet-capture analyzer — a mini-Wireshark — that is Rust end to end: an XDP program (Rust → eBPF bytecode) copies packet bytes out of the kernel, a Rust agent streams them over WebSocket, and Rust compiled to WebAssembly dissects every packet and evaluates a tcpdump-style display filter in the browser. SvelteKit renders the classic three-pane capture UI.
┌────────────────────── Linux (Docker Desktop VM) ────────────────────┐
│ kernel userspace │
│ ┌─────────────────────────┐ ring buffer ┌───────────────────────┐ │
│ │ netscope-ebpf (Rust → │ ───────────► │ netscope-agent (Rust) │ │
│ │ BPF via aya) │ hdr+snaplen │ aya loader + axum WS │ │
│ │ · XDP on eth0 (SKB mode)│ │ · one frame / packet │ │
│ │ · snaplen 256 bytes │ └───────────┬───────────┘ │
│ │ · skips its own WS port │ │ ws://:8080 │
│ └─────────────────────────┘ │ │
└───────────────────────────────────────────────────────┼─────────────┘
│ hdr + bytes
┌────────────────────────── Browser ────────────────────▼─────────────┐
│ netscope-wasm (Rust → WASM) SvelteKit │
│ · dissector: Eth/IPv4/IPv6/ARP/TCP/ ──► packet list (filtered) │
│ UDP/ICMP → labeled tree + facts dissection tree + hex │
│ · tcpdump-style filter parser/eval live protocol counters │
└─────────────────────────────────────────────────────────────────────┘
The netscope-common::CaptureHeader layout is shared byte-for-byte across all
three runtimes (bpfel-unknown-none → linux → wasm32).
agent/netscope-ebpf— XDP capture program (aya-ebpf, nightly,bpfel-unknown-none)agent/netscope-common— shared#[repr(C)]capture header (no_std)agent/netscope-agent— loader + WebSocket server (aya, axum, tokio)wasm/— browser-side dissector + filter engine (wasm-bindgen)web/— SvelteKit capture UI
XDP needs a Linux kernel; on macOS the agent runs in Docker Desktop's VM
(with scoped capabilities — BPF, NET_ADMIN, PERFMON, SYS_RESOURCE —
not full privileged) and captures on the container's own eth0.
One command — the agent serves the dashboard itself:
cd web && pnpm install && pnpm build # build the SPA once
cd .. && docker compose up -d --build # agent serves it + captures
docker compose --profile tools up -d traffic # optional traffic generator
# → http://localhost:8082Dev mode (hot reload) is still available separately:
cd web && pnpm dev
# → http://localhost:5173 (talks to the agent WS on ws://localhost:8082/ws)- conversations groups the buffer by 5-tuple (both directions collapse into one row); click a row to filter to it.
- save .pcap downloads the buffer as a classic pcap you can open in Wireshark; load .pcap replays a saved or real capture offline through the same dissector — so you can develop and demo without a live kernel.
You can also drive traffic yourself into the agent's namespace:
docker exec netscope-agent sh -c 'wget -q -O- http://example.com >/dev/null'A small tcpdump/BPF-flavored filter, parsed and evaluated in Rust/WASM:
tcp udp icmp / arp / ip / ip6
port 443 src port 53 dst port 80
host 1.1.1.1 src host 192.168.0.2 dst host 8.8.8.8
tcp and port 443 arp or icmp not (tcp or udp)
Terms combine with and, or, not, parentheses, and adjacency (implicit
and). An invalid filter is reported inline and treated as match-all. Pause
to inspect without the list moving; the WASM buffer keeps the last 5000
packets regardless of filter.
- XDP is ingress-only. You see packets arriving on the interface
(responses to outbound requests, inbound connections), not egress. A
bidirectional capture would use a
tc/clsact classifier instead; XDP is what the "X" in the name refers to. - Snaplen is 256 bytes — enough for L2–L4 headers plus a little payload. The hex pane notes when the original frame was longer.
- The capture program skips TCP to/from its own WebSocket port (8080) so the tool doesn't capture — and amplify — its own transport on the shared interface.
- The eBPF copy uses
bpf_xdp_load_byteswith an opaque (black_box) length guard; a hand-rolled indexed copy trips the verifier's packet-range tracking, and without the barrier LLVM folds the required non-zero length check into a pointer comparison the verifier can't use. See the comments inagent/netscope-ebpf/src/main.rs.
cd wasm && cargo test # dissector + filter + buffer, all native
wasm-pack build wasm --target web --release --out-dir ../web/src/lib/netscope-wasm --no-pack
cd web && pnpm run checkEach binary WS frame is one packet: a 16-byte little-endian CaptureHeader
(ts: u64 kernel-ns, orig_len: u32, cap_len: u32) immediately followed by
cap_len raw packet bytes. On connect the agent sends one JSON text frame
{type, iface, snaplen}.
A self-contained overview deck lives in docs/index.html — open it in any browser (arrow keys / space to navigate).
