A home network monitor built from the wire up.
Raw Ethernet frames captured in C, streamed through a lock-free shared-memory ring buffer,
and rendered as live per-device telemetry in the browser.
Most network monitors are a wrapper around libpcap. VoidWalker doesn't use one. It opens an
AF_PACKET/SOCK_RAW socket, bypassing the kernel's normal decapsulation so MAC addresses,
EtherTypes and full frame payloads arrive intact, and hands every frame to userspace itself.
From there the interesting problem is throughput: a busy Wi-Fi link produces thousands of frames a
second, and neither a pipe nor a socket nor a file is a good way to move them between a C process
and a Python one. The capture engine instead writes 10-byte entries into a POSIX shared-memory ring
buffer that the bridge mmaps directly — zero copies, no syscall per frame, no serialization. It's
the highest-throughput option short of writing a kernel module.
The dashboard turns the resulting stream into something a human can read: who is on the network, who made it, how much they're moving, and how that's changing right now.
flowchart LR
NIC([NIC · promiscuous]) --> C
subgraph C["capture-engine/ · C"]
direction TB
S["AF_PACKET / SOCK_RAW<br/>drops PACKET_OUTGOING"] --> P["parse ethhdr<br/>src MAC + length"]
end
C -->|"10-byte entries<br/>release store on head"| SHM[("/dev/shm/sentinel_ring<br/>1024-slot ring · mmap")]
subgraph B["bridge/ · Python asyncio"]
direction TB
R["mmap reader<br/>100 ms head poll"] --> V["OUI vendor lookup<br/>ThreadPoolExecutor(5) + cache"]
end
SHM -->|zero-copy read| B
B -->|"JSON batches · ws://:8765"| D
subgraph D["dashboard/ · Next.js 16"]
direction TB
W["WS client<br/>exponential backoff"] --> U["React table<br/>EMA-smoothed rates"]
end
| Layer | Technology |
|---|---|
| Packet capture | C, raw sockets (AF_PACKET, SOCK_RAW), POSIX shared memory |
| IPC | 1024-slot ring buffer, mmap, packed structs, release-store publication |
| Bridge | Python 3 (asyncio, websockets, mmap, struct, ThreadPoolExecutor) |
| Dashboard | Next.js 16, React 19, TypeScript |
| Protocol | WebSocket, JSON batches |
| Vendor lookup | macvendors.com API (async, cached) |
The capture engine is Linux-only. AF_PACKET has no portable equivalent on macOS or BSD, and
/dev/shm is a Linux tmpfs convention. The bridge and dashboard will run anywhere, so a common
setup is capture + bridge on a Linux box or Raspberry Pi, dashboard on your laptop.
- Linux with
gcc,make, andlibcap(forsetcap) - Python 3.10+
- Node.js 20+
Three processes, three terminals.
1. Capture engine
cd capture-engine
make
sudo setcap cap_net_raw,cap_net_admin=eip ./sentinel_capture # raw sockets without running as root
ip -brief link # find your interface
./sentinel_capture wlan02. Bridge
cd bridge
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
python3 streamer.py # serves ws://0.0.0.0:87653. Dashboard
cd dashboard
npm install
cp .env.example .env.local # set NEXT_PUBLIC_SENTINEL_HOST to the capture machine's IP
npm run devOpen http://localhost:3000.
Four problems that were more interesting than they looked:
- Struct alignment across the language boundary. Without
#pragma pack(push, 1), GCC inserted 2 bytes of padding after the 6-byte MAC, so Python'sstruct.unpack("<6sI")read every entry shifted by two bytes and reported garbage lengths. The ring buffer layout is now an explicit contract between the two processes. - Publishing ring entries safely. The writer fills a slot, then advances
head. At-O2the compiler is free to reorder those, letting the reader see aheadthat points at a half-written entry — so the pointer update is an explicit__ATOMIC_RELEASEstore. - Blocking I/O inside an event loop. A synchronous
urlopen()for vendor lookup froze the entire WebSocket stream for the duration of every HTTP request. Fixed with aThreadPoolExecutorplus a placeholder-cache pattern: the MAC is immediately cached asANALYZING...and the real name is filled in out-of-band, so the same OUI is never fetched twice. - Filtering your own traffic. A promiscuous socket also sees everything this host transmits,
which double-counts the monitoring machine.
recvfromfills insockaddr_ll.sll_pkttype, so frames markedPACKET_OUTGOINGare dropped before they ever reach the ring.
The full write-up — OSI background, raw socket theory, ring buffer mechanics, and the complete
debugging narrative — is in docs/VoidWalker-technical-writeup.pdf.
- Historical traffic graphs (SQLite-backed time series)
- Anomaly detection (new-device alerts, bandwidth baselines)
- PCAP export for Wireshark / tcpdump compatibility
- Offline OUI database, to drop the external vendor API
- Suricata / Snort integration
- Prometheus metrics endpoint
- Wireless monitor mode with Radiotap header parsing
VoidWalker is passive: it never transmits, injects or modifies traffic. It does, however, observe every frame on the link, including devices that are not yours. Run it only on networks you own or have written permission to monitor. Packet capture on networks you don't control is illegal in most jurisdictions.
MIT — see LICENSE.
Built by Banit Sriram Ambati
