Skip to content

Es lebt! – PiHole hochverfügbar mit keepalived

Ich verwende PiHole als rekursiven DNS Server sowie als DHCP Server in meinem Heimnetz. Das bewahrt mich vor unliebsamer Werbung und erlaubt mir, DNS Einträge nach meinem Belieben zu vergeben und mehrere Domains zu verwalten (etwas, dass meine Fritzbox nicht kann). Zwar synchronisiere ich DNS und DHCP Einstellungen zwischen meinen zwei Raspberry Pi’s, bei einem Ausfall meines primären DNS PiHole, muss ich dennoch (abhängig vom Betriebssystem meiner Clients) manuell tätig werden um den zweiten PiHole als primären DNS Server verfügbar zu machen. Hier kommt keepalived ins Spiel.

Das Prinzip von keepalived ist einfach, es wird eine virtuelle IP Adresse (Cluster IP) im Netz bereitgestellt. Zwei (Cluster)-Knoten (meine zwei Raspberries) tauschen sich über ihren Status aus und abhängig davon, wird die virtuelle IP Adresse an einen der beiden Knoten “gebunden”. Der Status kann eine Rückmeldung eines Skripts sein oder schlicht die Erreichbarkeit eines der beiden Knoten. Unseren PiHole DNS Server sprechen wir über diese virtuelle IP an, nachdem keepalived korrekt konfiguriert ist. Eine ausführliche Dokumentation von keepalived findet ihr hier.

Damit PiHole über keepalived abgesichert werden kann, sollten die DNS und gegebenenfalls die DHCP Einstellungen zwischen den beiden Knoten synchronisiert sein. Wir wollen ja in einem Failover Fall keine manuellen Tätigkeiten mehr durchführen. Diese Synchronisierung könnt ihr z.B. mit Grafity-Sync oder wie in meinem Fall über ein selbst geschriebenes Ansible Playbook durchführen. Das Ansible Playbook hat den Charme, dass es neben den DNS Einstellungen auch die DHCP Einstellungen synchronisiert. Eine Anleitung zur Einrichtung des Playbooks, könnt ihr hier finden.

Keepalived installieren

Auf beiden Knoten könnt ihr keepalived folgendermaßen installieren:

apt install keepalived

Um den Status unseres PiHole Dienstes für keepalived zu ermitteln, legen wir ein Skript auf beiden Knoten mit folgendem Inhalt ab:

#!/bin/sh

PIHOLESTATUS=$(ps ef | grep pihole-FTL | grep -v grep)

if [ "$PIHOLESTATUS" != "" ]
then
    exit 0
else
    exit 1

Das Skript prüft, ob es einen laufenden Prozess mit dem Namen pihole-FTL gibt und exkludiert unseren eigenen grep Befehl. Wird keine Rückmeldung erhalten, beendet das Skript mit dem Status 1 (also Fehler). Ansonsten mit Status 0. Die Skriptdatei bereichtigen wir dann noch zur Ausführung mit chmod +x /usr/local/scripts/check-pihole.

Keepalived konfigurieren

Nun legen wir die Konfiguration von keepalived auf beiden Knoten ab. Die beiden Konfigurationen unterscheiden sich zwischen der Rolle der beiden Knoten (Master und Backup), ihr solltet sie also nicht vertauschen. Die Konfiguration wird unter /etc/keepalived/keepalived.conf abgelegt.

Hier die Konfiguration des Master / Primary Knotens:

global_defs {
  router_id pihole-dns-01
  script_user root
  enable_script_security
}

vrrp_script chk_pihole {
  script "/usr/local/scripts/check-pihole"
  interval 1
  weight -100
}

vrrp_instance PIHOLE {
  state MASTER
  interface eth0
  virtual_router_id 55
  priority 150
  advert_int 1
  unicast_src_ip 192.168.1.2
  unicast_peer {
    192.168.1.3
  }

  authentication {
    auth_type PASS
    auth_pass secret
  }

  virtual_ipaddress {
    192.168.1.4/24
  }

  track_script {
    chk_pihole
  }
}

Hier die Konfiguration des Backup Knotens:

global_defs {
  router_id pihole-dns-02
  script_user root
  enable_script_security
}

vrrp_script chk_pihole {
  script "/usr/local/scripts/check-pihole"
  interval 1
  weight -100
}

vrrp_instance PIHOLE {
  state BACKUP
  interface eth0
  virtual_router_id 55
  priority 140
  advert_int 1
  unicast_src_ip 192.168.1.3
  unicast_peer {
    192.168.1.2
  }

  authentication {
    auth_type PASS
    auth_pass secret
  }

  virtual_ipaddress {
    192.168.1.4/24
  }

  track_script {
    chk_pihole
  }
}

In den Konfigurationen müsst ihr noch ein paar Anpassungen für eure Umgebung durchführen.

  • auth_pass: Hier wird ein Passwort angegeben, welches für den Statusaustausch von keepalived verwendet wird. Das Passwort darf maximal 8 Zeichen lang sein.
  • virtual_ipaddress: Hierbei handelt es sich um die oben erwähnte virtuelle IP Adresse die dem Master / Primary Knoten zugewiesen wird. In meinem Fall habe ich die 192.168.1.4 verwendet.
  • unicast_src_ip und unicast_peer: Als src_ip gebt ihr die jeweils lokale IP Adresse des jeweiligen Knotens an. Als peer wird die IP Adresse des keepalived “Partners” angegeben. In meinem Fall unterhält sich also 192.168.1.2 (Master) mit 192.168.1.3 (Backup) und umgekehrt.
  • script: Hierbei handelt es sich um das Skript, dass ihr oben angelegt habt. Solltet ihr das Skript unter anderem Namen oder Pfad abgelegt haben, müsst ihr das entsprechend hier berücksichtigen.
  • priority: Hier müsst ihr nichts anpassen, es ist jedoch wichtig zu verstehen. Die Priorität gibt an, welchen Knoten im “Normalzustand” welche Rolle inne hat. Sollten beide Knoten funktionstüchtig sein, gewinnt der Knoten mit der höheren Priorität das Rennen und hast die Master Rolle inne.
  • weight: Gibt das “Gewicht” unseres Prüfskriptes an. Wenn ein Status ungleich 0 zurückgeliefert wird, reduziert sich die priority unseres Knotens um die Höhe von weight. In meinem Fall, reduziert das Prüfskript die Priorität des Knotens um 100.

Nun aktivieren und starten wir den keepalived Dienst auf beiden Knoten:

systemctl enable --now keepalived.service

Anschließend prüfen wir den Status des keepalived Dienstes. Die Ausgabe sollte ungefähr so aussehen:

systemctl status keepalived.service

root@piHole:~# systemctl status keepalived.service
● keepalived.service - Keepalive Daemon (LVS and VRRP)
   Loaded: loaded (/lib/systemd/system/keepalived.service; enabled; vendor preset: enabled)
   Active: active (running) since Thu 2022-01-06 13:44:35 CET; 1s ago
 Main PID: 10698 (keepalived)
    Tasks: 2 (limit: 2059)
   CGroup: /system.slice/keepalived.service
           ├─10698 /usr/sbin/keepalived --dont-fork
           └─10699 /usr/sbin/keepalived --dont-fork

Jan 06 13:44:36 piHole Keepalived[10698]: Command line: '/usr/sbin/keepalived' '--dont-fork'
Jan 06 13:44:36 piHole Keepalived[10698]: Opening file '/etc/keepalived/keepalived.conf'.
Jan 06 13:44:36 piHole Keepalived[10698]: Starting VRRP child process, pid=10699
Jan 06 13:44:36 piHole Keepalived_vrrp[10699]: Registering Kernel netlink reflector
Jan 06 13:44:36 piHole Keepalived_vrrp[10699]: Registering Kernel netlink command channel
Jan 06 13:44:36 piHole Keepalived_vrrp[10699]: Opening file '/etc/keepalived/keepalived.conf'.
Jan 06 13:44:36 piHole Keepalived_vrrp[10699]: Registering gratuitous ARP shared channel
Jan 06 13:44:36 piHole Keepalived_vrrp[10699]: (PIHOLE) Entering BACKUP STATE (init)
Jan 06 13:44:36 piHole Keepalived_vrrp[10699]: VRRP_Script(chk_ftl) succeeded
Jan 06 13:44:36 piHole Keepalived_vrrp[10699]: (PIHOLE) received lower priority (140) advert from 192.168.1.3 - discarding

Failover testen

Soweit so gut, ob piHole auch korrekt funktioniert, können wir testen, indem wir die virtuelle IP Adresse in unserem Browser aufrufen. (Bspw: http://192.168.1.4/admin/) Wir sollten unseren gewünschten Master Knoten als Ziel zurückbekommen:

Stoppen wir den piHole Dienst auf unserem primären System (systemctl stop pihole-FTL.service) und rufen die virtuelle IP erneut auf, solltet ihr als Ziel den zweiten piHole Server erreichen.

Lasst uns nachschauen, was keepalived im Hintergrund gemacht hat. Ihr könnt sehen, dass der ehemalige Master Knoten, sich in den Backup Zustand gebracht hat, weil das Prüfscript ein entsprechendes Ergebnis zurückgeliefert hat.

root@piHole:~# systemctl status keepalived.service
● keepalived.service - Keepalive Daemon (LVS and VRRP)
   Loaded: loaded (/lib/systemd/system/keepalived.service; enabled; vendor preset: enabled)
   Active: active (running) since Thu 2022-01-06 13:44:35 CET; 13min ago
 Main PID: 10698 (keepalived)
    Tasks: 2 (limit: 2059)
   CGroup: /system.slice/keepalived.service
           ├─10698 /usr/sbin/keepalived --dont-fork
           └─10699 /usr/sbin/keepalived --dont-fork

Jan 06 13:44:36 piHole Keepalived_vrrp[10699]: (PIHOLE) received lower priority (145) advert from 192.168.1.3 - discarding
Jan 06 13:44:37 piHole Keepalived_vrrp[10699]: (PIHOLE) received lower priority (145) advert from 192.168.1.3 - discarding
Jan 06 13:44:38 piHole Keepalived_vrrp[10699]: (PIHOLE) received lower priority (145) advert from 192.168.1.3 - discarding
Jan 06 13:44:39 piHole Keepalived_vrrp[10699]: (PIHOLE) received lower priority (145) advert from 192.168.1.3 - discarding
Jan 06 13:44:39 piHole Keepalived_vrrp[10699]: (PIHOLE) Entering MASTER STATE
Jan 06 13:57:23 piHole Keepalived_vrrp[10699]: Script `chk_ftl` now returning 1
Jan 06 13:57:23 piHole Keepalived_vrrp[10699]: VRRP_Script(chk_ftl) failed (exited with status 1)
Jan 06 13:57:23 piHole Keepalived_vrrp[10699]: (PIHOLE) Changing effective priority from 150 to 140
Jan 06 13:57:26 piHole Keepalived_vrrp[10699]: (PIHOLE) Master received advert from 192.168.1.3 with higher priority 140, ours 50
Jan 06 13:57:26 piHole Keepalived_vrrp[10699]: (PIHOLE) Entering BACKUP STATE

Sollte uns dieser Test nicht ausreichen, können wir einen ping auf die virtuelle IP Adresse sowie einen nslookup Befehl in einer Schleife laufen lassen, während wir den piHole Dienst oder den ganzen Raspberry durchstarten:

while true; do
nslookup mydnsname.home.lab 192.168.1.4
sleep 1
done

Nun bleibt uns noch, die virtuelle IP Adresse als DNS Server auf unseren Clients oder auf unserem DHCP Server zu hinterlegen. In meinem Fall ist piHole auch der DHCP Server. Eine Abweichende IP Adresse als DNS Server in die DHCP Konfiguration zu übernehmen, können wir durch die hier durch die Ablage einer Konfigurationsdatei unter /etc/dnsmasq.d/99-dns-vip.conf mit folgendem Inhalt erreichen:

dhcp-option=option:dns-server,192.168.1.4

Im Anschluss starten wir piHole neu, damit die DHCP Konfiguration gezogen wird. Sobald sich unsere Clients nun neu bei dem DHCP Server anmelden, bekommen sie unsere virtuelle IP als DNS Server übergeben:

root@piHole:~# pihole restartdns
  [✓] Restarting DNS server

Das wars. Von nun an, haben wir (hoffentlich) immer einen aktiven primären DNS Server.

Philip

UPDATE 02.04.2022: alle benötigten Skripte aus diesem Artikel, sind jetzt auch auf meinem Github Repository zu finden.

7 thoughts on “Es lebt! – PiHole hochverfügbar mit keepalived”

  1. Hallo,

    danke für die tolle Beschreibung. Ich habe aber zwei Fragen als Anfänger.

    Was ist mit der router_id gemeint? Der Hostname vom RPi?

    global_defs {
    router_id pihole-dns-01
    script_user root
    enable_script_security
    }

    Was passiert wenn einer der RPi selbst ausfällt? Das Skript arbeitet dann ja nicht mehr?

    Danke

    Thorsten

    1. Avatar photo

      Hallo Thorsten,

      die router_id ist frei von dir wählbar und nicht von dem Hostnamen abhängig. Es sollte nur klar ersichtlich sein, welcher Knoten / Host sich dahinter verbirgt, ansonsten ist die Konfiguration komplizierter als sie sein muss.

      Zu deiner zweiten Frage. Sollte ein Pi nicht mehr erreichbar sein, wird der zweite Pi die Aufgabe übernehmen. Da sich beide Keepalive Installationen “unterhalten”, wissen sie um ihre Verfügbarkeit. Das heißt aber im Umkehrschluss, dass bei nur zwei Knoten, ein “Split-Brain” passieren könnte so beide Pi sich über das Netzwerk nicht mehr erreichen können und beide die Master Rolle übernehmen wollen. Das wird auf Netzwerkebene schiefgehen und du wirst mal hier und mal da die virtuelle IP angebunden bekommen. Das ist aber in einem Heimnetz eine ziemlich theoretische Annahme und selbst wenn das der Fall ist, hast du generelle Netzwerkprobleme. Ich betreibe dieses Konstrukt jetzt schon sehr lange und hatte noch nie Probleme mit Split Brain. Abhängig davon, wie beide Pi ans Netzwerk angebunden sind, kann das aber der Fall sein.

      Gruß Philip

      1. Frage: bei mir habe ich zwei PiHole Server hinter einer Firewall in zwei DMZ (172.16.222.0/30 und 172.16.222.4/30). Muss die VIP im selber Netz sein, oder kann die VIP auch in einem anderen Netz liegen? Ich denke nicht, wegen dem Routing, aber vielleicht gibt es eine Idee.
        Meine Grundidee ist: ein Prod-Netz sagen wir 192.168.100.0/24, die DNS VIP sollten dann z.B. 192.168.100.254 sein. Dann müsste es ein Routing geben wegen es DNS request auf die 192.168.100.254 gibt, das es dann ein DNS Forward Routing auf die beiden PiHoles in den /30 DMZ Netze gibt. Keepalived macht ja ein vrrp zwischen den PiHoles. Das erlaubt die Firewall schon. Aber ich denke das wird so nicht funktionieren, oder?

  2. Moin,
    besten Dank für diese ausführliche Beschreibung!
    Habe es soeben bei mir auf zwei älteren Raspberry PI’s 3 eingerichtet, und alles funktioniert super.

Leave a Reply to TheDatabaseMe Cancel reply

Your email address will not be published. Required fields are marked *