# Architektur & Funktionsweise Dieses Dokument erklärt, wie AdGuard Shield intern arbeitet. Es geht dabei nicht nur um die Dateien auf dem System, sondern auch um den Weg einer DNS-Anfrage vom AdGuard-Home-Querylog bis zur Firewall-Sperre und die Logik hinter jeder Erkennungsmethode. ## Kurzüberblick AdGuard Shield besteht in der Go-Version aus einem einzelnen Binary: ```text /opt/adguard-shield/adguard-shield ``` Das Binary übernimmt alle Aufgaben, die früher auf mehrere Shell-Skripte verteilt waren: - Querylog-Polling über die AdGuard-Home-API - Erkennung von Rate-Limit-Verstößen - Erkennung von Random-Subdomain-Floods - DNS-Flood-Watchlist mit sofortigem Permanent-Ban - Verwaltung aktiver Sperren in SQLite - Firewall-Steuerung über `ipset`, `iptables` und `ip6tables` - automatische Freigabe abgelaufener temporärer Sperren - externe Blocklisten und externe Whitelists - GeoIP-Länderfilter - progressive Sperren für Wiederholungstäter - Benachrichtigungen und AbuseIPDB-Reporting - E-Mail-Reports ## Datenfluss ```text Clients │ │ DNS, DoH, DoT, DoQ, DNSCrypt ▼ AdGuard Home │ │ /control/querylog ▼ AdGuard Shield Go-Daemon │ ├── Rate-Limit-Prüfung pro Client + Domain ├── Subdomain-Flood-Prüfung pro Client + Basisdomain ├── Watchlist-Prüfung ├── Whitelist-Prüfung (statisch + extern) ├── GeoIP-Prüfung ├── Progressive Ban-Berechnung ├── externe Listen-Abgleich ▼ SQLite State │ ▼ ipset + iptables/ip6tables │ ▼ DNS-relevante Ports werden für gesperrte Clients blockiert ``` **Wichtig:** AdGuard Shield analysiert nicht den Netzwerkverkehr direkt. Es liest das Querylog von AdGuard Home. Dadurch erkennt es auch Anfragen über verschlüsselte DNS-Protokolle (DoH, DoT, DoQ, DNSCrypt), solange diese in AdGuard Home sichtbar sind. ## Laufzeit im produktiven Betrieb Der systemd-Service startet den Daemon so: ```bash /opt/adguard-shield/adguard-shield -config /opt/adguard-shield/adguard-shield.conf run ``` Beim Start passiert in dieser Reihenfolge: | Schritt | Aktion | Beschreibung | |---:|---|---| | 1 | Konfiguration laden | Liest `adguard-shield.conf` und validiert alle Parameter | | 2 | SQLite-Datenbank öffnen | Öffnet oder erstellt die Datenbank unter `STATE_DIR` im WAL-Modus | | 3 | Logdatei öffnen | Initialisiert die Datei unter `LOG_FILE` | | 4 | Firewall vorbereiten | Erstellt Chain und ipsets, falls nicht vorhanden | | 5 | GeoIP öffnen | Lädt die MaxMind-Datenbank, falls GeoIP aktiviert ist | | 6 | Whitelist-Cache laden | Liest aufgelöste externe Whitelist-IPs aus SQLite | | 7 | GeoIP-Reconcile | Prüft bestehende GeoIP-Sperren gegen aktuelle Konfiguration | | 8 | Firewall-Reconcile | Überträgt aktive Sperren aus SQLite wieder in die Firewall | | 9 | Hintergrundjobs starten | Startet Goroutines für externe Listen und Offense-Cleanup | | 10 | Querylog-Poller starten | Beginnt mit der regelmäßigen Auswertung des Querylogs | Das Reconcile beim Start ist besonders wichtig: Wenn der Server neu startet oder `iptables`-Regeln verloren gehen (z.B. durch einen Reboot), bleiben die Sperren in SQLite erhalten und werden beim nächsten Start wieder in die Firewall übertragen. ## Querylog-Poller Der Daemon ruft regelmäßig den AdGuard-Home-Endpunkt ab: ```text /control/querylog?limit=&response_status=all ``` Gesteuert wird das über: ```bash CHECK_INTERVAL=10 # Abstand zwischen Abfragen in Sekunden API_QUERY_LIMIT=500 # Maximale Einträge pro API-Abfrage ``` Aus jedem Querylog-Eintrag werden diese Informationen extrahiert: | Feld | Verwendung | |---|---| | Zeitstempel | Bestimmt, ob die Anfrage im aktuellen Zeitfenster liegt | | Client-IP | Schlüssel für Rate-Limit, Whitelist, GeoIP und Firewall | | Domain | Schlüssel für Rate-Limit und Subdomain-Flood | | `client_proto` | Anzeige von DNS, DoH, DoT, DoQ oder DNSCrypt | Bereits gesehene Querylog-Einträge werden im Speicher dedupliziert. Der Daemon hält nur Ereignisse aus dem relevanten Zeitfenster plus kleinem Puffer vor, sodass der Speicherverbrauch auch bei hohem DNS-Aufkommen stabil bleibt. ## Erkennungsmethoden im Detail ### Rate-Limit-Sperre Eine Rate-Limit-Sperre entsteht, wenn ein Client dieselbe Domain innerhalb des konfigurierten Fensters zu oft abfragt. **Konfiguration:** ```bash RATE_LIMIT_MAX_REQUESTS=30 # Maximale Anfragen pro Client und Domain RATE_LIMIT_WINDOW=60 # Zeitfenster in Sekunden ``` **Ablauf am Beispiel:** 1. Client `192.168.1.50` fragt `example.com` 45-mal innerhalb von 60 Sekunden ab. 2. Der Poller sieht diese Einträge im Querylog. 3. AdGuard Shield zählt pro Client und Domain. 4. `45 > 30`, also ist das Limit überschritten. 5. Die IP wird gegen statische und externe Whitelists geprüft. 6. Falls die Domain nicht auf der Watchlist steht, entsteht eine normale `rate-limit`-Sperre. 7. Bei aktivem Progressive-Ban wird die aktuelle Offense-Stufe berechnet. 8. Die IP wird in SQLite gespeichert und per Firewall blockiert. 9. History, Log und optionale Benachrichtigung werden geschrieben. ### Subdomain-Flood-Erkennung Random-Subdomain-Floods sehen anders aus als normale Wiederholungen. Ein Client fragt nicht eine Domain ständig ab, sondern erzeugt viele verschiedene, oft zufällige Subdomains: ```text a8f3.example.com k29x.example.com z9p1.example.com m7q2.example.com ``` AdGuard Shield extrahiert daraus die Basisdomain `example.com` und zählt pro Client, wie viele **unterschiedliche** Subdomains im Fenster vorkommen. Direkte Anfragen an `example.com` selbst werden bei dieser Erkennung nicht mitgezählt. **Konfiguration:** ```bash SUBDOMAIN_FLOOD_ENABLED=true SUBDOMAIN_FLOOD_MAX_UNIQUE=50 # Maximale eindeutige Subdomains SUBDOMAIN_FLOOD_WINDOW=60 # Zeitfenster in Sekunden ``` **Ablauf am Beispiel:** 1. Client `10.0.0.99` fragt 63 verschiedene Subdomains von `example.com` ab. 2. Sobald mehr als `SUBDOMAIN_FLOOD_MAX_UNIQUE` eindeutige Subdomains erkannt werden, wird gesperrt. 3. In der History erscheint die Domain als `*.example.com`. 4. Der Grund lautet `subdomain-flood`, außer die Basisdomain steht auf der DNS-Flood-Watchlist. **Hinweise:** - Multi-Part-TLDs wie `.co.uk` werden korrekt als Basisdomain erkannt. - CDNs und manche Apps erzeugen legitim viele Subdomains. In solchen Fällen den Grenzwert erhöhen oder den Client whitelisten. ### DNS-Flood-Watchlist Die Watchlist ist für Domains gedacht, bei denen du nicht stufenweise reagieren möchtest. Wenn eine Domain auf der Watchlist steht und gleichzeitig ein Rate-Limit- oder Subdomain-Flood-Verstoß erkannt wird, wird sofort permanent gesperrt. **Konfiguration:** ```bash DNS_FLOOD_WATCHLIST_ENABLED=true DNS_FLOOD_WATCHLIST="microsoft.com,google.com" ``` **Matching-Logik:** | Anfrage | Watchlist-Eintrag | Treffer? | |---|---|---| | `microsoft.com` | `microsoft.com` | Ja | | `login.microsoft.com` | `microsoft.com` | Ja | | `evil-microsoft.com` | `microsoft.com` | Nein | **Bei einem Treffer:** - Reason wird `dns-flood-watchlist` - Sperre ist immer permanent - Progressive-Ban-Stufen werden für die Dauer ignoriert - AbuseIPDB-Reporting wird ausgelöst, wenn aktiviert und ein API-Key vorhanden ist ### Progressive Sperren Progressive Sperren erhöhen die Sperrdauer bei wiederholten Verstößen. Das Verhalten ähnelt fail2ban. **Konfiguration:** ```bash BAN_DURATION=3600 # Basis-Sperrdauer: 1 Stunde PROGRESSIVE_BAN_ENABLED=true PROGRESSIVE_BAN_MULTIPLIER=2 # Verdopplung pro Stufe PROGRESSIVE_BAN_MAX_LEVEL=5 # Ab Stufe 5 permanent PROGRESSIVE_BAN_RESET_AFTER=86400 # Zähler-Reset nach 24h ohne Vergehen ``` **Stufenverlauf mit Standardwerten:** | Vergehen | Stufe | Berechnung | Sperrdauer | |---:|---:|---|---| | 1 | 1 | 3600 × 2⁰ | 1 Stunde | | 2 | 2 | 3600 × 2¹ | 2 Stunden | | 3 | 3 | 3600 × 2² | 4 Stunden | | 4 | 4 | 3600 × 2³ | 8 Stunden | | 5 | 5 | Max-Level erreicht | Permanent | Der Offense-Zähler wird in SQLite gespeichert. Wenn eine IP länger als `PROGRESSIVE_BAN_RESET_AFTER` (Standard: 24 Stunden) nicht auffällig war, wird der Zähler vom Cleanup-Job entfernt. **Geltungsbereich:** Progressive Sperren gelten nur für Monitor-Sperren (`rate-limit`, `subdomain-flood`). Watchlist-Treffer sind sofort permanent. GeoIP- und externe Blocklist-Sperren haben eigene Regeln. ## Firewall-Modell AdGuard Shield nutzt eine eigene Chain und zwei ipsets, um gesperrte IPs effizient zu verwalten: ```text Chain: ADGUARD_SHIELD IPv4: adguard_shield_v4 IPv6: adguard_shield_v6 ``` ### Chain-Einbindung Die Chain wird je nach `FIREWALL_MODE` in die passende Host-Chain eingehängt: | Modus | Parent-Chain | Einsatzgebiet | |---|---|---| | `host` / `docker-host` | `INPUT` | Klassische Installation oder Docker mit Host-Netzwerk | | `docker-bridge` | `DOCKER-USER` | Docker mit veröffentlichten Ports (`-p 53:53`) | | `hybrid` | `INPUT` und `DOCKER-USER` | Gemischte Setups oder Migrationsphasen | ### Regelstruktur im Host-Modus ```text INPUT ├── tcp/53 → ADGUARD_SHIELD ├── udp/53 → ADGUARD_SHIELD ├── tcp/443 → ADGUARD_SHIELD ├── udp/443 → ADGUARD_SHIELD ├── tcp/853 → ADGUARD_SHIELD └── udp/853 → ADGUARD_SHIELD ADGUARD_SHIELD ├── src in adguard_shield_v4 → DROP └── src in adguard_shield_v6 → DROP ``` Bei Docker Bridge mit veröffentlichten Ports ersetzt `DOCKER-USER` die `INPUT`-Chain. Docker leitet solche Pakete nach DNAT über `FORWARD`; die `INPUT`-Chain sieht sie dort nicht zuverlässig. ### Blockierte Ports Die Ports werden über `BLOCKED_PORTS` konfiguriert: ```bash BLOCKED_PORTS="53 443 853" ``` | Port | Protokoll | Zweck | |---:|---|---| | 53 | UDP/TCP | Klassisches DNS | | 443 | TCP | DNS-over-HTTPS (DoH) | | 853 | TCP/UDP | DNS-over-TLS (DoT) und DNS-over-QUIC (DoQ) | Die Erkennung basiert auf dem AdGuard-Home-Querylog, die Sperre blockiert aber alle konfigurierten Ports, unabhängig davon, welches Protokoll den Verstoß ausgelöst hat. ### Warum ipset? - Viele gesperrte IPs erzeugen nicht tausende einzelne `iptables`-Regeln - IPv4 und IPv6 werden getrennt sauber verwaltet - Sperren und Freigaben sind performant, auch bei hunderten IPs - Die eigene Chain bleibt übersichtlich und beeinträchtigt bestehende Regeln nicht ## SQLite-State Der zentrale Zustand liegt standardmäßig hier: ```text /var/lib/adguard-shield/adguard-shield.db ``` ### Tabellen | Tabelle | Inhalt | Beschreibung | |---|---|---| | `active_bans` | Aktive Sperren | IP, Grund, Dauer, Quelle, Ablaufzeit, Offense-Level, GeoIP-Metadaten | | `ban_history` | Dauerhafte Historie | Zeitstempel, Aktion (BAN/UNBAN/DRY), Client-IP, Domain, Protokoll, Grund | | `offense_tracking` | Progressive-Ban-Stufen | Client-IP, aktuelle Offense-Stufe, letzter Verstoß | | `whitelist_cache` | Externe Whitelist | Aufgelöste IPs aus externen Whitelist-URLs mit Quellzuordnung | | `geoip_cache` | GeoIP-Ergebnisse | IP, Ländercode, Zeitstempel der Abfrage, DB-Änderungszeitpunkt | Die Datenbank nutzt WAL-Modus (Write-Ahead Logging) und einen Busy-Timeout, damit Daemon und CLI-Befehle gleichzeitig lesen und schreiben können, ohne sich gegenseitig zu blockieren. ### History-Aktionen | Aktion | Bedeutung | |---|---| | `BAN` | Aktive Sperre gesetzt (Firewall-Regel erstellt) | | `UNBAN` | Sperre aufgehoben (manuell, abgelaufen oder durch Whitelist) | | `DRY` | Sperre wäre gesetzt worden, wurde aber im Dry-Run nur protokolliert | ## Verzeichnisstruktur Nach einer Standardinstallation sieht die Struktur so aus: ```text /opt/adguard-shield/ ├── adguard-shield # Go-Binary ├── adguard-shield.conf # Konfiguration, chmod 600 ├── adguard-shield.conf.old # Backup nach Konfigurationsmigration └── geoip/ # automatische MaxMind-Downloads /etc/systemd/system/ └── adguard-shield.service /var/lib/adguard-shield/ ├── adguard-shield.db # SQLite State-Datenbank ├── adguard-shield.db-wal # WAL-Datei (im laufenden Betrieb) ├── adguard-shield.db-shm # Shared-Memory-Datei (im laufenden Betrieb) ├── external-blocklist/ # Cache für heruntergeladene Blocklisten ├── external-whitelist/ # Cache für heruntergeladene Whitelists ├── iptables-rules.v4 # Gesicherte IPv4-Regeln (nach firewall-save) └── iptables-rules.v6 # Gesicherte IPv6-Regeln (nach firewall-save) /var/log/ └── adguard-shield.log # Daemon-Logdatei /etc/cron.d/ └── adguard-shield-report # Cron-Job für Reports (optional) ``` ## Hintergrundjobs im Daemon Es gibt in der Go-Version keine separaten Worker-Skripte mehr. Diese Aufgaben laufen als Goroutines im Daemon: | Aufgabe | Wann aktiv | Intervall | Zweck | |---|---|---|---| | Querylog-Poller | Immer | `CHECK_INTERVAL` | Liest und analysiert AdGuard-Home-Querylogs | | Externe Whitelist | `EXTERNAL_WHITELIST_ENABLED=true` | `EXTERNAL_WHITELIST_INTERVAL` | Lädt Listen, löst Hostnamen auf, aktualisiert Whitelist-Cache | | Externe Blocklist | `EXTERNAL_BLOCKLIST_ENABLED=true` | `EXTERNAL_BLOCKLIST_INTERVAL` | Lädt Listen, sperrt neue IPs, hebt entfernte IPs optional auf | | Offense-Cleanup | `PROGRESSIVE_BAN_ENABLED=true` | Stündlich | Entfernt abgelaufene Offense-Zähler | | GeoIP-Lookups | `GEOIP_ENABLED=true` | Mit jedem Poll | Prüft neue öffentliche Client-IPs gegen Länderregeln | Externe Whitelist und Blocklist laufen sofort beim Start einmalig und danach im jeweiligen Intervall. Die Sperren-Freigabe abgelaufener Bans wird bei jedem Querylog-Poll mit geprüft. ## Whitelist-Logik Vor jeder Sperre wird geprüft, ob die IP vertrauenswürdig ist. **Quellen (in Prüfreihenfolge):** 1. Statische `WHITELIST` aus der Konfiguration (kommagetrennt) 2. Aufgelöste IPs aus externen Whitelists (gespeichert in SQLite) **Verhalten:** - Eine gewhitelistete IP wird nie gesperrt, unabhängig von der Sperrquelle. - Dies gilt für automatische Sperren, manuelle Sperren, GeoIP-Sperren und externe Blocklist-Sperren. - Wenn eine externe Whitelist später eine bereits gesperrte IP enthält, hebt der Daemon diese Sperre automatisch auf. ## GeoIP-Logik GeoIP arbeitet mit der MaxMind GeoLite2-Datenbank und filtert DNS-Clients nach ihrem geografischen Herkunftsland. ### Private IPs Wenn `GEOIP_SKIP_PRIVATE=true` gesetzt ist (Standard), werden folgende Adressbereiche übersprungen: - Private Netze (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) - Loopback (127.0.0.0/8, ::1) - Link-Local (169.254.0.0/16, fe80::/10) - CGNAT (100.64.0.0/10) ### Modi | Modus | Verhalten | |---|---| | `blocklist` | Nur die in `GEOIP_COUNTRIES` genannten Länder werden gesperrt. Alle anderen sind erlaubt. | | `allowlist` | Nur die in `GEOIP_COUNTRIES` genannten Länder sind erlaubt. Alle anderen öffentlichen IPs werden gesperrt. | Die Ländercodes folgen dem Standard ISO 3166-1 Alpha-2 (siehe [ISO-3166-1-Kodierliste auf Wikipedia](https://de.wikipedia.org/wiki/ISO-3166-1-Kodierliste)). ### GeoIP-Datenquellen (Priorität) | Priorität | Quelle | Konfiguration | |---:|---|---| | 1 | Manueller MMDB-Pfad | `GEOIP_MMDB_PATH="/pfad/zur/GeoLite2-Country.mmdb"` | | 2 | Automatischer MaxMind-Download | `GEOIP_LICENSE_KEY="dein_key"` | | 3 | Legacy-Fallback | `geoiplookup` / `geoiplookup6` (Systembefehle) | **GeoIP-Sperren sind permanent**, werden aber beim Start gegen die aktuelle Konfiguration geprüft. Wenn GeoIP deaktiviert wird, der Modus wechselt oder ein Land nicht mehr blockiert werden müsste, wird die Sperre automatisch aufgehoben. ## AbuseIPDB-Reporting AbuseIPDB wird nur für **permanente** Monitor-Sperren genutzt: | Wird gemeldet | Wird nicht gemeldet | |---|---| | DNS-Flood-Watchlist-Treffer | Temporäre Rate-Limit-Sperren | | Progressive-Ban auf Maximalstufe | Manuelle Sperren | | | GeoIP-Sperren | | | Externe Blocklist-Sperren | **Konfiguration:** ```bash ABUSEIPDB_ENABLED=true ABUSEIPDB_API_KEY="..." ABUSEIPDB_CATEGORIES="4" # 4 = DDoS Attack ``` Die Kategorie-Nummern sind auf [abuseipdb.com/categories](https://www.abuseipdb.com/categories) dokumentiert. ## Protokollerkennung AdGuard Shield liest das Feld `client_proto` aus der AdGuard-Home-API und zeigt das Protokoll in History, Logs und Benachrichtigungen an: | API-Wert | Anzeige | Beschreibung | |---|---|---| | leer oder `dns` | `DNS` | Klassisches DNS über UDP/TCP | | `doh` | `DoH` | DNS-over-HTTPS | | `dot` | `DoT` | DNS-over-TLS | | `doq` | `DoQ` | DNS-over-QUIC | | `dnscrypt` | `DNSCrypt` | DNSCrypt-Protokoll | Die Sperre blockiert die konfigurierten Ports unabhängig davon, welches Protokoll den Verstoß ausgelöst hat. So wird verhindert, dass ein gesperrter Client einfach auf ein anderes DNS-Protokoll ausweicht. ## History und Logs Es gibt zwei unterschiedliche Blickwinkel auf das Geschehen: | Quelle | Inhalt | Befehl | |---|---|---| | `ban_history` in SQLite | Sperren, Freigaben und Dry-Run-Ereignisse | `history [N]` | | `LOG_FILE` | Daemon-Ereignisse, Worker-Läufe, Warnungen, Fehler | `logs`, `logs-follow` | | Live-Ansicht | Aktuelle Queries, Top-Clients, Sperren, Systemereignisse | `live` | **Wichtig:** Query-Inhalte werden nicht dauerhaft in die Logdatei geschrieben. Für aktuelle Queries ist die Live-Ansicht (`live`) gedacht. ### History-Gründe | Grund | Bedeutung | |---|---| | `rate-limit` | Gleiche Domain zu oft angefragt | | `subdomain-flood` | Zu viele eindeutige Subdomains einer Basisdomain | | `dns-flood-watchlist` | Watchlist-Treffer mit sofortigem Permanent-Ban | | `external-blocklist` | Sperre aus externer Blocklist | | `geoip` | GeoIP-Länderfilter | | `manual` | Manueller Ban oder Unban | | `manual-flush` | Freigabe aller Sperren durch `flush` | | `expired` | Temporäre Sperre ist abgelaufen | | `external-whitelist` | Freigabe durch externe Whitelist | ## Unterschied zur alten Shell-Architektur Früher gab es unter anderem: - `adguard-shield.sh` (Hauptskript) - `iptables-helper.sh` (Firewall-Management) - `external-blocklist-worker.sh` (Blocklist-Synchronisation) - `external-whitelist-worker.sh` (Whitelist-Synchronisation) - `geoip-worker.sh` (GeoIP-Prüfung) - `offense-cleanup-worker.sh` (Offense-Bereinigung) - `report-generator.sh` (Report-Erstellung) - `unban-expired.sh` (Ablauf temporärer Sperren) - Watchdog-Service und Watchdog-Timer (Überwachung) In der Go-Version gibt es diese Skripte nicht mehr. Der systemd-Service nutzt `Restart=on-failure`; die eigentlichen Worker laufen als Goroutines im Daemon. Alte Artefakte werden vom Installer erkannt und müssen vor der Go-Installation entfernt werden, damit nicht zwei Implementierungen parallel dieselbe Firewall und dieselben Dateien verwalten.