Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

VoidWalker

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.

C Python Next.js Platform License

Dashboard


What it is

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.

How it works

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
Loading
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)

Requirements

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, and libcap (for setcap)
  • Python 3.10+
  • Node.js 20+

Quickstart

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 wlan0

2. 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:8765

3. Dashboard

cd dashboard
npm install
cp .env.example .env.local     # set NEXT_PUBLIC_SENTINEL_HOST to the capture machine's IP
npm run dev

Open http://localhost:3000.

Engineering notes

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's struct.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 -O2 the compiler is free to reorder those, letting the reader see a head that points at a half-written entry — so the pointer update is an explicit __ATOMIC_RELEASE store.
  • 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 a ThreadPoolExecutor plus a placeholder-cache pattern: the MAC is immediately cached as ANALYZING... 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. recvfrom fills in sockaddr_ll.sll_pkttype, so frames marked PACKET_OUTGOING are 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.

Roadmap

  • 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

Legal

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.

License

MIT — see LICENSE.

Built by Banit Sriram Ambati

About

Home network monitor built from the wire up: raw Ethernet capture in C (AF_PACKET), zero-copy shared-memory IPC, live per-device telemetry in a Next.js dashboard.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages