DNS-Filter ohne Single Point of Failure

Schema: Clients fragen die Domänencontroller, diese leiten an zwei AdGuard-Instanzen weiter; Konfiguration aus Git an Instanz 1, Sync auf Instanz 2, Root-Hints als Rückfall

Verfasst von

in

Ein DNS-Filter wie AdGuard Home ist schnell aufgesetzt: ein kleiner Container, ein paar Filterlisten. Wenn dieser eine Container hängt, steht im ganzen Haus das Internet still, obwohl die Leitung funktioniert. Kein Name wird mehr aufgelöst, und jeder fragt dich, warum nichts geht. Ich betreibe deshalb zwei Instanzen auf zwei verschiedenen Proxmox-Knoten, mit gleicher Konfiguration aus einem Git-Repository. Hier steht, wie das zusammenhängt und was bei einem Ausfall passiert.

Schema: Clients fragen die Domänencontroller, diese leiten an zwei AdGuard-Instanzen weiter; Konfiguration aus Git an Instanz 1, Sync auf Instanz 2, Root-Hints als Rückfall

Zwei Instanzen auf zwei Knoten

Beide Instanzen laufen als LXC-Container mit Debian 13, je 2 vCPU, 1 GB RAM und 6 GB Platte. Instanz 1 liegt auf Knoten 2, Instanz 2 auf Knoten 3. Ein einzelner Knotenausfall nimmt also immer nur einen Filter mit. Beim Start eines Knotens kommen die Filter früh: nach der Firewall und den Domänencontrollern, aber vor allen anderen Diensten.

Zusätzlich wird jede Instanz stündlich per ZFS auf den jeweils anderen Knoten repliziert, und beide sind im nächtlichen Backup. Die Replik ist aber nur der Rückfall für den Rückfall. Im Alltag sorgt die zweite Instanz für die Redundanz. Deshalb fasse ich bei Updates oder Neustarts immer nur eine Instanz an, die andere hält so lange das DNS.

Das Abfrageprotokoll wächst: Bei 90 Tagen Aufbewahrung werden die Logdateien bis zu 1,8 GB groß, und die Platten haben nur 6 GB.

Wer fragt wen

Die Clients im Netz fragen nicht direkt AdGuard. Sie bekommen per DHCP die beiden Domänencontroller als DNS-Server. Die DCs beantworten die Namen der internen Domäne selbst und leiten alles andere an beide AdGuard-Instanzen weiter. AdGuard filtert und fragt dann über verschlüsselte Verbindungen (DNS over TLS und DNS over HTTPS) bei einem öffentlichen Resolver nach.

Eine Ergänzung habe ich erst spät eingebaut: Fragt doch einmal ein Gerät direkt bei AdGuard nach einem internen Namen, leitet AdGuard ihn an die DCs weiter. Vorher bekam es „gibt es nicht“ zurück. Das ist eine Zeile im Upstream:

upstream_dns:
  - '[/example.lan/]192.0.2.12 192.0.2.13'
  - tls://1.1.1.1
  - https://dns.cloudflare.com/dns-query

Gefiltert wird mit 13 Listen, darunter mehrere Listen von HaGeZi, OISD Big und Listen gegen Phishing und Schadsoftware. Dazu kommen 33 eigene Regeln. Fast alle sind Ausnahmen für Streaming-Dienste, Zustimmungsdialoge und Kopierschutz, die sonst nicht mehr funktionieren. Die Antwortzeiten liegen nach der Umstellung auf verschlüsselte Upstreams bei 40 bis 130 Millisekunden.

Eine Konfiguration aus Git

Zwei Filter nützen wenig, wenn sie sich unterschiedlich verhalten. Bei mir gilt deshalb: Die Konfiguration wird nicht in der Weboberfläche geändert, sondern im Git-Repository. Die Datei AdGuardHome.yaml liegt in einem Projekt in meinem selbst betriebenen GitLab. Jede Änderung läuft als Merge Request, und gemergt wird nur von mir. Repository, Instanz 1 und Instanz 2 sind drei Orte für dieselbe Einstellung, geändert wird deshalb nur im Repository. So sehe ich jede Abweichung und kann jeden Stand zurückholen.

Nach dem Merge laufen zwei Prüfungen automatisch: eine Syntaxprüfung der YAML-Datei und ein Vergleich, ob der Stand im Repository dem Stand auf der Instanz entspricht. Der Rollout selbst ist ein manueller Klick in der Pipeline. So bestätige ich jede Änderung zweimal: beim Merge und beim Ausrollen.

Der Rollout geht nur an Instanz 1. Die Pipeline meldet sich dort mit einem eigenen SSH-Schlüssel an, der genau ein Programm ausführen darf. Dieses Programm kennt drei Befehle: Status, Konfiguration abholen, Konfiguration einspielen. Beim Einspielen prüft es die Datei, legt eine Sicherung an, startet den Dienst neu und testet, ob Port 53 antwortet. Klappt das nicht, rollt es selbst zurück.

# Ablauf von "deploy" auf der Instanz (vereinfacht)
AdGuardHome --check-config -c neu.yaml || exit 1
cp AdGuardHome.yaml "AdGuardHome.yaml.bak-$(date +%F-%H%M)"   # zehn Stück aufbewahren
mv neu.yaml AdGuardHome.yaml
systemctl restart AdGuardHome
# Dienst und Port 53 prüfen, sonst Sicherung zurück und neu starten

Kurz nach dem Aufbau meldete der Vergleich einen Unterschied: Auf Instanz 1 war HTTPS für die Weboberfläche aktiv, im Repository nicht. Es war direkt an der Instanz eingeschaltet worden, am Repository vorbei, und ein Klick auf „Rollout“ hätte es wieder abgeschaltet. Ich habe zuerst einen Merge Request angelegt, der den Live-Stand ins Repository übernimmt, und erst danach ausgerollt.

Rollout einer AdGuard-Konfiguration: Merge Request, automatische Prüfungen, manueller Klick, Einspielen auf Instanz 1 mit Sicherung und Rückfall, Sync auf Instanz 2

Instanz 2 bekommt die Einstellungen von Instanz 1. Das übernimmt das Werkzeug adguardhome-sync in einem Docker-Container: beim Start und danach alle 30 Minuten, mit einem eigenen Benutzer in beiden Instanzen. Instanz 1 ist der Ursprung, Instanz 2 das Abbild. Änderungen direkt auf Instanz 2 werden beim nächsten Lauf überschrieben. TLS-Einstellungen und die Liste der Admin-Benutzer überträgt das Werkzeug nicht, Instanz 2 hat ihr Zertifikat deshalb separat bekommen.

Was bei einem Ausfall passiert

AusfallFolge
eine InstanzDie DCs fragen die andere. Kaum spürbar.
Instanz 1 länger wegFiltern geht weiter. Änderungen und Sync pausieren, bis sie zurück ist.
beide InstanzenDie DCs lösen selbst über die Root-Server auf. Langsamer, aber kein Ausfall. Es wird nicht mehr gefiltert.
beide DCsKein DNS für die Clients, auch wenn AdGuard läuft.

Der dritte Fall ist der wichtigste. Auf beiden DCs sind die Root-Hints als Rückfall aktiv, mit einem Timeout von drei Sekunden. Fallen beide Filter aus, wird die Namensauflösung langsamer, bricht aber nicht ab. Für mich ist das der richtige Kompromiss: Ein Filter darf ausfallen, das Internet nicht. Der vierte Fall zeigt dafür die eigentliche Abhängigkeit: Der Single Point of Failure ist nicht AdGuard, sondern das Paar der DCs. Deshalb liegen auch sie auf zwei Knoten.

Das Monitoring prüft auf beiden Instanzen, ob Port 53 offen ist und eine echte DNS-Abfrage beantwortet wird. Am Anfang stand dort nur ein Ping. Ein Container, der antwortet, aber dessen Dienst hängt, wäre damit grün gewesen.

Offen ist bei mir noch ein Test: die Replik eines Filters im Wartungsfenster zu starten, während das Original aus ist. Den Ablauf habe ich aufgeschrieben, aber noch nicht geübt. Außerdem fehlt die AdGuardHome.yaml noch in meiner verschlüsselten Konfigurationssicherung. Sie liegt zwar im Repository, aber ein zweiter Ort schadet nicht.

Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht, deine IP-Adresse wird nicht gespeichert. Kommentare erscheinen erst nach Freigabe. Was mit deinen Angaben passiert, steht in der Datenschutzerklärung. Pflichtfelder sind mit * markiert.