274 lines
8.7 KiB
Markdown
274 lines
8.7 KiB
Markdown
# Pi-hole-Steuerung
|
||
|
||
Eigene Seite in der Landingpage zum Steuern von Pi-hole (v6) über dessen
|
||
REST-API: Blocking ein-/ausschalten, für 30 Minuten deaktivieren und den
|
||
aktuellen Status anzeigen (rot = Blocking aktiv, grün = kein Blocking).
|
||
|
||
---
|
||
|
||
## Funktionsumfang
|
||
|
||
- **Status-Anzeige**: farbiges Badge
|
||
- rot = Blocking aktiv
|
||
- grün = kein Blocking
|
||
- grau = unbekannt / nicht abrufbar
|
||
- **Schalter Blocking an/aus**: aktiviert bzw. deaktiviert das Blocking dauerhaft.
|
||
- **Schalter "30 Min deaktivieren"**: deaktiviert das Blocking temporär für
|
||
30 Minuten (Pi-hole-Timer = 1800 s). Live-Countdown (mm:ss) bis zur
|
||
automatischen Reaktivierung.
|
||
- Erreichbar unter der Route `/#/pihole` (Hash-Routing).
|
||
|
||
---
|
||
|
||
## Architektur
|
||
|
||
```
|
||
Browser (Angular SPA)
|
||
│ relative Calls: /pihole/api/...
|
||
▼
|
||
Reverse-Proxy (Dev: ng serve proxy.conf.json | Prod: Nginx Proxy Manager)
|
||
│ strippt /pihole/ -> /api/...
|
||
▼
|
||
Pi-hole v6 REST-API http://10.0.0.60:8088/api/...
|
||
```
|
||
|
||
**Wichtigstes Designprinzip — Same-Origin:**
|
||
Die App ruft die API **relativ** unter `/pihole/api/...` auf, also immer auf
|
||
demselben Host/Port, von dem die Seite geladen wurde. Dadurch gibt es **kein
|
||
CORS**. Ein vorgeschalteter Reverse-Proxy leitet `/pihole/...` an die echte
|
||
Pi-hole-API weiter und entfernt dabei das `/pihole`-Präfix.
|
||
|
||
---
|
||
|
||
## Dateien
|
||
|
||
| Datei | Zweck |
|
||
|---|---|
|
||
| `src/app/pihole.interface.ts` | TypeScript-Typen für Status/Auth-Antworten |
|
||
| `src/app/pihole.service.ts` | API-Service: Auth (SID), Status lesen, Blocking setzen |
|
||
| `src/app/pihole-control/pihole-control.component.ts` | Komponentenlogik, Status, Countdown |
|
||
| `src/app/pihole-control/pihole-control.component.html` | Template (Badge, Schalter) |
|
||
| `src/app/pihole-control/pihole-control.component.css` | Styles (rot/grün/grau) |
|
||
| `src/app/app.module.ts` | Deklaration der Komponente |
|
||
| `src/app/app-routing.module.ts` | Route `{ path: 'pihole', component: PiholeControlComponent }` |
|
||
| `src/app/app.component.html` | Navigationslink `<a routerLink="/pihole">Pi-hole</a>` |
|
||
| `src/environments/environment.ts` | Dev: `piholePassword` |
|
||
| `src/environments/environment.prod.ts` | Prod: `piholePassword` |
|
||
| `proxy.conf.json` | Dev-Proxy für `/pihole` |
|
||
| `webserver-docker/nginx.conf` | Prod-Nginx (nur relevant, wenn NICHT über NPM proxyt wird) |
|
||
|
||
> Das Feature folgt demselben Muster wie die bestehende `tasmota-control`-
|
||
> Komponente (Service + Interface + Component).
|
||
|
||
---
|
||
|
||
## Konfiguration
|
||
|
||
Das Pi-hole-Passwort steht in den Environment-Dateien:
|
||
|
||
```ts
|
||
// src/environments/environment.ts (Dev)
|
||
// src/environments/environment.prod.ts (Prod)
|
||
export const environment = {
|
||
production: true, // bzw. false in Dev
|
||
// ... bestehende Felder ...
|
||
piholePassword: 'DEIN_PIHOLE_PASSWORT'
|
||
};
|
||
```
|
||
|
||
> **Sicherheitshinweis:** Das Passwort wird ins JS-Bundle einkompiliert und ist
|
||
> damit im Browser lesbar. Das ist für ein reines LAN/VPN-Tool vertretbar.
|
||
> Für breitere Exposition: dediziertes Pi-hole **App-Passwort**
|
||
> (Pi-hole → Settings → API → App password) statt des Admin-Passworts, oder
|
||
> serverseitige Authentifizierung im Reverse-Proxy (Secret verlässt den
|
||
> Server nicht).
|
||
|
||
Die Pi-hole-Adresse (`http://10.0.0.60:8088`) ist im Reverse-Proxy
|
||
konfiguriert, **nicht** im Frontend.
|
||
|
||
---
|
||
|
||
## Entwicklung (Dev)
|
||
|
||
`ng serve` proxyt `/pihole` über `proxy.conf.json` an die Pi-hole-API.
|
||
|
||
```bash
|
||
ng serve --proxy-config proxy.conf.json
|
||
```
|
||
|
||
`proxy.conf.json` (relevanter Block):
|
||
|
||
```json
|
||
{
|
||
"/pihole": {
|
||
"target": "http://10.0.0.60:8088",
|
||
"secure": false,
|
||
"changeOrigin": true,
|
||
"pathRewrite": { "^/pihole": "" }
|
||
}
|
||
}
|
||
```
|
||
|
||
> Änderungen an `proxy.conf.json` oder den Environment-Dateien erfordern einen
|
||
> Neustart von `ng serve`.
|
||
|
||
Aufruf: `http://localhost:4200/#/pihole`
|
||
|
||
---
|
||
|
||
## Produktion (Prod)
|
||
|
||
### Setup-Überblick
|
||
|
||
- Auslieferung: Container `angular-web` (`nginx:stable-alpine`) auf der
|
||
Synology-NAS. Fertiges Bundle in `/volume1/docker/landingpage/dist`
|
||
(read-only gemountet), Host-Port `9999` → Container `80`.
|
||
- Reverse-Proxy für `/pihole`: **Nginx Proxy Manager (NPM)** auf separatem Host
|
||
(Variante A — ein Host, Custom Location).
|
||
|
||
### Bundle bauen und ausrollen
|
||
|
||
```bash
|
||
# 1. Passwort in src/environments/environment.prod.ts eintragen (siehe Konfiguration)
|
||
|
||
# 2. Prod-Build
|
||
ng build --configuration production
|
||
|
||
# 3. dist nach /volume1 kopieren (Pfad an angular.json anpassen)
|
||
rsync -a --delete dist/<projektname>/ /volume1/docker/landingpage/dist/
|
||
|
||
# 4. Container neu laden (statische Files sind sofort aktuell; Reload nur bei conf-Änderung nötig)
|
||
docker exec angular-web nginx -s reload
|
||
```
|
||
|
||
### Reverse-Proxy via Nginx Proxy Manager (Variante A)
|
||
|
||
Landingpage und Pi-hole-API laufen über **denselben** NPM-Host (same-origin),
|
||
damit der relative `/pihole/api`-Aufruf ohne CORS funktioniert.
|
||
|
||
**Proxy Host (Details):**
|
||
- Domain Names: z. B. `homenas.deine.domain`
|
||
- Scheme: `http`
|
||
- Forward Hostname / IP: `<NAS-IP>`
|
||
- Forward Port: `9999`
|
||
- Block Common Exploits: an
|
||
|
||
**Advanced → Custom Nginx Configuration:**
|
||
|
||
```nginx
|
||
location /pihole/ {
|
||
proxy_pass http://10.0.0.60:8088/;
|
||
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_set_header X-FTL-SID $http_x_ftl_sid;
|
||
|
||
proxy_buffering off;
|
||
proxy_read_timeout 30s;
|
||
proxy_connect_timeout 5s;
|
||
}
|
||
```
|
||
|
||
> Der **trailing slash** in `proxy_pass http://10.0.0.60:8088/;` strippt das
|
||
> `/pihole/`-Präfix → Pi-hole sieht `/api/...` (äquivalent zum Dev
|
||
> `pathRewrite "^/pihole" -> ""`). Ohne den trailing slash gibt es 404.
|
||
|
||
> Da NPM das `/pihole`-Proxying übernimmt, ist **kein** `/pihole`-Block in
|
||
> `webserver-docker/nginx.conf` nötig. Der Block dort wird nur gebraucht, wenn
|
||
> der NAS-Nginx selbst proxyt (ohne NPM).
|
||
|
||
Aufruf: `https://homenas.deine.domain/#/pihole`
|
||
|
||
---
|
||
|
||
## Pi-hole v6 REST-API (Referenz)
|
||
|
||
Basis: `http://10.0.0.60:8088/api`
|
||
|
||
### Authentifizierung
|
||
|
||
```
|
||
POST /api/auth
|
||
Body: {"password":"..."}
|
||
```
|
||
|
||
Antwort:
|
||
|
||
```json
|
||
{ "session": { "valid": true, "sid": "<SID>", "validity": 1800, "message": null } }
|
||
```
|
||
|
||
> **Fallstrick:** Bei falschem Passwort liefert Pi-hole `valid:true`, aber
|
||
> `sid:null` und `message:"password incorrect"`. Der Service prüft daher auf
|
||
> das Vorhandensein einer **`sid`**, nicht nur auf `valid`.
|
||
|
||
Die `sid` wird bei Folge-Requests im Header `X-FTL-SID` mitgeschickt.
|
||
|
||
### Status lesen
|
||
|
||
```
|
||
GET /api/dns/blocking
|
||
-> { "blocking": "enabled" | "disabled", "timer": <sek> | null }
|
||
```
|
||
|
||
### Blocking setzen
|
||
|
||
```
|
||
POST /api/dns/blocking
|
||
Body: { "blocking": true|false, "timer": <sek>|null }
|
||
```
|
||
|
||
- Blocking an: `{"blocking": true, "timer": null}`
|
||
- Blocking aus: `{"blocking": false, "timer": null}`
|
||
- 30 Min aus: `{"blocking": false, "timer": 1800}`
|
||
|
||
Offizielle Doku: https://docs.pi-hole.net/api/
|
||
|
||
---
|
||
|
||
## Service-Methoden (`pihole.service.ts`)
|
||
|
||
- `authenticate()` – holt eine SID; wirft bei Fehler mit der echten
|
||
Pi-hole-Meldung (z. B. "password incorrect").
|
||
- `ensureSid()` – stellt sicher, dass eine gültige SID vorliegt.
|
||
- `getStatus()` – aktueller Blocking-Status inkl. Timer.
|
||
- `isBlockingEnabled()` – Bool-Kurzform.
|
||
- `setBlocking(enabled, timerSeconds?)` – setzt den Status.
|
||
- `enableBlocking()` / `disableBlocking()` – Komfort-Wrapper.
|
||
- `disableFor30Minutes()` – deaktiviert mit Timer 1800 s.
|
||
- SID wird bei `401` einmal automatisch erneuert.
|
||
|
||
---
|
||
|
||
## Troubleshooting
|
||
|
||
| Symptom | Ursache | Lösung |
|
||
|---|---|---|
|
||
| 404 beim Statusabruf | `/pihole`-Proxy fehlt oder trailing slash falsch | Custom Location in NPM prüfen; `proxy_pass ...:8088/` mit trailing slash |
|
||
| "kein gültiges SID" / "password incorrect" | falsches Passwort | korrektes Admin-/App-Passwort in `environment(.prod).ts`; per `pihole setpassword` neu setzen |
|
||
| Status bleibt grau / nicht abrufbar | Proxy erreicht Pi-hole nicht | Erreichbarkeit `10.0.0.60:8088` aus dem Proxy-Container testen |
|
||
| Funktioniert in Dev, nicht in Prod | NPM-Host erreicht NAS/Pi-hole nicht | vom NPM-Container `wget` auf `:9999` und `10.0.0.60:8088` testen |
|
||
| Falsche Port-Annahme (80) | API läuft auf `:8088`, nicht `:80` | überall `http://10.0.0.60:8088` verwenden |
|
||
|
||
**Erreichbarkeitstest aus dem Reverse-Proxy-Container:**
|
||
|
||
```bash
|
||
docker exec <proxy-container> sh -c \
|
||
'wget -qO- http://10.0.0.60:8088/api/auth; echo " exit=$?"'
|
||
```
|
||
|
||
Eine JSON-Antwort (auch "password incorrect") beweist, dass das Netz steht.
|
||
|
||
---
|
||
|
||
## Sicherheit
|
||
|
||
- Passwort im Bundle ist im Browser lesbar → nur für LAN/VPN geeignet.
|
||
- Den `/pihole`-Proxy bei breiterer Erreichbarkeit absichern
|
||
(IP-Whitelist `allow/deny` oder Basic-Auth in NPM).
|
||
- Pi-hole-API möglichst nur über den Reverse-Proxy erreichbar machen,
|
||
`10.0.0.60:8088` nicht direkt nach außen exponieren.
|