Vor mittlerweile recht langer Zeit habe ich hier einen Artikel über die Einrichtung eines redundanten DNS-Systems mit Pi-hole und keepalived geschrieben, wobei sowohl die DNS-Server als auch Pi-hole mit Docker-Containern realisiert wurden. Wobei – so ganz stimmt das nicht. Genaugenommen sollten es zwei Artikel werden. Im ersten wollte ich die Struktur des Systems vorstellen, der zweite sollte sich mit der Konfiguration im Detail beschäftigen.
Pi-hole mit keepalived – von damals bis heute
Dankenswerterweise erhielt ich auch Kommentare und Nachfragen aufgrund des ersten Artikels, doch musste ich bislang den zweiten Teil schuldig bleiben. Um genau zu sein, war ich mit der bisherigen Konfiguration nicht ganz zufrieden. Zwar lief Pi-hole als Docker-Container, die Interaktion mit keepalived ließ jedoch ein wenig zu wünschen übrig, so dass bei Ausfall des ersten Nodes zwar dank keepalived die IP-Adresse vom zweiten System übernommen wurde, jedoch der Pi-hole-Container nicht automatisch startete. Dennoch liefen die DNS-Server und Pi-hole im Rahmen des Gesamtsystems seit geraumer Zeit problemlos vor sich hin und verrichteten ihren Dienst. Insofern war meine Motivation, das Thema nochmal aufzugreifen und so weit fortzuführen, dass die letzten Probleme behoben werden, um daraufhin dann die Konfiguration in einem Artikel vorzustellen, eher unterdurchschnittlich. Und so blieb es beim bisherigen Artikel über die Struktur der DNS-Server mit Pi-hole und keepalived. Inzwischen haben sich die Umstände jedoch geändert. Wesentlich dazu beigetragen hat der Aspekt, dass anstatt des bisherigen Bind-DNS-Servers inzwischen die DNS-Server mit PowerDNS betrieben werden, wiederum mit Hilfe von Docker realisiert. Die Gründe habe ich ausführlich vorgestellt, ebenso die Konfiguration und das Deployment. Nun fehlten also noch die Bausteine keepalived und Pi-hole. Beides wurde neu eingerichtet und wesentlich überarbeitet, so dass die Systeme insgesamt nun die zuvor gesteckten Erwartungen erfüllen. Auf diese, nun neue Konfiguration werde ich im Folgenden eingehen.
Voraussetzungen und Übersicht
Als Voraussetzungen befinden sich die beiden PowerDNS-Server innerhalb von Docker-Containern wie zuletzt beschrieben auf zwei VMs. Nun soll zunächst Pi-hole auf einem der Hosts eingerichtet werden, damit die grundsätzliche Funktionalität getestet werden kann. In einem nächsten Schritt folgt die Einrichtung von keepalived auf beiden Hosts.
Vorab noch die aktualisierte Übersicht der verwendeten Hosts und IP-Adressen:
Host | Name | IP | Verwendung |
VM 1 | pankow | 192.168.10.104 | IP der VM 1 |
VM 2 | mickten | 192.168.10.38 | IP der VM 2 |
VM 1 | nsintern | 192.168.10.20 (alt) | Pi-holed DNS (Master, falls aktiv) |
VM 2 | nsintern2 | 192.168.10.21 (alt) | Pi-holed DNS (Backup, falls aktiv) |
VM 1 | nsbackend1 | 192.168.10.220 | Primary DNS intern (ohne Pi-hole) |
VM 2 | nsbackend2 | 192.168.10.221 | Secondary DNS intern (ohne Pi-hole) |
VM 1 oder VM 2 (Floated) | – | 192.168.10.19 | Floating IP Pi-holed DNS |
Zur Verwendung des verteilten Dateisystems hatte ich im ersten Artikel ja bereits Stellung bezogen. Nach etwa eineinhalb Jahren Betrieb lässt sich feststellen, dass LizardFS seine Aufgabe sehr gut erfüllt, insbesondere sind keine Ausfälle oder gar Datenverluste während der gesamten Zeit aufgetreten. Die Performance ist ebenfalls gut, ich würde sie als „schnell genug“ bezeichnen, wobei dies nicht durch Benchmark-Ergebnisse gestützt wird, sondern auf den Erfahrungen während des Betriebs basiert. Dies gilt auch im Vergleich zu GlusterFS, das – aus welchen Gründen auch immer- seinerzeit bei ersten Tests inakzeptabel langsam war. Insofern wird auch weiterhin LizardFS verwendet, das beim Booten auf beiden VMs gemountet wird und Speicherplatz für Pi-hole im Verzeichnis /srv/liz/safe/pihole/
zur Verfügung stellt. Statt dessen könnte beispielsweise auch NFS verwendet werden, oder ein Samba-Share auf einem (redundanten) NAS – alleine wichtig an dieser Stelle ist, dass die Pi-hole-Instanz, gleichgültig auf welcher VM sie gerade läuft, auf denselben persistenten Speicher zugreift, damit Konfigurationen und Logs nahtlos übernommen werden können.
Vorbereitung des Kernels
Zur Vorbereitung muss noch ein Kernel-Parameter geändert werden. Damit Dienste an IP-Adressen gebunden werden können, die nicht auf dem System existieren, muss /proc/sys/net/ipv4/ip_nonlocal_bind
auf „1
“ gestellt werden. Für Ubuntu empfiehlt sich die Verwendung innerhalb eines eigenen Config-Files anstatt direkter Modifikation der zentralen Konfigurationsdatei:
sudo su sudo echo "net.ipv4.ip_nonlocal_bind = 1" > /etc/sysctl.d/20-keepalived.conf
Die neuen Optionen können übernommen werden durch das Kommando sysctl -p
, alternativ empfiehlt sich auch einfach ein Reboot, bei dem die Kernel-Parameter entsprechend gesetzt werden. Diese Option muss natürlich auf beiden Hosts gesetzt werden, zur Prüfung lässt sich der Wert auch wieder auslesen:
geschke@pankow:~$ cat /proc/sys/net/ipv4/ip_nonlocal_bind 1
Das sieht insofern gut aus, weiter geht es mit der Einrichtung und dem ersten Test von Pi-hole.
Pi-hole mit Docker Compose
Für Pi-hole auf dem Master bzw. dem ersten Host existiert nun ein Docker-Compose-File, das die Konfiguration beschreibt:
version: '3.7' services: pihole: image: pihole/pihole:latest env_file: .env restart: always volumes: - /srv/liz/safe/pihole/pihole/:/etc/pihole/ - /srv/liz/safe/pihole/dnsmasq.d/:/etc/dnsmasq.d/ ports: - "127.0.0.1:53:53/udp" - "127.0.0.1:53:53/tcp" - "192.168.10.20:53:53/tcp" - "192.168.10.20:53:53/udp" - "192.168.10.19:53:53/tcp" - "192.168.10.19:53:53/udp" - "192.168.10.20:67:67/udp" - "192.168.10.19:67:67/udp" - "192.168.10.19:80:80" - "192.168.10.19:443:443" environment: ServerIP: "${IP}" DNS1: "192.168.10.220" DNS2: "192.168.10.221" WEBPASSWORD: "PASSWORD" dns: - 192.168.10.220 - 192.168.10.221 cap_add: - NET_ADMIN
Umgebungsvariablen können beispielsweise mit einem Env-File, hier .env
eingerichtet werden, in dem sich die IP-Adresse des Hosts befindet:
geschke@pankow:~/services/pihole$ cat .env IP=192.168.10.104
Die Docker-Compose-Datei auf dem Backup- bzw. zweiten Host unterscheidet sich nur dahingehend, dass anstatt der IP-Adresse 192.168.10.20 in den Port-Optionen die IP-Adresse 192.168.10.21 genutzt wird, siehe Tabelle. Alle anderen Optionen sind identisch. Dazu sei erwähnt, dass beide IP-Adressen genaugenommen nicht notwendig sind, sondern nur aus Kompatibilitätsgründen existieren. Die alten DNS-Server befanden sich genau auf diesen IP-Adressen. Falls also irgendwo ein längst vergessenes System im Netz existiert, das noch statisch auf diese DNS-Server konfiguriert ist, wird es wenigstens nicht vom Internet abgeschnitten sein… Neue Systeme werden jedoch ausschließlich mit der Floating-IP von Pi-Hole, d.h. der 192.168.10.19 konfiguriert, die ja per Definition immer verfügbar ist.
Die meisten Einstellungen dürften sich selbst erklären. Für die Konfigurationsdateien von Pi-hole und des darin verwendeten Dnsmasq werden Verzeichnisse im verteilten Dateisystem zur Verfügung gestellt und in den Container gemountet. Es folgt eine Reihe von Ports, die nach außen geöffnet werden, dies betrifft Port 53 für DNS, Port 67 für DHCP und die Ports 80 und 443 für HTTP bzw. HTTPS für den Zugriff auf das Admin-Interface von Pi-hole. Da auf demselben Host auf Port 80 die Web-UI von PowerDNS-Admin läuft, wird die Web-UI von Pi-hole nur auf der Floating-IP 192.168.10.19 zur Verfügung gestellt. Als Umgebungsvariablen werden die IP-Adresse des Hosts (per .env-File), die vom Docker-Container zu verwendenden Upstream-DNS-Server und das Passwort für die Admin-Web-UI übergeben. Die DNS-Server lassen sich ebenfalls per Admin-UI ändern, standardmäßig sind die Public-DNS-Server von Google eingestellt. Das Passwort dient zum Zugriff auf die Admin-UI.
Dem Docker-Container werden mit der dns-Option ebenfalls die internen PowerDNS-Server als zu verwendende DNS-Server übergeben. Die Option cap_add
bezieht sich auf die Linux-Capabilities, wobei NET_ADMIN nur dann notwendig ist, wenn Pi-hole als DHCP-Server im Netz fungieren soll, andernfalls kann diese Einstellung entfernt werden. Hinweise zu weiteren Optionen befinden sich in der Dokumentation zum Pi-hole-Docker-Image.
Für einen ersten Test kann Pi-hole per Docker-Compose so gestartet werden, dass es zunächst nicht als Daemon im Hintergrund läuft. Dies kann auch bereits ohne keepalived stattfinden, damit sichergestellt werden kann, dass zumindest Pi-hole einzeln funktioniert. Später erfolgt der Start automatisch mittels Skript, das durch keepalived aufgerufen wird. Darüber hinaus muss zunächst das Pi-hole-Docker-Image geladen werden, und während des Starts werden einige Block-Listen-Dateien heruntergeladen – auch das dauert je nach Verbindung einen Moment. Der erste Start kann etwa so aussehen:
geschke@pankow:~/services/pihole$ docker-compose -f pihole.yml up Creating network "pihole_default" with the default driver Creating pihole_pihole_1 ... done Attaching to pihole_pihole_1 pihole_1 | [s6-init] making user provided files available at /var/run/s6/etc...exited 0. pihole_1 | [s6-init] ensuring user provided files have correct perms...exited 0. pihole_1 | [fix-attrs.d] applying ownership & permissions fixes... pihole_1 | [fix-attrs.d] 01-resolver-resolv: applying... [...viele Ausgaben später...] pihole_1 | [services.d] starting services pihole_1 | Starting crond pihole_1 | Starting pihole-FTL (no-daemon) as root pihole_1 | Starting lighttpd pihole_1 | [services.d] done.
Damit sollte Pi-hole laufen, jedoch ist die Admin-UI noch nicht erreichbar, da diese unter der Floating-IP gestartet wurde.
Keepalived: Die Installation
Zur Einrichtung der Floating-IP ist keepalived notwendig, der zunächst einmal installiert und aktiviert werden muss:
geschke@pankow:~$ sudo apt install keepalived
geschke@pankow:/etc/keepalived$ sudo systemctl enable keepalived.service Synchronizing state of keepalived.service with SysV service script with /lib/systemd/systemd-sysv-install. Executing: /lib/systemd/systemd-sysv-install enable keepalived
Der Status wird an dieser Stelle jedoch noch keinen Erfolg vermelden, denn bislang fehlt noch jegliche Konfiguration. Diese erfolgt im Verzeichnis /etc/keepalived/
mit zumindest einer Datei keepalived.conf
. Für einen ersten Test finden sich etwa im Artikel über den Alltag eines Sysadmins: Keepalived einige Hinweise.
Keepalived: Die zentrale(n) Konfigurationsdatei(en)
Doch genug der Vorrede – auf dem Master bzw. der VM1 sieht die Datei wie folgt aus:
global_defs { notification_email { ralf@kuerbis.org } notification_email_from keepalived@geschke.net smtp_server mailout.geschke.net smtp_connect_timeout 30 router_id pankow script_user root enable_script_security } vrrp_script chk_pihole { script "/etc/keepalived/check_pihole.sh" interval 15 timeout 45 fall 6 rise 10 } vrrp_instance PIHOLE { state MASTER interface ens3 virtual_router_id 52 priority 150 advert_int 5 smtp_alert notify /etc/keepalived/pihole_notify.sh unicast_src_ip 192.168.10.104 unicast_peer { 192.168.10.38 } authentication { auth_type PASS auth_pass PASSWORT } virtual_ipaddress { 192.168.10.19/24 } track_script { chk_pihole } }
Dasselbe auf dem Backup- bzw. zweiten Host:
global_defs { notification_email { ralf@kuerbis.org } notification_email_from keepalived@geschke.net smtp_server mailout.geschke.net smtp_connect_timeout 30 router_id mickten script_user root enable_script_security } vrrp_script chk_pihole { script "/etc/keepalived/check_pihole.sh" interval 15 timeout 45 fall 6 rise 10 } vrrp_instance PIHOLE { state BACKUP interface ens3 virtual_router_id 52 priority 50 advert_int 5 smtp_alert notify /etc/keepalived/pihole_notify.sh unicast_src_ip 192.168.10.38 unicast_peer { 192.168.10.104 } authentication { auth_type PASS auth_pass PASSWORT } virtual_ipaddress { 192.168.10.19/24 } track_script { chk_pihole } }
Ich werde nicht auf jede Option eingehen, vor allem ist der erste Abschnitt nahezu selbsterklärend. Kommt es zu Statusänderungen, kann keepalived Mails versenden – eine praktische Funktion, denn ansonsten würde eine Änderung im Stillen erfolgen bzw. nur in den Logfiles mitgeschrieben werden. Die Mail wird somit an die Adresse(n) in notification_email
geschickt, und zwar von der Adresse in notification_email_from
, als Mailserver wird der interne Ausgangs-Mailserver in smtp_server
genutzt. Im Betreff wird die Angabe in router_id
genannt, so dass sich die Zeile wie folgt darstellt: „[pankow] VRRP Instance PIHOLE – Entering MASTER state„, somit ist auf den ersten Blick erkennbar, welche Aktion ausgeführt wurde.
Mit vrrp_instance PIHOLE
werden unter der Bezeichnung PIHOLE
die Eigenschaften für ein Interface definiert. Was zunächst abstrakt klingt, bedeutet einfach, dass in diesem Abschnitt das Verhalten von keepalived festgelegt wird, inklusive Parameter wie Interface, IP-Adressen etc.. VRRP steht für Virtual Router Redundancy Protocol und bietet ein Verfahren zur Hochverfügbarkeit von Routern in lokalen Netzen. Dabei präsentieren sich mehrere Router als logische Einheit, bestehend aus einem Master- und einem oder mehreren Backup-Routern. Dabei erhält der Master die virtuelle (oder floating, failover) IP-Adresse, die bei Ausfall des Masters von einem der Backup-Router übernommen wird, der fortan als Master fungiert. Genau das soll hier passieren – die IP-Adresse soll entweder mit dem Master verbunden sein, oder bei Ausfall mit der Backup-Instanz. Gleichzeitig soll sichergestellt werden, dass Pi-hole entweder auf dem Master oder dem Backup läuft, wobei die Backup-Instanz bei Ausfall zum Master erhoben wird. Die Festlegung, welcher Host als Master und welcher als Backup fungiert, erfolgt allein durch die Option priority
. Dabei ist Master derjenige Host, der aktiv ist und die höchste Priorität in der Gruppe besitzt.
Die jeweiligen Optionen sind entweder identisch oder „spiegelverkehrt“, d.h. beziehen sich auf den jeweils anderen Host. Die Dokumentation befindet sich klassisch in der Manpage, die mit man 5 keepalived.conf
aufgerufen werden kann. Die weiteren Optionen in der Übersicht:
state MASTER
: initialer Status beim Starten von keepalived, entweder MASTER oder BACKUP, wobei die Wahl einzig durch den Wert vonpriority
bestimmt wird, so dass diese Option allenfalls Dokumentationscharakter besitztinterface ens3
: Das Interface, das für VRRP genutzt werden soll.virtual_router_id 52
: Falls mehrere Instanzen auf einem Interface genutzt werden, ist eine eindeutige Kennzeichnung notwendig, dabei können die Werte 1 bis 255 verwendet werden.priority 150
: Anhand der Priorität wird der Master ausgewählt, die höhere Priorität hat Vorrang.advert_int 5
: Intervall der VRRP-Pakete in Sekundensmtp_alert
: Aktiviert die Benachrichtigung per E-Mailnotify /etc/keepalived/pihole_notify.sh
: Bei Statusänderungen wird das hier angegebene Skript aufgerufen, dabei wird insbesondere der Status dem Skript übergeben, so dass darin eine entsprechende Aktion ausgeführt werden kann; dazu später mehr.unicast_src_ip 192.168.10.104
: Quelladresse für Unicast; optional, da die Adresse des Hosts per Default genutzt wird, die durch diese Angabe überschrieben werden kann, d.h. es ist möglich, hier eine andere Adresse anzugebenunicast_peer
: Liste von IP-Adressen der jeweils anderen beteiligten Hosts der VRRP-Gruppeauthentication
: Die Authentifizierung erfolgt hier über ein Passwort (auth_type PASS
), das auf den beteiligten Hosts identisch sein muss, die Angabe erfolgt inauth_pass
.virtual_ipaddress
: Die virtuelle bzw. floating IP-Adresse inkl. Subnetzmasketrack_script
: Mitvrrp_script chk_pihole
(siehe unten) kann ein Skript definiert werden, das aufgerufen wird, um ein Objekt zu überwachen, aus dessen Zustand sich das Verhalten von keepalived ergibt. Sollte also die Pi-hole-Instanz nicht funktionieren, wird nach einer bestimmten Anzahl von Versuchen der Master aufgeben und keepalived auf dem Backup-Host aktivieren.
Keepalived: VRRP-Skript-Konfiguration und Shellskript
Im Bereich vrrp_script
wird ein Skript namens chk_pihole
definiert. Die Bezeichnung „Skript“ mag hier missverständlich erscheinen, denn chk_pihole
ist letztlich eine Bezeichnung für ein Objekt, das sich aus verschiedenen Optionen zusammensetzt. Mittels track_script
wird festgelegt, dass dieses Objekt genutzt werden soll. Im Objekt – oder „Skript“ – chk_pihole
wiederum wird ein Shell-Skript aufgerufen, dessen Rückgabewert ausgewertet wird, dabei bedeuten wie üblich der Wert 0 Erfolg und der Wert 1 Misserfolg bzw. Fehler. Die Parameter im Einzelnen:
script "/etc/keepalived/check_pihole.sh"
: Name inkl. Pfad des Shellskripts, das periodisch aufgerufen wirdinterval 15
: Zeit in Sekunden zwischen einem und dem nächsten Aufruf des Shellskriptstimeout 45
: Zeit in Sekunden, nach der ein Skriptaufruf abgebrochen wirdfall 6
: Anzahl von Fehlerfällen für das „KO“-Kriterium, d.h. Häufigkeit, mit der das Shellskript einen Fehler zurückgeben muss, bis eine Reaktion seitens keepalived erfolgtrise 10
: Anzahl von Fällen für das „OK“-Kriterium, d.h. Häufigkeit, mit der das Shellskript Erfolg vermeldet, bis eine Reaktion seitens keepalived erfolgt
Es lohnt sich durchaus, mit den Parametern zu „spielen“. Beispielweise dauert der initiale Start von Pi-hole etwas länger, was dazu führen kann, dass bei zu geringem Intervall und gleichzeitig zu kleiner Angabe der notwendigen fehlerhaften Versuche fälschlicherweise keepalived einen Fehler erkennt, obwohl sich Pi-hole erst in der Startphase befindet.
Nun wurde bereits einige Male das Shellskript erwähnt, was zur Prüfung dient, ob Pi-hole läuft oder eben auch nicht, daher soll es nun endlich auch gezeigt werden. Es befindet sich ebenfalls im Pfad /etc/keepalived
unter dem Namen check_pihole.sh
:
#!/bin/bash KEEPALIVED="/etc/keepalived" if [ -e $KEEPALIVED/MASTER ]; then /usr/bin/logger "keepalived MASTER file found" if [ "$(docker inspect -f "{{.State.Health.Status}}" pihole_pihole_1)" == "healthy" ] ; then /usr/bin/logger "pihole is RUNNING, no action necessary" exit 0 else /usr/bin/logger "pihole is NOT RUNNING" exit 1 fi else /usr/bin/logger "keepalived NO MASTER file found" exit 0 fi
Die Prüfung soll nur auf dem jeweiligen Master stattfinden, denn Pi-hole soll ausschließlich auf dem Master laufen. Dazu wird beim Start von Pi-hole (siehe unten) eine Datei /etc/keepalived/MASTER angelegt, die wiederum hier abgefragt wird. Falls nicht vorhanden – und somit nicht auf dem Master, wird noch ein Eintrag ins Syslog geschrieben und daraufhin direkt abgebrochen.
Auf dem Master hingegen wird der „Health“-Status des Pi-hole-Containers geprüft. Wenn alles in Ordnung ist, wird der Exit-Code 0 zurückgegeben, ansonsten 1. In beiden Fällen erfolgt wiederum ein Eintrag im Logfile. Das Skript mag etwas „geschwätzig“ erscheinen, da es entsprechend häufig aufgerufen wird und sich im Logfile verewigt, aber für erste Tests und Debugging können derartige Einträge durchaus hilfreich sein. Natürlich spricht nichts dagegen, die logger
-Zeilen im produktiven Betrieb zu entfernen.
Keepalived: Was passiert, wenn..?
Nun fehlt noch der letzte Baustein, und zwar das Skript pihole_notify.sh
, das sich ebenfalls in /etc/keepalived
befindet. Dieses Skript wird von keepalived immer dann aufgerufen, wenn sich eine Statusänderung ergeben hat, beispielsweise wenn nach dem Booten des Systems der Master gewählt wurde oder etwa wenn der vorherige Master wieder zum Backup geworden ist.
#!/bin/bash # pihole notify TYPE=$1 NAME=$2 STATE=$3 pihole_start () { cd /home/geschke/services/pihole && docker-compose -f pihole.yml up -d } pihole_stop () { cd /home/geschke/services/pihole && docker-compose -f pihole.yml down } /usr/bin/logger "starting pihole notify " KEEPALIVED="/etc/keepalived" case $STATE in "MASTER") touch $KEEPALIVED/MASTER # mark node as master and start Pi-hole, if it is not running. Do nothing, if Pi-hole is ok /usr/bin/logger "MASTER state" /usr/bin/docker ps -f name=pihole_pihole_1 | grep pihole >/dev/null 2>&1 PIHOLE_RUN_STATE=$? if [ $PIHOLE_RUN_STATE -eq 0 ]; then /usr/bin/logger "pihole is RUNNING, no action necessary" else /usr/bin/logger "pihole is NOT RUNNING, starting pihole" pihole_start fi exit 0 ;; "BACKUP") rm $KEEPALIVED/MASTER /usr/bin/logger "pihole BACKUP state" if [ "$(docker inspect -f "{{.State.Health.Status}}" pihole_pihole_1)" == "healthy" ] ; then /usr/bin/logger "pihole is RUNNING, stopping" pihole_stop fi # else is not running, so do nothing exit 0 ;; "FAULT") rm $KEEPALIVED/MASTER /usr/bin/logger "FAULT state, stopping Pi-hole" if [ "$(docker inspect -f "{{.State.Health.Status}}" pihole_pihole_1)" == "healthy" ] ; then /usr/bin/logger "pihole is RUNNING, stopping" pihole_stop fi # else is not running, so do nothing exit 0 ;; *) /usr/bin/logger "pihole unknown state" exit 1 ;; esac
Von den übergebenen Parametern ist hier nur STATE
relevant, der die Werte MASTER, BACKUP oder FAULT annehmen kann. Bei anderen bzw. unbekannten Werten wird das Skript mit einem Exit-Code für einen Fehlerzustand beendet.
Zunächst werden zwei Funktionen definiert, und zwar für den Start und das Beenden von Pi-hole per Docker-Compose. Diese werden in den unteren Abschnitten je nach gewünschter Aktion aufgerufen.
Das Skript muss natürlich auf beiden Hosts vorhanden sein, dasselbe gilt für das zuvor genannte Prüf-Skript.
Für den Fall, dass keepalived den Host, auf dem das Skript läuft, als Master gewählt hat, wird zunächst die Datei /etc/keepalived/MASTER
angelegt (für check_pihole.sh
). Anschließend erfolgt eine Prüfung, ob der Pi-hole-Container vielleicht bereits läuft – falls ja, ist nichts weiter zu tun, falls nein, wird er per Docker-Compose über die Funktion pihole_start
gestartet. Die entsprechenden Aktionen werden jeweils im Syslog vermerkt, anschließend wird das Skript mit dem Exit-Code für „Erfolg“ beendet.
Im Fall von Backup oder gar einem Fehlerzustand wird zunächst das MASTER-File gelöscht, da der Host damit definitiv nicht mehr als Master fungieren soll. Anschließend wird geprüft, ob die Pi-hole-Instanz läuft, falls ja, wird sie beendet, falls nein, ist alles in Ordnung und das Skript wird beendet.
Damit ist auch die Konfiguration beendet, so dass einige Szenarien durchgespielt werden können. Beim Start bzw. Booten wird ein Host als Master gewählt, so dass das Skript mit dem Status MASTER aufgerufen wird. Da Pi-hole bis zu dem Zeitpunkt noch nicht läuft, wird es gestartet. Parallel erfolgen die Prüfungen mittels check_pihole.sh
– und eine entsprechende Reaktion darauf. Falls der Start erfolgreich verläuft, wird das Check-Skript nach einiger Zeit auch „Erfolg“ vermelden. Auf dem Backup-Host spielen sich ähnliche Szenen ab, nur dass das Skript mit dem Status BACKUP aufgerufen wird, nachdem keepalived sich aufgrund der Priorität für den anderen Host als Master entschieden hat. Daher wird pihole_notify.sh
entsprechend informiert, und da keine Pi-hole-Instanz läuft, die letztlich auch nicht laufen soll, wird keine weitere Aktion bis auf das Schreiben ins Logfile ausgeführt.
Fällt der erste Host aus oder wird keepalived gestoppt, wird die Floating IP-Adresse vom Backup-Host übernommen, darüber hinaus wird Pi-hole gestartet, so dass wenig später der Dienst wieder zur Verfügung steht. Dasselbe passiert, wenn auf dem Master-Host Pi-hole explizit gestoppt wird. Anhand des Check-Skriptes findet keepalived heraus, dass Pi-hole nicht läuft, so dass nach sechs erfolglosen Versuchen (Parameter vrrp_script
-> fall 6
) die Übernahme der IP durch den Backup-Host erfolgt und dort Pi-hole gestartet wird. Da keepalived in diesem Fall jedoch auf dem Master weiterhin läuft und eine höhere Priorität besitzt, erfolgt nach kurzer Zeit ein erneuter Wechsel der IP, wobei der Master wieder den Status MASTER erhält, der dafür sorgt, dass Pi-hole ebenfalls erneut gestartet wird. Gleichzeitig wird der zuvor auf dem Backup-Host laufende Pi-hole-Container wieder gestoppt. Dieses Verhalten ist zwar nachvollziehbar und sinnvoll, aber möglicherweise mitunter nicht erwünscht, etwa falls zu Wartungszwecken an der Master-Instanz von Pi-hole gearbeitet wird und diese nicht automatisch neu gestartet werden soll. In derartigen Fällen empfiehlt es sich, einfach den keepalived manuell zu stoppen (sudo systemctl stop keepalived.service
) und nach Beendigung der Arbeiten wieder zu starten.
Die Früchte der Arbeit: Pi-hole im Docker-Container
Wenn keepalived auf beiden Hosts erfolgreich gestartet wurde, sollte die Floating-IP-Adresse auf dem Master beheimatet sein. In jedem Fall lässt sich die Admin-UI mit dem Browser unter http://192.168.10.19 erreichen, nach Eingabe des gewählten Passworts gelangt man in das Dashboard.
Pi-hole läuft fast out-of-the-box, wobei die weitere Konfiguration unter „Settings“ vorgenommen wird. Im Bereich „DNS“ -> „Advanced DNS settings“ müssen zwei Optionen ggf. deaktiviert werden:
Da sich Pi-hole der internen Upstream-DNS-Server bedient, sollen private IP-Bereiche genutzt werden können, auch für das Reverse-Lookup. Ebenfalls sollen Hosts ohne Angabe der vollständigen Domain aufgelöst und somit an die internen DNS-Server weitergeleitet werden. Weitere Konfigurationsänderungen sind zunächst nicht nötig, wobei natürlich nichts dagegen spricht, von den Funktionen von Pi-hole, etwa bei der Möglichkeit, eigene White- und Blacklists zu definieren, ausgiebig Gebrauch zu machen. Nach etwa eineinhalb Jahren Nutzung befinden sich hier beispielsweise ca. 50 Domains auf der Whitelist, während auf der Blacklist nur zwei vorhanden sind. Manches Tracking- und Werbe-Pixel erlaube ich insofern mit voller Absicht.
So viel zum zweiten Teil zur Einrichtung von Pi-hole mit keepalived und Docker-Containern. Über Hinweise oder Kommentare freue ich mich natürlich auch dieses Mal!
Bei mir kann der Docker Container leider nicht erstellt werden, da er die virtuelle IP nicht an Port 53 binden kann. „Error starting userland proxy: listen tcp4 :53: bind: cannot assign requested address“. Irgendeine Idee, woran das liegen könnte?
Ich würde entweder auf ein Rechteproblem tippen, oder dass der Port 53 auch auf der virtuellen IP bereits belegt ist, wobei die Fehlermeldung bei Letzterem eigentlich anders aussehen müsste. Ansonsten leider keine Idee und würde Dich auf die Suchmaschine Deiner Wahl verweisen. 🙂
Die Suchmaschinen meiner Wahl bediene ich seit Stunden 😉 Bei mir funktioniert es mittlerweile ohne die IP Angabe vor dem Port, also mit „53:53“, dann hört der Container auch auf die virtuelle IP. Allerdings gibt’s dann etliche andere kleine Probleme, tl;dr
Ok, nach „einigen Zeiteinheiten“ 😉 der Recherche lag es daran, dass ich auf der Docker Maschine den Ratschlag net.ipv4.ip_nonlocal_bind = 1 zu setzen, zunächst ignoriert hatte, da es vorher in meinen non-Docker-Setup auch ohne diese Änderung funktionierte.