Cover art — AI-generated concept art, not a screenshot. Real frames from the live renderer are in the gallery below.
A single-page WebGPU visualization of a live Tailscale tailnet,
rendered as a living 4D graph. GPU-driven particle flows stream between your nodes in real
time, derived from tailscale status --json.
A real screenshot of the running app — this node (dev-cx53) at the center, live particle flows to active peers.
Tailnet-only. It reads the local tailnet and is meant to be served over the tailnet (via
tailscale serve, which also gives you the HTTPS that WebGPU requires).
On any machine in your tailnet that runs the Tailscale daemon:
git clone https://github.com/peterlodri-sec/tailscope
cd tailscope
python3 server.py & # serves 127.0.0.1:8799 — stdlib only, no deps
sudo tailscale serve --bg --https=443 8799 # expose it over the tailnet with HTTPStailscale serve prints the URL, e.g. https://<node>.<your-tailnet>.ts.net/. Open it from
any tailnet device in a WebGPU browser (recent Chrome / Edge, or Safari 18+) and you'll see
your tailnet come alive.
Plain
http://…over the LAN will not work — WebGPU requires a secure context, whichtailscale serveprovides automatically (it terminates TLS with a valid*.ts.netcert).
Stop it:
sudo tailscale serve --https=443 off && pkill -f server.py| axis | encoding |
|---|---|
| space | nodes laid out in 3D, clustered by DERP relay region; this node at the origin |
| time | particles stream continuously; the graph breathes as live byte-rates change |
| direction | warm particles leave this node (tx), cool particles arrive (rx) |
| channel | hue = DERP relay region (or direct), on a perceptual OKLCH wheel; particle size + speed ∝ byte-rate |
Rates are computed client-side from deltas between RxBytes/TxBytes snapshots, so no packet
capture or elevated privileges are needed — it visualizes exactly what the Tailscale daemon
already reports.
Real frames captured from the live renderer (this node = dev-cx53) — every image in this
section is an actual screenshot of the running WebGPU app, not a mock-up or concept render.
| overview | flow close-up |
|---|---|
![]() |
![]() |
![]() |
![]() |
The bright stream in these shots is a live flow (~3–7 K/s) between two nodes; offline peers fade to grey, and each ring is tinted by its DERP region. The HUD (top-left) and the flowing through <region> readout (bottom) update live as byte-rates change.
AI-generated concept art — not screenshots. These illustrate tailscope's four dimensions in the ENTHEA palette; the real renderer is in the gallery above.
| channels — relay regions | direction — tx vs rx |
|---|---|
![]() |
![]() |
space · time · direction · channel — the living 4D graph. Generated with gpt-image-1.
The chrome is tuned toward the ENTHEA visual synthesizer: the graph
floats in a slow-breathing OKLab aurora void, channel hues are spaced on a perceptual OKLCH
wheel, nodes bloom in time with their live byte-rate, and the HUD is a glass console with a
centered status pill that names the busiest channel. A vignette + scanline + film-grain pass
gives it a dithered, instrument-panel texture. Motion respects prefers-reduced-motion.
drag orbit · scroll zoom · L labels · Space pause · R recenter
server.py— dependency-free HTTP server (Python stdlib). Serves the page and/api/tailnet, a compact transform oftailscale status --json(per-peer rx/tx, online, OS, DERP relay), cached ~1s so polls are cheap.index.html— the whole app in one file: WebGPU (a WGSL compute pass advances ~6k particles along edges each frame; instanced render passes draw nodes, edges, and particles with additive glow), an orbit camera, and a projected-label / HUD overlay. Polls the API every 1.5 s and diffs byte counters into live rates.
tailscale status exposes per-peer cumulative byte counters, online state, OS, and the DERP
relay region — not per-packet type. So "packet type" is approximated honestly by
direction × relay-region × rate; the color/size system is built around what the daemon truly
reports. Swapping in a real per-packet feed (e.g. a tailscale0 capture streamed over a
WebSocket) is a natural extension — the renderer already keys everything off the edge buffer.
Built with 🤖 Claude Code.







