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

362 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```bash
ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
```
Nameserver ist nun direkt `192.168.11.1`.
---
## Ausgeführte Befehle
### MetalLB
```bash
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
```bash
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
- **Web-UI:** http://pihole.192.168.11.181.nip.io/admin
- **DNS-Server:** `192.168.11.181` (Port 53 TCP+UDP)
- **Passwort:** in Secret `pihole/pihole-secret`
---
## 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:**
```bash
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:**
```bash
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:
```yaml
env:
- name: DNSMASQ_LISTENING
value: "all"
```
```bash
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:**
```bash
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):
```yaml
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
```
```bash
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:
```bash
echo "precedence ::ffff:0:0/96 100" >> /etc/gai.conf
```
**Verifikation:**
```bash
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.
```bash
# 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
```bash
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.
```bash
kubectl delete pod <alter-pod> -n pihole
kubectl delete pod <stuck-pod> -n pihole # falls ContainerCreating auf falschem Node
```
**Verifikation:**
```bash
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
```bash
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:
```yaml
spec:
type: LoadBalancer
externalTrafficPolicy: Local
```
**Verifikation:**
```bash
# 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 |