Files
adguard-shield/docs/architektur.md
2026-05-01 01:17:05 +02:00

18 KiB
Raw Blame History

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:

/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

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:

/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:

/control/querylog?limit=<API_QUERY_LIMIT>&response_status=all

Gesteuert wird das über:

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:

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:

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:

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:

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:

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 Stunden
3 3 3600 × 4 Stunden
4 4 3600 × 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:

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

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:

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:

/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:

/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).

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:

ABUSEIPDB_ENABLED=true
ABUSEIPDB_API_KEY="..."
ABUSEIPDB_CATEGORIES="4"     # 4 = DDoS Attack

Die Kategorie-Nummern sind auf 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.