Projektübersicht
NetMonitor ist eine Android-App, die den gesamten Netzwerkverkehr eines Geräts lokal abfängt, protokolliert, analysiert und gezielt blockiert — ganz ohne Root-Rechte. Die technische Grundlage bildet Androids offizielle VpnService-API: NetMonitor meldet sich beim System als lokales VPN an, wodurch jedes IP-Paket, das eine App senden oder empfangen will, zunächst durch die eigene Verarbeitungslogik läuft, bevor es (falls erlaubt) den Weg ins echte Internet nimmt.
Diese Dokumentation beschreibt die Architektur auf Systemebene — nicht Zeile für Zeile, sondern als Landkarte der Bausteine und ihres Zusammenspiels. Zwei Bereiche werden dabei besonders vertieft, wie gewünscht: das VPN-Core-System (Kapitel 3–7) und das modulare Tools-System (Kapitel 8–9), da beide den technisch anspruchsvollsten und zugleich am stärksten erweiterbaren Teil der App darstellen.
Codebasis in Zahlen
| Bestandteil | Dateien | Zeilen (gesamt) | Zeilen (ohne Leerzeilen) |
|---|---|---|---|
| Java — Anwendungslogik & UI | 99 | 15.015 | 13.009 |
| C++ — native VPN-Engine | 1 | 1.349 | 1.215 |
| Java — Unit-/Instrumented-Tests | 12 | 1.313 | — |
| Summe Produktivcode (Java + C++) | 100 | 16.364 | 14.224 |
.java, .cpp). Layout-XML (112 Dateien, ca. 3.630 Zeilen) ist bewusst nicht mitgezählt, da sie deklarative UI-Beschreibung statt Programmlogik ist.Kernmerkmale
Die komplette Verarbeitung läuft lokal auf dem Gerät über die offizielle VpnService-API. Es existiert kein externer NetMonitor-Server.
Eine native C++-Engine (Standard) und eine reine Java-Engine (Fallback) implementieren dieselbe Schnittstelle und verhalten sich für Regeln, Log und UI identisch.
20 Diagnose-Tools sind als unabhängige, selbstregistrierende Module implementiert — neue Tools erfordern keine Änderung an bestehendem Code.
Zeitpläne, Datenquoten, WLAN-gebundene Profile und ein Panic-Modus ergänzen die klassische An/Aus-Blockierung um kontextsensitive Regeln.
Architektur
Die App gliedert sich in vier gleichberechtigte UI-Tabs, verwaltet von MainPagerAdapter, sowie einen davon unabhängigen Hintergrunddienst, der die eigentliche Paketverarbeitung übernimmt. Die Tabs sind reine Ansichten — sie starten oder stoppen nichts Kritisches selbst, sondern lesen und schreiben Zustand, der zentral in NetVpnService, BlockManager und der Room-Datenbank gehalten wird.
| Tab | Fragment-Klasse | Aufgabe |
|---|---|---|
| Monitor | MonitorFragment | Live-Traffic, Terminal-Konsole, 24h-Chart, VPN-Steuerung, Panic-Modus |
| Tools | ToolsFragment | Raster aller registrierten Diagnose-Werkzeuge |
| Notes | NotesFragment | Persistente Freitext-Notizen (eigener Tab seit v1.21.0) |
| Settings | SettingsFragment | Engine-Wahl, Profile, Berechtigungen, Info & Patch Notes |
Paketstruktur
Der Code ist strikt nach fachlicher Verantwortung in Pakete unterteilt, nicht nach technischer Schicht allein:
| Paket | Klassen | Verantwortung |
|---|---|---|
| vpn.core | 9 | Austauschbare VPN-Engines (Java/Native) und deren Verträge |
| vpn | 9 | Paketverarbeitung, Forwarder, Connection-Tracking |
| ui.* | 8+ | Fragmente, Adapter, Dialoge |
| block | 7 | Regel-Engine, Zeitpläne, Profile, Quoten, WLAN-Bindung |
| data.* | 10 | Room-Datenbank, DAOs, Entities, Repository |
| tools.* | 20 | Ein Unterpaket je Diagnose-Werkzeug |
| widget / tile | 4 | Home-Screen-Widget, Quick-Settings-Tile |
| pcap | 3 | Paketmitschnitt im libpcap-Format |
Zentraler Hintergrunddienst
NetVpnService läuft unabhängig vom UI-Lebenszyklus. Er besitzt das TUN-Interface, startet die gewählte Engine über VpnCoreFactory und bleibt aktiv, während die App im Hintergrund ist oder sogar geschlossen wurde.
Bedienung ohne App-Öffnen
Home-Screen-Widget und Quick-Settings-Tile sprechen denselben Dienst über Broadcasts bzw. eine transparente Bridge-Activity an — die App selbst muss dafür nicht sichtbar sein.
VPN-Core-System — Grundprinzip
Alles beginnt mit einem virtuellen Netzwerk-Interface, dem sogenannten TUN-Device. Man kann es sich wie eine Poststelle mitten in einem Bürogebäude vorstellen: Statt dass jede Abteilung (jede installierte App) ihre Post direkt zum Briefkasten an der Straße trägt, muss zwingend jeder Brief zuerst durch diese zentrale Poststelle. Dort wird er geöffnet, registriert, bei Bedarf zurückgehalten — und nur wenn er durchgelassen wird, geht er tatsächlich auf die Reise ins Internet.
Technisch richtet NetVpnService beim Start dieses Interface über einen VpnService.Builder ein:
| Protokoll | TUN-Adresse | Route | Bedingung |
|---|---|---|---|
| IPv4 | 10.0.0.2/32 | 0.0.0.0/0 | immer |
| IPv6 | fd00:1:fd00:1:fd00:1:fd00:1/128 | ::/0 | nur wenn das Gerät eine echte globale IPv6-Adresse besitzt |
Die IPv6-Prüfung ist bewusst konservativ: NetVpnService durchsucht alle aktiven Netzwerk-Interfaces nach einer nicht-linklokalen, nicht-Loopback-IPv6-Adresse. Findet sich keine, bleibt der Tunnel reines IPv4 — ein Gerät ohne echte IPv6-Anbindung bekäme sonst einen funktionslosen IPv6-Stack aufgezwungen, der nur Fehler produzieren würde.
Zwei Kategorien von Apps werden von vornherein aus dem Tunnel ausgeschlossen (addDisallowedApplication):
Alle übrigen Apps senden ihre Pakete unbemerkt in dieses TUN-Interface. Von dort liest eine der beiden VPN-Engines (Kapitel 5 und 6) die Rohdaten, entscheidet über Weiterleitung oder Blockierung, und schreibt erlaubte Antwortpakete zurück ins Interface — für die ursprüngliche App sieht das wie eine ganz normale Netzwerkantwort aus.
VpnCore-Abstraktion
Das Herzstück der Architektur ist eine bewusst schmal gehaltene Schnittstelle: VpnCore. Sie beschreibt nur fünf Operationen — starten, stoppen, laufend prüfen, Block-Modus live umschalten, Name der Engine — und lässt offen, wie die Paketverarbeitung tatsächlich passiert. Dieses Prinzip heißt in der Softwarearchitektur Strategy Pattern: Der aufrufende Code (NetVpnService) kennt nur den Vertrag, nicht die Implementierung.
| Methode | Zweck |
|---|---|
| start(tun, blockMode) | Startet die Capture-Schleife auf dem TUN-Deskriptor |
| stop() | Fordert die Schleife zum Beenden auf und wartet auf den Worker-Thread |
| isRunning() | Liefert true, solange die Schleife aktiv ist |
| setBlockMode(bool) | Schaltet den globalen Block sofort um — ohne TUN-Neustart |
| name() | Menschlich lesbarer Engine-Name für UI und Info-Bildschirm ("Java" / "Native") |
Erzeugt wird die aktive Engine über VpnCoreFactory, die die Nutzerpräferenz aus VpnCorePrefs ausliest:
NativeVpnCore zu instanziieren.JavaVpnCore zurück.JavaVpnCore erzeugt.Java-Engine
JavaVpnCore ist die historisch erste und am längsten erprobte Implementierung. Sie liest Rohpakete in einem eigenen Thread mit maximaler Thread-Priorität (Thread.MAX_PRIORITY) direkt aus dem Dateideskriptor des TUN-Interfaces — Android behandelt das Interface wie eine ganz normale Datei, aus der gelesen und in die geschrieben wird.
Pro gelesenem Paket durchläuft die Schleife vier Schritte:
ConnectionTracker.onPacket() ermittelt UID und App-Namen des Absenders und aktualisiert Byte-Zähler.PcapRecorder übergeben.UdpForwarder, TCP an TcpForwarder.Wichtig für das Zusammenspiel mit NetVpnService: JavaVpnCore arbeitet nicht mit dem TUN-Deskriptor allein, sondern mit einem Session-Objekt, das alle vom Dienst verwalteten Abhängigkeiten bündelt (TUN-Interface, Laufzustand, beide Forwarder, der Tracker). Dieses Bündeln verhindert, dass Zustand doppelt gehalten oder bei einem Neustart der Engine vergessen wird.
TCP-Forwarder
Baut für jede neue TCP-Verbindung einen echten Socket über das reale Netzwerk-Interface auf. Der Socket wird vor der Erstellung über VpnService.protect() geschützt — ohne diesen Schritt würde die App eine Routing-Schleife erzeugen, weil der neue Socket sonst selbst wieder durch den eigenen VPN-Tunnel liefe.
UDP-Forwarder
Identifiziert Sitzungen über das Tupel aus Quell- und Ziel-IP/Port. DNS-Anfragen (Port 53) erhalten ein verkürztes Idle-Timeout von 10 statt 60 Sekunden, da DNS-Antworten typischerweise sehr schnell eintreffen oder gar nicht.
Native Engine (C++)
NativeVpnCore ist seit Version 1.20.0 die Standard-Engine. Sie lädt über JNI eine kompilierte Shared Library (libnetmonitor_native.so, ca. 1.200 Zeilen C++) und übergibt ihr den rohen TUN-Dateideskriptor. Der native Code betreibt eine eigene select()-basierte Ereignisschleife, komplett unabhängig von der Java-VM — das spart den Overhead von Garbage Collection und Objekt-Allokationen auf dem heißesten Pfad der App.
| Merkmal | Wert |
|---|---|
| Maximale gleichzeitige UDP-Sitzungen | 128 |
| Maximale gleichzeitige TCP-Sitzungen | 128 |
| Adressfamilien | getrennte Session-Strukturen für IPv4 und IPv6 (UdpSession/Udp6Session, TcpSession/Tcp6Session) |
| I/O-Modell | select() |
Die Java-Policy-Bridge — der entscheidende Architekturpunkt
Eine naive native Engine würde nur ja/nein auf Paketebene kennen: durchlassen oder verwerfen. Das würde bedeuten, dass die komplette Regel-Logik (Zeitpläne, Quoten, Bypass, Blacklist/Whitelist) in C++ nachgebaut werden müsste — mit doppeltem Pflegeaufwand und Risiko für Inkonsistenzen zwischen beiden Engines. NetMonitor löst das anders:
NativePacketPolicy.handle()), per JNI-Callback.BlockManager befragen, Log-Eintrag schreiben.true = weiterleiten, false = verwerfen — den der native Code direkt umsetzt.Damit trifft die native Engine keine eigenständige Entscheidung. Sie transportiert Pakete extrem effizient, überlässt die eigentliche Regel-Logik aber vollständig der immer aktuellen Java-Implementierung. Beide Engines verhalten sich dadurch für Regeln und UI identisch — ein Nutzer bemerkt beim Umschalten keinen funktionalen Unterschied, nur einen Unterschied im Durchsatz.
onPacket, accept, handle, apply, test — mit boolean- oder void-Rückgabe). Das erlaubt es, den Sink künftig durch ein Lambda, ein anderes Interface oder eine ganz andere Klasse zu ersetzen, ohne den nativen Code anzufassen.UnsatisfiedLinkError), springt VpnCoreFactory automatisch auf die Java-Engine zurück, ohne dass der Nutzer eingreifen muss.Connection-Tracking & Hilfssysteme
ConnectionTracker ist das gemeinsame Beobachtungsobjekt, das von beiden Engines gleichermaßen genutzt wird. Für jedes Paket löst er die UID der sendenden App über ConnectivityManager auf, den Paketnamen über PackageManager, und führt Buch über übertragene Bytes getrennt nach IPv4/IPv6 und Richtung. Verbindungen werden in einer LinkedHashMap mit einer festen Obergrenze von 500 Einträgen nach LRU-Prinzip gehalten — die am längsten inaktive Verbindung fällt zuerst heraus, sodass der Speicherbedarf auch bei sehr aktiven Geräten begrenzt bleibt.
| Klasse | Aufgabe |
|---|---|
| IpPacket | Minimaler Parser für IPv4-/IPv6-Header (Version, Protokoll, Quelle/Ziel, Header-Länge) — bewusst ohne Unterstützung für IPv6-Extension-Header, da diese im mobilen Alltag praktisch nicht vorkommen |
| PacketBuilder | Konstruiert gültige IP-Antwortpakete inklusive korrekter Prüfsummen für den Rückweg ins TUN-Interface |
| HostnameResolver | Ermittelt Klartext-Hostnamen zu einer IP durch Mitlesen von DNS-Antworten und TLS-SNI — funktioniert für IPv4 und IPv6 gleichermaßen |
| SocketProtector | Schnittstelle, über die nativer Code VpnService.protect() aufrufen kann, um Routing-Schleifen bei eigenen Sockets zu vermeiden |
| PanicMode | Zustandsautomat für den zeitlich begrenzten Vollblock (siehe Kapitel 11) — technisch ein temporäres Overlay über dem globalen Block-Flag |
Diese Klassen sind bewusst zustandsarm und ohne Android-spezifische Abhängigkeiten geschrieben, wo immer möglich (IpPacket, PacketBuilder) — das macht sie ohne Emulator direkt mit Unit-Tests prüfbar (siehe Kapitel 17).
Modulares Tools-System — Architektur
Der Tools-Tab zeigt 20 unabhängige Diagnose-Werkzeuge in einem Kachel-Raster. Der bemerkenswerte Teil ist nicht die Anzahl, sondern wie leicht sich diese Zahl erhöhen lässt: ein neues Werkzeug erfordert keine einzige Codezeile Änderung an bestehenden Klassen. Das ist kein Zufall, sondern das Ergebnis einer bewussten Entwurfsentscheidung, die seit Version 1.19.0 gilt.
Das Vertrags-Interface
Jedes Werkzeug implementiert die schmale Schnittstelle ToolDefinition mit genau vier Elementen:
| Element | Bedeutung |
|---|---|
| id() | Stabiler, kleingeschriebener Bezeichner (für spätere Deep-Links / Einstellungen) |
| label() | Kurzer Anzeigename auf der Kachel (Empfehlung: höchstens 8 Zeichen) |
| iconRes() | Drawable-Ressource für das Kachel-Icon |
| launch(Fragment host) | Wird beim Antippen aufgerufen — das Werkzeug zeigt seine komplette UI selbst an |
Selbstregistrierung statt Eintragsliste
Klassische Plugin-Systeme verlangen oft eine zentrale Liste ("registriere dein Tool hier in dieser Datei"). NetMonitor verzichtet bewusst darauf und nutzt stattdessen Javas eingebauten ServiceLoader-Mechanismus. ToolsFragment fragt beim Öffnen des Tabs schlicht: "Gib mir alle Implementierungen von ToolDefinition, die es gibt" — ohne zu wissen, wie viele das sind oder wie sie heißen:
Die Zuordnung, welche Klassen dabei gefunden werden, geschieht über die Java-Standardkonvention einer Registrierungsdatei unter META-INF/services/, benannt nach dem vollqualifizierten Interface-Namen. Diese Datei ist die einzige Stelle, die beim Hinzufügen eines Tools berührt wird — und selbst das ist laut Projektnotizen als reiner Build-Schritt gedacht, nicht als manuelle Code-Änderung an einer bestehenden Klasse.
Die Schablone: TemplateTool.java
Um die Einstiegshürde für ein neues Werkzeug zu minimieren, liegt im Tools-Paket eine fertige Kopiervorlage, TemplateTool.java. Sie ist keine Dokumentation über den Prozess, sondern lauffähiger (wenn auch funktionsloser) Code mit klar markierten TODO-Kommentaren an jeder Stelle, die angepasst werden muss.
Der Ablauf zum Hinzufügen eines neuen Werkzeugs reduziert sich damit auf sechs mechanische Schritte:
tools/<name>/TemplateTool.java dorthin kopieren und in <Name>Tool.java umbenennenid(), label(), iconRes() anpassenlaunch() ergänzenJedes Werkzeug ist dabei vollständig in sich geschlossen: eigenes UI-Layout, eigene Netzwerklogik, eigener Hintergrund-Thread über Executors.newSingleThreadExecutor(), um die UI während länger laufender Operationen (z. B. Traceroute) nicht zu blockieren. Ergebnisse werden über NetMonitorDialogs (Kapitel 13) in einem einheitlich gestalteten Dialog mit Teilen-Funktion präsentiert — jedes Werkzeug muss dieses Dialog-Grundgerüst nicht selbst nachbauen.
Werkzeug-Katalog
Alle 20 aktuell registrierten Werkzeuge im Überblick, gruppiert nach fachlichem Zweck:
Adressen & Erreichbarkeit
| Tool | Funktion |
|---|---|
| Subnet | Berechnet aus IP + Maske Netzadresse, Broadcast, nutzbare Hosts und Wildcard-Maske |
| Ping | Erreichbarkeitstest per TCP-Connect (ICMP ist ohne Root auf Android nicht verfügbar) |
| WoL | Sendet ein Wake-on-LAN Magic Packet per UDP-Broadcast |
| Route | Zeigt aktives Netzwerk-Interface, IP-Adressen, Routing-Tabelle, DNS-Server und MTU |
| Trace | Traceroute mit ansteigendem TTL inklusive grober Geo-Zuordnung der Hops |
Namensauflösung
| Tool | Funktion |
|---|---|
| DNS | Vorwärts- (A/AAAA) und Rückwärtsauflösung mit Latenzmessung |
| DNS-ms | Vergleicht die Antwortzeit mehrerer Resolver (Google, Cloudflare, Quad9, OpenDNS, System-DNS) |
| WHOIS | Rohe WHOIS-Abfrage gegen den zuständigen Server |
| mDNS | Lauscht auf Bonjour-/mDNS-Ankündigungen im lokalen Netz |
Sicherheit & Diagnose
| Tool | Funktion |
|---|---|
| Leak | Prüft heuristisch auf DNS- und IPv6-Lecks außerhalb des VPN-Tunnels |
| TLS | Führt einen TLS-Handshake mit SNI durch und zeigt Zertifikatsdetails, Protokoll und Cipher-Suite |
| HTTP | HTTP-GET mit vollständigen Antwort-Headern und Redirect-Kette |
| Ports | TCP-Connect-Scan über einen Port-Bereich, parallel mit Timeout |
| Listen | Liest /proc/net/tcp und /proc/net/udp und zeigt lokal lauschende Dienste mit UID-Zuordnung |
Netzwerk-Umgebung
| Tool | Funktion |
|---|---|
| Discover | Scannt das lokale Subnetz nach antwortenden Hosts |
| Neigh | Liest die ARP-/Neighbor-Tabelle des Kernels |
| Wi-Fi | Listet sichtbare Access Points mit Kanal, Frequenzband und Signalstärke |
| Cell | Zeigt Mobilfunkzelle, Standard und Signalwerte, optional mit periodischem Log |
Leistung & System
| Tool | Funktion |
|---|---|
| Speed | Lädt eine Testdatei herunter und berechnet Peak- und Durchschnittsbandbreite |
| Usage | Liest die systemseitige App-Datennutzung über NetworkStatsManager (benötigt separat erteilte Berechtigung) |
Block-Engine
BlockManager ist ein zustandshaltendes Singleton, das auf jedes einzelne Paket angewendet wird — der sogenannte Hotpath der App. Aus diesem Grund ist er komplett lock-frei über ConcurrentHashMap implementiert: Ein klassisches synchronized hätte bei hoher Paketrate zum Flaschenhals werden können.
Regel-Modell
Jede App-Regel (AppRule) besteht aus vier unabhängigen booleschen Feldern:
| Feld | Bedeutung |
|---|---|
| blockOut | Ausgehender Verkehr dieser App wird verworfen |
| blockIn | Eingehender Verkehr dieser App wird verworfen |
| bypass | App komplett aus VPN, Monitoring und Blockierung ausgeschlossen |
| allowed | App steht auf der Erlaubnisliste (nur im Whitelist-Modus relevant) |
Auswertungsreihenfolge
Die Methode directionBlockedKey() ist die zentrale Entscheidungsfunktion und prüft in einer festen Priorität:
Unbekannte Verbindungen (weder Paketname noch UID auflösbar) werden über den Schlüssel "unknown" behandelt und im Whitelist-Modus sicherheitshalber grundsätzlich blockiert.
Zeitpläne (BlockSchedule)
Ein Zeitplan beschreibt ein Wochentag-Bitmuster (Bit 0 = Montag … Bit 6 = Sonntag) sowie Start- und Endzeit in Minuten seit Mitternacht. Die Prüfung behandelt Fenster, deren Ende rechnerisch vor dem Start liegt, als Übernacht-Fenster (z. B. 22:00–06:00 Uhr) und kehrt die Bedingung dafür logisch um — eine Zeit ist dann aktiv, wenn sie nach dem Start oder vor dem Ende liegt, statt wie im Normalfall beides gleichzeitig verlangen zu müssen.
Profile & Automatisierung
Oberhalb der reinen Regel-Engine sitzen mehrere Automatisierungsschichten, die seit Version 1.19 sukzessive dazugekommen sind.
Profile (ProfileManager)
Ein Profil bündelt einen Namen, einen Modus (Blacklist/Whitelist) und eine Menge persistenter Regeln. ProfileManager übernimmt Anlegen, Löschen, Zurücksetzen und — entscheidend — das Aktivieren: Beim Wechsel lädt er alle Regeln des Profils aus der Datenbank und schreibt sie geschlossen in den BlockManager. Legacy-Regeln aus einer älteren Datenbankstruktur (vor Einführung der Profile) werden beim ersten Start automatisch einmalig ins Default-Profil migriert.
WLAN-gebundene Profile (SsidProfileStore / SsidProfileWatcher)
Ein Profil lässt sich fest mit dem Namen (SSID) eines WLAN-Netzwerks verknüpfen — eine 1:1-Zuordnung, ein Netzwerk kann jeweils nur an ein Profil gebunden sein. SsidProfileWatcher registriert sich beim Start des VPN-Dienstes als Beobachter für Netzwerkwechsel und prüft bei jedem Wechsel des Standard-Netzwerks, ob die aktuell verbundene SSID an ein Profil gebunden ist. Ist das der Fall und unterscheidet es sich vom aktuell aktiven Profil, wird automatisch umgeschaltet — praktisch für unterschiedliche Regeln zuhause, im Büro oder in öffentlichen Netzen. Das Auslesen der SSID setzt aus Android-Systemgründen die Standort-Berechtigung voraus.
Tages-Datenquota (AppQuotaStore / AppQuotaLedger)
Unabhängig von Out/In-Blockregeln lässt sich pro App ein tägliches Datenlimit in Megabyte setzen. AppQuotaLedger ist dabei bewusst als reine, Android-freie Rechenlogik ausgelagert (kein Zugriff auf Context oder Systemdienste) — das macht sie einfach unit-testbar. Der Tageswechsel wird nicht über einen Alarm oder Hintergrundjob erkannt, sondern beiläufig bei jedem Zugriff: Ein Vergleich des gespeicherten Datums mit dem heutigen Datum (roll()) setzt den Zähler bei Bedarf automatisch zurück, ohne dass ein Wecker die App aufwecken müsste.
Panic-Modus (PanicMode)
Der Panic-Modus ist ein zeitlich begrenztes Overlay über dem globalen Block: Er merkt sich beim Start den bisherigen Block-Zustand, aktiviert den Vollblock für exakt 10 Minuten und stellt danach automatisch den vorherigen Zustand wieder her. Ein Handler-basierter Sekundentakt aktualisiert währenddessen die Restzeit-Anzeige in Konsole, Kachel-Untertitel und Schnelleinstellungen-Panel. Wird der Block-Modus während einer aktiven Panic-Phase manuell deaktiviert (z. B. über das Widget), verwirft onBlockDisabled() den Timer sofort, statt ihn im Hintergrund weiterlaufen zu lassen.
Zugriffspunkte für Panic
Panic lässt sich an drei Stellen auslösen: per langem Druck auf den Block-Schalter im Monitor-Tab, über das Panel der Schnelleinstellungen-Kachel, oder — sobald aktiv — jederzeit vorzeitig über denselben Schalter beenden.
Zusammenspiel der Schichten
Profile, WLAN-Bindung, Quota, Zeitpläne und Panic wirken alle auf denselben BlockManager ein. Es gibt keine konkurrierenden Entscheidungsinstanzen — jede Schicht verändert lediglich, welche Regeln im Manager aktiv sind oder überschreibt kurzzeitig den globalen Block.
Datenbank & Repository
Alles, was einen App-Neustart überdauern muss, liegt in einer Room-Datenbank (AppDatabase), die in sechs Tabellen unterteilt ist:
| Tabelle | Entity | Inhalt |
|---|---|---|
| log_events | LogEventEntity | Einzelne Verbindungsereignisse (App, Protokoll, Richtung, Zielhost) |
| block_rules | BlockRuleEntity | Legacy-Regeln vor Einführung der Profile — nur noch für die einmalige Migration relevant |
| profiles | ProfileEntity | Profilname und Modus (0 = Blacklist, 1 = Whitelist) |
| profile_rules | ProfileRuleEntity | Per-App-Regeln, zusammengesetzter Schlüssel aus Profil-ID und Paketname |
| schedules | ScheduleEntity | Zeitgesteuerte Blockfenster je App |
| traffic_samples | TrafficSampleEntity | Minutengenaue Traffic-Schnappschüsse für das 24h-Diagramm |
RuleRepository ist die einzige Klasse, die sowohl auf die Datenbank als auch auf den In-Memory-BlockManager schreibt — sie hält beide synchron, sodass eine Regeländerung sofort im Hotpath wirkt und dauerhaft gespeichert wird. LogWriter arbeitet dagegen bewusst nicht-blockierend: Ereignisse werden in eine Warteschlange gelegt und asynchron auf einem Hintergrund-Thread nach Room geschrieben, damit das Protokollieren niemals die Paketverarbeitung selbst verzögert.
TrafficSampler wird alle 60 Sekunden vom VPN-Dienst getriggert, summiert die aktuellen Byte-Zähler und schreibt einen Schnappschuss — Einträge älter als 24 Stunden werden dabei automatisch entfernt, sodass die Tabelle dauerhaft klein und die Diagramm-Abfrage konstant schnell bleibt. LogExporter liest bei Bedarf das komplette Protokoll asynchron aus und schreibt es als CSV oder JSON in eine temporäre Datei, die über den System-Share-Dialog geteilt wird.
UI-Struktur
Monitor-Fragment
MonitorFragment ist mit weitem Abstand die umfangreichste UI-Klasse. Sie vereint Terminal-Konsole, App-Kachel-Listen (Outbound/Inbound getrennt), das 24h-Diagramm, eine umschaltbare Log-Ansicht und die komplette Werkzeugleiste (Start, Stopp, Löschen, Aufnahme, Block-Schalter mit Panic-Long-Press). Die Trennung zwischen Kachel- und Datenaufbereitung wurde bewusst ausgelagert:
| Klasse | Aufgabe |
|---|---|
| AppGroup | UI-Modell für eine Kachel — aggregiert Verbindungen und Status-Badges einer App |
| AppGroupFactory | Baut AppGroup-Objekte aus rohen Verbindungsdaten, wertet dabei Bypass/Block/Allow aus und sortiert Bypass-Apps an den Anfang |
| AppTileAdapter | RecyclerView-Adapter für das Kachel-Raster |
| ConsoleRenderer | Baut den ASCII-Rahmen der Terminal-Konsole — feste Zeichenbreite, nur reines ASCII, damit die Ausrichtung geräte- und schriftartunabhängig stabil bleibt |
| TrafficChartView | Custom View für das 24h-Diagramm, berechnet Deltas zwischen Schnappschüssen und ignoriert negative Sprünge nach einem Reset |
Dialog-System
Damit nicht jedes der 20 Werkzeuge und jeder Einstellungsdialog sein eigenes Erscheinungsbild erfindet, existiert eine gemeinsame Fabrik: NetMonitorDialogs stellt fertige Bausteine für Bestätigungsdialoge und Formular-Dialoge (Titel, X-Schließen-Symbol, einheitlicher Abstand, optionale Teilen-Funktion) bereit. DialogStyles vereinheitlicht zusätzlich das Erscheinungsbild der Buttons über alle Dialoge hinweg. AppDetailDialog — der komplexeste Einzeldialog der App — nutzt diese Bausteine, um Block-Schalter, Tages-Quota, Zeitpläne (ScheduleAdapter), Nutzungsstatistik und PCAP-Aufnahme für eine einzelne App in einem konsistenten Fenster zu vereinen.
Notes-Fragment
Seit v1.21.0 ein eigener Tab statt eines Werkzeugs. Die Implementierung ist bewusst minimal: ein einzelnes EditText-Feld, dessen Inhalt in SharedPreferences gesichert wird — sowohl explizit über einen Speichern-Button als auch automatisch beim Verlassen des Tabs (onPause).
PCAP-Aufnahme
Sowohl die Java- als auch die native Engine reichen jedes verarbeitete Rohpaket zusätzlich an PcapRecorder weiter, sofern eine Aufnahme läuft — unabhängig von der gewählten Engine entsteht so ein identisches Mitschnitt-Format.
| Klasse | Aufgabe |
|---|---|
| PcapCaptureLauncher | Startet bei Bedarf zunächst den VPN-Dienst selbst, bevor die eigentliche Aufnahme beginnt — wird sowohl von der Monitor-Werkzeugleiste (gesamter Verkehr) als auch vom App-Detail-Dialog (eine einzelne App) genutzt |
| PcapRecorder | Sammelt Pakete für eine feste Dauer (Standard 60 Sekunden), gefiltert optional auf eine UID/App |
| PcapWriter | Schreibt das klassische libpcap-Format (Mikrosekunden-Zeitstempel, Linktyp DLT_RAW/101 für rohe IP-Pakete) — öffnet direkt und ohne Konvertierung in Wireshark |
Ist die Zielapp zum Zeitpunkt der Aufnahme eigentlich blockiert, hebt der Recorder die Blockierung für die Dauer der Aufnahme temporär auf und stellt sie danach automatisch wieder her — sonst gäbe es schlicht nichts aufzuzeichnen. Nach Ablauf der Aufnahmedauer öffnet sich automatisch der System-Share-Dialog.
Widget & Quick-Settings-Tile
Home-Screen-Widget
NetMonitorAppWidget zeigt Netz-Typ, IPv4/IPv6-Adresse und Übertragungsrate, aktualisiert über WidgetStatusStore (eine reine SharedPreferences-Brücke zwischen VPN-Dienst und Widget). Vier Tasten senden Broadcasts für Start, Stopp, Pause und Aufnahme. Da der Start einer VPN-Verbindung eine Nutzerbestätigung über eine Activity verlangt, leitet WidgetVpnBridgeActivity — eine unsichtbare Übergangs-Activity — diese Anfrage transparent weiter.
Quick-Settings-Tile
VpnTileService verhält sich kontextabhängig: Ist der Dienst aus, startet ein Tipp ihn direkt über die Bridge-Activity. Läuft er bereits, öffnet ein Tipp stattdessen ein kompaktes Panel mit drei Aktionen — Panic, Pause/Fortsetzen, Stopp. Der Kachel-Untertitel (ab Android 10) zeigt jederzeit den aktuellen Zustand: Off, Forward, Block oder Panic mm:ss.
Hilfsklassen & Sound-Feedback
Geiger-Sound (GeigerClick)
Eine bewusst spielerische UX-Funktion: Jedes N-te verarbeitete Paket löst einen kurzen Ton aus — einen hellen Klick für weitergeleiteten, einen dumpfen Ton für blockierten Verkehr. Seit v1.20.1 lassen sich beide Lautstärken getrennt regeln, seit v1.18.11 unterscheiden sich die beiden Klangfarben überhaupt erst. Ein Mindestabstand von 25 ms zwischen zwei Tönen verhindert eine Überlastung des Audiosystems bei hoher Paketrate. Die Dichte ist konfigurierbar: jedes Paket oder nur jedes 20. (Standard).
Weitere Hilfsklassen
| Klasse | Aufgabe |
|---|---|
| FormatUtils | Einheitliche menschenlesbare Formatierung von Byte-Mengen und Übertragungsraten — von jeder UI-Stelle wiederverwendet, damit "12,3 MB" überall gleich aussieht |
| SpeedTracker | Reine Rechenlogik zur Ermittlung von Auf-/Abwärtsrate aus kumulativen Byte-Zählern — ohne Android-Abhängigkeit, seit v1.19.2 aus MonitorFragment herausgelöst und eigenständig unit-getestet |
| NetworkStatusHelper | Momentaufnahme des aktuellen Netzwerks: Typ, Anbieter-Label, lokale IP-Adressen |
| LinkInfo | Aktueller Verbindungstyp und geschätzte Bandbreite für die Konsolen-Anzeige |
| ChecksumUtils | Berechnung von IP-/Transport-Prüfsummen für selbst konstruierte Pakete |
| TextShare | Teilt beliebigen Klartext über das System-Sheet — von jedem Werkzeug für seine Ergebnis-Ausgabe genutzt |
| ThemePrefs | Persistiert die Theme-Wahl (System/Hell/Dunkel) und wendet sie beim App-Start an |
Tests & Qualitätssicherung
Die Testsuite umfasst 123 Testfälle über 10 fachliche Testklassen (plus zwei generierte Beispielklassen aus dem Android-Projekt-Template). Bemerkenswert ist, welche Klassen getestet werden: durchweg jene, die bewusst ohne Android-Abhängigkeiten geschrieben wurden und sich dadurch als reine JVM-Unit-Tests ausführen lassen — schnell, ohne Emulator, ohne Gerät.
| Testklasse | Prüft |
|---|---|
| BlockManagerTest | Blacklist-Grundlogik: Out/In-Blockierung, Bypass-Vorrang |
| BlockManagerWhitelistTest | Whitelist-Modus, insbesondere das sichere Default-Verhalten für unbekannte Apps |
| BlockScheduleTest | Zeitfenster-Logik, einschließlich Übernacht-Fenster über Mitternacht hinweg |
| AppQuotaStoreTest | Tages-Rollover und Grenzwert-Verhalten der Datenquota |
| IpPacketTest | Korrektes Parsen von IPv4-/IPv6-Headern |
| ChecksumUtilsTest | Korrektheit berechneter Prüfsummen |
| SubnetCalculatorTest | CIDR-/Subnetz-Berechnungen des Subnet-Tools |
| SpeedTrackerTest | Raten-Berechnung aus kumulativen Byte-Zählern |
| FormatUtilsTest | Byte-/Raten-Formatierung über Größenordnungen hinweg |
| ConsoleRendererTest | Stabile ASCII-Ausrichtung der Terminal-Konsole |
AppQuotaLedger vs. AppQuotaStore, SpeedTracker vs. MonitorFragment) — ist der Hauptgrund, warum trotz der Größe des Projekts ein so hoher Anteil kritischer Logik ohne Emulator geprüft werden kann.