Compare commits

...

4 Commits

17 changed files with 1323 additions and 347 deletions

View File

@@ -94,15 +94,16 @@ docker run --rm -v "$PWD":/src -w /src -e GOOS=linux -e GOARCH=amd64 -e CGO_ENAB
# Binary auf dem Server installieren # Binary auf dem Server installieren
sudo ./adguard-shield install sudo ./adguard-shield install
# Der Installer fragt am Ende, ob AdGuard Shield direkt gestartet werden soll. # Der Installer fragt am Ende, ob AdGuard Shield direkt gestartet werden soll.
# Dabei wird der Befehl adguard-shield in /usr/local/bin registriert.
# Konfiguration anpassen (mindestens API-Zugangsdaten und Whitelist) # Konfiguration anpassen (mindestens API-Zugangsdaten und Whitelist)
sudo nano /opt/adguard-shield/adguard-shield.conf sudo nano /opt/adguard-shield/adguard-shield.conf
# API-Verbindung testen # API-Verbindung testen
sudo /opt/adguard-shield/adguard-shield test sudo adguard-shield test
# Dry-Run: loggt Erkennungen, sperrt aber nicht # Dry-Run: loggt Erkennungen, sperrt aber nicht
sudo /opt/adguard-shield/adguard-shield dry-run sudo adguard-shield dry-run
# Service starten und prüfen # Service starten und prüfen
sudo systemctl start adguard-shield sudo systemctl start adguard-shield
@@ -110,6 +111,7 @@ sudo systemctl status adguard-shield
``` ```
> Beim Installieren wird der systemd-Service für den Autostart registriert und am Ende nach dem direkten Start gefragt. Die Go-Version nutzt `Restart=on-failure`; einen separaten Watchdog-Timer wie in der alten Shell-Version gibt es nicht mehr. > Beim Installieren wird der systemd-Service für den Autostart registriert und am Ende nach dem direkten Start gefragt. Die Go-Version nutzt `Restart=on-failure`; einen separaten Watchdog-Timer wie in der alten Shell-Version gibt es nicht mehr.
> Zusätzlich legt der Installer standardmäßig `/usr/local/bin/adguard-shield` als Symlink auf das installierte Binary an. Danach kannst du `sudo adguard-shield <befehl>` statt `sudo /opt/adguard-shield/adguard-shield <befehl>` verwenden.
> **Bestehende Shell-Installation?** Der Go-Installer bricht ab und meldet die gefundenen Script-Artefakte. Die alte Version muss zuerst deinstalliert werden (Konfiguration behalten). Details unter [docs/update.md](docs/update.md). > **Bestehende Shell-Installation?** Der Go-Installer bricht ab und meldet die gefundenen Script-Artefakte. Die alte Version muss zuerst deinstalliert werden (Konfiguration behalten). Details unter [docs/update.md](docs/update.md).
@@ -120,7 +122,7 @@ sudo systemctl status adguard-shield
AdGuard Shield wird über ein einzelnes Binary bedient. Die Grundform lautet: AdGuard Shield wird über ein einzelnes Binary bedient. Die Grundform lautet:
```bash ```bash
sudo /opt/adguard-shield/adguard-shield <befehl> sudo adguard-shield <befehl>
``` ```
### Installation & Updates ### Installation & Updates
@@ -130,9 +132,11 @@ sudo /opt/adguard-shield/adguard-shield <befehl>
| `install` | Binary, Konfiguration und systemd-Service installieren | | `install` | Binary, Konfiguration und systemd-Service installieren |
| `install --skip-deps` | Installation ohne automatische Paketprüfung | | `install --skip-deps` | Installation ohne automatische Paketprüfung |
| `install --no-enable` | Installation ohne systemd-Autostart | | `install --no-enable` | Installation ohne systemd-Autostart |
| `install --no-register` | Installation ohne globalen CLI-Befehl in `/usr/local/bin` |
| `install --config-source <pfad>` | Bestehende Konfiguration als Vorlage übernehmen | | `install --config-source <pfad>` | Bestehende Konfiguration als Vorlage übernehmen |
| `update` | Binary, Service und Konfiguration aktualisieren | | `update` | Binary, Service und Konfiguration aktualisieren |
| `install-status` | Installationsstatus anzeigen (Binary, Service, Version) | | `update --no-register` | Update ohne Änderung des globalen CLI-Befehls |
| `install-status` | Installationsstatus anzeigen (Binary, CLI-Befehl, Service, Version) |
| `uninstall` | Vollständige Deinstallation | | `uninstall` | Vollständige Deinstallation |
| `uninstall --keep-config` | Deinstallation mit Erhalt der Konfiguration | | `uninstall --keep-config` | Deinstallation mit Erhalt der Konfiguration |

View File

@@ -244,7 +244,11 @@ func run() error {
case "report-test": case "report-test":
return report.SendTest(ctx, cfg) return report.SendTest(ctx, cfg)
case "report-install": case "report-install":
return report.InstallCron("/opt/adguard-shield/adguard-shield", cfg.Path, cfg) binary := "/opt/adguard-shield/adguard-shield"
if _, err := os.Stat(installer.CLICommandPath); err == nil {
binary = installer.CLICommandPath
}
return report.InstallCron(binary, cfg.Path, cfg)
case "report-remove": case "report-remove":
return report.RemoveCron() return report.RemoveCron()
case "firewall-create": case "firewall-create":
@@ -463,8 +467,8 @@ func usage() {
Nutzung: Nutzung:
adguard-shield version adguard-shield version
adguard-shield install [--config-source PATH] [--skip-deps] adguard-shield install [--config-source PATH] [--skip-deps] [--no-register]
adguard-shield update [--config-source PATH] [--skip-deps] adguard-shield update [--config-source PATH] [--skip-deps] [--no-register]
adguard-shield uninstall [--keep-config] adguard-shield uninstall [--keep-config]
adguard-shield install-status adguard-shield install-status
adguard-shield [-config PATH] run|start|stop|dry-run adguard-shield [-config PATH] run|start|stop|dry-run
@@ -653,9 +657,11 @@ func parseInstallFlags(name string, args []string) (installer.Options, error) {
fs.StringVar(&opts.ConfigSource, "config-source", "", "Konfiguration fuer Neuinstallation uebernehmen") fs.StringVar(&opts.ConfigSource, "config-source", "", "Konfiguration fuer Neuinstallation uebernehmen")
fs.BoolVar(&opts.SkipDeps, "skip-deps", false, "Paketpruefung ueberspringen") fs.BoolVar(&opts.SkipDeps, "skip-deps", false, "Paketpruefung ueberspringen")
noEnable := fs.Bool("no-enable", false, "systemd Autostart nicht aktivieren") noEnable := fs.Bool("no-enable", false, "systemd Autostart nicht aktivieren")
noRegister := fs.Bool("no-register", false, "CLI-Befehl nicht in /usr/local/bin registrieren")
if err := fs.Parse(args); err != nil { if err := fs.Parse(args); err != nil {
return opts, err return opts, err
} }
opts.Enable = !*noEnable opts.Enable = !*noEnable
opts.RegisterCLI = !*noRegister
return opts, nil return opts, nil
} }

View File

@@ -25,6 +25,12 @@ Die Go-Version bündelt alle Aufgaben in einem einzelnen Binary:
/opt/adguard-shield/adguard-shield /opt/adguard-shield/adguard-shield
``` ```
Bei Installation und Update wird zusätzlich ein Symlink in den üblichen PATH gelegt:
```text
/usr/local/bin/adguard-shield -> /opt/adguard-shield/adguard-shield
```
Dieses Binary ist gleichzeitig: Dieses Binary ist gleichzeitig:
- **Daemon** für den produktiven Betrieb (Querylog-Polling, Erkennung, Sperren) - **Daemon** für den produktiven Betrieb (Querylog-Polling, Erkennung, Sperren)
@@ -33,10 +39,10 @@ Dieses Binary ist gleichzeitig:
- **Report-Generator** für HTML- und Text-Reports - **Report-Generator** für HTML- und Text-Reports
- **Hintergrundprozess** für externe Whitelist, externe Blocklist, GeoIP und Offense-Cleanup - **Hintergrundprozess** für externe Whitelist, externe Blocklist, GeoIP und Offense-Cleanup
Die meisten Befehle beginnen daher mit: Die meisten Befehle beginnen daher nach der Installation mit:
```bash ```bash
sudo /opt/adguard-shield/adguard-shield <befehl> sudo adguard-shield <befehl>
``` ```
Für Installation oder Update nutzt du das neue Binary aus dem Repository, Release oder Build-Verzeichnis: Für Installation oder Update nutzt du das neue Binary aus dem Repository, Release oder Build-Verzeichnis:

View File

@@ -6,18 +6,24 @@ AdGuard Shield wird in der Go-Version über ein einzelnes Binary bedient:
/opt/adguard-shield/adguard-shield /opt/adguard-shield/adguard-shield
``` ```
Dieses Binary ist Daemon, CLI, Installer, Updater, Uninstaller und Report-Generator. Dadurch gibt es keine getrennten Shell-Skripte mehr. Bei Installation und Update registriert der Installer zusätzlich den globalen Befehl:
```bash
/usr/local/bin/adguard-shield
```
Dieser Symlink zeigt auf das installierte Binary. Dadurch gibt es keine getrennten Shell-Skripte mehr, und du kannst AdGuard Shield nach der Installation ohne vollständigen Pfad aufrufen.
## Grundform ## Grundform
```bash ```bash
sudo /opt/adguard-shield/adguard-shield <befehl> sudo adguard-shield <befehl>
``` ```
Wenn du eine andere Konfigurationsdatei verwenden möchtest, muss `-config` direkt vor dem Befehl stehen: Wenn du eine andere Konfigurationsdatei verwenden möchtest, muss `-config` direkt vor dem Befehl stehen:
```bash ```bash
sudo /opt/adguard-shield/adguard-shield -config /pfad/zur/adguard-shield.conf status sudo adguard-shield -config /pfad/zur/adguard-shield.conf status
``` ```
### Standardpfade ### Standardpfade
@@ -25,6 +31,7 @@ sudo /opt/adguard-shield/adguard-shield -config /pfad/zur/adguard-shield.conf st
| Datei | Pfad | | Datei | Pfad |
|---|---| |---|---|
| Binary | `/opt/adguard-shield/adguard-shield` | | Binary | `/opt/adguard-shield/adguard-shield` |
| CLI-Befehl | `/usr/local/bin/adguard-shield` |
| Konfiguration | `/opt/adguard-shield/adguard-shield.conf` | | Konfiguration | `/opt/adguard-shield/adguard-shield.conf` |
| SQLite-Datenbank | `/var/lib/adguard-shield/adguard-shield.db` | | SQLite-Datenbank | `/var/lib/adguard-shield/adguard-shield.db` |
| Logdatei | `/var/log/adguard-shield.log` | | Logdatei | `/var/log/adguard-shield.log` |
@@ -34,13 +41,13 @@ sudo /opt/adguard-shield/adguard-shield -config /pfad/zur/adguard-shield.conf st
```bash ```bash
# Version anzeigen # Version anzeigen
/opt/adguard-shield/adguard-shield version adguard-shield version
# Installation und Update # Installation und Update
sudo ./adguard-shield install sudo ./adguard-shield install
sudo ./adguard-shield update sudo ./adguard-shield update
sudo ./adguard-shield install-status sudo adguard-shield install-status
sudo /opt/adguard-shield/adguard-shield uninstall --keep-config sudo adguard-shield uninstall --keep-config
# Service-Management über systemd # Service-Management über systemd
sudo systemctl start adguard-shield sudo systemctl start adguard-shield
@@ -49,16 +56,16 @@ sudo systemctl restart adguard-shield
sudo systemctl status adguard-shield sudo systemctl status adguard-shield
# Diagnose und Monitoring # Diagnose und Monitoring
sudo /opt/adguard-shield/adguard-shield test sudo adguard-shield test
sudo /opt/adguard-shield/adguard-shield status sudo adguard-shield status
sudo /opt/adguard-shield/adguard-shield live sudo adguard-shield live
sudo /opt/adguard-shield/adguard-shield history 100 sudo adguard-shield history 100
sudo /opt/adguard-shield/adguard-shield logs --level warn --limit 100 sudo adguard-shield logs --level warn --limit 100
# Manuelle Eingriffe # Manuelle Eingriffe
sudo /opt/adguard-shield/adguard-shield ban 192.168.1.100 sudo adguard-shield ban 192.168.1.100
sudo /opt/adguard-shield/adguard-shield unban 192.168.1.100 sudo adguard-shield unban 192.168.1.100
sudo /opt/adguard-shield/adguard-shield flush sudo adguard-shield flush
``` ```
--- ---
@@ -71,6 +78,12 @@ Das installierte Binary landet standardmäßig unter:
/opt/adguard-shield/adguard-shield /opt/adguard-shield/adguard-shield
``` ```
Zusätzlich wird standardmäßig dieser CLI-Befehl angelegt:
```text
/usr/local/bin/adguard-shield -> /opt/adguard-shield/adguard-shield
```
### Standardinstallation ### Standardinstallation
```bash ```bash
@@ -87,6 +100,7 @@ Am Ende fragt der Installer, ob AdGuard Shield direkt gestartet oder neu gestart
| `--config-source <pfad>` | Bestehende Konfigurationsdatei als Vorlage übernehmen | | `--config-source <pfad>` | Bestehende Konfigurationsdatei als Vorlage übernehmen |
| `--skip-deps` | Automatische Paketprüfung und -installation überspringen | | `--skip-deps` | Automatische Paketprüfung und -installation überspringen |
| `--no-enable` | systemd-Autostart nicht aktivieren | | `--no-enable` | systemd-Autostart nicht aktivieren |
| `--no-register` | Globalen CLI-Befehl `/usr/local/bin/adguard-shield` nicht anlegen |
| `--install-dir <pfad>` | Abweichendes Installationsverzeichnis verwenden | | `--install-dir <pfad>` | Abweichendes Installationsverzeichnis verwenden |
**Beispiele:** **Beispiele:**
@@ -98,6 +112,9 @@ sudo ./adguard-shield install --config-source ./adguard-shield.conf
# Ohne Paketprüfung installieren # Ohne Paketprüfung installieren
sudo ./adguard-shield install --skip-deps sudo ./adguard-shield install --skip-deps
# Ohne globalen CLI-Befehl installieren
sudo ./adguard-shield install --no-register
# In anderes Verzeichnis installieren # In anderes Verzeichnis installieren
sudo ./adguard-shield install --install-dir /opt/adguard-shield-test sudo ./adguard-shield install --install-dir /opt/adguard-shield-test
``` ```
@@ -113,11 +130,12 @@ Der Installer führt diese Schritte automatisch durch:
| 3 | Installation fehlender Abhängigkeiten über `apt-get` (sofern möglich) | | 3 | Installation fehlender Abhängigkeiten über `apt-get` (sofern möglich) |
| 4 | Anlage von Installations- und State-Verzeichnissen | | 4 | Anlage von Installations- und State-Verzeichnissen |
| 5 | Kopieren des Binarys nach `/opt/adguard-shield/` | | 5 | Kopieren des Binarys nach `/opt/adguard-shield/` |
| 6 | Anlage oder Migration der Konfiguration | | 6 | CLI-Befehl `/usr/local/bin/adguard-shield` registrieren (sofern nicht `--no-register`) |
| 7 | Schreiben der systemd-Unit | | 7 | Report-Templates installieren |
| 8 | `systemctl daemon-reload` | | 8 | Anlage oder Migration der Konfiguration |
| 9 | Optional: Autostart aktivieren | | 9 | Schreiben der systemd-Unit |
| 10 | Nachfrage: Service direkt starten oder neu starten | | 10 | `systemctl daemon-reload` und optional Autostart aktivieren |
| 11 | Nachfrage: Service direkt starten oder neu starten |
### Benötigte Systembefehle ### Benötigte Systembefehle
@@ -149,9 +167,16 @@ Am Ende fragt der Updater, ob AdGuard Shield direkt neu gestartet werden soll.
sudo ./adguard-shield update --config-source ./adguard-shield.conf sudo ./adguard-shield update --config-source ./adguard-shield.conf
``` ```
### Update ohne CLI-Registrierung
```bash
sudo ./adguard-shield update --no-register
```
### Was beim Update passiert ### Was beim Update passiert
- Die Installation wird wie bei `install` aktualisiert - Die Installation wird wie bei `install` aktualisiert
- Der CLI-Befehl `/usr/local/bin/adguard-shield` wird angelegt oder bestätigt, sofern `--no-register` nicht gesetzt ist
- Vorhandene Konfiguration bleibt erhalten - Vorhandene Konfiguration bleibt erhalten
- Neue Konfigurationsparameter werden ergänzt - Neue Konfigurationsparameter werden ergänzt
- Bei einer Migration wird `adguard-shield.conf.old` geschrieben - Bei einer Migration wird `adguard-shield.conf.old` geschrieben
@@ -164,13 +189,14 @@ Weitere Details stehen in der [Update-Anleitung](update.md).
## Installationsstatus ## Installationsstatus
```bash ```bash
sudo ./adguard-shield install-status sudo adguard-shield install-status
``` ```
Zeigt eine Übersicht mit: Zeigt eine Übersicht mit:
- Installationspfad und Binary-Status - Installationspfad und Binary-Status
- Installierte Version - Installierte Version
- CLI-Befehl in `/usr/local/bin` vorhanden
- Konfiguration vorhanden - Konfiguration vorhanden
- systemd-Service vorhanden und Status - systemd-Service vorhanden und Status
- Autostart aktiv - Autostart aktiv
@@ -188,10 +214,10 @@ sudo ./adguard-shield install-status --install-dir /opt/adguard-shield-test
```bash ```bash
# Vollständige Deinstallation # Vollständige Deinstallation
sudo /opt/adguard-shield/adguard-shield uninstall sudo adguard-shield uninstall
# Deinstallation mit Konfigurationserhalt # Deinstallation mit Konfigurationserhalt
sudo /opt/adguard-shield/adguard-shield uninstall --keep-config sudo adguard-shield uninstall --keep-config
``` ```
**Was bei der Deinstallation passiert:** **Was bei der Deinstallation passiert:**
@@ -275,19 +301,19 @@ Für Debugging oder Dry-Run kann der Daemon im Vordergrund gestartet werden:
```bash ```bash
# Normaler Vordergrundlauf # Normaler Vordergrundlauf
sudo /opt/adguard-shield/adguard-shield run sudo adguard-shield run
# Alias für run # Alias für run
sudo /opt/adguard-shield/adguard-shield start sudo adguard-shield start
# Analysieren ohne echte Sperren # Analysieren ohne echte Sperren
sudo /opt/adguard-shield/adguard-shield dry-run sudo adguard-shield dry-run
``` ```
### Daemon über PID-Datei stoppen ### Daemon über PID-Datei stoppen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield stop sudo adguard-shield stop
``` ```
Für den Alltag gilt: Nutze `systemctl`. Der direkte Vordergrundlauf endet, sobald die Shell beendet wird oder du `Strg+C` drückst. Für den Alltag gilt: Nutze `systemctl`. Der direkte Vordergrundlauf endet, sobald die Shell beendet wird oder du `Strg+C` drückst.
@@ -297,7 +323,7 @@ Für den Alltag gilt: Nutze `systemctl`. Der direkte Vordergrundlauf endet, soba
## API-Test ## API-Test
```bash ```bash
sudo /opt/adguard-shield/adguard-shield test sudo adguard-shield test
``` ```
Der `test`-Befehl prüft die Verbindung zur AdGuard-Home-API: Der `test`-Befehl prüft die Verbindung zur AdGuard-Home-API:
@@ -322,7 +348,7 @@ Wenn der Test fehlschlägt, zuerst die Konfiguration und die AdGuard-Home-Webobe
## Status ## Status
```bash ```bash
sudo /opt/adguard-shield/adguard-shield status sudo adguard-shield status
``` ```
Zeigt eine Übersicht des aktuellen Zustands: Zeigt eine Übersicht des aktuellen Zustands:
@@ -341,7 +367,7 @@ Bei sehr vielen aktiven Sperren werden nur die ersten 50 angezeigt. Für Details
## Live-Ansicht ## Live-Ansicht
```bash ```bash
sudo /opt/adguard-shield/adguard-shield live sudo adguard-shield live
``` ```
Die `live`-Ansicht ist das beste Werkzeug, wenn du verstehen möchtest, was gerade passiert. Sie zeigt in Echtzeit: Die `live`-Ansicht ist das beste Werkzeug, wenn du verstehen möchtest, was gerade passiert. Sie zeigt in Echtzeit:
@@ -371,7 +397,7 @@ Die `live`-Ansicht ist das beste Werkzeug, wenn du verstehen möchtest, was gera
### Alias ### Alias
```bash ```bash
sudo /opt/adguard-shield/adguard-shield watch sudo adguard-shield watch
``` ```
--- ---
@@ -380,10 +406,10 @@ sudo /opt/adguard-shield/adguard-shield watch
```bash ```bash
# Letzte 50 Einträge (Standard) # Letzte 50 Einträge (Standard)
sudo /opt/adguard-shield/adguard-shield history sudo adguard-shield history
# Letzte 200 Einträge # Letzte 200 Einträge
sudo /opt/adguard-shield/adguard-shield history 200 sudo adguard-shield history 200
``` ```
Die History kommt aus der SQLite-Tabelle `ban_history`. Die History kommt aus der SQLite-Tabelle `ban_history`.
@@ -432,16 +458,16 @@ AdGuard Shield schreibt Daemon-Ereignisse in `LOG_FILE`, standardmäßig:
```bash ```bash
# Letzte INFO/WARN/ERROR-Einträge # Letzte INFO/WARN/ERROR-Einträge
sudo /opt/adguard-shield/adguard-shield logs sudo adguard-shield logs
# Letzte 100 Warnungen und Fehler # Letzte 100 Warnungen und Fehler
sudo /opt/adguard-shield/adguard-shield logs --level warn --limit 100 sudo adguard-shield logs --level warn --limit 100
# Kurzform (Level als Argument) # Kurzform (Level als Argument)
sudo /opt/adguard-shield/adguard-shield logs debug sudo adguard-shield logs debug
# Laufende Ansicht (wie tail -f) # Laufende Ansicht (wie tail -f)
sudo /opt/adguard-shield/adguard-shield logs-follow --level info sudo adguard-shield logs-follow --level info
``` ```
### Erlaubte Log-Level ### Erlaubte Log-Level
@@ -469,7 +495,7 @@ sudo journalctl -u adguard-shield --no-pager -n 100
### IP permanent sperren ### IP permanent sperren
```bash ```bash
sudo /opt/adguard-shield/adguard-shield ban 192.168.1.100 sudo adguard-shield ban 192.168.1.100
``` ```
Legt eine manuelle permanente Sperre an. Die IP wird sofort in die Firewall eingetragen. Legt eine manuelle permanente Sperre an. Die IP wird sofort in die Firewall eingetragen.
@@ -477,7 +503,7 @@ Legt eine manuelle permanente Sperre an. Die IP wird sofort in die Firewall eing
### IP entsperren ### IP entsperren
```bash ```bash
sudo /opt/adguard-shield/adguard-shield unban 192.168.1.100 sudo adguard-shield unban 192.168.1.100
``` ```
Entfernt die IP aus Firewall und Datenbank. Funktioniert für alle Sperrtypen (automatisch, manuell, GeoIP, Blocklist). Entfernt die IP aus Firewall und Datenbank. Funktioniert für alle Sperrtypen (automatisch, manuell, GeoIP, Blocklist).
@@ -485,7 +511,7 @@ Entfernt die IP aus Firewall und Datenbank. Funktioniert für alle Sperrtypen (a
### Alle Sperren aufheben ### Alle Sperren aufheben
```bash ```bash
sudo /opt/adguard-shield/adguard-shield flush sudo adguard-shield flush
``` ```
Hebt alle aktiven Sperren auf. Bei aktivierten Benachrichtigungen wird eine zusammenfassende Meldung gesendet, nicht eine Nachricht pro IP. Hebt alle aktiven Sperren auf. Bei aktivierten Benachrichtigungen wird eine zusammenfassende Meldung gesendet, nicht eine Nachricht pro IP.
@@ -499,7 +525,7 @@ Hebt alle aktiven Sperren auf. Bei aktivierten Benachrichtigungen wird eine zusa
### Offense-Status anzeigen ### Offense-Status anzeigen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield offense-status sudo adguard-shield offense-status
``` ```
Zeigt die Gesamtzahl der Offense-Zähler, davon abgelaufene, und die Konfiguration. Zeigt die Gesamtzahl der Offense-Zähler, davon abgelaufene, und die Konfiguration.
@@ -507,19 +533,19 @@ Zeigt die Gesamtzahl der Offense-Zähler, davon abgelaufene, und die Konfigurati
### Abgelaufene Zähler entfernen ### Abgelaufene Zähler entfernen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield offense-cleanup sudo adguard-shield offense-cleanup
``` ```
### Alle Offense-Zähler zurücksetzen ### Alle Offense-Zähler zurücksetzen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield reset-offenses sudo adguard-shield reset-offenses
``` ```
### Zähler für eine IP zurücksetzen ### Zähler für eine IP zurücksetzen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield reset-offenses 192.168.1.100 sudo adguard-shield reset-offenses 192.168.1.100
``` ```
### Typischer Ablauf nach Fehlkonfiguration ### Typischer Ablauf nach Fehlkonfiguration
@@ -528,10 +554,10 @@ Wenn ein Client fälschlicherweise eskaliert wurde:
```bash ```bash
# Sperre aufheben # Sperre aufheben
sudo /opt/adguard-shield/adguard-shield unban 192.168.1.100 sudo adguard-shield unban 192.168.1.100
# Offense-Zähler zurücksetzen # Offense-Zähler zurücksetzen
sudo /opt/adguard-shield/adguard-shield reset-offenses 192.168.1.100 sudo adguard-shield reset-offenses 192.168.1.100
# IP dauerhaft in Whitelist aufnehmen (in adguard-shield.conf) # IP dauerhaft in Whitelist aufnehmen (in adguard-shield.conf)
# WHITELIST="127.0.0.1,::1,192.168.1.100" # WHITELIST="127.0.0.1,::1,192.168.1.100"
@@ -545,13 +571,13 @@ sudo systemctl restart adguard-shield
### Chain und ipsets anlegen ### Chain und ipsets anlegen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield firewall-create sudo adguard-shield firewall-create
``` ```
### Status anzeigen ### Status anzeigen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield firewall-status sudo adguard-shield firewall-status
``` ```
Zeigt die aktuelle Firewall-Struktur: Chain, ipsets und eingehängte Regeln. Zeigt die aktuelle Firewall-Struktur: Chain, ipsets und eingehängte Regeln.
@@ -559,7 +585,7 @@ Zeigt die aktuelle Firewall-Struktur: Chain, ipsets und eingehängte Regeln.
### ipsets leeren ### ipsets leeren
```bash ```bash
sudo /opt/adguard-shield/adguard-shield firewall-flush sudo adguard-shield firewall-flush
``` ```
Entfernt alle IPs aus den ipsets. Die Firewall-Struktur (Chain, Regeln) bleibt bestehen. Entfernt alle IPs aus den ipsets. Die Firewall-Struktur (Chain, Regeln) bleibt bestehen.
@@ -567,13 +593,13 @@ Entfernt alle IPs aus den ipsets. Die Firewall-Struktur (Chain, Regeln) bleibt b
### Chain und ipsets vollständig entfernen ### Chain und ipsets vollständig entfernen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield firewall-remove sudo adguard-shield firewall-remove
``` ```
### Firewall-Regeln sichern ### Firewall-Regeln sichern
```bash ```bash
sudo /opt/adguard-shield/adguard-shield firewall-save sudo adguard-shield firewall-save
``` ```
Speichert die aktuellen Regeln nach: Speichert die aktuellen Regeln nach:
@@ -586,7 +612,7 @@ Speichert die aktuellen Regeln nach:
### Gesicherte Regeln wiederherstellen ### Gesicherte Regeln wiederherstellen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield firewall-restore sudo adguard-shield firewall-restore
``` ```
**Hinweis:** Normalerweise musst du diese Befehle nicht manuell ausführen. Der Daemon erstellt die Firewall beim Start und schreibt aktive Sperren aus SQLite wieder hinein. Welche Host-Chain genutzt wird, hängt von `FIREWALL_MODE` ab. Details stehen in [Docker-Installationen](docker.md). **Hinweis:** Normalerweise musst du diese Befehle nicht manuell ausführen. Der Daemon erstellt die Firewall beim Start und schreibt aktive Sperren aus SQLite wieder hinein. Welche Host-Chain genutzt wird, hängt von `FIREWALL_MODE` ab. Details stehen in [Docker-Installationen](docker.md).
@@ -598,19 +624,19 @@ sudo /opt/adguard-shield/adguard-shield firewall-restore
### Status anzeigen ### Status anzeigen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield whitelist-status sudo adguard-shield whitelist-status
``` ```
### Sofort synchronisieren ### Sofort synchronisieren
```bash ```bash
sudo /opt/adguard-shield/adguard-shield whitelist-sync sudo adguard-shield whitelist-sync
``` ```
### Aufgelöste externe Whitelist entfernen ### Aufgelöste externe Whitelist entfernen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield whitelist-flush sudo adguard-shield whitelist-flush
``` ```
### Hinweise ### Hinweise
@@ -629,19 +655,19 @@ sudo /opt/adguard-shield/adguard-shield whitelist-flush
### Status anzeigen ### Status anzeigen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield blocklist-status sudo adguard-shield blocklist-status
``` ```
### Sofort synchronisieren ### Sofort synchronisieren
```bash ```bash
sudo /opt/adguard-shield/adguard-shield blocklist-sync sudo adguard-shield blocklist-sync
``` ```
### Alle Sperren aus externer Blocklist aufheben ### Alle Sperren aus externer Blocklist aufheben
```bash ```bash
sudo /opt/adguard-shield/adguard-shield blocklist-flush sudo adguard-shield blocklist-flush
``` ```
### Hinweise ### Hinweise
@@ -658,13 +684,13 @@ sudo /opt/adguard-shield/adguard-shield blocklist-flush
### Status anzeigen ### Status anzeigen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield geoip-status sudo adguard-shield geoip-status
``` ```
### Einzelne IP nachschlagen ### Einzelne IP nachschlagen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield geoip-lookup 8.8.8.8 sudo adguard-shield geoip-lookup 8.8.8.8
``` ```
**Ausgabe:** **Ausgabe:**
@@ -676,7 +702,7 @@ IP: 8.8.8.8 -> Land: US
### Aktuelle Clients prüfen ### Aktuelle Clients prüfen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield geoip-sync sudo adguard-shield geoip-sync
``` ```
Liest das aktuelle Querylog und prüft alle darin enthaltenen Client-IPs einmalig gegen die GeoIP-Regeln. Liest das aktuelle Querylog und prüft alle darin enthaltenen Client-IPs einmalig gegen die GeoIP-Regeln.
@@ -684,13 +710,13 @@ Liest das aktuelle Querylog und prüft alle darin enthaltenen Client-IPs einmali
### Alle GeoIP-Sperren aufheben ### Alle GeoIP-Sperren aufheben
```bash ```bash
sudo /opt/adguard-shield/adguard-shield geoip-flush sudo adguard-shield geoip-flush
``` ```
### Cache leeren ### Cache leeren
```bash ```bash
sudo /opt/adguard-shield/adguard-shield geoip-flush-cache sudo adguard-shield geoip-flush-cache
``` ```
### Hinweise ### Hinweise
@@ -705,25 +731,25 @@ sudo /opt/adguard-shield/adguard-shield geoip-flush-cache
### Konfiguration und Cron-Status anzeigen ### Konfiguration und Cron-Status anzeigen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield report-status sudo adguard-shield report-status
``` ```
### HTML-Report in Datei schreiben ### HTML-Report in Datei schreiben
```bash ```bash
sudo /opt/adguard-shield/adguard-shield report-generate html /tmp/adguard-shield-report.html sudo adguard-shield report-generate html /tmp/adguard-shield-report.html
``` ```
### Text-Report auf stdout ausgeben ### Text-Report auf stdout ausgeben
```bash ```bash
sudo /opt/adguard-shield/adguard-shield report-generate txt sudo adguard-shield report-generate txt
``` ```
### Testmail senden ### Testmail senden
```bash ```bash
sudo /opt/adguard-shield/adguard-shield report-test sudo adguard-shield report-test
``` ```
Sendet eine einfache Testmail. Erst wenn diese funktioniert, lohnt sich die Fehlersuche am eigentlichen Report. Sendet eine einfache Testmail. Erst wenn diese funktioniert, lohnt sich die Fehlersuche am eigentlichen Report.
@@ -731,21 +757,21 @@ Sendet eine einfache Testmail. Erst wenn diese funktioniert, lohnt sich die Fehl
### Aktuellen Report erzeugen und versenden ### Aktuellen Report erzeugen und versenden
```bash ```bash
sudo /opt/adguard-shield/adguard-shield report-send sudo adguard-shield report-send
``` ```
### Cron-Job installieren ### Cron-Job installieren
```bash ```bash
sudo /opt/adguard-shield/adguard-shield report-install sudo adguard-shield report-install
``` ```
Erstellt die Datei `/etc/cron.d/adguard-shield-report` mit dem konfigurierten Intervall und der Versandzeit. Erstellt die Datei `/etc/cron.d/adguard-shield-report` mit dem konfigurierten Intervall und der Versandzeit. Wenn der globale CLI-Befehl vorhanden ist, verwendet der Cron-Job `/usr/local/bin/adguard-shield`; sonst fällt er auf das installierte Binary unter `/opt/adguard-shield/adguard-shield` zurück.
### Cron-Job entfernen ### Cron-Job entfernen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield report-remove sudo adguard-shield report-remove
``` ```
Details zum Report-System stehen in [E-Mail Report](report.md). Details zum Report-System stehen in [E-Mail Report](report.md).
@@ -755,7 +781,7 @@ Details zum Report-System stehen in [E-Mail Report](report.md).
## Dry-Run ## Dry-Run
```bash ```bash
sudo /opt/adguard-shield/adguard-shield dry-run sudo adguard-shield dry-run
``` ```
Der Dry-Run ist der sicherste Weg, neue Konfigurationen zu prüfen, bevor sie produktiv gehen. Der Dry-Run ist der sicherste Weg, neue Konfigurationen zu prüfen, bevor sie produktiv gehen.
@@ -773,11 +799,11 @@ Der Dry-Run ist der sicherste Weg, neue Konfigurationen zu prüfen, bevor sie pr
```bash ```bash
# Dry-Run starten (Strg+C zum Beenden) # Dry-Run starten (Strg+C zum Beenden)
sudo /opt/adguard-shield/adguard-shield dry-run sudo adguard-shield dry-run
# Ergebnisse prüfen # Ergebnisse prüfen
sudo /opt/adguard-shield/adguard-shield history 50 sudo adguard-shield history 50
sudo /opt/adguard-shield/adguard-shield logs --level warn --limit 80 sudo adguard-shield logs --level warn --limit 80
``` ```
--- ---
@@ -785,7 +811,7 @@ sudo /opt/adguard-shield/adguard-shield logs --level warn --limit 80
## Version ## Version
```bash ```bash
/opt/adguard-shield/adguard-shield version adguard-shield version
``` ```
Zeigt die installierte Version an. Aliase: `--version`, `-v`. Zeigt die installierte Version an. Aliase: `--version`, `-v`.
@@ -798,15 +824,15 @@ Zeigt die installierte Version an. Aliase: `--version`, `-v`.
```bash ```bash
sudo systemctl restart adguard-shield sudo systemctl restart adguard-shield
sudo /opt/adguard-shield/adguard-shield status sudo adguard-shield status
sudo /opt/adguard-shield/adguard-shield logs --level info --limit 80 sudo adguard-shield logs --level info --limit 80
``` ```
### Falsch gesperrte IP freigeben ### Falsch gesperrte IP freigeben
```bash ```bash
sudo /opt/adguard-shield/adguard-shield unban 192.168.1.100 sudo adguard-shield unban 192.168.1.100
sudo /opt/adguard-shield/adguard-shield reset-offenses 192.168.1.100 sudo adguard-shield reset-offenses 192.168.1.100
``` ```
Danach die IP dauerhaft in `WHITELIST` oder eine externe Whitelist aufnehmen. Danach die IP dauerhaft in `WHITELIST` oder eine externe Whitelist aufnehmen.
@@ -814,16 +840,16 @@ Danach die IP dauerhaft in `WHITELIST` oder eine externe Whitelist aufnehmen.
### Externe Listen neu laden ### Externe Listen neu laden
```bash ```bash
sudo /opt/adguard-shield/adguard-shield whitelist-sync sudo adguard-shield whitelist-sync
sudo /opt/adguard-shield/adguard-shield blocklist-sync sudo adguard-shield blocklist-sync
sudo /opt/adguard-shield/adguard-shield status sudo adguard-shield status
``` ```
### Firewall neu aufbauen ### Firewall neu aufbauen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield firewall-remove sudo adguard-shield firewall-remove
sudo /opt/adguard-shield/adguard-shield firewall-create sudo adguard-shield firewall-create
sudo systemctl restart adguard-shield sudo systemctl restart adguard-shield
``` ```
@@ -834,8 +860,8 @@ Nach dem Neustart schreibt der Daemon aktive Sperren aus SQLite wieder in die Fi
```bash ```bash
sudo systemctl status adguard-shield sudo systemctl status adguard-shield
sudo journalctl -u adguard-shield --no-pager -n 100 sudo journalctl -u adguard-shield --no-pager -n 100
sudo /opt/adguard-shield/adguard-shield test sudo adguard-shield test
sudo /opt/adguard-shield/adguard-shield logs --level debug --limit 100 sudo adguard-shield logs --level debug --limit 100
``` ```
--- ---
@@ -900,7 +926,7 @@ Die Beispielzahlen liegen bewusst nahe an den Standardlimits `RATE_LIMIT_MAX_REQ
## Eingebaute Hilfe ## Eingebaute Hilfe
```bash ```bash
/opt/adguard-shield/adguard-shield --help adguard-shield --help
``` ```
Bei unbekannten Befehlen gibt das Binary die Usage-Ausgabe aus. Bei unbekannten Befehlen gibt das Binary die Usage-Ausgabe aus.

View File

@@ -217,9 +217,9 @@ GEOIP_NOTIFY=false
Diese Befehle können viele IPs auf einmal freigeben: Diese Befehle können viele IPs auf einmal freigeben:
```bash ```bash
sudo /opt/adguard-shield/adguard-shield flush sudo adguard-shield flush
sudo /opt/adguard-shield/adguard-shield geoip-flush sudo adguard-shield geoip-flush
sudo /opt/adguard-shield/adguard-shield blocklist-flush sudo adguard-shield blocklist-flush
``` ```
AdGuard Shield sendet dafür **nicht** eine Nachricht pro IP, sondern eine zusammenfassende Meldung mit der Anzahl der freigegebenen Sperren. AdGuard Shield sendet dafür **nicht** eine Nachricht pro IP, sondern eine zusammenfassende Meldung mit der Anzahl der freigegebenen Sperren.
@@ -326,7 +326,7 @@ Aktion: Manual-Flush
Wenn keine Benachrichtigung ankommt: Wenn keine Benachrichtigung ankommt:
```bash ```bash
sudo /opt/adguard-shield/adguard-shield logs --level warn --limit 100 sudo adguard-shield logs --level warn --limit 100
sudo journalctl -u adguard-shield --no-pager -n 100 sudo journalctl -u adguard-shield --no-pager -n 100
``` ```

View File

@@ -143,8 +143,8 @@ BLOCKED_PORTS="53 443 853"
```bash ```bash
sudo systemctl restart adguard-shield sudo systemctl restart adguard-shield
sudo /opt/adguard-shield/adguard-shield firewall-status sudo adguard-shield firewall-status
sudo /opt/adguard-shield/adguard-shield status sudo adguard-shield status
``` ```
## Firewall neu aufbauen ## Firewall neu aufbauen
@@ -152,9 +152,9 @@ sudo /opt/adguard-shield/adguard-shield status
Falls der Modus gewechselt wurde: Falls der Modus gewechselt wurde:
```bash ```bash
sudo /opt/adguard-shield/adguard-shield firewall-remove sudo adguard-shield firewall-remove
sudo systemctl restart adguard-shield sudo systemctl restart adguard-shield
sudo /opt/adguard-shield/adguard-shield firewall-status sudo adguard-shield firewall-status
``` ```
Der Daemon erstellt die Firewall-Struktur beim Start automatisch neu und überträgt aktive Sperren aus SQLite. Der Daemon erstellt die Firewall-Struktur beim Start automatisch neu und überträgt aktive Sperren aus SQLite.

View File

@@ -20,7 +20,7 @@ Nach Änderungen muss der Service neu gestartet werden:
```bash ```bash
sudo systemctl restart adguard-shield sudo systemctl restart adguard-shield
sudo /opt/adguard-shield/adguard-shield status sudo adguard-shield status
``` ```
## Automatische Migration ## Automatische Migration
@@ -42,10 +42,10 @@ Nach dem Bearbeiten der Konfiguration:
```bash ```bash
# API-Verbindung testen # API-Verbindung testen
sudo /opt/adguard-shield/adguard-shield test sudo adguard-shield test
# Dry-Run: zeigt, was gesperrt würde, ohne die Firewall zu verändern # Dry-Run: zeigt, was gesperrt würde, ohne die Firewall zu verändern
sudo /opt/adguard-shield/adguard-shield dry-run sudo adguard-shield dry-run
``` ```
--- ---
@@ -262,10 +262,10 @@ Progressive Sperren gelten für Monitor-Sperren wie `rate-limit` und `subdomain-
### Verwaltungsbefehle ### Verwaltungsbefehle
```bash ```bash
sudo /opt/adguard-shield/adguard-shield offense-status # Zähler anzeigen sudo adguard-shield offense-status # Zähler anzeigen
sudo /opt/adguard-shield/adguard-shield offense-cleanup # Abgelaufene entfernen sudo adguard-shield offense-cleanup # Abgelaufene entfernen
sudo /opt/adguard-shield/adguard-shield reset-offenses # Alle zurücksetzen sudo adguard-shield reset-offenses # Alle zurücksetzen
sudo /opt/adguard-shield/adguard-shield reset-offenses <IP> # Eine IP zurücksetzen sudo adguard-shield reset-offenses <IP> # Eine IP zurücksetzen
``` ```
--- ---
@@ -289,9 +289,9 @@ sudo /opt/adguard-shield/adguard-shield reset-offenses <IP> # Eine IP zurück
### CLI-Befehle ### CLI-Befehle
```bash ```bash
sudo /opt/adguard-shield/adguard-shield logs --level warn --limit 100 sudo adguard-shield logs --level warn --limit 100
sudo /opt/adguard-shield/adguard-shield logs-follow debug sudo adguard-shield logs-follow debug
sudo /opt/adguard-shield/adguard-shield live sudo adguard-shield live
``` ```
**Hinweis:** Query-Inhalte werden nicht dauerhaft ins Log geschrieben. Für Query-nahe Diagnose ist die Live-Ansicht gedacht. **Hinweis:** Query-Inhalte werden nicht dauerhaft ins Log geschrieben. Für Query-nahe Diagnose ist die Live-Ansicht gedacht.
@@ -380,7 +380,7 @@ Details zu allen Kanälen stehen in [Benachrichtigungen](benachrichtigungen.md).
| `REPORT_EMAIL_FROM` | `adguard-shield@example.com` | Absenderadresse | | `REPORT_EMAIL_FROM` | `adguard-shield@example.com` | Absenderadresse |
| `REPORT_FORMAT` | `html` | Report-Format | | `REPORT_FORMAT` | `html` | Report-Format |
| `REPORT_MAIL_CMD` | `msmtp` | Mailprogramm für den Versand | | `REPORT_MAIL_CMD` | `msmtp` | Mailprogramm für den Versand |
| `REPORT_BUSIEST_DAY_RANGE` | `30` | Zeitraum für "Aktivster Tag" (Kompatibilitätsparameter) | | `REPORT_BUSIEST_DAY_RANGE` | `30` | Zeitraum für "Aktivster Tag"; `0` nutzt den Berichtszeitraum |
### Verfügbare Intervalle ### Verfügbare Intervalle
@@ -388,7 +388,7 @@ Details zu allen Kanälen stehen in [Benachrichtigungen](benachrichtigungen.md).
|---|---| |---|---|
| `daily` | Täglich zur konfigurierten Uhrzeit | | `daily` | Täglich zur konfigurierten Uhrzeit |
| `weekly` | Montags zur konfigurierten Uhrzeit | | `weekly` | Montags zur konfigurierten Uhrzeit |
| `biweekly` | Am 1. und 15. des Monats | | `biweekly` | Montags in ungeraden ISO-Kalenderwochen |
| `monthly` | Am 1. des Monats | | `monthly` | Am 1. des Monats |
### Verfügbare Formate ### Verfügbare Formate
@@ -413,11 +413,13 @@ REPORT_MAIL_CMD="msmtp"
### Cron-Job installieren ### Cron-Job installieren
```bash ```bash
sudo /opt/adguard-shield/adguard-shield report-install sudo adguard-shield report-install
``` ```
Details stehen in [E-Mail Report](report.md). Details stehen in [E-Mail Report](report.md).
Die Templates werden bei `install`/`update` nach `/opt/adguard-shield/templates` geschrieben. Eigene Templates können alternativ über `ADGUARD_SHIELD_TEMPLATE_DIR` bereitgestellt werden.
--- ---
## Externe Whitelist ## Externe Whitelist
@@ -640,11 +642,11 @@ Die Datenbank wird unter `/opt/adguard-shield/geoip/` gespeichert und nach 24 St
### GeoIP-Befehle ### GeoIP-Befehle
```bash ```bash
sudo /opt/adguard-shield/adguard-shield geoip-status # Status anzeigen sudo adguard-shield geoip-status # Status anzeigen
sudo /opt/adguard-shield/adguard-shield geoip-lookup 8.8.8.8 # IP nachschlagen sudo adguard-shield geoip-lookup 8.8.8.8 # IP nachschlagen
sudo /opt/adguard-shield/adguard-shield geoip-sync # Clients prüfen sudo adguard-shield geoip-sync # Clients prüfen
sudo /opt/adguard-shield/adguard-shield geoip-flush-cache # Cache leeren sudo adguard-shield geoip-flush-cache # Cache leeren
sudo /opt/adguard-shield/adguard-shield geoip-flush # Alle GeoIP-Sperren aufheben sudo adguard-shield geoip-flush # Alle GeoIP-Sperren aufheben
``` ```
--- ---
@@ -737,6 +739,6 @@ NTFY_TOPIC="adguard-shield-prod"
### Vor produktiver Aktivierung ### Vor produktiver Aktivierung
```bash ```bash
sudo /opt/adguard-shield/adguard-shield test sudo adguard-shield test
sudo /opt/adguard-shield/adguard-shield dry-run sudo adguard-shield dry-run
``` ```

View File

@@ -17,11 +17,26 @@ Der Report basiert auf der SQLite-Datenbank:
| Zeitraum | Start- und Enddatum des Berichtszeitraums | | Zeitraum | Start- und Enddatum des Berichtszeitraums |
| Sperren | Anzahl der Sperren im Zeitraum | | Sperren | Anzahl der Sperren im Zeitraum |
| Freigaben | Anzahl der Freigaben im Zeitraum | | Freigaben | Anzahl der Freigaben im Zeitraum |
| Zeitraum-Schnellübersicht | Heute, gestern, letzte 7/14/30 Tage |
| Aktive Sperren | Derzeit aktive Sperren zum Zeitpunkt der Report-Erstellung | | Aktive Sperren | Derzeit aktive Sperren zum Zeitpunkt der Report-Erstellung |
| Permanente Sperren | Anzahl permanenter Sperren im Zeitraum |
| AbuseIPDB | Anzahl erfolgreicher AbuseIPDB-Meldungen im Zeitraum |
| Angriffsarten | Rate-Limit, Subdomain-Flood und externe Blocklist |
| Top-Clients | Die am häufigsten gesperrten Client-IPs | | Top-Clients | Die am häufigsten gesperrten Client-IPs |
| Sperrgründe | Aufschlüsselung nach Grund (Rate-Limit, Subdomain-Flood, GeoIP usw.) | | Top-Domains | Die am häufigsten betroffenen Domains |
| Sperrquellen | Aufschlüsselung nach Quelle (Monitor, GeoIP, Blocklist, manuell) | | Protokolle | Verteilung nach DNS, DoH, DoT, DoQ usw. |
| Letzte Ereignisse | Die letzten 20 Einträge aus der Ban-History | | Letzte Sperren | Die letzten 10 Sperren im Berichtszeitraum |
### Templates
Die Standard-Templates liegen im Code unter:
```text
internal/report/templates/report.html
internal/report/templates/report.txt
```
Sie werden ins Binary eingebettet und bei `install` sowie `update` nach `/opt/adguard-shield/templates` geschrieben. Zur Laufzeit verwendet AdGuard Shield zuerst externe Templates aus `ADGUARD_SHIELD_TEMPLATE_DIR`, danach `templates` neben der Konfiguration bzw. neben dem Binary und fällt erst zuletzt auf die eingebetteten Standard-Templates zurück.
--- ---
@@ -36,7 +51,7 @@ Der Report basiert auf der SQLite-Datenbank:
| `REPORT_EMAIL_FROM` | `adguard-shield@example.com` | Absenderadresse | | `REPORT_EMAIL_FROM` | `adguard-shield@example.com` | Absenderadresse |
| `REPORT_FORMAT` | `html` | Report-Format (`html` oder `txt`) | | `REPORT_FORMAT` | `html` | Report-Format (`html` oder `txt`) |
| `REPORT_MAIL_CMD` | `msmtp` | Mailprogramm für den Versand | | `REPORT_MAIL_CMD` | `msmtp` | Mailprogramm für den Versand |
| `REPORT_BUSIEST_DAY_RANGE` | `30` | Kompatibilitätsparameter für den Zeitraum "Aktivster Tag" | | `REPORT_BUSIEST_DAY_RANGE` | `30` | Zeitraum für "Aktivster Tag"; `0` nutzt den Berichtszeitraum |
### Versandintervalle ### Versandintervalle
@@ -44,7 +59,7 @@ Der Report basiert auf der SQLite-Datenbank:
|---|---| |---|---|
| `daily` | Täglich zur Uhrzeit aus `REPORT_TIME` | | `daily` | Täglich zur Uhrzeit aus `REPORT_TIME` |
| `weekly` | Montags zur Uhrzeit aus `REPORT_TIME` | | `weekly` | Montags zur Uhrzeit aus `REPORT_TIME` |
| `biweekly` | Am 1. und 15. des Monats zur Uhrzeit aus `REPORT_TIME` | | `biweekly` | Montags in ungeraden ISO-Kalenderwochen zur Uhrzeit aus `REPORT_TIME` |
| `monthly` | Am 1. des Monats zur Uhrzeit aus `REPORT_TIME` | | `monthly` | Am 1. des Monats zur Uhrzeit aus `REPORT_TIME` |
### Formate ### Formate
@@ -73,13 +88,13 @@ REPORT_MAIL_CMD="msmtp"
### Konfiguration und Cron-Status anzeigen ### Konfiguration und Cron-Status anzeigen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield report-status sudo adguard-shield report-status
``` ```
### HTML-Report in Datei schreiben ### HTML-Report in Datei schreiben
```bash ```bash
sudo /opt/adguard-shield/adguard-shield report-generate html /tmp/adguard-shield-report.html sudo adguard-shield report-generate html /tmp/adguard-shield-report.html
``` ```
Die Datei kann im Browser geöffnet werden, um das Ergebnis zu prüfen. Die Datei kann im Browser geöffnet werden, um das Ergebnis zu prüfen.
@@ -87,13 +102,13 @@ Die Datei kann im Browser geöffnet werden, um das Ergebnis zu prüfen.
### Text-Report auf stdout ausgeben ### Text-Report auf stdout ausgeben
```bash ```bash
sudo /opt/adguard-shield/adguard-shield report-generate txt sudo adguard-shield report-generate txt
``` ```
### Testmail senden ### Testmail senden
```bash ```bash
sudo /opt/adguard-shield/adguard-shield report-test sudo adguard-shield report-test
``` ```
Sendet eine einfache Testmail. Erst wenn diese ankommt, lohnt sich die Fehlersuche am eigentlichen Report. Sendet eine einfache Testmail. Erst wenn diese ankommt, lohnt sich die Fehlersuche am eigentlichen Report.
@@ -101,19 +116,19 @@ Sendet eine einfache Testmail. Erst wenn diese ankommt, lohnt sich die Fehlersuc
### Aktuellen Report erzeugen und versenden ### Aktuellen Report erzeugen und versenden
```bash ```bash
sudo /opt/adguard-shield/adguard-shield report-send sudo adguard-shield report-send
``` ```
### Cron-Job installieren ### Cron-Job installieren
```bash ```bash
sudo /opt/adguard-shield/adguard-shield report-install sudo adguard-shield report-install
``` ```
### Cron-Job entfernen ### Cron-Job entfernen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield report-remove sudo adguard-shield report-remove
``` ```
--- ---
@@ -133,7 +148,7 @@ REPORT_MAIL_CMD="msmtp"
sudo apt install msmtp msmtp-mta sudo apt install msmtp msmtp-mta
# Testmail senden # Testmail senden
sudo /opt/adguard-shield/adguard-shield report-test sudo adguard-shield report-test
``` ```
### Eigene Mailprogramm-Argumente ### Eigene Mailprogramm-Argumente
@@ -160,7 +175,7 @@ REPORT_MAIL_CMD="msmtp --account=default"
### Cron-Job installieren ### Cron-Job installieren
```bash ```bash
sudo /opt/adguard-shield/adguard-shield report-install sudo adguard-shield report-install
``` ```
Dadurch wird diese Datei geschrieben: Dadurch wird diese Datei geschrieben:
@@ -169,25 +184,27 @@ Dadurch wird diese Datei geschrieben:
/etc/cron.d/adguard-shield-report /etc/cron.d/adguard-shield-report
``` ```
Der Cron-Eintrag ruft das installierte Binary mit der installierten Konfiguration auf: Der Cron-Eintrag ruft den globalen CLI-Befehl mit der installierten Konfiguration auf, sofern er registriert ist:
```text ```text
/opt/adguard-shield/adguard-shield -config /opt/adguard-shield/adguard-shield.conf report-send /usr/local/bin/adguard-shield -config /opt/adguard-shield/adguard-shield.conf report-send
``` ```
Falls die Installation mit `--no-register` erfolgt ist, verwendet `report-install` stattdessen `/opt/adguard-shield/adguard-shield`.
### Zeitplan nach Intervall ### Zeitplan nach Intervall
| Intervall | Cron-Verhalten | | Intervall | Cron-Verhalten |
|---|---| |---|---|
| `daily` | Täglich zur Uhrzeit aus `REPORT_TIME` | | `daily` | Täglich zur Uhrzeit aus `REPORT_TIME` |
| `weekly` | Montags zur Uhrzeit aus `REPORT_TIME` | | `weekly` | Montags zur Uhrzeit aus `REPORT_TIME` |
| `biweekly` | Am 1. und 15. des Monats | | `biweekly` | Montags in ungeraden ISO-Kalenderwochen |
| `monthly` | Am 1. des Monats | | `monthly` | Am 1. des Monats |
### Cron-Job entfernen ### Cron-Job entfernen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield report-remove sudo adguard-shield report-remove
``` ```
--- ---
@@ -197,33 +214,33 @@ sudo /opt/adguard-shield/adguard-shield report-remove
### Schritt 1: Status prüfen ### Schritt 1: Status prüfen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield report-status sudo adguard-shield report-status
``` ```
### Schritt 2: Report lokal erzeugen ### Schritt 2: Report lokal erzeugen
```bash ```bash
# HTML-Report zum Ansehen im Browser # HTML-Report zum Ansehen im Browser
sudo /opt/adguard-shield/adguard-shield report-generate html /tmp/adguard-shield-report.html sudo adguard-shield report-generate html /tmp/adguard-shield-report.html
# Text-Report in der Konsole # Text-Report in der Konsole
sudo /opt/adguard-shield/adguard-shield report-generate txt sudo adguard-shield report-generate txt
``` ```
### Schritt 3: Versand testen ### Schritt 3: Versand testen
```bash ```bash
# Einfache Testmail # Einfache Testmail
sudo /opt/adguard-shield/adguard-shield report-test sudo adguard-shield report-test
# Vollständigen Report senden # Vollständigen Report senden
sudo /opt/adguard-shield/adguard-shield report-send sudo adguard-shield report-send
``` ```
### Schritt 4: Logs prüfen ### Schritt 4: Logs prüfen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield logs --level warn --limit 100 sudo adguard-shield logs --level warn --limit 100
sudo journalctl -u cron --no-pager -n 100 sudo journalctl -u cron --no-pager -n 100
``` ```
@@ -262,7 +279,7 @@ Oder setze `REPORT_MAIL_CMD` auf dein vorhandenes Mailprogramm.
Prüfe die Konfiguration und den Cron-Job: Prüfe die Konfiguration und den Cron-Job:
```bash ```bash
sudo /opt/adguard-shield/adguard-shield report-send sudo adguard-shield report-send
sudo cat /etc/cron.d/adguard-shield-report sudo cat /etc/cron.d/adguard-shield-report
``` ```
@@ -281,6 +298,6 @@ sudo cat /etc/cron.d/adguard-shield-report
Du kannst das Format unabhängig von der Konfiguration wählen: Du kannst das Format unabhängig von der Konfiguration wählen:
```bash ```bash
sudo /opt/adguard-shield/adguard-shield report-generate txt sudo adguard-shield report-generate txt
sudo /opt/adguard-shield/adguard-shield report-generate html /tmp/report.html sudo adguard-shield report-generate html /tmp/report.html
``` ```

View File

@@ -14,19 +14,19 @@ sudo systemctl status adguard-shield
sudo journalctl -u adguard-shield --no-pager -n 100 sudo journalctl -u adguard-shield --no-pager -n 100
# 3. Funktioniert die API? # 3. Funktioniert die API?
sudo /opt/adguard-shield/adguard-shield test sudo adguard-shield test
# 4. Was ist der aktuelle Zustand? # 4. Was ist der aktuelle Zustand?
sudo /opt/adguard-shield/adguard-shield status sudo adguard-shield status
# 5. Gibt es Warnungen oder Fehler? # 5. Gibt es Warnungen oder Fehler?
sudo /opt/adguard-shield/adguard-shield logs --level warn --limit 100 sudo adguard-shield logs --level warn --limit 100
``` ```
Wenn du aktuelle Queries und den Echtzeit-Zustand sehen willst: Wenn du aktuelle Queries und den Echtzeit-Zustand sehen willst:
```bash ```bash
sudo /opt/adguard-shield/adguard-shield live sudo adguard-shield live
``` ```
--- ---
@@ -68,7 +68,7 @@ sudo systemctl daemon-reload
### Test ### Test
```bash ```bash
sudo /opt/adguard-shield/adguard-shield test sudo adguard-shield test
``` ```
### Konfiguration prüfen ### Konfiguration prüfen
@@ -104,9 +104,9 @@ Passe URL und Zugangsdaten entsprechend an.
### Prüfen ### Prüfen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield live --once sudo adguard-shield live --once
sudo /opt/adguard-shield/adguard-shield history 50 sudo adguard-shield history 50
sudo /opt/adguard-shield/adguard-shield logs --level debug --limit 100 sudo adguard-shield logs --level debug --limit 100
``` ```
### Mögliche Ursachen und Lösungen ### Mögliche Ursachen und Lösungen
@@ -131,8 +131,8 @@ sudo /opt/adguard-shield/adguard-shield logs --level debug --limit 100
### Übersicht verschaffen ### Übersicht verschaffen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield status sudo adguard-shield status
sudo /opt/adguard-shield/adguard-shield history 100 sudo adguard-shield history 100
``` ```
### Ursachen und Gegenmaßnahmen ### Ursachen und Gegenmaßnahmen
@@ -149,10 +149,10 @@ sudo /opt/adguard-shield/adguard-shield history 100
```bash ```bash
# Sperre aufheben # Sperre aufheben
sudo /opt/adguard-shield/adguard-shield unban 192.168.1.100 sudo adguard-shield unban 192.168.1.100
# Offense-Zähler zurücksetzen (damit progressive Sperren nicht sofort eskalieren) # Offense-Zähler zurücksetzen (damit progressive Sperren nicht sofort eskalieren)
sudo /opt/adguard-shield/adguard-shield reset-offenses 192.168.1.100 sudo adguard-shield reset-offenses 192.168.1.100
``` ```
### Dauerhaft ausnehmen ### Dauerhaft ausnehmen
@@ -174,7 +174,7 @@ sudo systemctl restart adguard-shield
### Status über AdGuard Shield ### Status über AdGuard Shield
```bash ```bash
sudo /opt/adguard-shield/adguard-shield firewall-status sudo adguard-shield firewall-status
``` ```
### Direkte Prüfung mit Systembefehlen ### Direkte Prüfung mit Systembefehlen
@@ -198,8 +198,8 @@ sudo iptables -n -L DOCKER-USER --line-numbers -v | grep ADGUARD
### Firewall neu aufbauen ### Firewall neu aufbauen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield firewall-remove sudo adguard-shield firewall-remove
sudo /opt/adguard-shield/adguard-shield firewall-create sudo adguard-shield firewall-create
sudo systemctl restart adguard-shield sudo systemctl restart adguard-shield
``` ```
@@ -212,8 +212,8 @@ Nach dem Neustart werden aktive Sperren aus SQLite wieder in die ipsets geschrie
### Prüfen ### Prüfen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield status sudo adguard-shield status
sudo /opt/adguard-shield/adguard-shield history 100 sudo adguard-shield history 100
``` ```
Temporäre Sperren werden beim Start und während jedes Pollings auf Ablauf geprüft. Wenn eine Sperre als permanent angezeigt wird, wird sie nicht automatisch freigegeben. Temporäre Sperren werden beim Start und während jedes Pollings auf Ablauf geprüft. Wenn eine Sperre als permanent angezeigt wird, wird sie nicht automatisch freigegeben.
@@ -231,7 +231,7 @@ Temporäre Sperren werden beim Start und während jedes Pollings auf Ablauf gepr
### Manuell freigeben ### Manuell freigeben
```bash ```bash
sudo /opt/adguard-shield/adguard-shield unban 192.168.1.100 sudo adguard-shield unban 192.168.1.100
``` ```
--- ---
@@ -242,13 +242,13 @@ Dry-Run ist ideal, um neue Konfigurationen zu prüfen, bevor sie produktiv gehen
```bash ```bash
# Dry-Run starten (Strg+C zum Beenden) # Dry-Run starten (Strg+C zum Beenden)
sudo /opt/adguard-shield/adguard-shield dry-run sudo adguard-shield dry-run
``` ```
Währenddessen die Ergebnisse prüfen: Währenddessen die Ergebnisse prüfen:
```bash ```bash
sudo /opt/adguard-shield/adguard-shield history 50 sudo adguard-shield history 50
``` ```
Im Dry-Run werden mögliche Sperren als `DRY` protokolliert. Es entstehen keine aktiven Sperren und keine Firewall-Änderungen. Im Dry-Run werden mögliche Sperren als `DRY` protokolliert. Es entstehen keine aktiven Sperren und keine Firewall-Änderungen.
@@ -260,13 +260,13 @@ Im Dry-Run werden mögliche Sperren als `DRY` protokolliert. Es entstehen keine
### Status prüfen ### Status prüfen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield whitelist-status sudo adguard-shield whitelist-status
``` ```
### Manuell synchronisieren ### Manuell synchronisieren
```bash ```bash
sudo /opt/adguard-shield/adguard-shield whitelist-sync sudo adguard-shield whitelist-sync
``` ```
### Typische Probleme ### Typische Probleme
@@ -295,19 +295,19 @@ trusted.example.com # Hostname (wird per DNS aufgelöst)
### Status prüfen ### Status prüfen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield blocklist-status sudo adguard-shield blocklist-status
``` ```
### Manuell synchronisieren ### Manuell synchronisieren
```bash ```bash
sudo /opt/adguard-shield/adguard-shield blocklist-sync sudo adguard-shield blocklist-sync
``` ```
### Alle Blocklist-Sperren freigeben ### Alle Blocklist-Sperren freigeben
```bash ```bash
sudo /opt/adguard-shield/adguard-shield blocklist-flush sudo adguard-shield blocklist-flush
``` ```
### Zu viele IPs gesperrt? ### Zu viele IPs gesperrt?
@@ -325,25 +325,25 @@ sudo /opt/adguard-shield/adguard-shield blocklist-flush
### Status prüfen ### Status prüfen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield geoip-status sudo adguard-shield geoip-status
``` ```
### Einzelne IP prüfen ### Einzelne IP prüfen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield geoip-lookup 8.8.8.8 sudo adguard-shield geoip-lookup 8.8.8.8
``` ```
### Cache leeren ### Cache leeren
```bash ```bash
sudo /opt/adguard-shield/adguard-shield geoip-flush-cache sudo adguard-shield geoip-flush-cache
``` ```
### Alle GeoIP-Sperren freigeben ### Alle GeoIP-Sperren freigeben
```bash ```bash
sudo /opt/adguard-shield/adguard-shield geoip-flush sudo adguard-shield geoip-flush
``` ```
### Typische Probleme und Lösungen ### Typische Probleme und Lösungen
@@ -368,17 +368,17 @@ Die GeoIP-Ländercodes folgen dem Standard ISO 3166-1 Alpha-2. Eine vollständig
### Status prüfen ### Status prüfen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield report-status sudo adguard-shield report-status
``` ```
### Funktionstest ### Funktionstest
```bash ```bash
# Testmail senden # Testmail senden
sudo /opt/adguard-shield/adguard-shield report-test sudo adguard-shield report-test
# Text-Report in der Konsole ansehen # Text-Report in der Konsole ansehen
sudo /opt/adguard-shield/adguard-shield report-generate txt sudo adguard-shield report-generate txt
``` ```
### Keine Mail kommt an? ### Keine Mail kommt an?
@@ -396,7 +396,7 @@ sudo /opt/adguard-shield/adguard-shield report-generate txt
```bash ```bash
sudo cat /etc/cron.d/adguard-shield-report sudo cat /etc/cron.d/adguard-shield-report
sudo /opt/adguard-shield/adguard-shield report-send sudo adguard-shield report-send
``` ```
--- ---
@@ -406,7 +406,7 @@ sudo /opt/adguard-shield/adguard-shield report-send
### Prüfen ### Prüfen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield logs --level warn --limit 100 sudo adguard-shield logs --level warn --limit 100
``` ```
### Checkliste ### Checkliste
@@ -497,13 +497,13 @@ Wenn der Zustand unklar ist und ein sauberer Neustart nötig ist:
sudo systemctl stop adguard-shield sudo systemctl stop adguard-shield
# Firewall-Struktur entfernen # Firewall-Struktur entfernen
sudo /opt/adguard-shield/adguard-shield firewall-remove sudo adguard-shield firewall-remove
# Service neu starten (baut Firewall aus SQLite wieder auf) # Service neu starten (baut Firewall aus SQLite wieder auf)
sudo systemctl start adguard-shield sudo systemctl start adguard-shield
# Status prüfen # Status prüfen
sudo /opt/adguard-shield/adguard-shield status sudo adguard-shield status
``` ```
Das entfernt die Firewall-Struktur und lässt den Daemon sie beim Start wieder aus dem SQLite-State aufbauen. Aktive Sperren bleiben in der Datenbank erhalten. Das entfernt die Firewall-Struktur und lässt den Daemon sie beim Start wieder aus dem SQLite-State aufbauen. Aktive Sperren bleiben in der Datenbank erhalten.
@@ -515,13 +515,13 @@ Das entfernt die Firewall-Struktur und lässt den Daemon sie beim Start wieder a
### Konfiguration behalten ### Konfiguration behalten
```bash ```bash
sudo /opt/adguard-shield/adguard-shield uninstall --keep-config sudo adguard-shield uninstall --keep-config
``` ```
### Alles entfernen ### Alles entfernen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield uninstall sudo adguard-shield uninstall
``` ```
Ohne `--keep-config` werden Installationsverzeichnis, State-Verzeichnis und Logdatei entfernt. Ohne `--keep-config` werden Installationsverzeichnis, State-Verzeichnis und Logdatei entfernt.

View File

@@ -14,11 +14,13 @@ sudo ./adguard-shield update
Am Ende fragt der Updater, ob AdGuard Shield direkt neu gestartet werden soll. Am Ende fragt der Updater, ob AdGuard Shield direkt neu gestartet werden soll.
Der Updater registriert dabei auch den globalen CLI-Befehl `/usr/local/bin/adguard-shield`. Nach dem Update kannst du die installierte Anwendung daher direkt mit `sudo adguard-shield <befehl>` verwenden.
### Nach dem Update prüfen ### Nach dem Update prüfen
```bash ```bash
sudo /opt/adguard-shield/adguard-shield install-status sudo adguard-shield install-status
sudo /opt/adguard-shield/adguard-shield status sudo adguard-shield status
sudo journalctl -u adguard-shield --no-pager -n 50 sudo journalctl -u adguard-shield --no-pager -n 50
``` ```
@@ -66,11 +68,12 @@ Der Update-Befehl nutzt intern dieselbe Routine wie die Installation:
| 3 | Systemabhängigkeiten prüfen (sofern nicht `--skip-deps`) | | 3 | Systemabhängigkeiten prüfen (sofern nicht `--skip-deps`) |
| 4 | Installationsverzeichnis sicherstellen | | 4 | Installationsverzeichnis sicherstellen |
| 5 | Neues Binary nach `/opt/adguard-shield/adguard-shield` kopieren | | 5 | Neues Binary nach `/opt/adguard-shield/adguard-shield` kopieren |
| 6 | Konfiguration migrieren (vorhandene Werte behalten, neue ergänzen) | | 6 | CLI-Befehl `/usr/local/bin/adguard-shield` registrieren/aktualisieren (sofern nicht `--no-register`) |
| 7 | systemd-Service neu schreiben | | 7 | Report-Templates installieren |
| 8 | `systemctl daemon-reload` | | 8 | Konfiguration migrieren (vorhandene Werte behalten, neue ergänzen) |
| 9 | Autostart aktivieren (sofern nicht `--no-enable`) | | 9 | systemd-Service neu schreiben |
| 10 | Nachfrage: Service direkt neu starten | | 10 | `systemctl daemon-reload` und Autostart aktivieren (sofern nicht `--no-enable`) |
| 11 | Nachfrage: Service direkt neu starten |
--- ---
@@ -113,8 +116,8 @@ Wenn du vorher manuell prüfen möchtest:
```bash ```bash
sudo ./adguard-shield update sudo ./adguard-shield update
sudo /opt/adguard-shield/adguard-shield test sudo adguard-shield test
sudo /opt/adguard-shield/adguard-shield dry-run sudo adguard-shield dry-run
sudo systemctl restart adguard-shield sudo systemctl restart adguard-shield
``` ```
@@ -126,6 +129,14 @@ sudo ./adguard-shield update --skip-deps
Sinnvoll, wenn `iptables`, `ip6tables`, `ipset` und `systemctl` bereits vorhanden sind oder die Paketinstallation nicht über `apt-get` laufen soll. Sinnvoll, wenn `iptables`, `ip6tables`, `ipset` und `systemctl` bereits vorhanden sind oder die Paketinstallation nicht über `apt-get` laufen soll.
### Update ohne CLI-Registrierung
```bash
sudo ./adguard-shield update --no-register
```
Damit wird kein Symlink unter `/usr/local/bin/adguard-shield` angelegt oder geändert. Die Anwendung bleibt dann weiterhin über `/opt/adguard-shield/adguard-shield` erreichbar.
### Update mit expliziter Konfigurationsquelle ### Update mit expliziter Konfigurationsquelle
```bash ```bash
@@ -138,7 +149,7 @@ sudo ./adguard-shield update --config-source ./adguard-shield.conf
sudo ./adguard-shield update --install-dir /opt/adguard-shield-test sudo ./adguard-shield update --install-dir /opt/adguard-shield-test
``` ```
**Hinweis:** Die systemd-Unit heißt weiterhin `adguard-shield.service`. Mehrere parallele produktive Installationen über dieselbe Unit sind nicht vorgesehen. **Hinweis:** Die systemd-Unit heißt weiterhin `adguard-shield.service`, und der globale CLI-Befehl heißt weiterhin `/usr/local/bin/adguard-shield`. Mehrere parallele produktive Installationen über dieselbe Unit oder denselben CLI-Befehl sind nicht vorgesehen.
--- ---
@@ -178,10 +189,10 @@ sudo cp /opt/adguard-shield/adguard-shield.conf /root/adguard-shield.conf.backup
sudo ./adguard-shield install --config-source /root/adguard-shield.conf.backup sudo ./adguard-shield install --config-source /root/adguard-shield.conf.backup
# 4. API-Verbindung prüfen # 4. API-Verbindung prüfen
sudo /opt/adguard-shield/adguard-shield test sudo adguard-shield test
# 5. Dry-Run: prüfen, was gesperrt würde # 5. Dry-Run: prüfen, was gesperrt würde
sudo /opt/adguard-shield/adguard-shield dry-run sudo adguard-shield dry-run
# 6. Produktiven Service starten # 6. Produktiven Service starten
sudo systemctl start adguard-shield sudo systemctl start adguard-shield
@@ -197,7 +208,7 @@ sudo systemctl status adguard-shield
### Installation ### Installation
```bash ```bash
sudo /opt/adguard-shield/adguard-shield install-status sudo adguard-shield install-status
``` ```
### Service ### Service
@@ -210,20 +221,20 @@ sudo journalctl -u adguard-shield --no-pager -n 100
### API-Verbindung ### API-Verbindung
```bash ```bash
sudo /opt/adguard-shield/adguard-shield test sudo adguard-shield test
``` ```
### Laufzeitstatus ### Laufzeitstatus
```bash ```bash
sudo /opt/adguard-shield/adguard-shield status sudo adguard-shield status
sudo /opt/adguard-shield/adguard-shield live --once sudo adguard-shield live --once
``` ```
### Firewall ### Firewall
```bash ```bash
sudo /opt/adguard-shield/adguard-shield firewall-status sudo adguard-shield firewall-status
``` ```
--- ---

View File

@@ -131,6 +131,16 @@ func (d *Daemon) Run(ctx context.Context) error {
} }
d.runJob(ctx, "external-whitelist", d.Config.ExternalWhitelistEnabled, time.Duration(d.Config.ExternalWhitelistInterval)*time.Second, d.SyncWhitelist) d.runJob(ctx, "external-whitelist", d.Config.ExternalWhitelistEnabled, time.Duration(d.Config.ExternalWhitelistInterval)*time.Second, d.SyncWhitelist)
d.runJob(ctx, "external-blocklist", d.Config.ExternalBlocklistEnabled, time.Duration(d.Config.ExternalBlocklistInterval)*time.Second, d.SyncBlocklist) d.runJob(ctx, "external-blocklist", d.Config.ExternalBlocklistEnabled, time.Duration(d.Config.ExternalBlocklistInterval)*time.Second, d.SyncBlocklist)
d.runJob(ctx, "ban-expiry", true, 60*time.Second, func(ctx context.Context) error {
expired, err := d.Store.ExpiredBans(time.Now().Unix())
if err != nil {
return err
}
for _, ip := range expired {
_ = d.Unban(ctx, ip, "expired")
}
return nil
})
d.runJob(ctx, "offense-cleanup", d.Config.ProgressiveBanEnabled, time.Hour, func(ctx context.Context) error { d.runJob(ctx, "offense-cleanup", d.Config.ProgressiveBanEnabled, time.Hour, func(ctx context.Context) error {
n, err := d.Store.CleanupOffenses(d.Config.ProgressiveBanResetAfter) n, err := d.Store.CleanupOffenses(d.Config.ProgressiveBanResetAfter)
if n > 0 { if n > 0 {

View File

@@ -26,15 +26,26 @@ type Ban struct {
} }
type ReportStats struct { type ReportStats struct {
Since int64 Since int64
Until int64 Until int64
TotalBans int TotalBans int
TotalUnbans int TotalUnbans int
ActiveBans int UniqueIPs int
TopClients []ReportCount PermanentBans int
Reasons []ReportCount ActiveBans int
Sources []ReportCount AbuseIPDBReports int
RecentEvents []string RateLimitBans int
SubdomainFloodBans int
ExternalBlocklistBans int
BusiestDay string
BusiestDayCount int
TopClients []ReportCount
TopDomains []ReportCount
Protocols []ReportCount
Reasons []ReportCount
Sources []ReportCount
RecentBans []ReportEvent
RecentEvents []string
} }
type ReportCount struct { type ReportCount struct {
@@ -42,6 +53,17 @@ type ReportCount struct {
Count int Count int
} }
type ReportEvent struct {
Timestamp string
Action string
IP string
Domain string
Count string
Duration string
Protocol string
Reason string
}
func Open(path string) (*Store, error) { func Open(path string) (*Store, error) {
db, err := sql.Open("sqlite", path+"?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)") db, err := sql.Open("sqlite", path+"?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)")
if err != nil { if err != nil {
@@ -356,7 +378,7 @@ func (s *Store) ClearGeoIPCache() (int64, error) {
return res.RowsAffected() return res.RowsAffected()
} }
func (s *Store) ReportStats(since, until int64, limit int) (ReportStats, error) { func (s *Store) ReportStats(since, until, busiestSince int64, limit int) (ReportStats, error) {
st := ReportStats{Since: since, Until: until} st := ReportStats{Since: since, Until: until}
if err := s.DB.QueryRow(`SELECT COUNT(*) FROM ban_history WHERE action='BAN' AND timestamp_epoch BETWEEN ? AND ?`, since, until).Scan(&st.TotalBans); err != nil { if err := s.DB.QueryRow(`SELECT COUNT(*) FROM ban_history WHERE action='BAN' AND timestamp_epoch BETWEEN ? AND ?`, since, until).Scan(&st.TotalBans); err != nil {
return st, err return st, err
@@ -364,14 +386,43 @@ func (s *Store) ReportStats(since, until int64, limit int) (ReportStats, error)
if err := s.DB.QueryRow(`SELECT COUNT(*) FROM ban_history WHERE action='UNBAN' AND timestamp_epoch BETWEEN ? AND ?`, since, until).Scan(&st.TotalUnbans); err != nil { if err := s.DB.QueryRow(`SELECT COUNT(*) FROM ban_history WHERE action='UNBAN' AND timestamp_epoch BETWEEN ? AND ?`, since, until).Scan(&st.TotalUnbans); err != nil {
return st, err return st, err
} }
if err := s.DB.QueryRow(`SELECT COUNT(DISTINCT client_ip) FROM ban_history WHERE action='BAN' AND timestamp_epoch BETWEEN ? AND ?`, since, until).Scan(&st.UniqueIPs); err != nil {
return st, err
}
if err := s.DB.QueryRow(`SELECT COUNT(*) FROM ban_history WHERE action='BAN' AND timestamp_epoch BETWEEN ? AND ? AND LOWER(COALESCE(duration,'')) LIKE '%permanent%'`, since, until).Scan(&st.PermanentBans); err != nil {
return st, err
}
if err := s.DB.QueryRow(`SELECT COUNT(*) FROM active_bans`).Scan(&st.ActiveBans); err != nil { if err := s.DB.QueryRow(`SELECT COUNT(*) FROM active_bans`).Scan(&st.ActiveBans); err != nil {
return st, err return st, err
} }
if err := s.DB.QueryRow(`SELECT COUNT(*) FROM ban_history WHERE action='BAN' AND timestamp_epoch BETWEEN ? AND ? AND LOWER(COALESCE(reason,'')) LIKE '%rate%limit%'`, since, until).Scan(&st.RateLimitBans); err != nil {
return st, err
}
if err := s.DB.QueryRow(`SELECT COUNT(*) FROM ban_history WHERE action='BAN' AND timestamp_epoch BETWEEN ? AND ? AND LOWER(COALESCE(reason,'')) LIKE '%subdomain%flood%'`, since, until).Scan(&st.SubdomainFloodBans); err != nil {
return st, err
}
if err := s.DB.QueryRow(`SELECT COUNT(*) FROM ban_history WHERE action='BAN' AND timestamp_epoch BETWEEN ? AND ? AND LOWER(COALESCE(reason,'')) LIKE '%external%blocklist%'`, since, until).Scan(&st.ExternalBlocklistBans); err != nil {
return st, err
}
if busiestSince <= 0 {
busiestSince = since
}
if err := s.DB.QueryRow(`SELECT SUBSTR(timestamp_text, 1, 10), COUNT(*) FROM ban_history WHERE action='BAN' AND timestamp_epoch BETWEEN ? AND ? GROUP BY SUBSTR(timestamp_text, 1, 10) ORDER BY COUNT(*) DESC, SUBSTR(timestamp_text, 1, 10) DESC LIMIT 1`, busiestSince, until).Scan(&st.BusiestDay, &st.BusiestDayCount); err != nil && err != sql.ErrNoRows {
return st, err
}
var err error var err error
st.TopClients, err = s.reportCounts(`SELECT client_ip, COUNT(*) FROM ban_history WHERE action='BAN' AND timestamp_epoch BETWEEN ? AND ? GROUP BY client_ip ORDER BY COUNT(*) DESC, client_ip LIMIT ?`, since, until, limit) st.TopClients, err = s.reportCounts(`SELECT client_ip, COUNT(*) FROM ban_history WHERE action='BAN' AND timestamp_epoch BETWEEN ? AND ? GROUP BY client_ip ORDER BY COUNT(*) DESC, client_ip LIMIT ?`, since, until, limit)
if err != nil { if err != nil {
return st, err return st, err
} }
st.TopDomains, err = s.reportCounts(`SELECT domain, COUNT(*) FROM ban_history WHERE action='BAN' AND timestamp_epoch BETWEEN ? AND ? AND COALESCE(domain,'') NOT IN ('', '-') GROUP BY domain ORDER BY COUNT(*) DESC, domain LIMIT ?`, since, until, limit)
if err != nil {
return st, err
}
st.Protocols, err = s.reportCounts(`SELECT COALESCE(NULLIF(protocol,''), 'unbekannt'), COUNT(*) FROM ban_history WHERE action='BAN' AND timestamp_epoch BETWEEN ? AND ? GROUP BY COALESCE(NULLIF(protocol,''), 'unbekannt') ORDER BY COUNT(*) DESC LIMIT ?`, since, until, limit)
if err != nil {
return st, err
}
st.Reasons, err = s.reportCounts(`SELECT COALESCE(NULLIF(reason,''), 'unknown'), COUNT(*) FROM ban_history WHERE action='BAN' AND timestamp_epoch BETWEEN ? AND ? GROUP BY COALESCE(NULLIF(reason,''), 'unknown') ORDER BY COUNT(*) DESC LIMIT ?`, since, until, limit) st.Reasons, err = s.reportCounts(`SELECT COALESCE(NULLIF(reason,''), 'unknown'), COUNT(*) FROM ban_history WHERE action='BAN' AND timestamp_epoch BETWEEN ? AND ? GROUP BY COALESCE(NULLIF(reason,''), 'unknown') ORDER BY COUNT(*) DESC LIMIT ?`, since, until, limit)
if err != nil { if err != nil {
return st, err return st, err
@@ -380,6 +431,10 @@ func (s *Store) ReportStats(since, until int64, limit int) (ReportStats, error)
if err != nil { if err != nil {
return st, err return st, err
} }
st.RecentBans, err = s.reportRecentBans(since, until, 10)
if err != nil {
return st, err
}
st.RecentEvents, err = s.RecentHistory(limit) st.RecentEvents, err = s.RecentHistory(limit)
return st, err return st, err
} }
@@ -406,3 +461,21 @@ func (s *Store) reportCounts(query string, since, until int64, limit int) ([]Rep
} }
return out, rows.Err() return out, rows.Err()
} }
func (s *Store) reportRecentBans(since, until int64, limit int) ([]ReportEvent, error) {
rows, err := s.DB.Query(`SELECT timestamp_text, action, client_ip, COALESCE(domain,''), COALESCE(count,''), COALESCE(duration,''), COALESCE(protocol,''), COALESCE(reason,'')
FROM ban_history WHERE action='BAN' AND timestamp_epoch BETWEEN ? AND ? ORDER BY id DESC LIMIT ?`, since, until, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var out []ReportEvent
for rows.Next() {
var e ReportEvent
if err := rows.Scan(&e.Timestamp, &e.Action, &e.IP, &e.Domain, &e.Count, &e.Duration, &e.Protocol, &e.Reason); err != nil {
return nil, err
}
out = append(out, e)
}
return out, rows.Err()
}

View File

@@ -12,12 +12,15 @@ import (
"runtime" "runtime"
"sort" "sort"
"strings" "strings"
"adguard-shield/internal/report"
) )
const ( const (
DefaultInstallDir = "/opt/adguard-shield" DefaultInstallDir = "/opt/adguard-shield"
DefaultStateDir = "/var/lib/adguard-shield" DefaultStateDir = "/var/lib/adguard-shield"
DefaultLogFile = "/var/log/adguard-shield.log" DefaultLogFile = "/var/log/adguard-shield.log"
CLICommandPath = "/usr/local/bin/adguard-shield"
ServiceName = "adguard-shield.service" ServiceName = "adguard-shield.service"
ServicePath = "/etc/systemd/system/adguard-shield.service" ServicePath = "/etc/systemd/system/adguard-shield.service"
) )
@@ -28,19 +31,22 @@ type Options struct {
Enable bool Enable bool
SkipDeps bool SkipDeps bool
KeepConfig bool KeepConfig bool
RegisterCLI bool
} }
type Status struct { type Status struct {
InstallDir string InstallDir string
BinaryPath string BinaryPath string
ConfigPath string ConfigPath string
BinaryExists bool CLICommandPath string
ConfigExists bool BinaryExists bool
ServiceExists bool ConfigExists bool
ServiceEnabled bool CLICommandInstalled bool
ServiceActive bool ServiceExists bool
Version string ServiceEnabled bool
LegacyFindings []string ServiceActive bool
Version string
LegacyFindings []string
} }
type LegacyError struct { type LegacyError struct {
@@ -52,30 +58,30 @@ func (e *LegacyError) Error() string {
} }
func DefaultOptions() Options { func DefaultOptions() Options {
return Options{InstallDir: DefaultInstallDir, Enable: true} return Options{InstallDir: DefaultInstallDir, Enable: true, RegisterCLI: true}
} }
func Install(opts Options) error { func Install(opts Options) error {
opts = normalize(opts) opts = normalize(opts)
fmt.Println("AdGuard Shield Go-Installation") fmt.Println("AdGuard Shield Go-Installation")
fmt.Printf("Installationspfad: %s\n", opts.InstallDir) fmt.Printf("Installationspfad: %s\n", opts.InstallDir)
fmt.Println("1/8 Pruefe Betriebssystem und root-Rechte ...") fmt.Println("1/10 Pruefe Betriebssystem und root-Rechte ...")
if err := requireLinuxRoot(); err != nil { if err := requireLinuxRoot(); err != nil {
return err return err
} }
fmt.Println("2/8 Pruefe auf scriptbasierte Altinstallation ...") fmt.Println("2/10 Pruefe auf scriptbasierte Altinstallation ...")
if findings := DetectLegacy(opts.InstallDir); len(findings) > 0 { if findings := DetectLegacy(opts.InstallDir); len(findings) > 0 {
return &LegacyError{Findings: findings} return &LegacyError{Findings: findings}
} }
if !opts.SkipDeps { if !opts.SkipDeps {
fmt.Println("3/8 Pruefe System-Abhaengigkeiten ...") fmt.Println("3/10 Pruefe System-Abhaengigkeiten ...")
if err := ensureDependencies(); err != nil { if err := ensureDependencies(); err != nil {
return err return err
} }
} else { } else {
fmt.Println("3/8 System-Abhaengigkeiten uebersprungen (--skip-deps)") fmt.Println("3/10 System-Abhaengigkeiten uebersprungen (--skip-deps)")
} }
fmt.Println("4/8 Erstelle Verzeichnisse ...") fmt.Println("4/10 Erstelle Verzeichnisse ...")
if err := os.MkdirAll(opts.InstallDir, 0755); err != nil { if err := os.MkdirAll(opts.InstallDir, 0755); err != nil {
return err return err
} }
@@ -85,19 +91,31 @@ func Install(opts Options) error {
if err := os.MkdirAll(filepath.Join(opts.InstallDir, "geoip"), 0755); err != nil { if err := os.MkdirAll(filepath.Join(opts.InstallDir, "geoip"), 0755); err != nil {
return err return err
} }
fmt.Println("5/8 Installiere Binary ...") fmt.Println("5/10 Installiere Binary ...")
if err := copySelf(filepath.Join(opts.InstallDir, "adguard-shield")); err != nil { if err := copySelf(filepath.Join(opts.InstallDir, "adguard-shield")); err != nil {
return err return err
} }
fmt.Println("6/8 Installiere oder migriere Konfiguration ...") if opts.RegisterCLI {
fmt.Printf("6/10 Registriere CLI-Befehl (%s) ...\n", CLICommandPath)
if err := registerCLICommand(opts.InstallDir); err != nil {
return err
}
} else {
fmt.Println("6/10 CLI-Registrierung uebersprungen (--no-register)")
}
fmt.Println("7/10 Installiere Report-Templates ...")
if err := report.InstallTemplates(filepath.Join(opts.InstallDir, "templates")); err != nil {
return err
}
fmt.Println("8/10 Installiere oder migriere Konfiguration ...")
if err := ensureConfig(opts); err != nil { if err := ensureConfig(opts); err != nil {
return err return err
} }
fmt.Println("7/8 Schreibe systemd-Service ...") fmt.Println("9/10 Schreibe systemd-Service ...")
if err := writeService(opts.InstallDir); err != nil { if err := writeService(opts.InstallDir); err != nil {
return err return err
} }
fmt.Println("8/8 Aktualisiere systemd ...") fmt.Println("10/10 Aktualisiere systemd ...")
_ = run("systemctl", "daemon-reload") _ = run("systemctl", "daemon-reload")
if opts.Enable { if opts.Enable {
fmt.Println("Aktiviere Autostart ...") fmt.Println("Aktiviere Autostart ...")
@@ -138,6 +156,7 @@ func Uninstall(opts Options) error {
} }
_ = os.Remove(ServicePath) _ = os.Remove(ServicePath)
_ = run("systemctl", "daemon-reload") _ = run("systemctl", "daemon-reload")
_ = unregisterCLICommand(opts.InstallDir)
if opts.KeepConfig { if opts.KeepConfig {
for _, p := range []string{ for _, p := range []string{
filepath.Join(opts.InstallDir, "adguard-shield"), filepath.Join(opts.InstallDir, "adguard-shield"),
@@ -160,13 +179,15 @@ func GetStatus(installDir string) Status {
bin := filepath.Join(installDir, "adguard-shield") bin := filepath.Join(installDir, "adguard-shield")
conf := filepath.Join(installDir, "adguard-shield.conf") conf := filepath.Join(installDir, "adguard-shield.conf")
st := Status{ st := Status{
InstallDir: installDir, InstallDir: installDir,
BinaryPath: bin, BinaryPath: bin,
ConfigPath: conf, ConfigPath: conf,
BinaryExists: fileExists(bin), CLICommandPath: CLICommandPath,
ConfigExists: fileExists(conf), BinaryExists: fileExists(bin),
ServiceExists: fileExists(ServicePath), ConfigExists: fileExists(conf),
LegacyFindings: DetectLegacy(installDir), CLICommandInstalled: cliCommandInstalled(installDir),
ServiceExists: fileExists(ServicePath),
LegacyFindings: DetectLegacy(installDir),
} }
if st.BinaryExists { if st.BinaryExists {
if out, err := exec.Command(bin, "version").Output(); err == nil { if out, err := exec.Command(bin, "version").Output(); err == nil {
@@ -247,6 +268,7 @@ func PrintStatus(st Status) string {
if st.Version != "" { if st.Version != "" {
b.WriteString(fmt.Sprintf("Version: %s\n", st.Version)) b.WriteString(fmt.Sprintf("Version: %s\n", st.Version))
} }
b.WriteString(fmt.Sprintf("CLI-Befehl (%s): %s\n", st.CLICommandPath, yesNo(st.CLICommandInstalled)))
b.WriteString(fmt.Sprintf("Konfiguration: %s\n", yesNo(st.ConfigExists))) b.WriteString(fmt.Sprintf("Konfiguration: %s\n", yesNo(st.ConfigExists)))
b.WriteString(fmt.Sprintf("systemd Service: %s\n", yesNo(st.ServiceExists))) b.WriteString(fmt.Sprintf("systemd Service: %s\n", yesNo(st.ServiceExists)))
b.WriteString(fmt.Sprintf("Autostart: %s\n", yesNo(st.ServiceEnabled))) b.WriteString(fmt.Sprintf("Autostart: %s\n", yesNo(st.ServiceEnabled)))
@@ -385,6 +407,62 @@ func sameFile(a, b string) bool {
return errA == nil && errB == nil && os.SameFile(ai, bi) return errA == nil && errB == nil && os.SameFile(ai, bi)
} }
func registerCLICommand(installDir string) error {
target := filepath.Join(installDir, "adguard-shield")
if err := os.MkdirAll(filepath.Dir(CLICommandPath), 0755); err != nil {
return err
}
if existingTarget, err := os.Readlink(CLICommandPath); err == nil {
resolved := resolveLinkTarget(CLICommandPath, existingTarget)
if filepath.Clean(resolved) == filepath.Clean(target) {
return nil
}
return fmt.Errorf("%s existiert bereits und verweist auf %s", CLICommandPath, existingTarget)
} else if !os.IsNotExist(err) {
return err
}
if fileExists(CLICommandPath) {
return fmt.Errorf("%s existiert bereits und ist kein Symlink", CLICommandPath)
}
return os.Symlink(target, CLICommandPath)
}
func unregisterCLICommand(installDir string) error {
existingTarget, err := os.Readlink(CLICommandPath)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return nil
}
target := filepath.Join(installDir, "adguard-shield")
resolved := resolveLinkTarget(CLICommandPath, existingTarget)
if filepath.Clean(resolved) != filepath.Clean(target) {
return nil
}
return os.Remove(CLICommandPath)
}
func cliCommandInstalled(installDir string) bool {
existingTarget, err := os.Readlink(CLICommandPath)
if err != nil {
return false
}
target := filepath.Join(installDir, "adguard-shield")
if !fileExists(target) {
return false
}
resolved := resolveLinkTarget(CLICommandPath, existingTarget)
return filepath.Clean(resolved) == filepath.Clean(target)
}
func resolveLinkTarget(linkPath, target string) string {
if filepath.IsAbs(target) {
return target
}
return filepath.Join(filepath.Dir(linkPath), target)
}
func ensureConfig(opts Options) error { func ensureConfig(opts Options) error {
target := filepath.Join(opts.InstallDir, "adguard-shield.conf") target := filepath.Join(opts.InstallDir, "adguard-shield.conf")
defaults := []byte(defaultConfig) defaults := []byte(defaultConfig)

View File

@@ -1,10 +1,15 @@
package report package report
import ( import (
"bytes" "bufio"
"context" "context"
"embed"
"encoding/json"
"fmt" "fmt"
"html" "html"
"io/fs"
"mime"
"net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -12,12 +17,16 @@ import (
"strings" "strings"
"time" "time"
"adguard-shield/internal/appinfo"
"adguard-shield/internal/config" "adguard-shield/internal/config"
"adguard-shield/internal/db" "adguard-shield/internal/db"
) )
//go:embed templates/report.html templates/report.txt
var embeddedTemplates embed.FS
type Store interface { type Store interface {
ReportStats(since, until int64, limit int) (db.ReportStats, error) ReportStats(since, until, busiestSince int64, limit int) (db.ReportStats, error)
} }
const cronPath = "/etc/cron.d/adguard-shield-report" const cronPath = "/etc/cron.d/adguard-shield-report"
@@ -27,50 +36,90 @@ func Status(c *config.Config) string {
if _, err := os.Stat(cronPath); err == nil { if _, err := os.Stat(cronPath); err == nil {
cron = "installiert (" + cronPath + ")" cron = "installiert (" + cronPath + ")"
} }
return fmt.Sprintf(`E-Mail Report mailer := "nicht gefunden"
Aktiv: %v if parts := strings.Fields(c.ReportMailCmd); len(parts) > 0 {
Intervall: %s if p, err := exec.LookPath(parts[0]); err == nil {
Zeit: %s mailer = "gefunden (" + p + ")"
Empfaenger: %s }
Absender: %s }
Format: %s return fmt.Sprintf(`AdGuard Shield - Report Status
Mail-Befehl: %s
Cron: %s Report aktiviert: %v
`, c.ReportEnabled, c.ReportInterval, c.ReportTime, c.ReportEmailTo, c.ReportEmailFrom, c.ReportFormat, c.ReportMailCmd, cron) Intervall: %s
Uhrzeit: %s
Format: %s
Empfaenger: %s
Absender: %s
Mail-Befehl: %s
Mail-Befehl Status: %s
Aktivster Tag: letzte %d Tage
Cron: %s
`, c.ReportEnabled, c.ReportInterval, c.ReportTime, c.ReportFormat, empty(c.ReportEmailTo, "nicht konfiguriert"), c.ReportEmailFrom, c.ReportMailCmd, mailer, c.ReportBusiestDayRange, cron)
}
func InstallTemplates(dir string) error {
if dir == "" {
return fmt.Errorf("Template-Zielverzeichnis ist leer")
}
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
for _, name := range []string{"report.html", "report.txt"} {
data, err := embeddedTemplates.ReadFile("templates/" + name)
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join(dir, name), data, 0644); err != nil {
return err
}
}
return nil
} }
func Generate(c *config.Config, st Store, format string) (string, error) { func Generate(c *config.Config, st Store, format string) (string, error) {
if format == "" { format = normalizeFormat(format, c.ReportFormat)
format = c.ReportFormat since, until, period := reportWindow(c.ReportInterval, time.Now())
} busiestSince := busiestWindowStart(c, since)
since, until := window(c.ReportInterval) stats, err := st.ReportStats(since, until, busiestSince, 20)
stats, err := st.ReportStats(since, until, 20)
if err != nil { if err != nil {
return "", err return "", err
} }
if strings.EqualFold(format, "html") { stats.AbuseIPDBReports = countAbuseReports(c.LogFile, since, until)
return renderHTML(c, stats), nil
tpl, err := loadTemplate(c, format)
if err != nil {
return "", err
} }
return renderText(c, stats), nil values, err := templateValues(c, st, stats, period, format)
if err != nil {
return "", err
}
for key, value := range values {
tpl = strings.ReplaceAll(tpl, "{{"+key+"}}", value)
}
return tpl, nil
} }
func Send(ctx context.Context, c *config.Config, st Store) error { func Send(ctx context.Context, c *config.Config, st Store) error {
body, err := Generate(c, st, c.ReportFormat) format := normalizeFormat(c.ReportFormat, "html")
body, err := Generate(c, st, format)
if err != nil { if err != nil {
return err return err
} }
return sendMail(ctx, c, "AdGuard Shield Report", body) _, _, period := reportWindow(c.ReportInterval, time.Now())
return sendMail(ctx, c, "AdGuard Shield "+period+" - "+hostname(), body, format, "AdGuard Shield Report Generator")
} }
func SendTest(ctx context.Context, c *config.Config) error { func SendTest(ctx context.Context, c *config.Config) error {
body := fmt.Sprintf("AdGuard Shield Test-Mail\n\nHostname: %s\nZeitpunkt: %s\nEmpfaenger: %s\nAbsender: %s\n", hostname(), time.Now().Format("2006-01-02 15:04:05"), c.ReportEmailTo, c.ReportEmailFrom) format := normalizeFormat(c.ReportFormat, "html")
if strings.EqualFold(c.ReportFormat, "html") { body := testBody(c, format)
body = "<!doctype html><html><body><h1>AdGuard Shield Test-Mail</h1><p>Hostname: " + html.EscapeString(hostname()) + "</p><p>Zeitpunkt: " + html.EscapeString(time.Now().Format("2006-01-02 15:04:05")) + "</p></body></html>" return sendMail(ctx, c, "AdGuard Shield Test-Mail - "+hostname(), body, format, "AdGuard Shield Report Generator (Test)")
}
return sendMail(ctx, c, "AdGuard Shield Test-Mail", body)
} }
func InstallCron(binary, configPath string, c *config.Config) error { func InstallCron(binary, configPath string, c *config.Config) error {
if !c.ReportEnabled {
return fmt.Errorf("Report ist deaktiviert (REPORT_ENABLED=false)")
}
minute, hour, err := parseReportTime(c.ReportTime) minute, hour, err := parseReportTime(c.ReportTime)
if err != nil { if err != nil {
return err return err
@@ -82,7 +131,18 @@ func InstallCron(binary, configPath string, c *config.Config) error {
if configPath == "" { if configPath == "" {
configPath = "/opt/adguard-shield/adguard-shield.conf" configPath = "/opt/adguard-shield/adguard-shield.conf"
} }
line := fmt.Sprintf("SHELL=/bin/sh\nPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\n%s root %s -config %s report-send\n", schedule, binary, configPath) command := shellQuote(binary) + " -config " + shellQuote(configPath) + " report-send"
if strings.EqualFold(c.ReportInterval, "biweekly") {
command = "[ $(( $(date +\\%V) \\% 2 )) -eq 1 ] && " + command
}
line := fmt.Sprintf(`# AdGuard Shield - Automatischer Report
# Intervall: %s
# Uhrzeit: %s
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
%s root %s >> %s 2>&1
`, c.ReportInterval, c.ReportTime, schedule, command, shellQuote(c.LogFile))
return os.WriteFile(cronPath, []byte(line), 0644) return os.WriteFile(cronPath, []byte(line), 0644)
} }
@@ -93,25 +153,32 @@ func RemoveCron() error {
return nil return nil
} }
func sendMail(ctx context.Context, c *config.Config, subject, body string) error { func sendMail(ctx context.Context, c *config.Config, subject, body, format, xMailer string) error {
if c.ReportEmailTo == "" { if strings.TrimSpace(c.ReportEmailTo) == "" {
return fmt.Errorf("REPORT_EMAIL_TO ist leer") return fmt.Errorf("REPORT_EMAIL_TO ist leer")
} }
if c.ReportMailCmd == "" {
return fmt.Errorf("REPORT_MAIL_CMD ist leer")
}
contentType := "text/plain; charset=utf-8"
if strings.EqualFold(c.ReportFormat, "html") {
contentType = "text/html; charset=utf-8"
}
msg := "From: " + c.ReportEmailFrom + "\n" +
"To: " + c.ReportEmailTo + "\n" +
"Subject: " + subject + "\n" +
"Content-Type: " + contentType + "\n\n" + body
parts := strings.Fields(c.ReportMailCmd) parts := strings.Fields(c.ReportMailCmd)
if len(parts) == 0 { if len(parts) == 0 {
return fmt.Errorf("REPORT_MAIL_CMD ist leer") return fmt.Errorf("REPORT_MAIL_CMD ist leer")
} }
if _, err := exec.LookPath(parts[0]); err != nil {
return fmt.Errorf("Mail-Befehl nicht gefunden: %s", parts[0])
}
contentType := "text/plain; charset=UTF-8"
if strings.EqualFold(format, "html") {
contentType = "text/html; charset=UTF-8"
}
msg := strings.Join([]string{
"From: " + c.ReportEmailFrom,
"To: " + c.ReportEmailTo,
"Subject: " + mime.QEncoding.Encode("utf-8", subject),
"MIME-Version: 1.0",
"Content-Type: " + contentType,
"Content-Transfer-Encoding: 8bit",
"X-Mailer: " + xMailer,
"",
body,
}, "\n")
args := append(parts[1:], "-t") args := append(parts[1:], "-t")
cmd := exec.CommandContext(ctx, parts[0], args...) cmd := exec.CommandContext(ctx, parts[0], args...)
cmd.Stdin = strings.NewReader(msg) cmd.Stdin = strings.NewReader(msg)
@@ -120,6 +187,273 @@ func sendMail(ctx context.Context, c *config.Config, subject, body string) error
return cmd.Run() return cmd.Run()
} }
func templateValues(c *config.Config, st Store, stats db.ReportStats, period, format string) (map[string]string, error) {
updateHTML, updateText := checkForUpdate()
busiestDay := "-"
if stats.BusiestDay != "" {
if t, err := time.ParseInLocation("2006-01-02", stats.BusiestDay, time.Local); err == nil {
busiestDay = t.Format("02.01.2006") + " (" + strconv.Itoa(stats.BusiestDayCount) + ")"
} else {
busiestDay = stats.BusiestDay + " (" + strconv.Itoa(stats.BusiestDayCount) + ")"
}
}
busiestLabel := "Aktivster Tag"
if c.ReportBusiestDayRange > 0 {
busiestLabel = fmt.Sprintf("Aktivster Tag (%d Tage)", c.ReportBusiestDayRange)
}
values := map[string]string{
"REPORT_PERIOD": period,
"REPORT_DATE": time.Now().Format("02.01.2006 15:04:05"),
"HOSTNAME": hostname(),
"VERSION": appinfo.Version,
"TOTAL_BANS": strconv.Itoa(stats.TotalBans),
"TOTAL_UNBANS": strconv.Itoa(stats.TotalUnbans),
"UNIQUE_IPS": strconv.Itoa(stats.UniqueIPs),
"PERMANENT_BANS": strconv.Itoa(stats.PermanentBans),
"ACTIVE_BANS": strconv.Itoa(stats.ActiveBans),
"ABUSEIPDB_REPORTS": strconv.Itoa(stats.AbuseIPDBReports),
"RATELIMIT_BANS": strconv.Itoa(stats.RateLimitBans),
"SUBDOMAIN_FLOOD_BANS": strconv.Itoa(stats.SubdomainFloodBans),
"EXTERNAL_BLOCKLIST_BANS": strconv.Itoa(stats.ExternalBlocklistBans),
"BUSIEST_DAY": busiestDay,
"BUSIEST_DAY_LABEL": busiestLabel,
"TOP10_IPS_TABLE": topCountsHTML(stats.TopClients, "IP-Adresse"),
"TOP10_DOMAINS_TABLE": topCountsHTML(stats.TopDomains, "Domain"),
"PROTOCOL_TABLE": protocolHTML(stats.Protocols),
"RECENT_BANS_TABLE": recentBansHTML(stats.RecentBans),
"TOP10_IPS_TEXT": topCountsText(stats.TopClients, "IP-Adresse"),
"TOP10_DOMAINS_TEXT": topCountsText(stats.TopDomains, "Domain"),
"PROTOCOL_TEXT": protocolText(stats.Protocols),
"RECENT_BANS_TEXT": recentBansText(stats.RecentBans),
"UPDATE_NOTICE": updateHTML,
"UPDATE_NOTICE_TXT": updateText,
"PERIOD_OVERVIEW_TABLE": "",
"PERIOD_OVERVIEW_TEXT": "",
}
if strings.EqualFold(format, "html") {
values["PERIOD_OVERVIEW_TABLE"] = periodOverviewHTML(st)
} else {
values["PERIOD_OVERVIEW_TEXT"] = periodOverviewText(st)
}
return values, nil
}
func topCountsHTML(rows []db.ReportCount, nameHeader string) string {
if len(rows) == 0 {
return `<div class="no-data">Keine Daten im Berichtszeitraum</div>`
}
maxCount := rows[0].Count
var b strings.Builder
b.WriteString("<table><tr><th>#</th><th>" + html.EscapeString(nameHeader) + "</th><th>Sperren</th></tr>")
for i, r := range rows {
width := 100
if maxCount > 0 {
width = r.Count * 100 / maxCount
}
class := ""
if i < 3 {
class = " top3"
}
cellClass := ""
if strings.Contains(strings.ToLower(nameHeader), "ip") {
cellClass = ` class="ip-cell"`
}
fmt.Fprintf(&b, `<tr><td><span class="rank%s">%d</span></td><td%s>%s</td><td><div class="bar-container"><div class="bar" style="width:%d%%"></div><span class="bar-value">%d</span></div></td></tr>`, class, i+1, cellClass, html.EscapeString(r.Name), width, r.Count)
}
b.WriteString("</table>")
return b.String()
}
func protocolHTML(rows []db.ReportCount) string {
if len(rows) == 0 {
return `<div class="no-data">Keine Daten im Berichtszeitraum</div>`
}
var b strings.Builder
b.WriteString("<table><tr><th>Protokoll</th><th>Anzahl Sperren</th></tr>")
for _, r := range rows {
class := protocolClass(r.Name)
fmt.Fprintf(&b, `<tr><td><span class="protocol-badge %s">%s</span></td><td>%d</td></tr>`, class, html.EscapeString(r.Name), r.Count)
}
b.WriteString("</table>")
return b.String()
}
func recentBansHTML(rows []db.ReportEvent) string {
if len(rows) == 0 {
return `<div class="no-data">Keine Sperren im Berichtszeitraum</div>`
}
var b strings.Builder
b.WriteString("<table><tr><th>Zeitpunkt</th><th>IP</th><th>Domain</th><th>Grund</th></tr>")
for _, e := range rows {
reason := fallback(e.Reason, "rate-limit")
domain := fallbackDash(e.Domain)
fmt.Fprintf(&b, `<tr><td>%s</td><td class="ip-cell">%s</td><td>%s</td><td><span class="reason-badge %s">%s</span></td></tr>`, html.EscapeString(shortTime(e.Timestamp)), html.EscapeString(e.IP), html.EscapeString(domain), reasonClass(reason), html.EscapeString(reason))
}
b.WriteString("</table>")
return b.String()
}
func periodOverviewHTML(st Store) string {
rows := periodOverviewRows(st)
var b strings.Builder
b.WriteString("<table><tr><th>Zeitraum</th><th>Sperren</th><th>Entsperrt</th><th>Unique IPs</th><th>Dauerhaft gebannt</th></tr>")
for _, r := range rows {
class := ""
if r.Label == "Heute" {
class = ` class="period-today"`
} else if r.Label == "Gestern" {
class = ` class="period-gestern"`
}
fmt.Fprintf(&b, `<tr%s><td><strong>%s</strong></td><td>%d</td><td>%d</td><td>%d</td><td>%d</td></tr>`, class, html.EscapeString(r.Label), r.TotalBans, r.TotalUnbans, r.UniqueIPs, r.PermanentBans)
}
b.WriteString("</table>")
return b.String()
}
func topCountsText(rows []db.ReportCount, nameHeader string) string {
if len(rows) == 0 {
return " Keine Daten im Berichtszeitraum"
}
var b strings.Builder
fmt.Fprintf(&b, " %-4s %-42s %s\n", "#", nameHeader, "Sperren")
fmt.Fprintf(&b, " %-4s %-42s %s\n", "--", strings.Repeat("-", 42), "-------")
for i, r := range rows {
fmt.Fprintf(&b, " %-4s %-42s %d\n", strconv.Itoa(i+1)+".", r.Name, r.Count)
}
return strings.TrimRight(b.String(), "\n")
}
func protocolText(rows []db.ReportCount) string {
if len(rows) == 0 {
return " Keine Daten im Berichtszeitraum"
}
var b strings.Builder
fmt.Fprintf(&b, " %-20s %s\n", "Protokoll", "Anzahl")
fmt.Fprintf(&b, " %-20s %s\n", strings.Repeat("-", 20), "------")
for _, r := range rows {
fmt.Fprintf(&b, " %-20s %d\n", r.Name, r.Count)
}
return strings.TrimRight(b.String(), "\n")
}
func recentBansText(rows []db.ReportEvent) string {
if len(rows) == 0 {
return " Keine Sperren im Berichtszeitraum"
}
var b strings.Builder
fmt.Fprintf(&b, " %-17s %-42s %-30s %s\n", "Zeitpunkt", "IP", "Domain", "Grund")
fmt.Fprintf(&b, " %-17s %-42s %-30s %s\n", strings.Repeat("-", 17), strings.Repeat("-", 42), strings.Repeat("-", 30), "----------")
for _, e := range rows {
fmt.Fprintf(&b, " %-17s %-42s %-30s %s\n", shortTime(e.Timestamp), e.IP, fallbackDash(e.Domain), fallback(e.Reason, "rate-limit"))
}
return strings.TrimRight(b.String(), "\n")
}
func periodOverviewText(st Store) string {
rows := periodOverviewRows(st)
var b strings.Builder
fmt.Fprintf(&b, " %-15s %-9s %-12s %-14s %-11s\n", "Zeitraum", "Sperren", "Entsperrt", "Unique IPs", "Dauerhaft")
fmt.Fprintf(&b, " %-15s %-9s %-12s %-14s %-11s\n", strings.Repeat("-", 15), strings.Repeat("-", 9), strings.Repeat("-", 12), strings.Repeat("-", 14), strings.Repeat("-", 11))
for _, r := range rows {
fmt.Fprintf(&b, " %-15s %-9d %-12d %-14d %-11d\n", r.Label, r.TotalBans, r.TotalUnbans, r.UniqueIPs, r.PermanentBans)
}
return strings.TrimRight(b.String(), "\n")
}
type overviewRow struct {
Label string
db.ReportStats
}
func periodOverviewRows(st Store) []overviewRow {
now := time.Now()
today := midnight(now)
defs := []struct {
label string
since time.Time
until time.Time
}{}
if now.Hour() >= 20 {
defs = append(defs, struct {
label string
since time.Time
until time.Time
}{"Heute", today, now})
}
defs = append(defs,
struct {
label string
since time.Time
until time.Time
}{"Gestern", today.AddDate(0, 0, -1), today.Add(-time.Second)},
struct {
label string
since time.Time
until time.Time
}{"Letzte 7 Tage", today.AddDate(0, 0, -7), now},
struct {
label string
since time.Time
until time.Time
}{"Letzte 14 Tage", today.AddDate(0, 0, -14), now},
struct {
label string
since time.Time
until time.Time
}{"Letzte 30 Tage", today.AddDate(0, 0, -30), now},
)
rows := make([]overviewRow, 0, len(defs))
for _, d := range defs {
stats, err := st.ReportStats(d.since.Unix(), d.until.Unix(), d.since.Unix(), 0)
if err != nil {
stats = db.ReportStats{}
}
rows = append(rows, overviewRow{Label: d.label, ReportStats: stats})
}
return rows
}
func loadTemplate(c *config.Config, format string) (string, error) {
name := "report." + format
for _, dir := range templateDirs(c) {
data, err := os.ReadFile(filepath.Join(dir, name))
if err == nil {
return string(data), nil
}
}
data, err := fs.ReadFile(embeddedTemplates, "templates/"+name)
if err != nil {
return "", fmt.Errorf("Report-Template nicht gefunden: %s", name)
}
return string(data), nil
}
func templateDirs(c *config.Config) []string {
var dirs []string
if v := strings.TrimSpace(os.Getenv("ADGUARD_SHIELD_TEMPLATE_DIR")); v != "" {
dirs = append(dirs, v)
}
if c.Path != "" {
dirs = append(dirs, filepath.Join(filepath.Dir(c.Path), "templates"))
}
if wd, err := os.Getwd(); err == nil {
dirs = append(dirs, filepath.Join(wd, "templates"))
}
if exe, err := os.Executable(); err == nil {
dirs = append(dirs, filepath.Join(filepath.Dir(exe), "templates"))
}
seen := map[string]bool{}
out := dirs[:0]
for _, d := range dirs {
if d != "" && !seen[d] {
seen[d] = true
out = append(out, d)
}
}
return out
}
func parseReportTime(value string) (string, string, error) { func parseReportTime(value string) (string, string, error) {
parts := strings.Split(value, ":") parts := strings.Split(value, ":")
if len(parts) != 2 { if len(parts) != 2 {
@@ -140,8 +474,8 @@ func cronSchedule(interval, minute, hour string) string {
switch strings.ToLower(interval) { switch strings.ToLower(interval) {
case "daily": case "daily":
return fmt.Sprintf("%s %s * * *", minute, hour) return fmt.Sprintf("%s %s * * *", minute, hour)
case "biweekly": case "biweekly", "weekly":
return fmt.Sprintf("%s %s 1,15 * *", minute, hour) return fmt.Sprintf("%s %s * * 1", minute, hour)
case "monthly": case "monthly":
return fmt.Sprintf("%s %s 1 * *", minute, hour) return fmt.Sprintf("%s %s 1 * *", minute, hour)
default: default:
@@ -149,88 +483,213 @@ func cronSchedule(interval, minute, hour string) string {
} }
} }
func window(interval string) (int64, int64) { func reportWindow(interval string, now time.Time) (int64, int64, string) {
now := time.Now() today := midnight(now)
days := 7 days, label := 7, "Bericht"
switch strings.ToLower(interval) { switch strings.ToLower(interval) {
case "daily": case "daily":
days = 1 days, label = 1, "Tagesbericht"
case "weekly":
days, label = 7, "Wochenbericht"
case "biweekly": case "biweekly":
days = 14 days, label = 14, "Zweiwochenbericht"
case "monthly": case "monthly":
days = 30 days, label = 30, "Monatsbericht"
} }
return now.AddDate(0, 0, -days).Unix(), now.Unix() start := today.AddDate(0, 0, -days)
end := today.Add(-time.Second)
if strings.EqualFold(interval, "daily") {
return start.Unix(), end.Unix(), label + ": " + start.Format("02.01.2006")
}
return start.Unix(), end.Unix(), label + ": " + start.Format("02.01.2006") + " - " + end.Format("02.01.2006")
} }
func renderText(c *config.Config, st db.ReportStats) string { func busiestWindowStart(c *config.Config, fallbackSince int64) int64 {
var b strings.Builder if c.ReportBusiestDayRange <= 0 {
b.WriteString("AdGuard Shield Report\n") return fallbackSince
b.WriteString("Zeitraum: " + formatTime(st.Since) + " bis " + formatTime(st.Until) + "\n\n") }
b.WriteString("Bans: " + strconv.Itoa(st.TotalBans) + "\n") return midnight(time.Now()).AddDate(0, 0, -c.ReportBusiestDayRange).Unix()
b.WriteString("Unbans: " + strconv.Itoa(st.TotalUnbans) + "\n") }
b.WriteString("Aktive Sperren: " + strconv.Itoa(st.ActiveBans) + "\n\n")
writeCountsText(&b, "Top Clients", st.TopClients) func midnight(t time.Time) time.Time {
writeCountsText(&b, "Gruende", st.Reasons) y, m, d := t.Date()
writeCountsText(&b, "Aktive Quellen", st.Sources) return time.Date(y, m, d, 0, 0, 0, 0, t.Location())
if len(st.RecentEvents) > 0 { }
b.WriteString("Letzte Ereignisse\n")
for _, e := range st.RecentEvents { func countAbuseReports(path string, since, until int64) int {
b.WriteString("- " + e + "\n") f, err := os.Open(path)
if err != nil {
return 0
}
defer f.Close()
count := 0
sc := bufio.NewScanner(f)
for sc.Scan() {
line := sc.Text()
if !strings.Contains(line, "AbuseIPDB:") || !strings.Contains(line, "erfolgreich gemeldet") || len(line) < 21 {
continue
}
ts := strings.TrimPrefix(line[:20], "[")
t, err := time.ParseInLocation("2006-01-02 15:04:05", ts, time.Local)
if err == nil && t.Unix() >= since && t.Unix() <= until {
count++
} }
} }
_ = c return count
return b.String()
} }
func renderHTML(c *config.Config, st db.ReportStats) string { func checkForUpdate() (string, string) {
var b bytes.Buffer if appinfo.Version == "" || appinfo.Version == "unknown" || strings.EqualFold(os.Getenv("ADGUARD_SHIELD_SKIP_UPDATE_CHECK"), "true") {
b.WriteString("<!doctype html><html><head><meta charset=\"utf-8\"><title>AdGuard Shield Report</title>") return "", ""
b.WriteString("<style>body{font-family:Arial,sans-serif;color:#1f2937}table{border-collapse:collapse;margin:12px 0}td,th{border:1px solid #d1d5db;padding:6px 9px;text-align:left}th{background:#f3f4f6}</style>") }
b.WriteString("</head><body>") client := http.Client{Timeout: 5 * time.Second}
b.WriteString("<h1>AdGuard Shield Report</h1>") resp, err := client.Get("https://git.techniverse.net/api/v1/repos/scriptos/adguard-shield/releases?limit=1&page=1")
b.WriteString("<p>Zeitraum: " + html.EscapeString(formatTime(st.Since)) + " bis " + html.EscapeString(formatTime(st.Until)) + "</p>") if err != nil {
b.WriteString("<ul><li>Bans: " + strconv.Itoa(st.TotalBans) + "</li><li>Unbans: " + strconv.Itoa(st.TotalUnbans) + "</li><li>Aktive Sperren: " + strconv.Itoa(st.ActiveBans) + "</li></ul>") return "", ""
writeCountsHTML(&b, "Top Clients", st.TopClients) }
writeCountsHTML(&b, "Gruende", st.Reasons) defer resp.Body.Close()
writeCountsHTML(&b, "Aktive Quellen", st.Sources) if resp.StatusCode < 200 || resp.StatusCode > 299 {
if len(st.RecentEvents) > 0 { return "", ""
b.WriteString("<h2>Letzte Ereignisse</h2><table><tr><th>Ereignis</th></tr>") }
for _, e := range st.RecentEvents { var releases []struct {
b.WriteString("<tr><td>" + html.EscapeString(e) + "</td></tr>") TagName string `json:"tag_name"`
}
if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil || len(releases) == 0 {
return "", ""
}
latest := releases[0].TagName
if !versionGreater(latest, appinfo.Version) {
return "", ""
}
htmlNotice := `<div class="update-notice">Update verfuegbar: <strong>` + html.EscapeString(latest) + `</strong> · <a href="https://git.techniverse.net/scriptos/adguard-shield/releases">Jetzt aktualisieren</a></div>`
textNotice := " Neue Version verfuegbar: " + latest + "\n Update: https://git.techniverse.net/scriptos/adguard-shield/releases\n"
return htmlNotice, textNotice
}
func versionGreater(a, b string) bool {
ap := versionParts(a)
bp := versionParts(b)
max := len(ap)
if len(bp) > max {
max = len(bp)
}
for i := 0; i < max; i++ {
ai, bi := 0, 0
if i < len(ap) {
ai = ap[i]
}
if i < len(bp) {
bi = bp[i]
}
if ai != bi {
return ai > bi
} }
b.WriteString("</table>")
} }
b.WriteString("</body></html>") return false
_ = c
return b.String()
} }
func writeCountsText(b *strings.Builder, title string, rows []db.ReportCount) { func versionParts(v string) []int {
b.WriteString(title + "\n") v = strings.TrimPrefix(strings.TrimSpace(v), "v")
if len(rows) == 0 { fields := strings.FieldsFunc(v, func(r rune) bool { return r == '.' || r == '-' || r == '+' })
b.WriteString("- keine Daten\n\n") out := make([]int, 0, len(fields))
return for _, f := range fields {
n, err := strconv.Atoi(f)
if err != nil {
break
}
out = append(out, n)
} }
for _, r := range rows { return out
b.WriteString("- " + r.Name + ": " + strconv.Itoa(r.Count) + "\n")
}
b.WriteByte('\n')
} }
func writeCountsHTML(b *bytes.Buffer, title string, rows []db.ReportCount) { func testBody(c *config.Config, format string) string {
b.WriteString("<h2>" + html.EscapeString(title) + "</h2><table><tr><th>Name</th><th>Anzahl</th></tr>") now := time.Now().Format("02.01.2006 15:04:05")
if len(rows) == 0 { host := hostname()
b.WriteString("<tr><td colspan=\"2\">keine Daten</td></tr>") if strings.EqualFold(format, "html") {
return `<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8"></head><body style="font-family:sans-serif;background:#f0f2f5;padding:30px;"><div style="max-width:600px;margin:0 auto;background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 2px 12px rgba(0,0,0,0.08);"><div style="background:#0f3460;color:#fff;padding:30px;text-align:center;"><h1 style="margin:0;">AdGuard Shield Test-Mail</h1><p style="margin:6px 0 0;color:#ccd6f6;">E-Mail-Versand funktioniert</p></div><div style="padding:30px;"><table style="width:100%;border-collapse:collapse;"><tr><td>Hostname</td><td><strong>` + html.EscapeString(host) + `</strong></td></tr><tr><td>Zeitpunkt</td><td><strong>` + html.EscapeString(now) + `</strong></td></tr><tr><td>Empfaenger</td><td><strong>` + html.EscapeString(c.ReportEmailTo) + `</strong></td></tr><tr><td>Absender</td><td><strong>` + html.EscapeString(c.ReportEmailFrom) + `</strong></td></tr><tr><td>Mail-Befehl</td><td><strong>` + html.EscapeString(c.ReportMailCmd) + `</strong></td></tr><tr><td>Format</td><td><strong>` + html.EscapeString(format) + `</strong></td></tr></table></div></div></body></html>`
} }
for _, r := range rows { return fmt.Sprintf(`AdGuard Shield - Test-Mail
b.WriteString("<tr><td>" + html.EscapeString(r.Name) + "</td><td>" + strconv.Itoa(r.Count) + "</td></tr>")
} E-Mail-Versand funktioniert.
b.WriteString("</table>")
Hostname: %s
Zeitpunkt: %s
Empfaenger: %s
Absender: %s
Mail-Befehl: %s
Format: %s
`, host, now, c.ReportEmailTo, c.ReportEmailFrom, c.ReportMailCmd, format)
} }
func formatTime(epoch int64) string { func normalizeFormat(value, fallback string) string {
return time.Unix(epoch, 0).Format("2006-01-02 15:04:05") format := strings.ToLower(strings.TrimSpace(value))
if format == "" {
format = strings.ToLower(strings.TrimSpace(fallback))
}
if format != "txt" {
format = "html"
}
return format
}
func shortTime(value string) string {
if len(value) >= 16 {
return value[5:16]
}
return value
}
func protocolClass(proto string) string {
s := strings.ToLower(proto)
switch {
case strings.HasPrefix(s, "dns"):
return "dns"
case strings.HasPrefix(s, "doh"):
return "doh"
case strings.HasPrefix(s, "dot"):
return "dot"
case strings.HasPrefix(s, "doq"):
return "doq"
default:
return ""
}
}
func reasonClass(reason string) string {
s := strings.ToLower(reason)
switch {
case strings.Contains(s, "subdomain"):
return "subdomain-flood"
case strings.Contains(s, "external"):
return "external"
default:
return "rate-limit"
}
}
func fallback(value, def string) string {
if strings.TrimSpace(value) == "" {
return def
}
return value
}
func fallbackDash(value string) string {
if strings.TrimSpace(value) == "" || strings.TrimSpace(value) == "-" {
return "-"
}
return value
}
func empty(s, fallback string) string {
if strings.TrimSpace(s) == "" {
return fallback
}
return s
}
func shellQuote(s string) string {
return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
} }
func hostname() string { func hostname() string {

View File

@@ -0,0 +1,108 @@
package report
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
"adguard-shield/internal/config"
"adguard-shield/internal/db"
)
func TestGenerateUsesTemplatesAndFullStats(t *testing.T) {
t.Setenv("ADGUARD_SHIELD_SKIP_UPDATE_CHECK", "true")
store, err := db.Open(filepath.Join(t.TempDir(), "report.db"))
if err != nil {
t.Fatal(err)
}
defer store.Close()
yesterday := time.Now().AddDate(0, 0, -1)
events := []struct {
ip string
domain string
duration string
proto string
reason string
}{
{"1.2.3.4", "example.com", "3600s", "DNS", "rate-limit"},
{"1.2.3.4", "example.com", "permanent", "DoH", "subdomain-flood"},
{"5.6.7.8", "block.example", "permanent", "DoT", "external-blocklist"},
}
for i, e := range events {
ts := yesterday.Add(time.Duration(i) * time.Hour)
_, err := store.DB.Exec(`INSERT INTO ban_history (timestamp_epoch, timestamp_text, action, client_ip, domain, count, duration, protocol, reason) VALUES (?, ?, 'BAN', ?, ?, '42', ?, ?, ?)`,
ts.Unix(), ts.Format("2006-01-02 15:04:05"), e.ip, e.domain, e.duration, e.proto, e.reason)
if err != nil {
t.Fatal(err)
}
}
ts := yesterday.Add(4 * time.Hour)
if _, err := store.DB.Exec(`INSERT INTO ban_history (timestamp_epoch, timestamp_text, action, client_ip, domain, count, duration, protocol, reason) VALUES (?, ?, 'UNBAN', '1.2.3.4', '-', '-', '-', '-', 'manual')`, ts.Unix(), ts.Format("2006-01-02 15:04:05")); err != nil {
t.Fatal(err)
}
if err := store.InsertBan(db.Ban{IP: "5.6.7.8", Domain: "block.example", Permanent: true, Reason: "external-blocklist", Protocol: "DoT", Source: "external-blocklist"}); err != nil {
t.Fatal(err)
}
logPath := filepath.Join(t.TempDir(), "shield.log")
if err := writeTestLog(logPath, yesterday); err != nil {
t.Fatal(err)
}
cfg := &config.Config{
Path: filepath.Join(t.TempDir(), "adguard-shield.conf"),
ReportInterval: "weekly",
ReportFormat: "html",
ReportBusiestDayRange: 30,
LogFile: logPath,
ReportEmailTo: "admin@example.test",
ReportEmailFrom: "shield@example.test",
ReportMailCmd: "msmtp",
}
htmlReport, err := Generate(cfg, store, "html")
if err != nil {
t.Fatal(err)
}
for _, want := range []string{"AdGuard Shield", "example.com", "1.2.3.4", "DoH", "Subdomain-Flood Sperren", "AbuseIPDB Reports"} {
if !strings.Contains(htmlReport, want) {
t.Fatalf("HTML report missing %q\n%s", want, htmlReport)
}
}
if strings.Contains(htmlReport, "{{") {
t.Fatalf("HTML report still contains placeholders")
}
txtReport, err := Generate(cfg, store, "txt")
if err != nil {
t.Fatal(err)
}
for _, want := range []string{"Sperren gesamt: 3", "Entsperrungen: 1", "Permanente Sperren: 2", "block.example"} {
if !strings.Contains(txtReport, want) {
t.Fatalf("TXT report missing %q\n%s", want, txtReport)
}
}
}
func TestVersionGreater(t *testing.T) {
tests := []struct {
a, b string
want bool
}{
{"v1.0.1", "v1.0.0", true},
{"v1.0.0", "v1.0.0", false},
{"v1.2.0", "v1.10.0", false},
}
for _, tt := range tests {
if got := versionGreater(tt.a, tt.b); got != tt.want {
t.Fatalf("versionGreater(%q, %q) = %v, want %v", tt.a, tt.b, got, tt.want)
}
}
}
func writeTestLog(path string, when time.Time) error {
line := "[" + when.Format("2006-01-02 15:04:05") + "] [INFO] AbuseIPDB: 5.6.7.8 erfolgreich gemeldet\n"
return os.WriteFile(path, []byte(line), 0644)
}

View File

@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AdGuard Shield - Report</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background-color: #f0f2f5; margin: 0; padding: 0; color: #1a1a2e; }
.container { max-width: 700px; margin: 30px auto; background: #ffffff; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); overflow: hidden; }
.header { background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); color: #ffffff; padding: 30px 35px; text-align: center; }
.header h1 { margin: 0 0 6px 0; font-size: 26px; font-weight: 700; letter-spacing: 0.5px; }
.header .subtitle { font-size: 14px; color: #a8b2d1; margin: 0; }
.header .period { display: inline-block; margin-top: 14px; padding: 6px 18px; background: rgba(255,255,255,0.12); border-radius: 20px; font-size: 13px; color: #ccd6f6; }
.content { padding: 30px 35px; }
.stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 28px; }
.stat-card { background: #f8f9fc; border-radius: 10px; padding: 18px 20px; border-left: 4px solid #0f3460; }
.stat-card.danger { border-left-color: #e74c3c; }
.stat-card.warning { border-left-color: #f39c12; }
.stat-card.success { border-left-color: #27ae60; }
.stat-card.info { border-left-color: #3498db; }
.stat-card .stat-value { font-size: 28px; font-weight: 700; color: #1a1a2e; line-height: 1.2; }
.stat-card .stat-label { font-size: 12px; color: #6c757d; text-transform: uppercase; letter-spacing: 0.5px; margin-top: 4px; }
h2 { font-size: 18px; color: #1a1a2e; margin: 28px 0 14px 0; padding-bottom: 8px; border-bottom: 2px solid #f0f2f5; }
h2:first-child { margin-top: 0; }
table { width: 100%; border-collapse: collapse; margin-bottom: 20px; font-size: 14px; }
th { background: #f8f9fc; color: #1a1a2e; font-weight: 600; text-align: left; padding: 10px 14px; border-bottom: 2px solid #e8ecf1; font-size: 12px; text-transform: uppercase; letter-spacing: 0.3px; }
td { padding: 10px 14px; border-bottom: 1px solid #f0f2f5; color: #495057; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: #fafbfd; }
.rank { display: inline-block; width: 24px; height: 24px; line-height: 24px; text-align: center; background: #e8ecf1; border-radius: 50%; font-size: 12px; font-weight: 600; color: #495057; }
.rank.top3 { background: #0f3460; color: #ffffff; }
.ip-cell { font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; font-size: 13px; color: #1a1a2e; }
.bar-container { display: flex; align-items: center; gap: 8px; }
.bar { height: 8px; background: linear-gradient(90deg, #0f3460, #3498db); border-radius: 4px; min-width: 4px; }
.bar-value { font-size: 13px; font-weight: 600; color: #1a1a2e; white-space: nowrap; }
.protocol-badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; background: #e8ecf1; color: #495057; margin: 2px; }
.protocol-badge.dns { background: #dff0d8; color: #3c763d; }
.protocol-badge.doh { background: #d9edf7; color: #31708f; }
.protocol-badge.dot { background: #fcf8e3; color: #8a6d3b; }
.protocol-badge.doq { background: #f2dede; color: #a94442; }
.reason-badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; }
.reason-badge.rate-limit { background: #fcf8e3; color: #8a6d3b; }
.reason-badge.subdomain-flood { background: #f2dede; color: #a94442; }
.reason-badge.external { background: #d9edf7; color: #31708f; }
.no-data { text-align: center; padding: 30px; color: #adb5bd; font-style: italic; }
.footer { background: #f8f9fc; padding: 24px 35px; text-align: center; font-size: 12px; color: #6c757d; border-top: 1px solid #e8ecf1; }
.footer a { color: #0f3460; text-decoration: none; font-weight: 600; }
.footer .links { margin-top: 10px; display: flex; justify-content: space-between; align-items: center; gap: 12px; }
.footer .separator { margin: 0 8px; color: #ced4da; }
.version-tag { display: block; margin-top: 8px; font-size: 11px; color: #adb5bd; }
.update-notice { display: inline-block; margin-top: 10px; padding: 7px 14px; background: #fff8e1; border: 1px solid #ffc107; border-radius: 8px; color: #7a5700; font-size: 12px; font-weight: 600; }
.update-notice a { color: #7a5700; text-decoration: none; font-weight: 700; }
.period-today td { background: #eef4ff; font-weight: 600; }
.period-today td:first-child { color: #0f3460; }
.period-gestern td { background: #f0faf3; font-weight: 600; }
.period-gestern td:first-child { color: #27ae60; }
@media (max-width: 700px) { .container { margin: 0; border-radius: 0; } .content, .header, .footer { padding-left: 18px; padding-right: 18px; } .stats-grid { grid-template-columns: 1fr; } table { font-size: 12px; } th, td { padding: 8px 8px; } .footer .links { display: block; } }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>AdGuard Shield</h1>
<p class="subtitle">Sicherheits-Report</p>
<div class="period">{{REPORT_PERIOD}}</div>
</div>
<div class="content">
<h2>Zeitraum-Schnelluebersicht</h2>
{{PERIOD_OVERVIEW_TABLE}}
<h2>Uebersicht</h2>
<div class="stats-grid">
<div class="stat-card danger"><div class="stat-value">{{TOTAL_BANS}}</div><div class="stat-label">Sperren gesamt</div></div>
<div class="stat-card success"><div class="stat-value">{{TOTAL_UNBANS}}</div><div class="stat-label">Entsperrungen</div></div>
<div class="stat-card warning"><div class="stat-value">{{UNIQUE_IPS}}</div><div class="stat-label">Eindeutige IPs</div></div>
<div class="stat-card info"><div class="stat-value">{{PERMANENT_BANS}}</div><div class="stat-label">Permanente Sperren</div></div>
<div class="stat-card"><div class="stat-value">{{ACTIVE_BANS}}</div><div class="stat-label">Aktuell aktive Sperren</div></div>
<div class="stat-card info"><div class="stat-value">{{ABUSEIPDB_REPORTS}}</div><div class="stat-label">AbuseIPDB Reports</div></div>
</div>
<h2>Angriffsarten</h2>
<div class="stats-grid">
<div class="stat-card warning"><div class="stat-value">{{RATELIMIT_BANS}}</div><div class="stat-label">Rate-Limit Sperren</div></div>
<div class="stat-card danger"><div class="stat-value">{{SUBDOMAIN_FLOOD_BANS}}</div><div class="stat-label">Subdomain-Flood Sperren</div></div>
<div class="stat-card"><div class="stat-value">{{EXTERNAL_BLOCKLIST_BANS}}</div><div class="stat-label">Externe Blocklist</div></div>
<div class="stat-card success"><div class="stat-value">{{BUSIEST_DAY}}</div><div class="stat-label">{{BUSIEST_DAY_LABEL}}</div></div>
</div>
<h2>Top 10 - Auffaelligste IPs</h2>
{{TOP10_IPS_TABLE}}
<h2>Top 10 - Meistbetroffene Domains</h2>
{{TOP10_DOMAINS_TABLE}}
<h2>Protokoll-Verteilung</h2>
{{PROTOCOL_TABLE}}
<h2>Letzte 10 Sperren</h2>
{{RECENT_BANS_TABLE}}
</div>
<div class="footer">
<div class="links">
<span><a href="https://www.patrick-asmus.de">Patrick-Asmus.de</a><span class="separator">|</span><a href="https://www.cleveradmin.de">CleverAdmin.de</a></span>
<span><a href="https://git.techniverse.net/scriptos/adguard-shield.git">AdGuard Shield auf Gitea</a><span class="separator">|</span><a href="https://git.techniverse.net/scriptos/adguard-shield/src/branch/main/docs">docs</a></span>
</div>
<br>
Dieser Report wurde automatisch von <strong>AdGuard Shield</strong> generiert.<br>
Generiert am: {{REPORT_DATE}}
<div class="version-tag">AdGuard Shield {{VERSION}} · {{HOSTNAME}}</div>
{{UPDATE_NOTICE}}
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,68 @@
================================================================
AdGuard Shield - Sicherheits-Report
================================================================
Zeitraum: {{REPORT_PERIOD}}
Erstellt: {{REPORT_DATE}}
Host: {{HOSTNAME}}
----------------------------------------------------------------
ZEITRAUM-SCHNELLUEBERSICHT
----------------------------------------------------------------
{{PERIOD_OVERVIEW_TEXT}}
----------------------------------------------------------------
UEBERSICHT (Berichtszeitraum)
----------------------------------------------------------------
Sperren gesamt: {{TOTAL_BANS}}
Entsperrungen: {{TOTAL_UNBANS}}
Eindeutige IPs: {{UNIQUE_IPS}}
Permanente Sperren: {{PERMANENT_BANS}}
Aktuell aktive Sperren: {{ACTIVE_BANS}}
AbuseIPDB Reports: {{ABUSEIPDB_REPORTS}}
----------------------------------------------------------------
ANGRIFFSARTEN
----------------------------------------------------------------
Rate-Limit Sperren: {{RATELIMIT_BANS}}
Subdomain-Flood Sperren: {{SUBDOMAIN_FLOOD_BANS}}
Externe Blocklist: {{EXTERNAL_BLOCKLIST_BANS}}
{{BUSIEST_DAY_LABEL}}: {{BUSIEST_DAY}}
----------------------------------------------------------------
TOP 10 - AUFFAELLIGSTE IPs
----------------------------------------------------------------
{{TOP10_IPS_TEXT}}
----------------------------------------------------------------
TOP 10 - MEISTBETROFFENE DOMAINS
----------------------------------------------------------------
{{TOP10_DOMAINS_TEXT}}
----------------------------------------------------------------
PROTOKOLL-VERTEILUNG
----------------------------------------------------------------
{{PROTOCOL_TEXT}}
----------------------------------------------------------------
LETZTE 10 SPERREN
----------------------------------------------------------------
{{RECENT_BANS_TEXT}}
{{UPDATE_NOTICE_TXT}}
================================================================
Dieser Report wurde automatisch von AdGuard Shield generiert.
AdGuard Shield {{VERSION}}
Web: https://www.patrick-asmus.de
Blog: https://www.cleveradmin.de
Repo: https://git.techniverse.net/scriptos/adguard-shield.git
Docs: https://git.techniverse.net/scriptos/adguard-shield/src/branch/main/docs
================================================================