diff --git a/README.md b/README.md index 3c893bf..30e57a0 100644 --- a/README.md +++ b/README.md @@ -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 sudo ./adguard-shield install # 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) sudo nano /opt/adguard-shield/adguard-shield.conf # API-Verbindung testen -sudo /opt/adguard-shield/adguard-shield test +sudo adguard-shield test # 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 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. +> 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 ` statt `sudo /opt/adguard-shield/adguard-shield ` 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). @@ -120,7 +122,7 @@ sudo systemctl status adguard-shield AdGuard Shield wird über ein einzelnes Binary bedient. Die Grundform lautet: ```bash -sudo /opt/adguard-shield/adguard-shield +sudo adguard-shield ``` ### Installation & Updates @@ -130,9 +132,11 @@ sudo /opt/adguard-shield/adguard-shield | `install` | Binary, Konfiguration und systemd-Service installieren | | `install --skip-deps` | Installation ohne automatische Paketprüfung | | `install --no-enable` | Installation ohne systemd-Autostart | +| `install --no-register` | Installation ohne globalen CLI-Befehl in `/usr/local/bin` | | `install --config-source ` | Bestehende Konfiguration als Vorlage übernehmen | | `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 --keep-config` | Deinstallation mit Erhalt der Konfiguration | diff --git a/cmd/adguard-shieldd/main.go b/cmd/adguard-shieldd/main.go index 3558455..bff71dc 100644 --- a/cmd/adguard-shieldd/main.go +++ b/cmd/adguard-shieldd/main.go @@ -244,7 +244,11 @@ func run() error { case "report-test": return report.SendTest(ctx, cfg) 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": return report.RemoveCron() case "firewall-create": @@ -463,8 +467,8 @@ func usage() { Nutzung: adguard-shield version - adguard-shield install [--config-source PATH] [--skip-deps] - adguard-shield update [--config-source PATH] [--skip-deps] + adguard-shield install [--config-source PATH] [--skip-deps] [--no-register] + adguard-shield update [--config-source PATH] [--skip-deps] [--no-register] adguard-shield uninstall [--keep-config] adguard-shield install-status 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.BoolVar(&opts.SkipDeps, "skip-deps", false, "Paketpruefung ueberspringen") 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 { return opts, err } opts.Enable = !*noEnable + opts.RegisterCLI = !*noRegister return opts, nil } diff --git a/docs/README.md b/docs/README.md index b6fb880..0ed583d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -25,6 +25,12 @@ Die Go-Version bündelt alle Aufgaben in einem einzelnen Binary: /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: - **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 - **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 -sudo /opt/adguard-shield/adguard-shield +sudo adguard-shield ``` Für Installation oder Update nutzt du das neue Binary aus dem Repository, Release oder Build-Verzeichnis: diff --git a/docs/befehle.md b/docs/befehle.md index ed997a2..d5a511b 100644 --- a/docs/befehle.md +++ b/docs/befehle.md @@ -6,18 +6,24 @@ AdGuard Shield wird in der Go-Version über ein einzelnes Binary bedient: /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 ```bash -sudo /opt/adguard-shield/adguard-shield +sudo adguard-shield ``` Wenn du eine andere Konfigurationsdatei verwenden möchtest, muss `-config` direkt vor dem Befehl stehen: ```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 @@ -25,6 +31,7 @@ sudo /opt/adguard-shield/adguard-shield -config /pfad/zur/adguard-shield.conf st | Datei | Pfad | |---|---| | Binary | `/opt/adguard-shield/adguard-shield` | +| CLI-Befehl | `/usr/local/bin/adguard-shield` | | Konfiguration | `/opt/adguard-shield/adguard-shield.conf` | | SQLite-Datenbank | `/var/lib/adguard-shield/adguard-shield.db` | | Logdatei | `/var/log/adguard-shield.log` | @@ -34,13 +41,13 @@ sudo /opt/adguard-shield/adguard-shield -config /pfad/zur/adguard-shield.conf st ```bash # Version anzeigen -/opt/adguard-shield/adguard-shield version +adguard-shield version # Installation und Update sudo ./adguard-shield install sudo ./adguard-shield update -sudo ./adguard-shield install-status -sudo /opt/adguard-shield/adguard-shield uninstall --keep-config +sudo adguard-shield install-status +sudo adguard-shield uninstall --keep-config # Service-Management über systemd sudo systemctl start adguard-shield @@ -49,16 +56,16 @@ sudo systemctl restart adguard-shield sudo systemctl status adguard-shield # Diagnose und Monitoring -sudo /opt/adguard-shield/adguard-shield test -sudo /opt/adguard-shield/adguard-shield status -sudo /opt/adguard-shield/adguard-shield live -sudo /opt/adguard-shield/adguard-shield history 100 -sudo /opt/adguard-shield/adguard-shield logs --level warn --limit 100 +sudo adguard-shield test +sudo adguard-shield status +sudo adguard-shield live +sudo adguard-shield history 100 +sudo adguard-shield logs --level warn --limit 100 # Manuelle Eingriffe -sudo /opt/adguard-shield/adguard-shield ban 192.168.1.100 -sudo /opt/adguard-shield/adguard-shield unban 192.168.1.100 -sudo /opt/adguard-shield/adguard-shield flush +sudo adguard-shield ban 192.168.1.100 +sudo adguard-shield unban 192.168.1.100 +sudo adguard-shield flush ``` --- @@ -71,6 +78,12 @@ Das installierte Binary landet standardmäßig unter: /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 ```bash @@ -87,6 +100,7 @@ Am Ende fragt der Installer, ob AdGuard Shield direkt gestartet oder neu gestart | `--config-source ` | Bestehende Konfigurationsdatei als Vorlage übernehmen | | `--skip-deps` | Automatische Paketprüfung und -installation überspringen | | `--no-enable` | systemd-Autostart nicht aktivieren | +| `--no-register` | Globalen CLI-Befehl `/usr/local/bin/adguard-shield` nicht anlegen | | `--install-dir ` | Abweichendes Installationsverzeichnis verwenden | **Beispiele:** @@ -98,6 +112,9 @@ sudo ./adguard-shield install --config-source ./adguard-shield.conf # Ohne Paketprüfung installieren sudo ./adguard-shield install --skip-deps +# Ohne globalen CLI-Befehl installieren +sudo ./adguard-shield install --no-register + # In anderes Verzeichnis installieren 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) | | 4 | Anlage von Installations- und State-Verzeichnissen | | 5 | Kopieren des Binarys nach `/opt/adguard-shield/` | -| 6 | Anlage oder Migration der Konfiguration | -| 7 | Schreiben der systemd-Unit | -| 8 | `systemctl daemon-reload` | -| 9 | Optional: Autostart aktivieren | -| 10 | Nachfrage: Service direkt starten oder neu starten | +| 6 | CLI-Befehl `/usr/local/bin/adguard-shield` registrieren (sofern nicht `--no-register`) | +| 7 | Report-Templates installieren | +| 8 | Anlage oder Migration der Konfiguration | +| 9 | Schreiben der systemd-Unit | +| 10 | `systemctl daemon-reload` und optional Autostart aktivieren | +| 11 | Nachfrage: Service direkt starten oder neu starten | ### 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 ``` +### Update ohne CLI-Registrierung + +```bash +sudo ./adguard-shield update --no-register +``` + ### Was beim Update passiert - 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 - Neue Konfigurationsparameter werden ergänzt - Bei einer Migration wird `adguard-shield.conf.old` geschrieben @@ -164,13 +189,14 @@ Weitere Details stehen in der [Update-Anleitung](update.md). ## Installationsstatus ```bash -sudo ./adguard-shield install-status +sudo adguard-shield install-status ``` Zeigt eine Übersicht mit: - Installationspfad und Binary-Status - Installierte Version +- CLI-Befehl in `/usr/local/bin` vorhanden - Konfiguration vorhanden - systemd-Service vorhanden und Status - Autostart aktiv @@ -188,10 +214,10 @@ sudo ./adguard-shield install-status --install-dir /opt/adguard-shield-test ```bash # Vollständige Deinstallation -sudo /opt/adguard-shield/adguard-shield uninstall +sudo adguard-shield uninstall # Deinstallation mit Konfigurationserhalt -sudo /opt/adguard-shield/adguard-shield uninstall --keep-config +sudo adguard-shield uninstall --keep-config ``` **Was bei der Deinstallation passiert:** @@ -275,19 +301,19 @@ Für Debugging oder Dry-Run kann der Daemon im Vordergrund gestartet werden: ```bash # Normaler Vordergrundlauf -sudo /opt/adguard-shield/adguard-shield run +sudo adguard-shield run # Alias für run -sudo /opt/adguard-shield/adguard-shield start +sudo adguard-shield start # Analysieren ohne echte Sperren -sudo /opt/adguard-shield/adguard-shield dry-run +sudo adguard-shield dry-run ``` ### Daemon über PID-Datei stoppen ```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. @@ -297,7 +323,7 @@ Für den Alltag gilt: Nutze `systemctl`. Der direkte Vordergrundlauf endet, soba ## API-Test ```bash -sudo /opt/adguard-shield/adguard-shield test +sudo adguard-shield test ``` 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 ```bash -sudo /opt/adguard-shield/adguard-shield status +sudo adguard-shield status ``` 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 ```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: @@ -371,7 +397,7 @@ Die `live`-Ansicht ist das beste Werkzeug, wenn du verstehen möchtest, was gera ### Alias ```bash -sudo /opt/adguard-shield/adguard-shield watch +sudo adguard-shield watch ``` --- @@ -380,10 +406,10 @@ sudo /opt/adguard-shield/adguard-shield watch ```bash # Letzte 50 Einträge (Standard) -sudo /opt/adguard-shield/adguard-shield history +sudo adguard-shield history # 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`. @@ -432,16 +458,16 @@ AdGuard Shield schreibt Daemon-Ereignisse in `LOG_FILE`, standardmäßig: ```bash # Letzte INFO/WARN/ERROR-Einträge -sudo /opt/adguard-shield/adguard-shield logs +sudo adguard-shield logs # 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) -sudo /opt/adguard-shield/adguard-shield logs debug +sudo adguard-shield logs debug # 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 @@ -469,7 +495,7 @@ sudo journalctl -u adguard-shield --no-pager -n 100 ### IP permanent sperren ```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. @@ -477,7 +503,7 @@ Legt eine manuelle permanente Sperre an. Die IP wird sofort in die Firewall eing ### IP entsperren ```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). @@ -485,7 +511,7 @@ Entfernt die IP aus Firewall und Datenbank. Funktioniert für alle Sperrtypen (a ### Alle Sperren aufheben ```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. @@ -499,7 +525,7 @@ Hebt alle aktiven Sperren auf. Bei aktivierten Benachrichtigungen wird eine zusa ### Offense-Status anzeigen ```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. @@ -507,19 +533,19 @@ Zeigt die Gesamtzahl der Offense-Zähler, davon abgelaufene, und die Konfigurati ### Abgelaufene Zähler entfernen ```bash -sudo /opt/adguard-shield/adguard-shield offense-cleanup +sudo adguard-shield offense-cleanup ``` ### Alle Offense-Zähler zurücksetzen ```bash -sudo /opt/adguard-shield/adguard-shield reset-offenses +sudo adguard-shield reset-offenses ``` ### Zähler für eine IP zurücksetzen ```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 @@ -528,10 +554,10 @@ Wenn ein Client fälschlicherweise eskaliert wurde: ```bash # 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 -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) # WHITELIST="127.0.0.1,::1,192.168.1.100" @@ -545,13 +571,13 @@ sudo systemctl restart adguard-shield ### Chain und ipsets anlegen ```bash -sudo /opt/adguard-shield/adguard-shield firewall-create +sudo adguard-shield firewall-create ``` ### Status anzeigen ```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. @@ -559,7 +585,7 @@ Zeigt die aktuelle Firewall-Struktur: Chain, ipsets und eingehängte Regeln. ### ipsets leeren ```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. @@ -567,13 +593,13 @@ Entfernt alle IPs aus den ipsets. Die Firewall-Struktur (Chain, Regeln) bleibt b ### Chain und ipsets vollständig entfernen ```bash -sudo /opt/adguard-shield/adguard-shield firewall-remove +sudo adguard-shield firewall-remove ``` ### Firewall-Regeln sichern ```bash -sudo /opt/adguard-shield/adguard-shield firewall-save +sudo adguard-shield firewall-save ``` Speichert die aktuellen Regeln nach: @@ -586,7 +612,7 @@ Speichert die aktuellen Regeln nach: ### Gesicherte Regeln wiederherstellen ```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). @@ -598,19 +624,19 @@ sudo /opt/adguard-shield/adguard-shield firewall-restore ### Status anzeigen ```bash -sudo /opt/adguard-shield/adguard-shield whitelist-status +sudo adguard-shield whitelist-status ``` ### Sofort synchronisieren ```bash -sudo /opt/adguard-shield/adguard-shield whitelist-sync +sudo adguard-shield whitelist-sync ``` ### Aufgelöste externe Whitelist entfernen ```bash -sudo /opt/adguard-shield/adguard-shield whitelist-flush +sudo adguard-shield whitelist-flush ``` ### Hinweise @@ -629,19 +655,19 @@ sudo /opt/adguard-shield/adguard-shield whitelist-flush ### Status anzeigen ```bash -sudo /opt/adguard-shield/adguard-shield blocklist-status +sudo adguard-shield blocklist-status ``` ### Sofort synchronisieren ```bash -sudo /opt/adguard-shield/adguard-shield blocklist-sync +sudo adguard-shield blocklist-sync ``` ### Alle Sperren aus externer Blocklist aufheben ```bash -sudo /opt/adguard-shield/adguard-shield blocklist-flush +sudo adguard-shield blocklist-flush ``` ### Hinweise @@ -658,13 +684,13 @@ sudo /opt/adguard-shield/adguard-shield blocklist-flush ### Status anzeigen ```bash -sudo /opt/adguard-shield/adguard-shield geoip-status +sudo adguard-shield geoip-status ``` ### Einzelne IP nachschlagen ```bash -sudo /opt/adguard-shield/adguard-shield geoip-lookup 8.8.8.8 +sudo adguard-shield geoip-lookup 8.8.8.8 ``` **Ausgabe:** @@ -676,7 +702,7 @@ IP: 8.8.8.8 -> Land: US ### Aktuelle Clients prüfen ```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. @@ -684,13 +710,13 @@ Liest das aktuelle Querylog und prüft alle darin enthaltenen Client-IPs einmali ### Alle GeoIP-Sperren aufheben ```bash -sudo /opt/adguard-shield/adguard-shield geoip-flush +sudo adguard-shield geoip-flush ``` ### Cache leeren ```bash -sudo /opt/adguard-shield/adguard-shield geoip-flush-cache +sudo adguard-shield geoip-flush-cache ``` ### Hinweise @@ -705,25 +731,25 @@ sudo /opt/adguard-shield/adguard-shield geoip-flush-cache ### Konfiguration und Cron-Status anzeigen ```bash -sudo /opt/adguard-shield/adguard-shield report-status +sudo adguard-shield report-status ``` ### HTML-Report in Datei schreiben ```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 ```bash -sudo /opt/adguard-shield/adguard-shield report-generate txt +sudo adguard-shield report-generate txt ``` ### Testmail senden ```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. @@ -731,21 +757,21 @@ Sendet eine einfache Testmail. Erst wenn diese funktioniert, lohnt sich die Fehl ### Aktuellen Report erzeugen und versenden ```bash -sudo /opt/adguard-shield/adguard-shield report-send +sudo adguard-shield report-send ``` ### Cron-Job installieren ```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 ```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). @@ -755,7 +781,7 @@ Details zum Report-System stehen in [E-Mail Report](report.md). ## Dry-Run ```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. @@ -773,11 +799,11 @@ Der Dry-Run ist der sicherste Weg, neue Konfigurationen zu prüfen, bevor sie pr ```bash # Dry-Run starten (Strg+C zum Beenden) -sudo /opt/adguard-shield/adguard-shield dry-run +sudo adguard-shield dry-run # Ergebnisse prüfen -sudo /opt/adguard-shield/adguard-shield history 50 -sudo /opt/adguard-shield/adguard-shield logs --level warn --limit 80 +sudo adguard-shield history 50 +sudo adguard-shield logs --level warn --limit 80 ``` --- @@ -785,7 +811,7 @@ sudo /opt/adguard-shield/adguard-shield logs --level warn --limit 80 ## Version ```bash -/opt/adguard-shield/adguard-shield version +adguard-shield version ``` Zeigt die installierte Version an. Aliase: `--version`, `-v`. @@ -798,15 +824,15 @@ Zeigt die installierte Version an. Aliase: `--version`, `-v`. ```bash sudo systemctl restart adguard-shield -sudo /opt/adguard-shield/adguard-shield status -sudo /opt/adguard-shield/adguard-shield logs --level info --limit 80 +sudo adguard-shield status +sudo adguard-shield logs --level info --limit 80 ``` ### Falsch gesperrte IP freigeben ```bash -sudo /opt/adguard-shield/adguard-shield unban 192.168.1.100 -sudo /opt/adguard-shield/adguard-shield reset-offenses 192.168.1.100 +sudo adguard-shield unban 192.168.1.100 +sudo adguard-shield reset-offenses 192.168.1.100 ``` 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 ```bash -sudo /opt/adguard-shield/adguard-shield whitelist-sync -sudo /opt/adguard-shield/adguard-shield blocklist-sync -sudo /opt/adguard-shield/adguard-shield status +sudo adguard-shield whitelist-sync +sudo adguard-shield blocklist-sync +sudo adguard-shield status ``` ### Firewall neu aufbauen ```bash -sudo /opt/adguard-shield/adguard-shield firewall-remove -sudo /opt/adguard-shield/adguard-shield firewall-create +sudo adguard-shield firewall-remove +sudo adguard-shield firewall-create sudo systemctl restart adguard-shield ``` @@ -834,8 +860,8 @@ Nach dem Neustart schreibt der Daemon aktive Sperren aus SQLite wieder in die Fi ```bash sudo systemctl status adguard-shield sudo journalctl -u adguard-shield --no-pager -n 100 -sudo /opt/adguard-shield/adguard-shield test -sudo /opt/adguard-shield/adguard-shield logs --level debug --limit 100 +sudo adguard-shield test +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 ```bash -/opt/adguard-shield/adguard-shield --help +adguard-shield --help ``` Bei unbekannten Befehlen gibt das Binary die Usage-Ausgabe aus. diff --git a/docs/benachrichtigungen.md b/docs/benachrichtigungen.md index bfcf334..fbbec5d 100644 --- a/docs/benachrichtigungen.md +++ b/docs/benachrichtigungen.md @@ -217,9 +217,9 @@ GEOIP_NOTIFY=false Diese Befehle können viele IPs auf einmal freigeben: ```bash -sudo /opt/adguard-shield/adguard-shield flush -sudo /opt/adguard-shield/adguard-shield geoip-flush -sudo /opt/adguard-shield/adguard-shield blocklist-flush +sudo adguard-shield flush +sudo adguard-shield geoip-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. @@ -326,7 +326,7 @@ Aktion: Manual-Flush Wenn keine Benachrichtigung ankommt: ```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 ``` diff --git a/docs/docker.md b/docs/docker.md index 82d7ef0..852f541 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -143,8 +143,8 @@ BLOCKED_PORTS="53 443 853" ```bash sudo systemctl restart adguard-shield -sudo /opt/adguard-shield/adguard-shield firewall-status -sudo /opt/adguard-shield/adguard-shield status +sudo adguard-shield firewall-status +sudo adguard-shield status ``` ## Firewall neu aufbauen @@ -152,9 +152,9 @@ sudo /opt/adguard-shield/adguard-shield status Falls der Modus gewechselt wurde: ```bash -sudo /opt/adguard-shield/adguard-shield firewall-remove +sudo adguard-shield firewall-remove 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. diff --git a/docs/konfiguration.md b/docs/konfiguration.md index 49c4cf7..c739a6d 100644 --- a/docs/konfiguration.md +++ b/docs/konfiguration.md @@ -20,7 +20,7 @@ Nach Änderungen muss der Service neu gestartet werden: ```bash sudo systemctl restart adguard-shield -sudo /opt/adguard-shield/adguard-shield status +sudo adguard-shield status ``` ## Automatische Migration @@ -42,10 +42,10 @@ Nach dem Bearbeiten der Konfiguration: ```bash # 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 -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 ```bash -sudo /opt/adguard-shield/adguard-shield offense-status # Zähler anzeigen -sudo /opt/adguard-shield/adguard-shield offense-cleanup # Abgelaufene entfernen -sudo /opt/adguard-shield/adguard-shield reset-offenses # Alle zurücksetzen -sudo /opt/adguard-shield/adguard-shield reset-offenses # Eine IP zurücksetzen +sudo adguard-shield offense-status # Zähler anzeigen +sudo adguard-shield offense-cleanup # Abgelaufene entfernen +sudo adguard-shield reset-offenses # Alle zurücksetzen +sudo adguard-shield reset-offenses # Eine IP zurücksetzen ``` --- @@ -289,9 +289,9 @@ sudo /opt/adguard-shield/adguard-shield reset-offenses # Eine IP zurück ### CLI-Befehle ```bash -sudo /opt/adguard-shield/adguard-shield logs --level warn --limit 100 -sudo /opt/adguard-shield/adguard-shield logs-follow debug -sudo /opt/adguard-shield/adguard-shield live +sudo adguard-shield logs --level warn --limit 100 +sudo adguard-shield logs-follow debug +sudo adguard-shield live ``` **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_FORMAT` | `html` | Report-Format | | `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 @@ -388,7 +388,7 @@ Details zu allen Kanälen stehen in [Benachrichtigungen](benachrichtigungen.md). |---|---| | `daily` | Täglich 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 | ### Verfügbare Formate @@ -413,11 +413,13 @@ REPORT_MAIL_CMD="msmtp" ### Cron-Job installieren ```bash -sudo /opt/adguard-shield/adguard-shield report-install +sudo adguard-shield report-install ``` 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 @@ -640,11 +642,11 @@ Die Datenbank wird unter `/opt/adguard-shield/geoip/` gespeichert und nach 24 St ### GeoIP-Befehle ```bash -sudo /opt/adguard-shield/adguard-shield geoip-status # Status anzeigen -sudo /opt/adguard-shield/adguard-shield geoip-lookup 8.8.8.8 # IP nachschlagen -sudo /opt/adguard-shield/adguard-shield geoip-sync # Clients prüfen -sudo /opt/adguard-shield/adguard-shield geoip-flush-cache # Cache leeren -sudo /opt/adguard-shield/adguard-shield geoip-flush # Alle GeoIP-Sperren aufheben +sudo adguard-shield geoip-status # Status anzeigen +sudo adguard-shield geoip-lookup 8.8.8.8 # IP nachschlagen +sudo adguard-shield geoip-sync # Clients prüfen +sudo adguard-shield geoip-flush-cache # Cache leeren +sudo adguard-shield geoip-flush # Alle GeoIP-Sperren aufheben ``` --- @@ -737,6 +739,6 @@ NTFY_TOPIC="adguard-shield-prod" ### Vor produktiver Aktivierung ```bash -sudo /opt/adguard-shield/adguard-shield test -sudo /opt/adguard-shield/adguard-shield dry-run +sudo adguard-shield test +sudo adguard-shield dry-run ``` diff --git a/docs/report.md b/docs/report.md index 5b74d60..bb5ae97 100644 --- a/docs/report.md +++ b/docs/report.md @@ -17,11 +17,26 @@ Der Report basiert auf der SQLite-Datenbank: | Zeitraum | Start- und Enddatum des Berichtszeitraums | | Sperren | Anzahl der Sperren 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 | +| 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 | -| Sperrgründe | Aufschlüsselung nach Grund (Rate-Limit, Subdomain-Flood, GeoIP usw.) | -| Sperrquellen | Aufschlüsselung nach Quelle (Monitor, GeoIP, Blocklist, manuell) | -| Letzte Ereignisse | Die letzten 20 Einträge aus der Ban-History | +| Top-Domains | Die am häufigsten betroffenen Domains | +| Protokolle | Verteilung nach DNS, DoH, DoT, DoQ usw. | +| 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_FORMAT` | `html` | Report-Format (`html` oder `txt`) | | `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 @@ -44,7 +59,7 @@ Der Report basiert auf der SQLite-Datenbank: |---|---| | `daily` | Täglich 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` | ### Formate @@ -73,13 +88,13 @@ REPORT_MAIL_CMD="msmtp" ### Konfiguration und Cron-Status anzeigen ```bash -sudo /opt/adguard-shield/adguard-shield report-status +sudo adguard-shield report-status ``` ### HTML-Report in Datei schreiben ```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. @@ -87,13 +102,13 @@ Die Datei kann im Browser geöffnet werden, um das Ergebnis zu prüfen. ### Text-Report auf stdout ausgeben ```bash -sudo /opt/adguard-shield/adguard-shield report-generate txt +sudo adguard-shield report-generate txt ``` ### Testmail senden ```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. @@ -101,19 +116,19 @@ Sendet eine einfache Testmail. Erst wenn diese ankommt, lohnt sich die Fehlersuc ### Aktuellen Report erzeugen und versenden ```bash -sudo /opt/adguard-shield/adguard-shield report-send +sudo adguard-shield report-send ``` ### Cron-Job installieren ```bash -sudo /opt/adguard-shield/adguard-shield report-install +sudo adguard-shield report-install ``` ### Cron-Job entfernen ```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 # Testmail senden -sudo /opt/adguard-shield/adguard-shield report-test +sudo adguard-shield report-test ``` ### Eigene Mailprogramm-Argumente @@ -160,7 +175,7 @@ REPORT_MAIL_CMD="msmtp --account=default" ### Cron-Job installieren ```bash -sudo /opt/adguard-shield/adguard-shield report-install +sudo adguard-shield report-install ``` Dadurch wird diese Datei geschrieben: @@ -169,25 +184,27 @@ Dadurch wird diese Datei geschrieben: /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 -/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 | Intervall | Cron-Verhalten | |---|---| | `daily` | Täglich 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 | ### Cron-Job entfernen ```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 ```bash -sudo /opt/adguard-shield/adguard-shield report-status +sudo adguard-shield report-status ``` ### Schritt 2: Report lokal erzeugen ```bash # 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 -sudo /opt/adguard-shield/adguard-shield report-generate txt +sudo adguard-shield report-generate txt ``` ### Schritt 3: Versand testen ```bash # Einfache Testmail -sudo /opt/adguard-shield/adguard-shield report-test +sudo adguard-shield report-test # Vollständigen Report senden -sudo /opt/adguard-shield/adguard-shield report-send +sudo adguard-shield report-send ``` ### Schritt 4: Logs prüfen ```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 ``` @@ -262,7 +279,7 @@ Oder setze `REPORT_MAIL_CMD` auf dein vorhandenes Mailprogramm. Prüfe die Konfiguration und den Cron-Job: ```bash -sudo /opt/adguard-shield/adguard-shield report-send +sudo adguard-shield report-send 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: ```bash -sudo /opt/adguard-shield/adguard-shield report-generate txt -sudo /opt/adguard-shield/adguard-shield report-generate html /tmp/report.html +sudo adguard-shield report-generate txt +sudo adguard-shield report-generate html /tmp/report.html ``` diff --git a/docs/tipps-und-troubleshooting.md b/docs/tipps-und-troubleshooting.md index 6a44b69..6e09613 100644 --- a/docs/tipps-und-troubleshooting.md +++ b/docs/tipps-und-troubleshooting.md @@ -14,19 +14,19 @@ sudo systemctl status adguard-shield sudo journalctl -u adguard-shield --no-pager -n 100 # 3. Funktioniert die API? -sudo /opt/adguard-shield/adguard-shield test +sudo adguard-shield test # 4. Was ist der aktuelle Zustand? -sudo /opt/adguard-shield/adguard-shield status +sudo adguard-shield status # 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: ```bash -sudo /opt/adguard-shield/adguard-shield live +sudo adguard-shield live ``` --- @@ -68,7 +68,7 @@ sudo systemctl daemon-reload ### Test ```bash -sudo /opt/adguard-shield/adguard-shield test +sudo adguard-shield test ``` ### Konfiguration prüfen @@ -104,9 +104,9 @@ Passe URL und Zugangsdaten entsprechend an. ### Prüfen ```bash -sudo /opt/adguard-shield/adguard-shield live --once -sudo /opt/adguard-shield/adguard-shield history 50 -sudo /opt/adguard-shield/adguard-shield logs --level debug --limit 100 +sudo adguard-shield live --once +sudo adguard-shield history 50 +sudo adguard-shield logs --level debug --limit 100 ``` ### Mögliche Ursachen und Lösungen @@ -131,8 +131,8 @@ sudo /opt/adguard-shield/adguard-shield logs --level debug --limit 100 ### Übersicht verschaffen ```bash -sudo /opt/adguard-shield/adguard-shield status -sudo /opt/adguard-shield/adguard-shield history 100 +sudo adguard-shield status +sudo adguard-shield history 100 ``` ### Ursachen und Gegenmaßnahmen @@ -149,10 +149,10 @@ sudo /opt/adguard-shield/adguard-shield history 100 ```bash # 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) -sudo /opt/adguard-shield/adguard-shield reset-offenses 192.168.1.100 +sudo adguard-shield reset-offenses 192.168.1.100 ``` ### Dauerhaft ausnehmen @@ -174,7 +174,7 @@ sudo systemctl restart adguard-shield ### Status über AdGuard Shield ```bash -sudo /opt/adguard-shield/adguard-shield firewall-status +sudo adguard-shield firewall-status ``` ### Direkte Prüfung mit Systembefehlen @@ -198,8 +198,8 @@ sudo iptables -n -L DOCKER-USER --line-numbers -v | grep ADGUARD ### Firewall neu aufbauen ```bash -sudo /opt/adguard-shield/adguard-shield firewall-remove -sudo /opt/adguard-shield/adguard-shield firewall-create +sudo adguard-shield firewall-remove +sudo adguard-shield firewall-create sudo systemctl restart adguard-shield ``` @@ -212,8 +212,8 @@ Nach dem Neustart werden aktive Sperren aus SQLite wieder in die ipsets geschrie ### Prüfen ```bash -sudo /opt/adguard-shield/adguard-shield status -sudo /opt/adguard-shield/adguard-shield history 100 +sudo adguard-shield status +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. @@ -231,7 +231,7 @@ Temporäre Sperren werden beim Start und während jedes Pollings auf Ablauf gepr ### Manuell freigeben ```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 # 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: ```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. @@ -260,13 +260,13 @@ Im Dry-Run werden mögliche Sperren als `DRY` protokolliert. Es entstehen keine ### Status prüfen ```bash -sudo /opt/adguard-shield/adguard-shield whitelist-status +sudo adguard-shield whitelist-status ``` ### Manuell synchronisieren ```bash -sudo /opt/adguard-shield/adguard-shield whitelist-sync +sudo adguard-shield whitelist-sync ``` ### Typische Probleme @@ -295,19 +295,19 @@ trusted.example.com # Hostname (wird per DNS aufgelöst) ### Status prüfen ```bash -sudo /opt/adguard-shield/adguard-shield blocklist-status +sudo adguard-shield blocklist-status ``` ### Manuell synchronisieren ```bash -sudo /opt/adguard-shield/adguard-shield blocklist-sync +sudo adguard-shield blocklist-sync ``` ### Alle Blocklist-Sperren freigeben ```bash -sudo /opt/adguard-shield/adguard-shield blocklist-flush +sudo adguard-shield blocklist-flush ``` ### Zu viele IPs gesperrt? @@ -325,25 +325,25 @@ sudo /opt/adguard-shield/adguard-shield blocklist-flush ### Status prüfen ```bash -sudo /opt/adguard-shield/adguard-shield geoip-status +sudo adguard-shield geoip-status ``` ### Einzelne IP prüfen ```bash -sudo /opt/adguard-shield/adguard-shield geoip-lookup 8.8.8.8 +sudo adguard-shield geoip-lookup 8.8.8.8 ``` ### Cache leeren ```bash -sudo /opt/adguard-shield/adguard-shield geoip-flush-cache +sudo adguard-shield geoip-flush-cache ``` ### Alle GeoIP-Sperren freigeben ```bash -sudo /opt/adguard-shield/adguard-shield geoip-flush +sudo adguard-shield geoip-flush ``` ### 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 ```bash -sudo /opt/adguard-shield/adguard-shield report-status +sudo adguard-shield report-status ``` ### Funktionstest ```bash # Testmail senden -sudo /opt/adguard-shield/adguard-shield report-test +sudo adguard-shield report-test # 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? @@ -396,7 +396,7 @@ sudo /opt/adguard-shield/adguard-shield report-generate txt ```bash 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 ```bash -sudo /opt/adguard-shield/adguard-shield logs --level warn --limit 100 +sudo adguard-shield logs --level warn --limit 100 ``` ### Checkliste @@ -497,13 +497,13 @@ Wenn der Zustand unklar ist und ein sauberer Neustart nötig ist: sudo systemctl stop adguard-shield # 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) sudo systemctl start adguard-shield # 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. @@ -515,13 +515,13 @@ Das entfernt die Firewall-Struktur und lässt den Daemon sie beim Start wieder a ### Konfiguration behalten ```bash -sudo /opt/adguard-shield/adguard-shield uninstall --keep-config +sudo adguard-shield uninstall --keep-config ``` ### Alles entfernen ```bash -sudo /opt/adguard-shield/adguard-shield uninstall +sudo adguard-shield uninstall ``` Ohne `--keep-config` werden Installationsverzeichnis, State-Verzeichnis und Logdatei entfernt. diff --git a/docs/update.md b/docs/update.md index fae0af9..aceb364 100644 --- a/docs/update.md +++ b/docs/update.md @@ -14,11 +14,13 @@ sudo ./adguard-shield update 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 ` verwenden. + ### Nach dem Update prüfen ```bash -sudo /opt/adguard-shield/adguard-shield install-status -sudo /opt/adguard-shield/adguard-shield status +sudo adguard-shield install-status +sudo adguard-shield status 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`) | | 4 | Installationsverzeichnis sicherstellen | | 5 | Neues Binary nach `/opt/adguard-shield/adguard-shield` kopieren | -| 6 | Konfiguration migrieren (vorhandene Werte behalten, neue ergänzen) | -| 7 | systemd-Service neu schreiben | -| 8 | `systemctl daemon-reload` | -| 9 | Autostart aktivieren (sofern nicht `--no-enable`) | -| 10 | Nachfrage: Service direkt neu starten | +| 6 | CLI-Befehl `/usr/local/bin/adguard-shield` registrieren/aktualisieren (sofern nicht `--no-register`) | +| 7 | Report-Templates installieren | +| 8 | Konfiguration migrieren (vorhandene Werte behalten, neue ergänzen) | +| 9 | systemd-Service neu schreiben | +| 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 sudo ./adguard-shield update -sudo /opt/adguard-shield/adguard-shield test -sudo /opt/adguard-shield/adguard-shield dry-run +sudo adguard-shield test +sudo adguard-shield dry-run 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. +### 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 ```bash @@ -138,7 +149,7 @@ sudo ./adguard-shield update --config-source ./adguard-shield.conf 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 # 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 -sudo /opt/adguard-shield/adguard-shield dry-run +sudo adguard-shield dry-run # 6. Produktiven Service starten sudo systemctl start adguard-shield @@ -197,7 +208,7 @@ sudo systemctl status adguard-shield ### Installation ```bash -sudo /opt/adguard-shield/adguard-shield install-status +sudo adguard-shield install-status ``` ### Service @@ -210,20 +221,20 @@ sudo journalctl -u adguard-shield --no-pager -n 100 ### API-Verbindung ```bash -sudo /opt/adguard-shield/adguard-shield test +sudo adguard-shield test ``` ### Laufzeitstatus ```bash -sudo /opt/adguard-shield/adguard-shield status -sudo /opt/adguard-shield/adguard-shield live --once +sudo adguard-shield status +sudo adguard-shield live --once ``` ### Firewall ```bash -sudo /opt/adguard-shield/adguard-shield firewall-status +sudo adguard-shield firewall-status ``` --- diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 5bbf5d6..c705da6 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -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-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 { n, err := d.Store.CleanupOffenses(d.Config.ProgressiveBanResetAfter) if n > 0 { diff --git a/internal/db/db.go b/internal/db/db.go index 0358215..8ea12de 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -26,15 +26,26 @@ type Ban struct { } type ReportStats struct { - Since int64 - Until int64 - TotalBans int - TotalUnbans int - ActiveBans int - TopClients []ReportCount - Reasons []ReportCount - Sources []ReportCount - RecentEvents []string + Since int64 + Until int64 + TotalBans int + TotalUnbans int + UniqueIPs int + PermanentBans int + ActiveBans int + AbuseIPDBReports int + 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 { @@ -42,6 +53,17 @@ type ReportCount struct { 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) { db, err := sql.Open("sqlite", path+"?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)") if err != nil { @@ -356,7 +378,7 @@ func (s *Store) ClearGeoIPCache() (int64, error) { 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} 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 @@ -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 { 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 { 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 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 { 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) if err != nil { return st, err @@ -380,6 +431,10 @@ func (s *Store) ReportStats(since, until int64, limit int) (ReportStats, error) if err != nil { return st, err } + st.RecentBans, err = s.reportRecentBans(since, until, 10) + if err != nil { + return st, err + } st.RecentEvents, err = s.RecentHistory(limit) return st, err } @@ -406,3 +461,21 @@ func (s *Store) reportCounts(query string, since, until int64, limit int) ([]Rep } 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() +} diff --git a/internal/installer/installer.go b/internal/installer/installer.go index 13b0c05..0a65bea 100644 --- a/internal/installer/installer.go +++ b/internal/installer/installer.go @@ -12,12 +12,15 @@ import ( "runtime" "sort" "strings" + + "adguard-shield/internal/report" ) const ( DefaultInstallDir = "/opt/adguard-shield" DefaultStateDir = "/var/lib/adguard-shield" DefaultLogFile = "/var/log/adguard-shield.log" + CLICommandPath = "/usr/local/bin/adguard-shield" ServiceName = "adguard-shield.service" ServicePath = "/etc/systemd/system/adguard-shield.service" ) @@ -28,19 +31,22 @@ type Options struct { Enable bool SkipDeps bool KeepConfig bool + RegisterCLI bool } type Status struct { - InstallDir string - BinaryPath string - ConfigPath string - BinaryExists bool - ConfigExists bool - ServiceExists bool - ServiceEnabled bool - ServiceActive bool - Version string - LegacyFindings []string + InstallDir string + BinaryPath string + ConfigPath string + CLICommandPath string + BinaryExists bool + ConfigExists bool + CLICommandInstalled bool + ServiceExists bool + ServiceEnabled bool + ServiceActive bool + Version string + LegacyFindings []string } type LegacyError struct { @@ -52,30 +58,30 @@ func (e *LegacyError) Error() string { } func DefaultOptions() Options { - return Options{InstallDir: DefaultInstallDir, Enable: true} + return Options{InstallDir: DefaultInstallDir, Enable: true, RegisterCLI: true} } func Install(opts Options) error { opts = normalize(opts) fmt.Println("AdGuard Shield Go-Installation") 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 { 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 { return &LegacyError{Findings: findings} } if !opts.SkipDeps { - fmt.Println("3/8 Pruefe System-Abhaengigkeiten ...") + fmt.Println("3/10 Pruefe System-Abhaengigkeiten ...") if err := ensureDependencies(); err != nil { return err } } 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 { return err } @@ -85,19 +91,31 @@ func Install(opts Options) error { if err := os.MkdirAll(filepath.Join(opts.InstallDir, "geoip"), 0755); err != nil { 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 { 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 { return err } - fmt.Println("7/8 Schreibe systemd-Service ...") + fmt.Println("9/10 Schreibe systemd-Service ...") if err := writeService(opts.InstallDir); err != nil { return err } - fmt.Println("8/8 Aktualisiere systemd ...") + fmt.Println("10/10 Aktualisiere systemd ...") _ = run("systemctl", "daemon-reload") if opts.Enable { fmt.Println("Aktiviere Autostart ...") @@ -138,6 +156,7 @@ func Uninstall(opts Options) error { } _ = os.Remove(ServicePath) _ = run("systemctl", "daemon-reload") + _ = unregisterCLICommand(opts.InstallDir) if opts.KeepConfig { for _, p := range []string{ filepath.Join(opts.InstallDir, "adguard-shield"), @@ -160,13 +179,15 @@ func GetStatus(installDir string) Status { bin := filepath.Join(installDir, "adguard-shield") conf := filepath.Join(installDir, "adguard-shield.conf") st := Status{ - InstallDir: installDir, - BinaryPath: bin, - ConfigPath: conf, - BinaryExists: fileExists(bin), - ConfigExists: fileExists(conf), - ServiceExists: fileExists(ServicePath), - LegacyFindings: DetectLegacy(installDir), + InstallDir: installDir, + BinaryPath: bin, + ConfigPath: conf, + CLICommandPath: CLICommandPath, + BinaryExists: fileExists(bin), + ConfigExists: fileExists(conf), + CLICommandInstalled: cliCommandInstalled(installDir), + ServiceExists: fileExists(ServicePath), + LegacyFindings: DetectLegacy(installDir), } if st.BinaryExists { if out, err := exec.Command(bin, "version").Output(); err == nil { @@ -247,6 +268,7 @@ func PrintStatus(st Status) string { if 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("systemd Service: %s\n", yesNo(st.ServiceExists))) 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) } +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 { target := filepath.Join(opts.InstallDir, "adguard-shield.conf") defaults := []byte(defaultConfig) diff --git a/internal/report/report.go b/internal/report/report.go index fa2ffc5..9e7a732 100644 --- a/internal/report/report.go +++ b/internal/report/report.go @@ -1,10 +1,15 @@ package report import ( - "bytes" + "bufio" "context" + "embed" + "encoding/json" "fmt" "html" + "io/fs" + "mime" + "net/http" "os" "os/exec" "path/filepath" @@ -12,12 +17,16 @@ import ( "strings" "time" + "adguard-shield/internal/appinfo" "adguard-shield/internal/config" "adguard-shield/internal/db" ) +//go:embed templates/report.html templates/report.txt +var embeddedTemplates embed.FS + 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" @@ -27,50 +36,90 @@ func Status(c *config.Config) string { if _, err := os.Stat(cronPath); err == nil { cron = "installiert (" + cronPath + ")" } - return fmt.Sprintf(`E-Mail Report -Aktiv: %v -Intervall: %s -Zeit: %s -Empfaenger: %s -Absender: %s -Format: %s -Mail-Befehl: %s -Cron: %s -`, c.ReportEnabled, c.ReportInterval, c.ReportTime, c.ReportEmailTo, c.ReportEmailFrom, c.ReportFormat, c.ReportMailCmd, cron) + mailer := "nicht gefunden" + if parts := strings.Fields(c.ReportMailCmd); len(parts) > 0 { + if p, err := exec.LookPath(parts[0]); err == nil { + mailer = "gefunden (" + p + ")" + } + } + return fmt.Sprintf(`AdGuard Shield - Report Status + +Report aktiviert: %v +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) { - if format == "" { - format = c.ReportFormat - } - since, until := window(c.ReportInterval) - stats, err := st.ReportStats(since, until, 20) + format = normalizeFormat(format, c.ReportFormat) + since, until, period := reportWindow(c.ReportInterval, time.Now()) + busiestSince := busiestWindowStart(c, since) + stats, err := st.ReportStats(since, until, busiestSince, 20) if err != nil { return "", err } - if strings.EqualFold(format, "html") { - return renderHTML(c, stats), nil + stats.AbuseIPDBReports = countAbuseReports(c.LogFile, since, until) + + 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 { - body, err := Generate(c, st, c.ReportFormat) + format := normalizeFormat(c.ReportFormat, "html") + body, err := Generate(c, st, format) if err != nil { 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 { - 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) - if strings.EqualFold(c.ReportFormat, "html") { - body = "

AdGuard Shield Test-Mail

Hostname: " + html.EscapeString(hostname()) + "

Zeitpunkt: " + html.EscapeString(time.Now().Format("2006-01-02 15:04:05")) + "

" - } - return sendMail(ctx, c, "AdGuard Shield Test-Mail", body) + format := normalizeFormat(c.ReportFormat, "html") + body := testBody(c, format) + return sendMail(ctx, c, "AdGuard Shield Test-Mail - "+hostname(), body, format, "AdGuard Shield Report Generator (Test)") } 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) if err != nil { return err @@ -82,7 +131,18 @@ func InstallCron(binary, configPath string, c *config.Config) error { if configPath == "" { 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) } @@ -93,25 +153,32 @@ func RemoveCron() error { return nil } -func sendMail(ctx context.Context, c *config.Config, subject, body string) error { - if c.ReportEmailTo == "" { +func sendMail(ctx context.Context, c *config.Config, subject, body, format, xMailer string) error { + if strings.TrimSpace(c.ReportEmailTo) == "" { 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) if len(parts) == 0 { 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") cmd := exec.CommandContext(ctx, parts[0], args...) cmd.Stdin = strings.NewReader(msg) @@ -120,6 +187,273 @@ func sendMail(ctx context.Context, c *config.Config, subject, body string) error 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 `
Keine Daten im Berichtszeitraum
` + } + maxCount := rows[0].Count + var b strings.Builder + b.WriteString("") + 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, `%s`, class, i+1, cellClass, html.EscapeString(r.Name), width, r.Count) + } + b.WriteString("
#" + html.EscapeString(nameHeader) + "Sperren
%d
%d
") + return b.String() +} + +func protocolHTML(rows []db.ReportCount) string { + if len(rows) == 0 { + return `
Keine Daten im Berichtszeitraum
` + } + var b strings.Builder + b.WriteString("") + for _, r := range rows { + class := protocolClass(r.Name) + fmt.Fprintf(&b, ``, class, html.EscapeString(r.Name), r.Count) + } + b.WriteString("
ProtokollAnzahl Sperren
%s%d
") + return b.String() +} + +func recentBansHTML(rows []db.ReportEvent) string { + if len(rows) == 0 { + return `
Keine Sperren im Berichtszeitraum
` + } + var b strings.Builder + b.WriteString("") + for _, e := range rows { + reason := fallback(e.Reason, "rate-limit") + domain := fallbackDash(e.Domain) + fmt.Fprintf(&b, ``, html.EscapeString(shortTime(e.Timestamp)), html.EscapeString(e.IP), html.EscapeString(domain), reasonClass(reason), html.EscapeString(reason)) + } + b.WriteString("
ZeitpunktIPDomainGrund
%s%s%s%s
") + return b.String() +} + +func periodOverviewHTML(st Store) string { + rows := periodOverviewRows(st) + var b strings.Builder + b.WriteString("") + 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, ``, class, html.EscapeString(r.Label), r.TotalBans, r.TotalUnbans, r.UniqueIPs, r.PermanentBans) + } + b.WriteString("
ZeitraumSperrenEntsperrtUnique IPsDauerhaft gebannt
%s%d%d%d%d
") + 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) { parts := strings.Split(value, ":") if len(parts) != 2 { @@ -140,8 +474,8 @@ func cronSchedule(interval, minute, hour string) string { switch strings.ToLower(interval) { case "daily": return fmt.Sprintf("%s %s * * *", minute, hour) - case "biweekly": - return fmt.Sprintf("%s %s 1,15 * *", minute, hour) + case "biweekly", "weekly": + return fmt.Sprintf("%s %s * * 1", minute, hour) case "monthly": return fmt.Sprintf("%s %s 1 * *", minute, hour) default: @@ -149,88 +483,213 @@ func cronSchedule(interval, minute, hour string) string { } } -func window(interval string) (int64, int64) { - now := time.Now() - days := 7 +func reportWindow(interval string, now time.Time) (int64, int64, string) { + today := midnight(now) + days, label := 7, "Bericht" switch strings.ToLower(interval) { case "daily": - days = 1 + days, label = 1, "Tagesbericht" + case "weekly": + days, label = 7, "Wochenbericht" case "biweekly": - days = 14 + days, label = 14, "Zweiwochenbericht" 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 { - var b strings.Builder - b.WriteString("AdGuard Shield Report\n") - b.WriteString("Zeitraum: " + formatTime(st.Since) + " bis " + formatTime(st.Until) + "\n\n") - b.WriteString("Bans: " + strconv.Itoa(st.TotalBans) + "\n") - b.WriteString("Unbans: " + strconv.Itoa(st.TotalUnbans) + "\n") - b.WriteString("Aktive Sperren: " + strconv.Itoa(st.ActiveBans) + "\n\n") - writeCountsText(&b, "Top Clients", st.TopClients) - writeCountsText(&b, "Gruende", st.Reasons) - writeCountsText(&b, "Aktive Quellen", st.Sources) - if len(st.RecentEvents) > 0 { - b.WriteString("Letzte Ereignisse\n") - for _, e := range st.RecentEvents { - b.WriteString("- " + e + "\n") +func busiestWindowStart(c *config.Config, fallbackSince int64) int64 { + if c.ReportBusiestDayRange <= 0 { + return fallbackSince + } + return midnight(time.Now()).AddDate(0, 0, -c.ReportBusiestDayRange).Unix() +} + +func midnight(t time.Time) time.Time { + y, m, d := t.Date() + return time.Date(y, m, d, 0, 0, 0, 0, t.Location()) +} + +func countAbuseReports(path string, since, until int64) int { + 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 b.String() + return count } -func renderHTML(c *config.Config, st db.ReportStats) string { - var b bytes.Buffer - b.WriteString("AdGuard Shield Report") - b.WriteString("") - b.WriteString("") - b.WriteString("

AdGuard Shield Report

") - b.WriteString("

Zeitraum: " + html.EscapeString(formatTime(st.Since)) + " bis " + html.EscapeString(formatTime(st.Until)) + "

") - b.WriteString("
  • Bans: " + strconv.Itoa(st.TotalBans) + "
  • Unbans: " + strconv.Itoa(st.TotalUnbans) + "
  • Aktive Sperren: " + strconv.Itoa(st.ActiveBans) + "
") - writeCountsHTML(&b, "Top Clients", st.TopClients) - writeCountsHTML(&b, "Gruende", st.Reasons) - writeCountsHTML(&b, "Aktive Quellen", st.Sources) - if len(st.RecentEvents) > 0 { - b.WriteString("

Letzte Ereignisse

") - for _, e := range st.RecentEvents { - b.WriteString("") +func checkForUpdate() (string, string) { + if appinfo.Version == "" || appinfo.Version == "unknown" || strings.EqualFold(os.Getenv("ADGUARD_SHIELD_SKIP_UPDATE_CHECK"), "true") { + return "", "" + } + client := http.Client{Timeout: 5 * time.Second} + resp, err := client.Get("https://git.techniverse.net/api/v1/repos/scriptos/adguard-shield/releases?limit=1&page=1") + if err != nil { + return "", "" + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return "", "" + } + var releases []struct { + 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 := `
Update verfuegbar: ` + html.EscapeString(latest) + ` · Jetzt aktualisieren
` + 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("
Ereignis
" + html.EscapeString(e) + "
") } - b.WriteString("") - _ = c - return b.String() + return false } -func writeCountsText(b *strings.Builder, title string, rows []db.ReportCount) { - b.WriteString(title + "\n") - if len(rows) == 0 { - b.WriteString("- keine Daten\n\n") - return +func versionParts(v string) []int { + v = strings.TrimPrefix(strings.TrimSpace(v), "v") + fields := strings.FieldsFunc(v, func(r rune) bool { return r == '.' || r == '-' || r == '+' }) + out := make([]int, 0, len(fields)) + for _, f := range fields { + n, err := strconv.Atoi(f) + if err != nil { + break + } + out = append(out, n) } - for _, r := range rows { - b.WriteString("- " + r.Name + ": " + strconv.Itoa(r.Count) + "\n") - } - b.WriteByte('\n') + return out } -func writeCountsHTML(b *bytes.Buffer, title string, rows []db.ReportCount) { - b.WriteString("

" + html.EscapeString(title) + "

") - if len(rows) == 0 { - b.WriteString("") +func testBody(c *config.Config, format string) string { + now := time.Now().Format("02.01.2006 15:04:05") + host := hostname() + if strings.EqualFold(format, "html") { + return `

AdGuard Shield Test-Mail

E-Mail-Versand funktioniert

NameAnzahl
keine Daten
Hostname` + html.EscapeString(host) + `
Zeitpunkt` + html.EscapeString(now) + `
Empfaenger` + html.EscapeString(c.ReportEmailTo) + `
Absender` + html.EscapeString(c.ReportEmailFrom) + `
Mail-Befehl` + html.EscapeString(c.ReportMailCmd) + `
Format` + html.EscapeString(format) + `
` } - for _, r := range rows { - b.WriteString("" + html.EscapeString(r.Name) + "" + strconv.Itoa(r.Count) + "") - } - b.WriteString("") + return fmt.Sprintf(`AdGuard Shield - Test-Mail + +E-Mail-Versand funktioniert. + +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 { - return time.Unix(epoch, 0).Format("2006-01-02 15:04:05") +func normalizeFormat(value, fallback string) string { + 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 { diff --git a/internal/report/report_test.go b/internal/report/report_test.go new file mode 100644 index 0000000..0a07521 --- /dev/null +++ b/internal/report/report_test.go @@ -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) +} diff --git a/internal/report/templates/report.html b/internal/report/templates/report.html new file mode 100644 index 0000000..d94ed91 --- /dev/null +++ b/internal/report/templates/report.html @@ -0,0 +1,108 @@ + + + + + + AdGuard Shield - Report + + + +
+
+

AdGuard Shield

+

Sicherheits-Report

+
{{REPORT_PERIOD}}
+
+
+

Zeitraum-Schnelluebersicht

+ {{PERIOD_OVERVIEW_TABLE}} +

Uebersicht

+
+
{{TOTAL_BANS}}
Sperren gesamt
+
{{TOTAL_UNBANS}}
Entsperrungen
+
{{UNIQUE_IPS}}
Eindeutige IPs
+
{{PERMANENT_BANS}}
Permanente Sperren
+
{{ACTIVE_BANS}}
Aktuell aktive Sperren
+
{{ABUSEIPDB_REPORTS}}
AbuseIPDB Reports
+
+

Angriffsarten

+
+
{{RATELIMIT_BANS}}
Rate-Limit Sperren
+
{{SUBDOMAIN_FLOOD_BANS}}
Subdomain-Flood Sperren
+
{{EXTERNAL_BLOCKLIST_BANS}}
Externe Blocklist
+
{{BUSIEST_DAY}}
{{BUSIEST_DAY_LABEL}}
+
+

Top 10 - Auffaelligste IPs

+ {{TOP10_IPS_TABLE}} +

Top 10 - Meistbetroffene Domains

+ {{TOP10_DOMAINS_TABLE}} +

Protokoll-Verteilung

+ {{PROTOCOL_TABLE}} +

Letzte 10 Sperren

+ {{RECENT_BANS_TABLE}} +
+ +
+ + diff --git a/internal/report/templates/report.txt b/internal/report/templates/report.txt new file mode 100644 index 0000000..2673ba1 --- /dev/null +++ b/internal/report/templates/report.txt @@ -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 +================================================================