Files
homelab/docs/09-metallb-pihole.md

11 KiB
Raw Permalink Blame History

09 · MetalLB + Pi-hole Installation

Datum: 2026-03-18


Was wurde installiert

  • MetalLB v0.14.x — Layer-2 Load Balancer für den k3s-Cluster
  • Pi-hole (pihole/pihole:latest) — DNS-Adblocker mit Web-UI

Vorbereitungen

Klipper (k3s ServiceLB) deaktiviert

k3s hat einen eingebauten Load-Balancer (Klipper/ServiceLB), der mit MetalLB kollidiert. Deaktiviert durch Ergänzung in /etc/systemd/system/k3s.service:

'--disable=servicelb' \

Dann: systemctl daemon-reload && systemctl restart k3s

DNS-Fix auf rnk-wrk02

systemd-resolved Stub-Resolver (127.0.0.53) war defekt → containerd konnte keine Images pullen.

Fix: /etc/resolv.conf Symlink auf direkte DNS-Auflösung umgestellt:

ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf

Nameserver ist nun direkt 192.168.11.1.


Ausgeführte Befehle

MetalLB

helm repo add metallb https://metallb.github.io/metallb
helm repo update
helm upgrade --install metallb metallb/metallb \
  --namespace metallb-system \
  --create-namespace \
  --wait
kubectl apply -f ~/homelab/k8s/metallb/metallb-config.yaml

Pi-hole

kubectl create namespace pihole
kubectl apply -f ~/homelab/k8s/pihole/secret.yaml
kubectl apply -k ~/homelab/k8s/pihole/

IP-Adressen

Service IP Protokoll
Traefik (Ingress) 192.168.11.180 TCP 80/443
Pi-hole DNS 192.168.11.181 TCP+UDP 53

MetalLB Pool: 192.168.11.180 192.168.11.199


Ergebnis

NAME                        READY   STATUS    RESTARTS
pod/pihole-f7d664fd-v65wn   1/1     Running   0

NAME                     TYPE           CLUSTER-IP     EXTERNAL-IP
pihole-dns-tcp           LoadBalancer   10.43.200.43   192.168.11.181
pihole-dns-udp           LoadBalancer   10.43.65.201   192.168.11.181
pihole-web               ClusterIP      10.43.25.107   <none>

Zugang


Dateien

~/homelab/k8s/metallb/
  metallb-config.yaml       # IPAddressPool + L2Advertisement

~/homelab/k8s/pihole/
  kustomization.yaml
  namespace.yaml
  secret.yaml               # WEBPASSWORD (nicht ins Git!)
  deployment.yaml
  services.yaml             # DNS TCP/UDP LoadBalancer + Web ClusterIP
  ingress.yaml              # Traefik Ingress für Web-UI

Nächste Schritte

  • Router/DHCP auf DNS 192.168.11.181 umstellen
  • Pi-hole Blocklisten konfigurieren
  • Ggf. Persistent Volume für /etc/pihole hinzufügen → erledigt (siehe unten)

Troubleshooting 2026-03-19

Problem 1: Web-UI nicht erreichbar

Symptom: Browser kann http://pihole.192.168.11.180.nip.io/admin/ nicht laden.

Diagnose:

kubectl get pods -n pihole          # Pod läuft (Running)
kubectl logs -n pihole deployment/pihole --tail=50  # FTL startet normal

Ursache: Pi-hole v6 verwendet keinen lighttpd mehr — der Webserver ist direkt in FTL integriert (Port 80/443). service lighttpd status schlägt in v6 fehl.

Ergebnis: Web-UI war tatsächlich erreichbar, Ingress funktionierte korrekt (HTTP 302 → /admin/login).


Problem 2: DNS antwortet nicht (192.168.11.181:53)

Symptom: dig @192.168.11.181 google.com läuft in Timeout. nc -vzu 192.168.11.181 53 zeigt Port als offen.

Diagnose:

kubectl exec -n pihole <POD> -- grep -v "^#" /etc/pihole/pihole.toml | grep listeningMode
# → listeningMode = "LOCAL"

Ursache: Pi-hole v6 FTL startet standardmäßig mit dns.listeningMode = "LOCAL". In Kubernetes kommt LoadBalancer-Traffic (192.168.11.0/24) nicht direkt vom Pod-Interface → dnsmasq verwirft alle Anfragen (logged: ignoring query from non-local network).

Fix: DNSMASQ_LISTENING=all in deployment.yaml hinzugefügt:

env:
  - name: DNSMASQ_LISTENING
    value: "all"
kubectl patch deployment pihole -n pihole --type=json \
  -p='[{"op":"add","path":"/spec/template/spec/containers/0/env/-","value":{"name":"DNSMASQ_LISTENING","value":"all"}}]'
kubectl rollout status deployment/pihole -n pihole

Verifikation:

dig @192.168.11.181 google.com +short
# → 142.250.201.78  ✓

Persistenz: Fix ist in ~/homelab/k8s/pihole/deployment.yaml und in etcd gespeichert — überlebt Pod-Neustarts.


Problem 3: HTTPS-Zugriff auf Web-UI schlägt fehl (2026-03-19)

Symptom: https://pihole.192.168.11.180.nip.io/admin nicht erreichbar — Ingress war nur auf HTTP (Port 80) konfiguriert.

Ursache: ingress.yaml hatte traefik.ingress.kubernetes.io/router.entrypoints: web — nur HTTP-Entrypoint.

Fix: Ingress auf websecure umgestellt + TLS aktiviert (Traefik Default-Zertifikat):

annotations:
  traefik.ingress.kubernetes.io/router.entrypoints: websecure
  traefik.ingress.kubernetes.io/router.tls: "true"
spec:
  tls:
    - hosts:
        - pihole.192.168.11.180.nip.io
kubectl apply -f ~/homelab/k8s/pihole/ingress.yaml

Hinweis: Browser zeigt Zertifikatswarnung (self-signed) — Ausnahme hinzufügen.

Zugang: https://pihole.192.168.11.180.nip.io/admin


Problem 4: Claude Code ConnectionRefused wenn Pi-hole als DNS-Proxy gesetzt (2026-03-19)

Symptom: API Error: Unable to connect to API (ConnectionRefused) in Claude Code wenn 192.168.11.181 als DNS-Proxy in Omada gesetzt ist.

Ursache: Pi-hole gibt für api.anthropic.com auch eine IPv6-Adresse (2607:6bc0::10) zurück. Der Node hat kein funktionierendes globales IPv6 → Verbindungsversuch auf IPv6 schlägt mit ConnectionRefused fehl.

Fix: IPv4 in /etc/gai.conf bevorzugen:

echo "precedence ::ffff:0:0/96  100" >> /etc/gai.conf

Verifikation:

curl --connect-timeout 5 https://api.anthropic.com
# → Anthropic API erreichbar ✓

Persistenz: /etc/gai.conf ist persistent auf rnk-cp01.


Migration von altem Pi-hole (2026-03-19)

Ausgangslage

Altes Pi-hole lief als Docker-Container (Pihole-DoT-DoH, Image devzwf/pihole-dot-doh:latest) auf Unraid (192.168.11.124), erreichbar unter 192.168.11.123.

Vorgehen: Teleporter-Backup via HTTP-API

Pi-hole v6 änderte die CLI-Syntax — pihole -a -t funktioniert nicht mehr. Shell-Umleitung (>) korrumpiert Binärdaten (null bytes). Lösung: Backup direkt über die REST-API laden.

# 1. Authentifizieren und Backup als gültige ZIP herunterladen
SID=$(curl -s -X POST http://192.168.11.123/api/auth \
  -H "Content-Type: application/json" \
  -d '{"password":"<altes-passwort>"}' | grep -o '"sid":"[^"]*"' | cut -d'"' -f4)

curl -s -X GET http://192.168.11.123/api/teleporter \
  -H "sid: $SID" \
  -o /tmp/pihole-backup.zip

# 2. In neuen Pod kopieren und importieren
POD=$(kubectl get pods -n pihole -l app=pihole -o jsonpath='{.items[0].metadata.name}')
kubectl cp /tmp/pihole-backup.zip pihole/$POD:/tmp/pihole-backup.zip

SID=$(kubectl exec -n pihole $POD -- curl -s -X POST \
  http://localhost/api/auth \
  -H "Content-Type: application/json" \
  -d '{"password":"<neues-passwort>"}' | grep -o '"sid":"[^"]*"' | cut -d'"' -f4)

kubectl exec -n pihole $POD -- curl -s -X POST \
  http://localhost/api/teleporter \
  -H "sid: $SID" \
  -F "file=@/tmp/pihole-backup.zip"

Importierte Objekte: pihole.toml, gravity.db (adlists, domainlists, clients, groups), dhcp.leases, hosts

Ingress-URL korrigiert

nip.io-Hostname enthält die eingebettete IP — pihole.192.168.11.181.nip.io löst auf die DNS-IP (Port 53) auf, nicht auf Traefik.

Fix: ingress.yaml Host auf Traefik-IP geändert:

pihole.192.168.11.180.nip.io  →  Traefik (192.168.11.180) → pihole-web Service

DNS-Loop-Fix: Pod nutzt direkt 1.1.1.1 (2026-03-19)

Problem

Pi-hole Pod nutzte CoreDNS (10.43.0.10) als Upstream → DNS-Loop, da CoreDNS intern Pi-hole anfragen kann.

Fix: dnsPolicy auf None

kubectl patch deployment pihole -n pihole --patch '
spec:
  template:
    spec:
      dnsPolicy: "None"
      dnsConfig:
        nameservers:
          - 1.1.1.1
          - 8.8.8.8
        searches: []
'

Hinweis: Bei RWO-Volumes (Longhorn) kann der neue Pod beim Rollout auf einem anderen Node landen → Multi-Attach error. Lösung: alten Pod manuell löschen, danach ggf. stuck Pod ebenfalls löschen damit Scheduler neu plant.

kubectl delete pod <alter-pod> -n pihole
kubectl delete pod <stuck-pod> -n pihole   # falls ContainerCreating auf falschem Node

Verifikation:

kubectl exec -n pihole <POD> -- cat /etc/resolv.conf
# nameserver 1.1.1.1
# nameserver 8.8.8.8

Persistenz: In deployment.yaml gespeichert + in etcd.


Fix: Echte Client-IPs in Pi-hole (2026-03-19)

Problem

Alle DNS-Queries in Pi-hole zeigten als Client 10.42.0.0 (Kubernetes Pod-Netzwerk) statt der echten Geräte-IPs (192.168.11.x).

Ursache

kube-proxy führt SNAT (Source NAT) durch wenn Traffic über einen LoadBalancer-Service läuft — die Original-Source-IP wird durch die Pod-Netzwerk-IP ersetzt.

Zusätzlich: Der Omada DNS-Proxy leitet alle Client-Anfragen über sich selbst weiter → Pi-hole sieht nur die Router-IP als Client. Auch wenn der DNS-Proxy aktiv bleibt, muss kube-proxy die echte Router-IP durchreichen.

Fix: externalTrafficPolicy: Local

kubectl patch svc pihole-dns-tcp -n pihole -p '{"spec":{"externalTrafficPolicy":"Local"}}'
kubectl patch svc pihole-dns-udp -n pihole -p '{"spec":{"externalTrafficPolicy":"Local"}}'

In ~/homelab/k8s/pihole/services.yaml für beide LoadBalancer-Services ergänzt:

spec:
  type: LoadBalancer
  externalTrafficPolicy: Local

Verifikation:

# Queries aus Unraid (192.168.11.124) testen
dig @192.168.11.181 google.com +short
# Pi-hole Query Log zeigt danach: 192.168.11.124 → google.com ✓

Hinweis: externalTrafficPolicy: Local bedeutet, dass Traffic nur an Nodes weitergeleitet wird auf denen der Pod läuft. Ist der Pod auf einem anderen Node, gibt es keinen Fallback — dies ist bei Pi-hole gewünscht (kein NAT, echte IPs).

Omada DNS-Proxy Konfiguration

  • DNS-Proxy in Omada leitet Anfragen weiter an 192.168.11.181 (Pi-hole)
  • Clients erhalten die Router-IP als DNS → alle Queries gehen über den Proxy
  • Pi-hole sieht die Router-IP als Client (nicht einzelne Geräte) — akzeptabler Kompromiss
  • Secondary DNS (8.8.8.8) wurde entfernt damit Pi-hole Blocking nicht umgangen wird

Finaler Status

Komponente Status
Pi-hole DNS 192.168.11.181:53 ✓ Erreichbar
Web-UI https://pihole.192.168.11.180.nip.io/admin ✓ Erreichbar (HTTPS, self-signed)
Blocking aktiv
Echte Client-IPs sichtbar ✓ (nach externalTrafficPolicy: Local)
Queries heute ~40.000