# netviz3d — Retro-Pixel-Paketskop für Omarchy *[English version](README.md)* Visualisiert den ein- und ausgehenden Netzwerkverkehr deines Rechners in 3D, im Look eines CRT-Monitors aus den frühen 90ern: niedrig aufgelöst, hart gerastert, ditherfarben, mit Scanlines und Bildröhrenwölbung. Dein Rechner ist der Monolith in der Mitte. Jeder Gegenpeer, mit dem der Kernel gerade spricht, wird zu einem Voxel-Knoten auf einem Ring darum herum — platziert über einen stabilen Hash der IP, damit ein Host immer an derselben Stelle auftaucht. Jedes gemessene Byte wird in kleine Würfel umgerechnet, die über die Verbindung fliegen: eingehend zu dir hin, ausgehend von dir weg. ![netviz3d](assets/screenshot.png) ``` ┌───────────────┐ /proc/net/dev ┌──────────────┐ WebSocket ┌───────────┐ │ Linux-Kernel │ ────────────────▶ │ Backend │ ──────────▶ │ WebGL2 3D │ │ │ ss -tuHnpi │ (Python, │ 20 Hz │ Frontend │ └───────────────┘ ────────────────▶ │ stdlib) │ └───────────┘ └──────────────┘ ``` ## Installation Als Omarchy-Plugin — das Repo *ist* das Plugin, `manifest.json` liegt im Wurzelverzeichnis: ```bash omarchy plugin add https://github.com/olivgrau/omarchy-netviz3d.git --enable ``` Damit landet alles in `~/.config/omarchy/plugins/olivgrau.netviz3d/`, das Pill steht in der Bar und ein Klick startet das Skop — der Launcher wird aus dem Plugin-Verzeichnis heraus aufgerufen, es muss nichts in `$PATH` liegen. Für die volle Desktop-Integration (Keybinding, App-Launcher-Eintrag, Fensterregeln) zusätzlich aus dem geklonten Verzeichnis: ```bash ./install.sh ``` Das legt an: | Was | Wohin | |-----|-------| | Launcher `netviz3d` | `~/.local/bin/netviz3d` (Symlink hierher) | | App-Eintrag + Icon | `~/.local/share/applications/netviz3d.desktop` | | Omarchy-Bar-Plugin | `~/.config/omarchy/plugins/olivgrau.netviz3d` (Symlink aufs Repo) | | Keybinding | `SUPER + SHIFT + N` in `~/.config/hypr/bindings.lua` | | Fensterregeln | Block in `~/.config/hypr/hyprland.lua` | Alles, was der Installer in fremde Dateien schreibt, steht zwischen `-- >>> netviz3d >>>` und `-- <<< netviz3d <<<`. Rückbau: ```bash ./install.sh uninstall ``` Anderes Keybinding: `NETVIZ_KEYBIND="SUPER + ALT + N" ./install.sh` ## Benutzung ```bash netviz3d # Backend starten (falls nötig) und Fenster öffnen netviz3d serve # nur das Backend, im Vordergrund (zum Debuggen) netviz3d status # läuft das Backend? netviz3d stop # Backend beenden ``` Oder `SUPER + SHIFT + N`, oder über den App-Launcher, oder per Klick auf das ↓/↑-Pill in der Omarchy-Bar. ### Tastatur | Taste | Wirkung | |-------|---------| | Ziehen | Kamera umkreisen | | Mausrad | Zoom | | Klick auf Knoten | Peer auswählen → Detail-Panel | | `N` / `B` | nächster / vorheriger Peer (Tastatur-Triage) | | `T` | Verbindungstabelle (volle Breite, nichts abgeschnitten) | | `-` / `+` | HUD-Größe (60 % … 145 %, wird gespeichert) | | `Space` | Paketfluss pausieren | | `H` | HUD an/aus | | `P` | Pixelgröße durchschalten (auto → 2 → 3 → 4 → 6 → 8) | | `G` | Gitter an/aus | | `L` | Peer-Labels an/aus | | `C` | Kamerapreset (Orbit / Top / Low / Close) | | `R` | Ansicht zurücksetzen · `Esc` Auswahl aufheben | | `F` | Vollbild | | `?` | Hilfe | `P` ändert nur die Auflösung der 3D-Szene, `-`/`+` nur die des HUD — beides ist unabhängig. HUD-Größe, Pixelgröße und Ansicht überleben einen Neustart (localStorage). ### Farben Die Farbe eines Knotens sagt, um welche Art Verkehr es geht — TLS, HTTP, DNS, SSH, Infrastruktur (DHCP/NTP/SSDP), LAN-Peer. Pakete sind eingehend bzw. ausgehend eingefärbt. Alle Farben kommen aus dem aktiven Omarchy-Theme (`~/.local/state/omarchy/current/theme/colors.toml`) und wechseln live mit, wenn du das Theme umschaltest — dunkle Themes werden dabei so weit aufgehellt, dass sie gegen das Dithering noch lesbar bleiben. ## Was die Daten hergeben (Security-Perspektive) Das Skop beantwortet nicht „bin ich gehackt", aber es beantwortet Fragen, die man sonst mit drei Terminals gleichzeitig beantworten müsste: **wer redet gerade mit wem, über was, und seit wann.** Pro Verbindung sammelt das Backend: | Feld | Woher | Wozu | |------|-------|------| | Remote-IP, Port, Dienst | `ss` | die Grundfrage | | Reverse-DNS **+ Forward-Bestätigung** | `getnameinfo` + `getaddrinfo` | ein Name, der nicht auf dieselbe IP zurück auflöst, ist kein Beleg für irgendwas | | ASN, Betreiber, Land, Präfix | Team Cymru über DNS, Fallback `whois` | „Google" vs. „ein Hoster, den ich nicht kenne" | | Prozess, PID, **Binärpfad**, Benutzer | `/proc//` | zwei Prozesse heißen `python3`, einer davon liegt in `/tmp` | | Bytes live und kumuliert, RTT | `ss -i` (TCP_INFO) | Volumen und grobe Entfernung | | Alter, Anzahl Verbindungen zur Adresse | eigene Buchführung | kurzlebig-wiederholt vs. eine lange Sitzung | ![Verbindungstabelle](assets/screenshot-table.png) Daraus werden Flags, die in der Tabelle rot erscheinen und den Knoten in der 3D-Szene pulsieren lassen: | Flag | Bedeutung | Warum das interessiert | |------|-----------|------------------------| | `!PLAIN` | Klartextprotokoll (80, 21, 23, 25, 3306, 6379, …) ins öffentliche Netz | Inhalte und oft Zugangsdaten liegen auf der Leitung | | `!RDNS` | Reverse-Name löst nicht auf dieselbe Adresse zurück auf | schwaches, aber billiges Signal | | `~BEACON` | ≥ 4 Verbindungen zur selben Adresse in auffallend regelmäßigem Takt | so sieht C2-Beaconing aus — und leider auch jeder Sync-Client | | `+NEW` | Adresse zum ersten Mal in dieser Sitzung gesehen | „das war vorher nicht da" | | `?PROC` | Socket ohne sichtbaren Besitzerprozess | gehört einem anderen Benutzer oder root | | `?DNS` | überhaupt kein Reverse-Name | bei CDNs normal, bei Einzel-IPs weniger | **Die Flags sind Hinweise, keine Urteile.** `~BEACON` trifft Dropbox genauso wie eine Backdoor; `!PLAIN` trifft jede HTTP-Seite. Der Wert liegt darin, dass Abweichungen vom eigenen Normalzustand sichtbar werden — und den kennt man erst, wenn man das Ding ein paar Mal im Leerlauf hat laufen lassen. Praktische Einstiege: * **Leerlauf-Baseline**: Rechner nichts tun lassen, `T` drücken, notieren wer trotzdem redet. Alles, was danach neu auftaucht, ist erklärungsbedürftig. * **Prozess statt Adresse fragen**: das PROCESSES-Panel rollt Verkehr pro Programm zusammen. Ein Programm, das dort auftaucht und keinen Grund hat, online zu sein, ist der interessanteste Fund. * **`exe`-Pfad im Detail-Panel lesen**, nicht den Prozessnamen. Namen sind frei wählbar, Pfade sind es weniger. * **Kumulierte Bytes** (`Σ▼/Σ▲`) statt der Momentanrate ansehen: Exfiltration ist selten schnell, sie ist ausdauernd. Ein ausgehendes Σ▲, das nicht zu dem passt, was das Programm tun sollte, sticht hier heraus. * **RTT als grobe Entfernung**: 1–5 ms ist die eigene Stadt/CDN, 100 ms+ ist ein anderer Kontinent. ![Peer-Detail](assets/screenshot-inspector.png) `N` und `B` gehen die Gegenstellen der Reihe nach durch, ohne die Maus. Grenzen, damit klar ist was das Ding *nicht* kann: es sieht nur Sockets, die zum Abtastzeitpunkt offen sind (eine 200-ms-Verbindung dazwischen fehlt), es liest keine Nutzdaten, es erkennt kein DNS-Tunneling, und es sieht keinen Verkehr, der den Kernel dieses Rechners nicht durchläuft. Für echtes Mitschneiden ist `tcpdump`/Wireshark zuständig, für dauerhafte Aufzeichnung etwas wie Zeek. ## Das Bar-Plugin `manifest.json` + `BarWidget.qml` im Wurzelverzeichnis machen das Repo zu einem vollwertigen Omarchy-Shell-Plugin (`kinds: ["bar-widget"]`), das im laufenden `omarchy-shell`-Prozess läuft: * zeigt die aggregierte Down-/Up-Rate, einmal pro Sekunde aus `/proc/net/dev` * **Linksklick** öffnet das 3D-Skop, **Mittelklick** wirft `ss -tunp` in ein Terminal, **Rechtsklick** beendet das Backend wieder * funktioniert unabhängig davon, ob Backend oder Fenster laufen Einstellungen (über `shell.json` oder die Plugin-Einstellungen): `interface` (leer = alle), `showRates`, `command`. `install.sh` verlinkt das Repo-Verzeichnis direkt nach `~/.config/omarchy/plugins/olivgrau.netviz3d` — also genau das Layout, das `omarchy plugin add` erzeugt. Änderungen an der QML-Datei laden dadurch sofort neu. Falls nicht: `omarchy-shell shell rescanPlugins`. ## Datenquellen Kein Root, kein Paketmitschnitt, kein `CAP_NET_RAW`: * **`/proc/net/dev`** — Bytes und Pakete pro Interface, alle 250 ms. Das ist die Wahrheit über das Verkehrsvolumen. * **`ss -tuHnpi`** — offene TCP/UDP-Sockets alle 500 ms, inklusive der `bytes_sent`/`bytes_received`-Zähler aus `TCP_INFO`. Daraus kommt der Verkehr *pro Verbindung* und der zugehörige Prozessname. * **Reverse DNS** — verzögert, gecacht, in einem Worker-Thread; mit `--no-dns` abschaltbar. Der Name wird zusätzlich vorwärts aufgelöst, um zu sehen ob er zur Adresse zurückführt. * **Team Cymru (DNS) + `whois`** — ASN, Betreiber, Land und angekündigtes Präfix der Gegenstelle. Der Cymru-Teil ist eine einzige UDP-DNS-Abfrage (das Backend bringt dafür einen minimalen DNS-Client mit, `dig` wird nicht gebraucht); weil die Antwort das Präfix enthält, ist jede weitere Adresse im selben Netz gratis. `whois` läuft nur, wenn Cymru keinen Namen liefert. Gecacht in `~/.cache/netviz3d/netinfo.json`, abschaltbar mit `--no-whois`. Beide Dienste erfahren dabei, nach welchen Adressen du fragst — deshalb der Schalter. * **`/proc//`** — Binärpfad, Kommandozeile und Benutzer des Prozesses, dem der Socket gehört. Was das Interface zeigt, aber kein Socket erklärt (UDP ohne Zähler, Broadcast, Sockets anderer Benutzer), wird als diffuser Hintergrundstrom dargestellt statt still unter den Tisch zu fallen. Das Backend lauscht ausschließlich auf `127.0.0.1` und sendet nichts nach außen. Es gibt keine Payload-Inspektion — nur Zähler, Adressen und Ports. Weil in diesem Stream PIDs, Benutzernamen und Kommandozeilen stecken, reicht die Bindung an Loopback allein nicht — ein Browser spricht bereitwillig im Auftrag jeder geöffneten Seite mit `127.0.0.1`. Deshalb zusätzlich: * Prüfung des **`Host`**-Headers bei jedem Request. Das verhindert, dass eine Domain, deren DNS auf `127.0.0.1` zeigt (Rebinding), die Socket-Tabelle liest. * Prüfung des **`Origin`**-Headers beim WebSocket. WebSockets sind von der Same-Origin-Policy ausgenommen, ein Handshake von beliebigen Seiten würde sonst angenommen (Cross-Site WebSocket Hijacking). * Obergrenzen für gleichzeitige Verbindungen (am Listener), WebSocket-Clients, Framegrößen und untätige Sockets. Die Verbindungsgrenze muss dort sitzen: der Handler-Thread existiert, bevor ein Header gelesen wurde — eine Prüfung im Handler kommt zu spät, um noch irgendetwas zu begrenzen. * Ein `--host` außerhalb von Loopback wird abgelehnt, solange nicht `--insecure-allow-remote` gesetzt ist — im Netz authentifiziert nichts den Leser. ## Aufbau ``` manifest.json Omarchy-Plugin-Manifest (muss im Wurzelverzeichnis liegen) BarWidget.qml Bar-Widget für omarchy-shell backend/netviz_server.py Sampler + Anreicherung + HTTP + WebSocket (stdlib) web/app.js Bootstrap, Kamera, Eingaben, WebSocket web/scene.js 3D-Welt: Host, Peers, Pakete, Gitter web/post.js Pixel-/CRT-Shader (Dither, Bloom, Scanlines) web/hud.js HUD, gezeichnet in echten Pixeln web/vendor/ three.js (MIT), lokal — keine CDN-Abhängigkeit test/hud-smoke.mjs HUD-Zeichenpfade gegen einen Canvas-Stub test/backend-security.py Zugriffsschutz und Ressourcengrenzen des Backends assets/make_icon.py erzeugt das Pixel-Icon bin/netviz3d Launcher install.sh Integration in Omarchy ``` Der Trick beim Look: die Szene wird in einen absichtlich winzigen Framebuffer gerendert (Breite/Pixelgröße), dann von einem Fullscreen-Shader wieder hochgezogen — mit Bayer-Dithering auf wenige Farbstufen, Bloom, Scanlines, Röhrenwölbung, Farbsäumen und Rollbalken. Dabei bekommt der Render-Target ausdrücklich `SRGBColorSpace`: three.js rechnet intern linear und wandelt normalerweise auf dem Weg zum Bildschirm um — dieser Pass schreibt aber über einen rohen Shader aufs Canvas und bekäme die Umwandlung sonst nie, was alles auf etwa halber Helligkeit landen ließe. Pakete werden in **Bildschirmgröße** statt in Weltgröße bemessen (konstant etwa sechs Pixel des Low-Res-Buffers, mit Near-Clip vor der Linse): bei dieser Auflösung verschwindet ein perspektivisch schrumpfendes Paket schlicht, und eines direkt vor der Kamera füllt das halbe Bild. Ihre Anzahl ist bewusst gedeckelt — jenseits von rund zwei Dutzend gleichzeitig pro Verbindung verschmelzen sie zu einem massiven Band, das weniger aussagt als ein lockerer Strom. Das HUD wird zwar im selben virtuellen Pixelraster *angeordnet* (Kästen, Balken und Ditherflächen bleiben grobpixelig), aber in **Geräteauflösung gezeichnet** und erst danach draufkopiert — sonst wäre 8px-Schrift schlicht nicht lesbar. Aus demselben Grund sind die Peer-Beschriftungen keine Sprites in der Szene: ihre 3D-Position wird ins HUD-Koordinatensystem projiziert und der Text dort gezeichnet, inklusive Ausweichen vor den HUD-Panels. ## Fehlersuche ```bash netviz3d serve # Backend im Vordergrund, mit Logs netviz3d serve --no-whois --no-dns # ohne jede externe Abfrage curl -s localhost:8787/api/state | head -c 400 # Rohdaten ansehen tail -f ~/.local/state/netviz3d/backend.log node test/hud-smoke.mjs # HUD-Layouts durchtesten python3 test/backend-security.py # Zugriffsschutz, adversarial omarchy plugin validate . # Manifest gegen das Schema prüfen ``` Läuft die 3D-Ansicht ruckelig, hilft eine größere Pixelgröße (`P`) — sie senkt die Renderauflösung quadratisch. ## Lizenz MIT für den eigenen Code. `web/vendor/three.*` ist three.js (MIT), Copyright three.js authors.