60 Commits

Author SHA1 Message Date
Patrick Asmus
16b2c5950e Release: Version v0.9.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-28 23:34:29 +02:00
Patrick Asmus
b93689bbaf feat: DNS-Flood-Watchlist – sofortiger permanenter Ban für definierte Domains 2026-04-28 23:31:58 +02:00
e555db8092 README.md aktualisiert 2026-04-21 13:45:21 +00:00
b04b8bf87d update Readme.md 2026-04-21 12:03:54 +02:00
6b6a77a98c Merge pull request 'fix: IPv4-Erkennung korrigiert – Hostnamen mit führender Ziffer werden nicht mehr fehlklassifiziert' (#17) from v0.8.2 into main
Reviewed-on: #17
2026-04-19 14:18:40 +00:00
ac21922178 Release: Version v0.8.2 2026-04-19 16:17:56 +02:00
edd8cd4806 fix: IPv4-Erkennung korrigiert – Hostnamen mit führender Ziffer werden nicht mehr fehlklassifiziert 2026-04-19 16:15:10 +02:00
44936e9f20 Merge branch 'main' of https://git.techniverse.net/scriptos/adguard-shield 2026-04-17 13:40:46 +02:00
440694925e Merge pull request 'fix: Offense-Cleanup-Worker mit niedrigster CPU/IO-Priorität ausführen' (#16) from v0.8.1 into main
Reviewed-on: #16
2026-04-16 20:23:48 +00:00
c97e327f0d Merge pull request 'fix: Offense-Cleanup-Worker mit niedrigster CPU/IO-Priorität ausführen' (#16) from v0.8.1 into main
Reviewed-on: #16
2026-04-16 20:23:48 +00:00
12745c3fef Release: Version v0.8.1 2026-04-16 22:22:13 +02:00
c2d6f872f5 Release: Version v0.8.1 2026-04-16 22:22:13 +02:00
ccdc555246 fix: Offense-Cleanup-Worker mit niedrigster CPU/IO-Priorität ausführen 2026-04-16 21:33:47 +02:00
633331748f fix: Offense-Cleanup-Worker mit niedrigster CPU/IO-Priorität ausführen 2026-04-16 21:33:47 +02:00
2559ed89ea Merge pull request 'v0.8.0' (#15) from v0.8.0 into main
Reviewed-on: #15
2026-04-14 19:30:31 +00:00
df8b18ae08 Merge pull request 'v0.8.0' (#15) from v0.8.0 into main
Reviewed-on: #15
2026-04-14 19:30:31 +00:00
6f9f7eba8e Merge tag 'v0.8.0' of https://git.techniverse.net/scriptos/adguard-shield into v0.8.0 2026-04-14 21:24:44 +02:00
a79586de94 Merge tag 'v0.8.0' of https://git.techniverse.net/scriptos/adguard-shield into v0.8.0 2026-04-14 21:24:44 +02:00
a132b2a0f1 Merge pull request 'v0.8.0' (#14) from v0.8.0 into main
Reviewed-on: #14
2026-04-14 19:17:15 +00:00
b42f458d5a Merge pull request 'v0.8.0' (#14) from v0.8.0 into main
Reviewed-on: #14
2026-04-14 19:17:15 +00:00
70818698d1 Release: Version v0.8.0 2026-04-14 21:06:52 +02:00
83075f2782 Release: Version v0.8.0 2026-04-14 21:06:52 +02:00
0264e1e896 feat: Offense-Cleanup-Worker für automatisches Aufräumen abgelaufener Offense-Zähler 2026-04-14 21:01:51 +02:00
2a1d8ae975 feat: Offense-Cleanup-Worker für automatisches Aufräumen abgelaufener Offense-Zähler 2026-04-14 21:01:51 +02:00
df15a587ee update: Konfigurationsdatei aufräumen – Kommentare gekürzt, Verweis auf Doku ergänzt, fehlende Variable EXTERNAL_WHITELIST_CACHE_DIR hinzugefügt 2026-04-14 20:44:06 +02:00
0da5d01641 update: Konfigurationsdatei aufräumen – Kommentare gekürzt, Verweis auf Doku ergänzt, fehlende Variable EXTERNAL_WHITELIST_CACHE_DIR hinzugefügt 2026-04-14 20:44:06 +02:00
3d60771a1b feat: GeoIP-Länderfilter mit MaxMind Auto-Download 2026-04-14 20:30:37 +02:00
535be66b55 feat: GeoIP-Länderfilter mit MaxMind Auto-Download 2026-04-14 20:30:37 +02:00
4d7e053ce7 Merge pull request 'v0.7.1' (#13) from v0.7.1 into main
Reviewed-on: #13
2026-04-12 12:37:09 +00:00
2e78b9c14e Merge pull request 'v0.7.1' (#13) from v0.7.1 into main
Reviewed-on: #13
2026-04-12 12:37:09 +00:00
23deae7d81 Release: Version v0.7.1 2026-04-12 14:35:47 +02:00
0af79e7a28 Release: Version v0.7.1 2026-04-12 14:35:47 +02:00
0602fbb596 docs: README für docs-Verzeichnis hinzugefügt 2026-04-12 14:28:37 +02:00
606a28ed8e docs: README für docs-Verzeichnis hinzugefügt 2026-04-12 14:28:37 +02:00
a27c093d83 docs: Projektstruktur entfernt 2026-04-12 14:19:08 +02:00
77a5ebb144 docs: Projektstruktur entfernt 2026-04-12 14:19:08 +02:00
cfd6fa9b70 feat: Watchdog-Service für automatischen Health-Check und Recovery bei Service-Ausfall 2026-04-12 14:17:59 +02:00
01a99489ab Hier soll in wenigen Worten drin stehen, was geändert wurde, hinzugefügt wurde oder entfernt wurde. Du kannst Formate wie “fix”, “feat”, “update” und co verwenden. 2026-04-12 14:17:59 +02:00
2200e80f87 Merge pull request 'v0.7.0' (#12) from v0.7.0 into main
Reviewed-on: #12
2026-04-04 18:02:27 +00:00
6bdeb5bc31 Release: Version v0.7.0 2026-04-04 19:59:46 +02:00
5451c01603 Externe Whitelist mit DNS-Auflösung für dynamische IPs (DynDNS) 2026-04-04 19:57:07 +02:00
6daaf67f7c Merge pull request 'v0.6.2' (#11) from v0.6.2 into main
Reviewed-on: #11
2026-03-24 10:31:03 +00:00
0970218f9b Release: Version v0.6.2 2026-03-24 11:28:32 +01:00
db128f3076 Ignore .ki-workspace 2026-03-24 11:24:38 +01:00
6f14219445 Report: Aktivster Tag über konfigurierbaren Zeitraum statt Berichtsperiode 2026-03-24 10:50:58 +01:00
cb31aa48eb Merge pull request 'v0.6.1' (#10) from v0.6.1 into main
Reviewed-on: #10
2026-03-13 13:28:01 +00:00
1e8b7557e7 Release v0.6.1 2026-03-13 14:27:41 +01:00
4d1870cc85 Notifications optimiert. 2026-03-13 14:27:07 +01:00
ebcd70ce8b Merge pull request 'v0.6.0' (#9) from v0.6.0 into main
Reviewed-on: #9
2026-03-06 21:15:58 +00:00
ba342dd571 Release v0.6.0 2026-03-06 22:15:16 +01:00
ac1af85810 Performance stark optimiert. 2026-03-06 22:14:30 +01:00
54b6c877e5 Merge pull request 'v0.5.4' (#8) from v0.5.4 into main
Reviewed-on: #8
2026-03-06 20:23:43 +00:00
8562202aa7 Release v0.5.4 2026-03-06 21:23:06 +01:00
3361b571cf Report optimiert + weitere Fehler beseitigt. 2026-03-06 21:21:37 +01:00
86eeb2b947 Template überarbeitet + fix in der Generierung 2026-03-06 19:45:18 +01:00
cf915c5c80 Merge pull request 'v0.5.3' (#7) from v0.5.3 into main
Reviewed-on: #7
2026-03-06 16:38:00 +00:00
cf1e554a28 Das zusätzliche 'v' für Version entfernt. Wird jetzt in den Versionen selbst gesetzt. 2026-03-06 17:34:02 +01:00
657fdbaf6b 'doc' zu 'docs' umbenannt. 2026-03-06 16:07:55 +01:00
a39dc88770 Version in Uninstaller geändert. 2026-03-06 14:47:10 +01:00
19f72d5be4 Merge pull request 'v0.5.2' (#6) from v0.5.2 into main
Reviewed-on: #6
2026-03-06 12:14:05 +00:00
27 changed files with 3730 additions and 571 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.ki-workspace

View File

@@ -2,6 +2,7 @@
"files.eol": "\n",
"chat.tools.terminal.autoApprove": {
"Rename-Item": true,
"ForEach-Object": true
"ForEach-Object": true,
"&": true
}
}

236
README.md
View File

@@ -1,166 +1,168 @@
```
▄▄▄ ▓█████▄ ▄████ █ ██ ▄▄▄ ██▀███ ▓█████▄ ██████ ██░ ██ ██▓▓█████ ██▓ ▓█████▄
▒████▄ ▒██▀ ██▌ ██▒ ▀█▒ ██ ▓██▒▒████▄ ▓██ ▒ ██▒▒██▀ ██▌ ▒██ ▒ ▓██░ ██▒▓██▒▓█ ▀ ▓██▒ ▒██▀ ██▌
▒██ ▀█▄ ░██ █▌▒██░▄▄▄░▓██ ▒██░▒██ ▀█▄ ▓██ ░▄█ ▒░██ █▌ ░ ▓██▄ ▒██▀▀██░▒██▒▒███ ▒██░ ░██ █▌
░██▄▄▄▄██ ░▓█▄ ▌░▓█ ██▓▓▓█ ░██░░██▄▄▄▄██ ▒██▀▀█▄ ░▓█▄ ▌ ▒ ██▒░▓█ ░██ ░██░▒▓█ ▄ ▒██░ ░▓█▄ ▌
▓█ ▓██▒░▒████▓ ░▒▓███▀▒▒▒█████▓ ▓█ ▓██▒░██▓ ▒██▒░▒████▓ ▒██████▒▒░▓█▒░██▓░██░░▒████▒░██████▒░▒████▓
▒▒ ▓▒█░ ▒▒▓ ▒ ░▒ ▒ ░▒▓▒ ▒ ▒ ▒▒ ▓▒█░░ ▒▓ ░▒▓░ ▒▒▓ ▒ ▒ ▒▓▒ ▒ ░ ▒ ░░▒░▒░▓ ░░ ▒░ ░░ ▒░▓ ░ ▒▒▓ ▒
▒ ▒▒ ░ ░ ▒ ▒ ░ ░ ░░▒░ ░ ░ ▒ ▒▒ ░ ░▒ ░ ▒░ ░ ▒ ▒ ░ ░▒ ░ ░ ▒ ░▒░ ░ ▒ ░ ░ ░ ░░ ░ ▒ ░ ░ ▒ ▒
░ ▒ ░ ░ ░ ░ ░ ░ ░░░ ░ ░ ░ ▒ ░░ ░ ░ ░ ░ ░ ░ ░ ░ ░░ ░ ▒ ░ ░ ░ ░ ░ ░ ░
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
░ ░ ░
```
<p align="center">
<a href="https://techniverse.net">
<img src="https://assets.techniverse.net/f1/git/graphics/repo-techniverse-logo.png" alt="Techniverse Community" height="70" />
</a>
</p>
# AdGuard Shield
<h1 align="center">AdGuard Shield</h1>
Automatischer Schutz für deinen AdGuard Home DNS-Server gegen übermäßige Anfragen einzelner Clients. Überwacht die AdGuard Home API, erkennt Rate-Limit-Verstöße und sperrt missbrauchende Clients per iptables — für alle DNS-Protokolle (DNS, DoH, DoT, DoQ).
<h4 align="center">
Automatischer Schutz für AdGuard Home: erkennt auffällige DNS-Clients, sperrt sie per Firewall und hebt temporäre Sperren selbstständig wieder auf.
</h4>
## Was macht das Tool?
<h6 align="center">
<a href="https://www.cleveradmin.de">🏰 Website</a>
·
<a href="https://techniverse.net">📰 Community</a>
·
<a href="https://social.techniverse.net/@donnerwolke">🐘 Mastodon</a>
·
<a href="https://matrix.to/#/#support:techniverse.net">💬 Support</a>
</h6>
<br><br>
Wenn ein Client eine bestimmte Domain zu oft anfragt (z.B. >30x pro Minute), wird er automatisch auf Firewall-Ebene für alle DNS-Ports gesperrt. Nach einer konfigurierbaren Zeitspanne wird die Sperre automatisch aufgehoben.
## Features
## ✨ Was ist AdGuard Shield?
- Automatische Erkennung und Sperre bei Rate-Limit-Verstößen
- **Subdomain-Flood-Erkennung** — erkennt Random-Subdomain-Attacken (z.B. `abc123.microsoft.com`, `xyz456.microsoft.com`, ...)
- **Progressive Sperren (Recidive)** — Wiederholungstäter werden stufenweise länger gesperrt (wie bei fail2ban)
- Unterstützt **alle DNS-Protokolle**: DNS (53), DoH (443), DoT (853), DoQ (784/853/8853)
- **IPv4 + IPv6**
- Eigene iptables Chain — greift nicht in bestehende Regeln ein
- Automatisches Entsperren nach konfigurierbarer Dauer
- **Externe Blocklisten** — IP-Adressen von externen Textdateien (URLs) laden und automatisch sperren
- **AbuseIPDB Reporting** — permanent gesperrte IPs automatisch an AbuseIPDB melden
- **E-Mail Reports** — periodische Statistik-Reports als HTML oder TXT (täglich, wöchentlich, zweiwöchentlich, monatlich)
- **Ban-History** — lückenlose Protokollierung aller Sperren/Entsperrungen mit Zeitstempel
- Whitelist für vertrauenswürdige IPs
- Dry-Run Modus zum gefahrlosen Testen
- Benachrichtigungen (Discord, Slack, Gotify, Ntfy)
- systemd Service für dauerhaften Betrieb
AdGuard Shield überwacht das Query Log deiner AdGuard-Home-Instanz und erkennt Clients, die eine Domain oder viele zufällige Subdomains in kurzer Zeit übermäßig oft anfragen. Auffällige Clients werden über eine eigene `iptables`/`ip6tables`-Chain auf DNS-relevanten Ports blockiert.
## Voraussetzungen
Das schützt klassische DNS-Anfragen genauso wie DoH, DoT und DoQ, ohne deine bestehenden Firewall-Regeln unnötig anzufassen.
- Linux Server mit AdGuard Home (bare metal)
- Root-Zugriff (`sudo`)
- AdGuard Home Web-API erreichbar (Standard: Port 3000)
- Pakete: `curl`, `jq`, `iptables`, `gawk`, `systemd` — werden bei der Installation **automatisch** installiert
## 🚀 Highlights
## Schnellstart
- Automatische Sperren bei Rate-Limit-Verstößen
- Erkennung von Random-Subdomain-Floods, z.B. `abc123.example.com`
- DNS-Flood-Watchlist: sofortiger permanenter Ban + AbuseIPDB-Meldung für definierte Domains
- Progressive Sperren für Wiederholungstäter, ähnlich wie bei fail2ban
- Unterstützung für DNS, DoH, DoT, DoQ und DNSCrypt
- IPv4 und IPv6
- Eigene Firewall-Chain für sauberes Debugging und einfache Entfernung
- Externe Blocklisten und dynamische externe Whitelists
- GeoIP-Länderfilter mit Blocklist- oder Allowlist-Modus
- AbuseIPDB-Reporting für permanent gesperrte IPs
- Benachrichtigungen über Ntfy, Discord, Slack, Gotify oder Generic Webhook
- E-Mail-Reports als HTML oder Text
- Watchdog mit automatischem Health Check und Recovery
## ✅ Voraussetzungen
- Linux-Server mit AdGuard Home
- Root-Zugriff per `sudo`
- Erreichbare AdGuard Home Web-API, standardmäßig `http://127.0.0.1:3000`
- `curl`, `jq`, `iptables`, `gawk` und `systemd`
Die benötigten Pakete werden vom Installer automatisch installiert.
## ⚡ Schnellstart
```bash
# 1. Repository klonen
git clone https://git.techniverse.net/scriptos/adguard-shield.git /tmp/adguard-shield
cd /tmp/adguard-shield
# 2. Installer aufrufen (interaktives Menü)
# Interaktives Installationsmenü
sudo bash install.sh
# Oder direkt installieren:
sudo bash install.sh install
# 3. Erst im Dry-Run testen (loggt nur, sperrt nichts)
# Vor dem produktiven Start testen: loggt nur, sperrt nichts
sudo /opt/adguard-shield/adguard-shield.sh dry-run
# 4. Wenn alles passt — Service starten
# Service starten und prüfen
sudo systemctl start adguard-shield
sudo systemctl status adguard-shield
```
> **Hinweis:** Bei der Installation werden alle benötigten Abhängigkeiten automatisch installiert und der Service wird für den Autostart beim Booten registriert.
> Beim Installieren wird der systemd-Service für den Autostart registriert. Der Watchdog-Timer wird ebenfalls eingerichtet und prüft den Service regelmäßig.
[![asciicast](https://asciinema.techniverse.net/a/77.svg)](https://asciinema.techniverse.net/a/77)
## Wichtigste Befehle
## 🔧 Wichtigste Befehle
### Installation & Updates
```bash
# Installer-Menü
sudo bash install.sh # Interaktives Menü (Install/Update/Uninstall/Status)
sudo bash install.sh --help # Hilfe anzeigen
sudo bash install.sh update # Update mit automatischer Konfigurations-Migration
sudo bash install.sh status # Installationsstatus prüfen
# Deinstallation (kein install.sh benötigt)
sudo bash /opt/adguard-shield/uninstall.sh # Direkt aus dem Installationsverzeichnis
# Monitor
sudo /opt/adguard-shield/adguard-shield.sh status # Aktive Sperren anzeigen
sudo /opt/adguard-shield/adguard-shield.sh history # Ban-History anzeigen
sudo /opt/adguard-shield/adguard-shield.sh unban IP # Einzelne IP entsperren
sudo /opt/adguard-shield/adguard-shield.sh flush # Alle Sperren aufheben
sudo /opt/adguard-shield/adguard-shield.sh reset-offenses # Offense-Zähler zurücksetzen
sudo /opt/adguard-shield/adguard-shield.sh test # API-Verbindung testen
sudo /opt/adguard-shield/adguard-shield.sh blocklist-status # Externe Blocklisten Status
sudo /opt/adguard-shield/adguard-shield.sh blocklist-sync # Blocklisten manuell synchronisieren
sudo /opt/adguard-shield/report-generator.sh send # Report jetzt senden
sudo /opt/adguard-shield/report-generator.sh status # Report-Status anzeigen
sudo /opt/adguard-shield/report-generator.sh install # Cron-Job einrichten
sudo journalctl -u adguard-shield -f # Logs live verfolgen
sudo bash install.sh # Interaktives Menü
sudo bash install.sh install # Direkt installieren
sudo bash install.sh update # Update inkl. Konfigurations-Migration
sudo bash install.sh status # Installationsstatus prüfen
sudo bash /opt/adguard-shield/uninstall.sh
```
## Projektstruktur
### Betrieb & Diagnose
```
├── adguard-shield.sh # Haupt-Monitor-Script
├── adguard-shield.conf # Konfiguration
├── adguard-shield.service # systemd Unit
├── external-blocklist-worker.sh # Externer Blocklist-Worker
├── iptables-helper.sh # Manuelle iptables-Verwaltung
├── unban-expired.sh # Cron-basiertes Entsperren
├── report-generator.sh # E-Mail Report Generator
├── install.sh # Installer / Updater
├── uninstall.sh # Uninstaller (wird ins Installationsverzeichnis kopiert)
├── templates/
│ ├── report.html # HTML-Report-Template
│ └── report.txt # TXT-Report-Template
├── README.md
└── doc/
├── architektur.md # Architektur & Funktionsweise
├── konfiguration.md # Alle Parameter erklärt + Konfig-Migration
├── befehle.md # Vollständige Befehlsreferenz inkl. Installer
├── benachrichtigungen.md # Webhook-Setup (Discord, Slack, Gotify, Ntfy)
├── report.md # E-Mail Report Setup & Konfiguration
└── tipps-und-troubleshooting.md
```bash
sudo systemctl status adguard-shield
sudo systemctl restart adguard-shield
sudo journalctl -u adguard-shield -f
sudo /opt/adguard-shield/adguard-shield.sh status
sudo /opt/adguard-shield/adguard-shield.sh history
sudo /opt/adguard-shield/adguard-shield.sh test
sudo /opt/adguard-shield/adguard-shield.sh unban 192.0.2.10
sudo /opt/adguard-shield/adguard-shield.sh flush
```
## Dokumentation
### Optionale Module
| Dokument | Inhalt |
|----------|--------|
| [Architektur](doc/architektur.md) | Wie das Tool funktioniert, iptables-Strategie, Konfig-Migration |
| [Konfiguration](doc/konfiguration.md) | Alle Parameter, Ports, Whitelist-Pflege, automatische Migration |
| [Befehle](doc/befehle.md) | Vollständige Befehlsreferenz für Installer, Monitor, iptables-Helper und systemd |
| [Benachrichtigungen](doc/benachrichtigungen.md) | Setup für Discord, Slack, Gotify, Ntfy |
| [E-Mail Report](doc/report.md) | Periodische Statistik-Reports per E-Mail (HTML/TXT) |
| [Tipps & Troubleshooting](doc/tipps-und-troubleshooting.md) | Best Practices, häufige Probleme, Deinstallation |
```bash
sudo /opt/adguard-shield/adguard-shield.sh blocklist-status
sudo /opt/adguard-shield/adguard-shield.sh whitelist-status
sudo /opt/adguard-shield/adguard-shield.sh geoip-status
## Lizenz
sudo /opt/adguard-shield/report-generator.sh status
sudo /opt/adguard-shield/report-generator.sh send
sudo /opt/adguard-shield/report-generator.sh install
```
[MIT](LICENSE)
Die vollständige Befehlsreferenz steht in [docs/befehle.md](docs/befehle.md).
---
## ⚙️ Konfiguration
## 👥 Techniverse Community
Die zentrale Konfiguration liegt nach der Installation hier:
Lust auf Austausch rund um Matrix, Selfhosting und andere smarte IT-Lösungen?
In der **Techniverse Community** triffst du Gleichgesinnte, kannst Fragen stellen oder einfach nerdigen Talk genießen. 🚀
```text
/opt/adguard-shield/adguard-shield.conf
```
👉 **[Jetzt der Gruppe auf Matrix beitreten](https://matrix.to/#/#community:techniverse.net)**
~ Direkte Raumadresse: `#community:techniverse.net`
Wichtige Startpunkte:
👉 **[Für lockere Gespräche abseits der Kernthemen komm in den Talkraum](https://matrix.to/#/#talk:techniverse.net)**
~ Direkte Raumadresse: `#talk:techniverse.net`
- `ADGUARD_URL`, `ADGUARD_USER`, `ADGUARD_PASS` für die AdGuard-Home-API
- `RATE_LIMIT_MAX_REQUESTS`, `RATE_LIMIT_WINDOW` und `CHECK_INTERVAL` für die Erkennung
- `BAN_DURATION` und `PROGRESSIVE_BAN_*` für temporäre und progressive Sperren
- `WHITELIST` für vertrauenswürdige Clients wie Router, Management-IPs oder lokale Resolver
- `DNS_FLOOD_WATCHLIST_*` für sofortigen Permanent-Ban bei bekannten Flood-Domains
- `NOTIFY_*`, `REPORT_*`, `GEOIP_*`, `EXTERNAL_BLOCKLIST_*` und `EXTERNAL_WHITELIST_*` für optionale Funktionen
Wir freuen uns, wenn du dabei bist!
Bei Updates migriert der Installer die bestehende Konfiguration automatisch: vorhandene Werte bleiben erhalten, neue Parameter werden ergänzt und die alte Datei wird als `adguard-shield.conf.old` gesichert.
---
Mehr Details findest du in [docs/konfiguration.md](docs/konfiguration.md).
📝 **Blog:** [www.cleveradmin.de](https://www.cleveradmin.de)
🌐 **Webseite:** [www.patrick-asmus.de](https://www.patrick-asmus.de)
📧 **E-Mail:** [support@techniverse.net](mailto:support@techniverse.net)
## 🧭 Dokumentation
| Thema | Link |
|---|---|
| Architektur & Funktionsweise | [docs/architektur.md](docs/architektur.md) |
| Befehle & Nutzung | [docs/befehle.md](docs/befehle.md) |
| Konfiguration | [docs/konfiguration.md](docs/konfiguration.md) |
| Benachrichtigungen | [docs/benachrichtigungen.md](docs/benachrichtigungen.md) |
| E-Mail Report | [docs/report.md](docs/report.md) |
| Updates | [docs/update.md](docs/update.md) |
| Tipps & Troubleshooting | [docs/tipps-und-troubleshooting.md](docs/tipps-und-troubleshooting.md) |
## 🧩 Wie es arbeitet
1. AdGuard Shield liest regelmäßig das AdGuard-Home-Query-Log über die API.
2. Anfragen werden pro Client, Domain und Protokoll ausgewertet.
3. Überschreitet ein Client die konfigurierten Limits, wird er gegen Whitelist und Sonderregeln geprüft.
4. Die Sperre landet in der eigenen Firewall-Chain `ADGUARD_SHIELD`.
5. Ban-History, Logs und optionale Benachrichtigungen dokumentieren das Ereignis.
6. Temporäre Sperren werden automatisch entfernt, permanente Sperren bleiben bis zur manuellen Freigabe aktiv.
<br><br>
<p align="center">
<img src="https://assets.techniverse.net/f1/git/graphics/gray0-catonline.svg" alt="">
</p>
<p align="center">
<img src="https://assets.techniverse.net/f1/logos/small/license.png" alt="License" width="15" height="15"> <a href="./LICENSE">License</a> | <img src="https://assets.techniverse.net/f1/logos/small/matrix2.svg" alt="Matrix" width="15" height="15"> <a href="https://matrix.to/#/#community:techniverse.net">Matrix</a> | <img src="https://assets.techniverse.net/f1/logos/small/mastodon2.svg" alt="Mastodon" width="15" height="15"> <a href="https://social.techniverse.net/@donnerwolke">Mastodon</a>
</p>
<sub>
Patrick Asmus · Techniverse Network · <a href="./LICENSE">Lizenz</a>
</sub>
</p>

View File

@@ -0,0 +1,7 @@
[Unit]
Description=AdGuard Shield - Watchdog Health Check
Documentation=https://git.techniverse.net/scriptos/adguard-shield
[Service]
Type=oneshot
ExecStart=/opt/adguard-shield/adguard-shield-watchdog.sh

166
adguard-shield-watchdog.sh Normal file
View File

@@ -0,0 +1,166 @@
#!/bin/bash
###############################################################################
# AdGuard Shield - Watchdog
# Prüft ob der Hauptservice läuft und startet ihn bei Bedarf neu.
# Wird über adguard-shield-watchdog.timer alle 5 Minuten ausgeführt.
#
# Autor: Patrick Asmus
# E-Mail: support@techniverse.net
# Lizenz: MIT
###############################################################################
set -euo pipefail
INSTALL_DIR="/opt/adguard-shield"
CONFIG_FILE="${INSTALL_DIR}/adguard-shield.conf"
SERVICE_NAME="adguard-shield.service"
LOG_FILE="/var/log/adguard-shield.log"
WATCHDOG_STATE_FILE="/var/lib/adguard-shield/watchdog.state"
# ─── Logging ──────────────────────────────────────────────────────────────────
log() {
local level="$1"
shift
local message="$*"
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
local log_entry="[$timestamp] [WATCHDOG] [$level] $message"
echo "$log_entry" | tee -a "$LOG_FILE"
}
# ─── Benachrichtigung senden ──────────────────────────────────────────────────
send_watchdog_notification() {
local action="$1" # "recovery" oder "failure"
local detail="$2"
# Konfiguration laden für Benachrichtigungs-Einstellungen
if [[ ! -f "$CONFIG_FILE" ]]; then
return
fi
source "$CONFIG_FILE"
if [[ "${NOTIFY_ENABLED:-false}" != "true" ]]; then
return
fi
local my_hostname
my_hostname=$(hostname)
local title message
if [[ "$action" == "recovery" ]]; then
title="🔄 AdGuard Shield Watchdog"
message="🔄 AdGuard Shield Watchdog auf ${my_hostname}
---
Der Service war ausgefallen und wurde automatisch neu gestartet.
${detail}"
elif [[ "$action" == "failure" ]]; then
title="🚨 AdGuard Shield Watchdog"
message="🚨 AdGuard Shield Watchdog auf ${my_hostname}
---
Der Service konnte NICHT automatisch neu gestartet werden!
Manuelles Eingreifen erforderlich.
${detail}"
fi
case "${NOTIFY_TYPE:-}" in
discord)
local json_payload
json_payload=$(jq -nc --arg msg "$message" '{content: $msg}')
curl -s -H "Content-Type: application/json" \
-d "$json_payload" \
"$NOTIFY_WEBHOOK_URL" &>/dev/null || true
;;
slack)
local json_payload
json_payload=$(jq -nc --arg msg "$message" '{text: $msg}')
curl -s -H "Content-Type: application/json" \
-d "$json_payload" \
"$NOTIFY_WEBHOOK_URL" &>/dev/null || true
;;
gotify)
curl -s -X POST "$NOTIFY_WEBHOOK_URL" \
-F "title=${title}" \
-F "message=${message}" \
-F "priority=5" &>/dev/null || true
;;
ntfy)
if [[ -n "${NTFY_TOPIC:-}" ]]; then
local ntfy_url="${NTFY_SERVER_URL:-https://ntfy.sh}"
local auth_args=()
if [[ -n "${NTFY_TOKEN:-}" ]]; then
auth_args=(-H "Authorization: Bearer ${NTFY_TOKEN}")
fi
curl -s \
-H "Title: ${title}" \
-H "Priority: ${NTFY_PRIORITY:-5}" \
-H "Tags: warning,watchdog" \
"${auth_args[@]}" \
-d "$message" \
"${ntfy_url}/${NTFY_TOPIC}" &>/dev/null || true
fi
;;
generic)
local json_payload
json_payload=$(jq -nc --arg msg "$message" --arg act "watchdog_${action}" \
'{message: $msg, action: $act}')
curl -s -H "Content-Type: application/json" \
-d "$json_payload" \
"$NOTIFY_WEBHOOK_URL" &>/dev/null || true
;;
esac
}
# ─── Hauptlogik ──────────────────────────────────────────────────────────────
main() {
# Verzeichnis für State-Datei sicherstellen
mkdir -p "$(dirname "$WATCHDOG_STATE_FILE")"
# Prüfen ob der Service aktiv ist
if systemctl is-active --quiet "$SERVICE_NAME"; then
# Service läuft falls vorher ausgefallen war, Status zurücksetzen
if [[ -f "$WATCHDOG_STATE_FILE" ]]; then
rm -f "$WATCHDOG_STATE_FILE"
fi
exit 0
fi
# Service läuft NICHT Recovery versuchen
log "WARN" "Service $SERVICE_NAME ist nicht aktiv starte Recovery..."
# Zähler für fehlgeschlagene Recovery-Versuche
local fail_count=0
if [[ -f "$WATCHDOG_STATE_FILE" ]]; then
fail_count=$(cat "$WATCHDOG_STATE_FILE" 2>/dev/null || echo "0")
fi
# systemd reset-failed damit StartLimit zurückgesetzt wird
systemctl reset-failed "$SERVICE_NAME" 2>/dev/null || true
# Service starten
if systemctl start "$SERVICE_NAME" 2>/dev/null; then
# Kurz warten und prüfen ob er auch wirklich läuft
sleep 3
if systemctl is-active --quiet "$SERVICE_NAME"; then
log "INFO" "Service $SERVICE_NAME erfolgreich neu gestartet (Watchdog Recovery)"
send_watchdog_notification "recovery" "Versuch: $((fail_count + 1))"
rm -f "$WATCHDOG_STATE_FILE"
exit 0
fi
fi
# Start fehlgeschlagen
fail_count=$((fail_count + 1))
echo "$fail_count" > "$WATCHDOG_STATE_FILE"
log "ERROR" "Service $SERVICE_NAME konnte nicht gestartet werden (Fehlversuch: $fail_count)"
# Bei jedem 3. Fehlversuch eine Benachrichtigung senden (Spam vermeiden)
if [[ $((fail_count % 3)) -eq 1 ]]; then
send_watchdog_notification "failure" "Fehlversuche: $fail_count
Letzter Fehler: $(systemctl status "$SERVICE_NAME" 2>&1 | tail -5)"
fi
exit 1
}
main

View File

@@ -0,0 +1,11 @@
[Unit]
Description=AdGuard Shield - Watchdog Timer
Documentation=https://git.techniverse.net/scriptos/adguard-shield
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
AccuracySec=30s
[Install]
WantedBy=timers.target

View File

@@ -1,201 +1,106 @@
###############################################################################
# AdGuard Shield - Konfigurationsdatei
# Schutz vor übermäßigen DNS-Anfragen einzelner Clients
# Ausführliche Dokumentation: docs/konfiguration.md
###############################################################################
# --- AdGuard Home API Einstellungen ---
# URL der AdGuard Home Web-Oberfläche (ohne trailing slash)
# --- AdGuard Home API ---
ADGUARD_URL="https://dns1.domain.com"
# AdGuard Home Zugangsdaten (Web-UI Login)
ADGUARD_USER="admin"
ADGUARD_PASS='changeme'
# --- Rate-Limit Einstellungen ---
# Maximale Anfragen pro Domain pro Client innerhalb des Zeitfensters
RATE_LIMIT_MAX_REQUESTS=30
# --- Rate-Limit ---
RATE_LIMIT_MAX_REQUESTS=30 # Max. Anfragen pro Domain/Client im Zeitfenster
RATE_LIMIT_WINDOW=60 # Zeitfenster in Sekunden
CHECK_INTERVAL=10 # Prüfintervall in Sekunden
# Zeitfenster in Sekunden (60 = 1 Minute)
RATE_LIMIT_WINDOW=60
# Wie oft das Script die Logs prüft (in Sekunden)
CHECK_INTERVAL=10
# --- Subdomain-Flood-Erkennung (Random Subdomain Attack) ---
# Erkennt Bots/Clients die massenhaft zufällige Subdomains einer Domain abfragen
# Beispiel: abc123.microsoft.com, xyz456.microsoft.com, ...
# Dabei wird pro Client gezählt, wie viele EINDEUTIGE Subdomains einer
# Basisdomain (z.B. microsoft.com) im Zeitfenster aufgerufen werden.
# Subdomain-Flood-Erkennung aktivieren
# --- Subdomain-Flood-Erkennung ---
SUBDOMAIN_FLOOD_ENABLED=true
SUBDOMAIN_FLOOD_MAX_UNIQUE=50 # Max. eindeutige Subdomains pro Basisdomain/Client
SUBDOMAIN_FLOOD_WINDOW=60 # Zeitfenster in Sekunden
# Maximale Anzahl eindeutiger Subdomains pro Basisdomain pro Client im Zeitfenster
# Beispiel: 50 = ein Client darf max. 50 verschiedene Subdomains von microsoft.com abfragen
SUBDOMAIN_FLOOD_MAX_UNIQUE=50
# Zeitfenster in Sekunden für die Subdomain-Flood-Erkennung (60 = 1 Minute)
SUBDOMAIN_FLOOD_WINDOW=60
# --- DNS-Flood-Watchlist ---
DNS_FLOOD_WATCHLIST_ENABLED=false
DNS_FLOOD_WATCHLIST="" # Kommagetrennt, z.B. "example.com,evil.org"
# --- Sperr-Einstellungen ---
# Wie lange ein Client gesperrt wird (in Sekunden, 3600 = 1 Stunde)
BAN_DURATION=3600
# iptables Chain-Name für die Sperren
BAN_DURATION=3600 # Basis-Sperrdauer in Sekunden
IPTABLES_CHAIN="ADGUARD_SHIELD"
# Welche Ports gesperrt werden sollen (IPv4 + IPv6)
# Port 53 = DNS (UDP + TCP)
# Port 443 = DNS-over-HTTPS (DoH)
# Port 853 = DNS-over-TLS (tls://...:853) / DNS-over-QUIC (quic://...:853)
# Hinweis: Das verwendete Protokoll (DNS/DoH/DoT/DoQ) wird automatisch
# aus der AdGuard Home API erkannt und in Logs/History angezeigt.
BLOCKED_PORTS="53 443 853"
BLOCKED_PORTS="53 443 853" # DNS(53), DoH(443), DoT/DoQ(853)
# --- Whitelist ---
# IP-Adressen die NIEMALS gesperrt werden (kommagetrennt)
# Lokale Netze und wichtige Server hier eintragen
# IPs die niemals gesperrt werden (kommagetrennt)
WHITELIST="127.0.0.1,::1"
# --- Logging ---
# Log-Datei Pfad
LOG_FILE="/var/log/adguard-shield.log"
# Log-Level: DEBUG, INFO, WARN, ERROR
LOG_LEVEL="INFO"
# Maximale Größe der Log-Datei in MB (danach wird rotiert)
LOG_MAX_SIZE_MB=50
# Ban-History Datei (protokolliert alle Sperren & Entsperrungen dauerhaft)
LOG_LEVEL="INFO" # DEBUG, INFO, WARN, ERROR
LOG_MAX_SIZE_MB=50 # Max. Größe in MB (danach Rotation)
BAN_HISTORY_FILE="/var/log/adguard-shield-bans.log"
BAN_HISTORY_RETENTION_DAYS=0 # 0 = unbegrenzt
# Maximale Aufbewahrungsdauer der Ban-History in Tagen
# 0 = unbegrenzt (niemals automatisch löschen)
# Beispiel: 90 = Einträge älter als 90 Tage werden beim nächsten Report entfernt
BAN_HISTORY_RETENTION_DAYS=0
# --- Benachrichtigungen (optional) ---
# Aktiviert Benachrichtigungen bei Sperren/Entsperrungen
# --- Benachrichtigungen ---
NOTIFY_ENABLED=false
NOTIFY_TYPE="ntfy" # ntfy, discord, slack, gotify, generic
NOTIFY_WEBHOOK_URL="" # Webhook-URL (nicht für ntfy)
# Benachrichtigungs-Typ: "ntfy", "discord", "slack", "gotify", "generic"
NOTIFY_TYPE="ntfy"
# Webhook-URL (nur für discord, slack, gotify, generic bei ntfy nicht nötig)
# Discord: https://discord.com/api/webhooks/xxx/yyy
# Gotify: https://gotify.example.com/message?token=xxx
NOTIFY_WEBHOOK_URL=""
# --- Ntfy Einstellungen (nur bei NOTIFY_TYPE="ntfy") ---
# Server-URL der Ntfy-Instanz (ohne trailing slash)
# Ntfy-Einstellungen (nur bei NOTIFY_TYPE="ntfy")
NTFY_SERVER_URL="https://ntfy.sh"
# Topic-Name für die Benachrichtigungen
NTFY_TOPIC=""
# Optionaler Access-Token (leer lassen wenn nicht benötigt)
NTFY_TOKEN=""
NTFY_PRIORITY="4" # 1=min, 3=default, 5=max
# Priorität der Ntfy-Nachrichten (1=min, 3=default, 5=max)
NTFY_PRIORITY="4"
# --- E-Mail Report (optional) ---
# Regelmäßiger Statistik-Report per E-Mail
# Voraussetzung: Ein funktionierender Mail-Transport (z.B. msmtp)
# Anleitung für msmtp: https://www.cleveradmin.de/blog/2024/12/linux-einfach-emails-versenden-mit-msmtp/
# --- E-Mail Report ---
REPORT_ENABLED=false
# Report-Intervall: "daily", "weekly", "biweekly", "monthly"
# daily = täglich um die konfigurierte Uhrzeit
# weekly = wöchentlich am Montag
# biweekly = alle zwei Wochen am Montag
# monthly = monatlich am 1. des Monats
REPORT_INTERVAL="weekly"
# Uhrzeit für den Report-Versand (Format: HH:MM, 24h)
REPORT_INTERVAL="weekly" # daily, weekly, biweekly, monthly
REPORT_TIME="08:00"
# E-Mail-Empfänger
REPORT_EMAIL_TO="admin@example.com"
# E-Mail-Absender
REPORT_EMAIL_FROM="adguard-shield@example.com"
# E-Mail-Format: "html" oder "txt"
REPORT_FORMAT="html"
# Mail-Befehl (z.B. "msmtp", "sendmail", "mail")
REPORT_FORMAT="html" # html, txt
REPORT_MAIL_CMD="msmtp"
REPORT_BUSIEST_DAY_RANGE=30 # Tage für "Aktivster Tag" (0 = nur Berichtszeitraum)
# --- Externe Blocklist (optional) ---
# Aktiviert den externen Blocklist-Worker
# --- Externe Whitelist ---
# Externe Whitelist-Dateien mit Domains/IPs; Domains werden per DNS aufgelöst
EXTERNAL_WHITELIST_ENABLED=false
EXTERNAL_WHITELIST_URLS="" # URL(s) kommagetrennt
EXTERNAL_WHITELIST_INTERVAL=300 # Prüfintervall in Sekunden
EXTERNAL_WHITELIST_CACHE_DIR="/var/lib/adguard-shield/external-whitelist"
# --- Externe Blocklist ---
EXTERNAL_BLOCKLIST_ENABLED=false
# URL(s) zu externen Textdateien mit IP-Adressen (eine IP pro Zeile)
# Mehrere URLs kommagetrennt angeben
# Beispiel: "https://example.com/blocklist.txt,https://other.com/bad-ips.txt"
EXTERNAL_BLOCKLIST_URLS=""
# Wie oft die externe Blocklist geprüft wird (in Sekunden, 300 = 5 Minuten)
EXTERNAL_BLOCKLIST_INTERVAL=300
# Sperrdauer für externe Blocklist-IPs in Sekunden (0 = permanent bis IP aus Liste entfernt)
EXTERNAL_BLOCKLIST_BAN_DURATION=0
# Automatisch IPs entsperren die aus der externen Liste entfernt wurden?
EXTERNAL_BLOCKLIST_URLS="" # URL(s) kommagetrennt
EXTERNAL_BLOCKLIST_INTERVAL=300 # Prüfintervall in Sekunden
EXTERNAL_BLOCKLIST_BAN_DURATION=0 # 0 = permanent bis IP aus Liste entfernt
EXTERNAL_BLOCKLIST_AUTO_UNBAN=true
# Benachrichtigungen bei Blocklist-Sperren senden?
# Bei Listen mit vielen IPs empfiehlt sich false, da sonst beim Sync
# hunderte Benachrichtigungen auf einmal verschickt werden.
EXTERNAL_BLOCKLIST_NOTIFY=false
# Lokaler Cache-Pfad für die heruntergeladene Blocklist
EXTERNAL_BLOCKLIST_NOTIFY=false # Bei großen Listen auf false lassen
EXTERNAL_BLOCKLIST_CACHE_DIR="/var/lib/adguard-shield/external-blocklist"
# --- Progressive Sperren (Recidive) ---
# Wiederholungstäter werden stufenweise länger gesperrt (wie bei fail2ban)
# Aktiviert das progressive Sperrsystem
# Wiederholungstäter werden stufenweise länger gesperrt
PROGRESSIVE_BAN_ENABLED=true
PROGRESSIVE_BAN_MULTIPLIER=2 # Multiplikator pro Stufe (2 = Verdopplung)
PROGRESSIVE_BAN_MAX_LEVEL=5 # Ab dieser Stufe permanent sperren (0 = nie)
PROGRESSIVE_BAN_RESET_AFTER=86400 # Zähler-Reset nach X Sekunden ohne Vergehen
# Multiplikator pro Wiederholung (2 = Verdopplung der Sperrdauer)
# Stufe 1: BAN_DURATION × 1 (Standard-Sperrdauer)
# Stufe 2: BAN_DURATION × 2
# Stufe 3: BAN_DURATION × 4
# Stufe 4: BAN_DURATION × 8 ... usw.
PROGRESSIVE_BAN_MULTIPLIER=2
# Ab dieser Stufe wird die IP permanent gesperrt (0 = nie permanent sperren)
# Beispiel: 5 = nach dem 5. Vergehen wird die IP dauerhaft gesperrt
PROGRESSIVE_BAN_MAX_LEVEL=5
# Nach wie vielen Sekunden ohne erneutes Vergehen wird der Zähler zurückgesetzt
# (86400 = 24 Stunden, 604800 = 7 Tage)
PROGRESSIVE_BAN_RESET_AFTER=86400
# --- AbuseIPDB Reporting (optional) ---
# Meldet permanent gesperrte IPs automatisch an AbuseIPDB
# Nur bei PERMANENTEN Sperren wird ein Report gesendet.
# --- AbuseIPDB Reporting ---
# Meldet nur permanent gesperrte IPs an AbuseIPDB
ABUSEIPDB_ENABLED=false
# AbuseIPDB API-Key (https://www.abuseipdb.com/account/api)
ABUSEIPDB_API_KEY=""
ABUSEIPDB_CATEGORIES="4" # 4 = DDoS Attack (siehe abuseipdb.com/categories)
# Kategorien für den Report (kommagetrennt)
# 4 = DDoS Attack
# Siehe: https://www.abuseipdb.com/categories
ABUSEIPDB_CATEGORIES="4"
# --- GeoIP-basierte Länderfilter ---
# Sperrt/erlaubt DNS-Anfragen nach Herkunftsland (lokale DB, keine Online-API)
GEOIP_ENABLED=false
GEOIP_MODE="blocklist" # blocklist oder allowlist
GEOIP_COUNTRIES="" # ISO 3166-1 Alpha-2 Codes, z.B. "CN,RU,KP,IR"
GEOIP_CHECK_INTERVAL=0 # 0 = nutzt CHECK_INTERVAL
GEOIP_NOTIFY=true
GEOIP_SKIP_PRIVATE=true # Private IPs ausnehmen
GEOIP_LICENSE_KEY="" # MaxMind GeoLite2 Key (optional, für Auto-Download)
GEOIP_MMDB_PATH="" # Manueller DB-Pfad (optional, hat Vorrang)
# --- Erweiterte Einstellungen ---
# Pfad zur State-Datei (speichert aktive Sperren)
STATE_DIR="/var/lib/adguard-shield"
# Pfad zum PID-File
PID_FILE="/var/run/adguard-shield.pid"
# Anzahl der API-Einträge die pro Abfrage geholt werden (max 5000)
API_QUERY_LIMIT=500
# Dry-Run Modus: true = nur loggen, nicht sperren (zum Testen)
DRY_RUN=false
API_QUERY_LIMIT=500 # API-Einträge pro Abfrage (max 5000)
DRY_RUN=false # true = nur loggen, nicht sperren

View File

@@ -4,7 +4,7 @@ Documentation=https://git.techniverse.net/scriptos/adguard-shield
After=network.target AdGuardHome.service
Wants=AdGuardHome.service
StartLimitBurst=5
StartLimitIntervalSec=60
StartLimitIntervalSec=300
[Service]
Type=simple
@@ -14,7 +14,7 @@ ExecReload=/bin/kill -HUP $MAINPID
# Neustart-Verhalten
Restart=on-failure
RestartSec=10
RestartSec=30
# Sicherheits-Hardening
ProtectSystem=full

View File

@@ -5,11 +5,10 @@
#
# Autor: Patrick Asmus
# E-Mail: support@techniverse.net
# Datum: 2026-03-04
# Lizenz: MIT
###############################################################################
VERSION="v0.5.2"
VERSION="v0.9.0"
set -euo pipefail
@@ -231,6 +230,30 @@ format_protocol() {
esac
}
# ─── Hostname-Auflösung ──────────────────────────────────────────────────────
# Versucht den Hostnamen einer IP per Reverse-DNS aufzulösen
resolve_hostname() {
local ip="$1"
local hostname=""
# Versuche Reverse-DNS-Auflösung via dig
if command -v dig &>/dev/null; then
hostname=$(dig +short -x "$ip" 2>/dev/null | head -1 | sed 's/\.$//')
fi
# Fallback via host
if [[ -z "$hostname" ]] && command -v host &>/dev/null; then
hostname=$(host "$ip" 2>/dev/null | awk '/domain name pointer/ {print $NF}' | sed 's/\.$//' | head -1)
fi
# Fallback via getent
if [[ -z "$hostname" ]] && command -v getent &>/dev/null; then
hostname=$(getent hosts "$ip" 2>/dev/null | awk '{print $2}' | head -1)
fi
echo "${hostname:-(unbekannt)}"
}
# ─── AbuseIPDB Reporting ─────────────────────────────────────────────────────
# Meldet eine IP an AbuseIPDB (nur bei permanenten Sperren)
report_to_abuseipdb() {
@@ -306,6 +329,9 @@ cleanup() {
sleep 1
fi
stop_blocklist_worker
stop_whitelist_worker
stop_geoip_worker
stop_offense_cleanup_worker
rm -f "$PID_FILE"
exit 0
}
@@ -333,6 +359,39 @@ is_whitelisted() {
return 0
fi
done
# Externe Whitelist prüfen (aufgelöste IPs aus dem Whitelist-Worker)
local ext_wl_file="${EXTERNAL_WHITELIST_CACHE_DIR:-/var/lib/adguard-shield/external-whitelist}/resolved_ips.txt"
if [[ -f "$ext_wl_file" ]] && grep -qxF "$ip" "$ext_wl_file" 2>/dev/null; then
return 0
fi
return 1
}
# ─── DNS-Flood-Watchlist Prüfung ────────────────────────────────────────────
is_dns_flood_watchlist_match() {
local domain="$1"
if [[ "${DNS_FLOOD_WATCHLIST_ENABLED:-false}" != "true" ]]; then
return 1
fi
if [[ -z "${DNS_FLOOD_WATCHLIST:-}" ]]; then
return 1
fi
local entry
IFS=',' read -ra watchlist_entries <<< "$DNS_FLOOD_WATCHLIST"
for entry in "${watchlist_entries[@]}"; do
entry=$(echo "$entry" | xargs)
[[ -z "$entry" ]] && continue
if [[ "$domain" == "$entry" || "$domain" == *".$entry" ]]; then
return 0
fi
done
return 1
}
@@ -392,6 +451,13 @@ ban_client() {
fi
fi
# DNS-Flood-Watchlist: Sofort permanent sperren
if [[ "$reason" == "dns-flood-watchlist" ]]; then
is_permanent=true
effective_duration=0
log "WARN" "DNS-Flood-Watchlist: Erzwinge permanente Sperre für $client_ip ($domain)"
fi
local ban_until
local ban_until_display
if [[ "$is_permanent" == "true" ]]; then
@@ -406,16 +472,16 @@ ban_client() {
duration_display=$(format_duration "$effective_duration")
if [[ "$DRY_RUN" == "true" ]]; then
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" ]]; then
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" && "$reason" != "dns-flood-watchlist" ]]; then
log "WARN" "[DRY-RUN] WÜRDE sperren: $client_ip (${count}x $domain in ${window}s via $protocol) für ${duration_display} [Stufe $offense_level] [${reason}]"
else
log "WARN" "[DRY-RUN] WÜRDE sperren: $client_ip (${count}x $domain in ${window}s via $protocol) [${reason}]"
log "WARN" "[DRY-RUN] WÜRDE sperren: $client_ip (${count}x $domain in ${window}s via $protocol) für ${duration_display} [${reason}]"
fi
log_ban_history "DRY" "$client_ip" "$domain" "$count" "dry-run (${reason})" "${duration_display}" "$protocol"
return 0
fi
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" ]]; then
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" && "$reason" != "dns-flood-watchlist" ]]; then
log "WARN" "SPERRE Client: $client_ip (${count}x $domain in ${window}s via $protocol) für ${duration_display} [Stufe ${offense_level}/${PROGRESSIVE_BAN_MAX_LEVEL:-0}] [${reason}]"
else
log "WARN" "SPERRE Client: $client_ip (${count}x $domain in ${window}s via $protocol) für ${duration_display} [${reason}]"
@@ -447,12 +513,12 @@ EOF
# Ban-History Eintrag
local history_duration="${duration_display}"
[[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" ]] && history_duration="${duration_display} (Stufe ${offense_level})"
[[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" && "$reason" != "dns-flood-watchlist" ]] && history_duration="${duration_display} (Stufe ${offense_level})"
log_ban_history "BAN" "$client_ip" "$domain" "$count" "$reason" "$history_duration" "$protocol"
# Benachrichtigung senden
if [[ "$NOTIFY_ENABLED" == "true" ]]; then
send_notification "ban" "$client_ip" "$domain" "$count" "$offense_level" "$duration_display" "$reason" "$window" "$protocol"
send_notification "ban" "$client_ip" "$domain" "$count" "$offense_level" "$duration_display" "$reason" "$window" "$protocol" "$is_permanent"
fi
# AbuseIPDB Report (nur bei permanenter Sperre)
@@ -490,7 +556,7 @@ unban_client() {
log_ban_history "UNBAN" "$client_ip" "$domain" "-" "$reason" "-" "$protocol"
if [[ "$NOTIFY_ENABLED" == "true" ]]; then
send_notification "unban" "$client_ip" "" ""
send_notification "unban" "$client_ip" "$domain" ""
fi
}
@@ -532,6 +598,7 @@ send_notification() {
local reason="${7:-rate-limit}"
local window="${8:-$RATE_LIMIT_WINDOW}"
local protocol="${9:-DNS}"
local is_permanent="${10:-false}"
# Ntfy benötigt keine Webhook-URL (nutzt NTFY_SERVER_URL + NTFY_TOPIC)
if [[ "$NOTIFY_TYPE" != "ntfy" && -z "$NOTIFY_WEBHOOK_URL" ]]; then
@@ -540,47 +607,94 @@ send_notification() {
local reason_label="Rate-Limit"
[[ "$reason" == "subdomain-flood" ]] && reason_label="Subdomain-Flood"
[[ "$reason" == "dns-flood-watchlist" ]] && reason_label="DNS-Flood-Watchlist"
local title
local message
local my_hostname
my_hostname=$(hostname)
if [[ "$action" == "ban" ]]; then
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" && -n "$offense_level" ]]; then
message="🚫 AdGuard Shield: Client **$client_ip** gesperrt (${count}x $domain in ${window}s via **$protocol**, ${reason_label}). Sperre für **${duration_display}** [Stufe ${offense_level}/${PROGRESSIVE_BAN_MAX_LEVEL:-0}]."
else
local simple_dur
simple_dur=$(format_duration "${BAN_DURATION}")
message="🚫 AdGuard Shield: Client **$client_ip** gesperrt (${count}x $domain in ${window}s via **$protocol**, ${reason_label}). Sperre für ${simple_dur}."
title="🚨 🛡️ AdGuard Shield"
local client_hostname
client_hostname=$(resolve_hostname "$client_ip")
# AbuseIPDB-Hinweis bei permanenter Sperre
local abuseipdb_hint=""
if [[ "$is_permanent" == "true" && "${ABUSEIPDB_ENABLED:-false}" == "true" ]]; then
abuseipdb_hint=$'\n⚠ IP wurde an AbuseIPDB gemeldet'
fi
# Dauer-Anzeige mit Stufe (nicht bei Watchlist dort ist es immer permanent)
local dur_line
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" && -n "$offense_level" && "$reason" != "dns-flood-watchlist" ]]; then
dur_line="**${duration_display}** [Stufe ${offense_level}/${PROGRESSIVE_BAN_MAX_LEVEL:-0}]"
else
dur_line="**${duration_display}**"
fi
message="🚫 AdGuard Shield Ban auf ${my_hostname}${abuseipdb_hint}
---
IP: ${client_ip}
Hostname: ${client_hostname}
Grund: ${count}x ${domain} in ${window}s via ${protocol}, ${reason_label}
Dauer: ${dur_line}
Whois: https://www.whois.com/whois/${client_ip}
AbuseIPDB: https://www.abuseipdb.com/check/${client_ip}"
elif [[ "$action" == "unban" ]]; then
title="✅ AdGuard Shield"
local client_hostname
client_hostname=$(resolve_hostname "$client_ip")
message="✅ AdGuard Shield Freigabe auf ${my_hostname}
---
IP: ${client_ip}
Hostname: ${client_hostname}
AbuseIPDB: https://www.abuseipdb.com/check/${client_ip}"
elif [[ "$action" == "service_start" ]]; then
message="🟢 AdGuard Shield v${VERSION} wurde gestartet."
title=" AdGuard Shield"
message="🟢 AdGuard Shield ${VERSION} wurde auf ${my_hostname} gestartet."
elif [[ "$action" == "service_stop" ]]; then
message="🔴 AdGuard Shield v${VERSION} wurde gestoppt."
else
message="✅ AdGuard Shield: Client **$client_ip** wurde entsperrt."
title="🚨 🛡️ AdGuard Shield"
message="🔴 AdGuard Shield ${VERSION} wurde auf ${my_hostname} gestoppt."
fi
case "$NOTIFY_TYPE" in
discord)
local json_payload
json_payload=$(jq -nc --arg msg "$message" '{content: $msg}')
curl -s -H "Content-Type: application/json" \
-d "{\"content\": \"$message\"}" \
-d "$json_payload" \
"$NOTIFY_WEBHOOK_URL" &>/dev/null &
;;
slack)
local json_payload
json_payload=$(jq -nc --arg msg "$message" '{text: $msg}')
curl -s -H "Content-Type: application/json" \
-d "{\"text\": \"$message\"}" \
-d "$json_payload" \
"$NOTIFY_WEBHOOK_URL" &>/dev/null &
;;
gotify)
local clean_message
clean_message=$(echo "$message" | sed 's/\*\*//g')
curl -s -X POST "$NOTIFY_WEBHOOK_URL" \
-F "title=AdGuard Shield" \
-F "message=$message" \
-F "title=${title}" \
-F "message=${clean_message}" \
-F "priority=5" &>/dev/null &
;;
ntfy)
send_ntfy_notification "$action" "$message"
send_ntfy_notification "$action" "$title" "$message"
;;
generic)
local json_payload
json_payload=$(jq -nc --arg msg "$message" --arg act "$action" --arg cl "${client_ip:-}" --arg dom "${domain:-}" \
'{message: $msg, action: $act, client: $cl, domain: $dom}')
curl -s -H "Content-Type: application/json" \
-d "{\"message\": \"$message\", \"action\": \"$action\", \"client\": \"${client_ip:-}\", \"domain\": \"${domain:-}\"}" \
-d "$json_payload" \
"$NOTIFY_WEBHOOK_URL" &>/dev/null &
;;
esac
@@ -589,7 +703,8 @@ send_notification() {
# ─── Ntfy Benachrichtigung ───────────────────────────────────────────────────
send_ntfy_notification() {
local action="$1"
local message="$2"
local title="$2"
local message="$3"
if [[ -z "${NTFY_TOPIC:-}" ]]; then
log "WARN" "Ntfy: Kein Topic konfiguriert (NTFY_TOPIC ist leer)"
@@ -598,7 +713,6 @@ send_ntfy_notification() {
local ntfy_url="${NTFY_SERVER_URL:-https://ntfy.sh}"
local priority="${NTFY_PRIORITY:-4}"
local title="AdGuard Shield"
local tags
if [[ "$action" == "ban" ]]; then
@@ -615,11 +729,18 @@ send_ntfy_notification() {
local clean_message
clean_message=$(echo "$message" | sed 's/\*\*//g')
# Ntfy fügt Emojis über Tags hinzu → Titel ohne führende Emojis setzen
local ntfy_title
case "$action" in
ban) ntfy_title="🛡️ AdGuard Shield" ;;
*) ntfy_title="AdGuard Shield" ;;
esac
local -a curl_args=(
-s
-X POST
"${ntfy_url}/${NTFY_TOPIC}"
-H "Title: ${title}"
-H "Title: ${ntfy_title}"
-H "Priority: ${priority}"
-H "Tags: ${tags}"
-d "${clean_message}"
@@ -752,7 +873,13 @@ analyze_queries() {
continue
fi
ban_client "$client" "$domain" "$count" "rate-limit" "$RATE_LIMIT_WINDOW" "$proto_display"
local ban_reason="rate-limit"
if is_dns_flood_watchlist_match "$domain"; then
ban_reason="dns-flood-watchlist"
log "WARN" "DNS-Flood-Watchlist Treffer: $client$domain (${count}x in ${RATE_LIMIT_WINDOW}s) → permanenter Ban + AbuseIPDB"
fi
ban_client "$client" "$domain" "$count" "$ban_reason" "$RATE_LIMIT_WINDOW" "$proto_display"
fi
done <<< "$violations"
}
@@ -874,7 +1001,13 @@ analyze_subdomain_flood() {
continue
fi
ban_client "$client" "*.${base_domain}" "$unique_count" "subdomain-flood" "$window" "$proto_display"
local ban_reason="subdomain-flood"
if is_dns_flood_watchlist_match "$base_domain"; then
ban_reason="dns-flood-watchlist"
log "WARN" "DNS-Flood-Watchlist Treffer (Subdomain-Flood): $client → *.${base_domain} → permanenter Ban + AbuseIPDB"
fi
ban_client "$client" "*.${base_domain}" "$unique_count" "$ban_reason" "$window" "$proto_display"
done <<< "$violations"
}
@@ -902,6 +1035,25 @@ show_status() {
echo ""
fi
# DNS-Flood-Watchlist Info
if [[ "${DNS_FLOOD_WATCHLIST_ENABLED:-false}" == "true" ]]; then
echo " 🎯 DNS-Flood-Watchlist: AKTIV"
echo " Domains: ${DNS_FLOOD_WATCHLIST:-<keine>}"
echo " Aktion: Sofort permanenter Ban + AbuseIPDB-Meldung"
echo ""
fi
# GeoIP-Filter Info
if [[ "${GEOIP_ENABLED:-false}" == "true" ]]; then
local geoip_mode_label
[[ "${GEOIP_MODE:-blocklist}" == "blocklist" ]] && geoip_mode_label="Blocklist" || geoip_mode_label="Allowlist"
echo " 🌍 GeoIP-Filter: AKTIV"
echo " Modus: ${geoip_mode_label}"
echo " Länder: ${GEOIP_COUNTRIES:-<keine>}"
echo " Sperrdauer: PERMANENT (Auto-Unban bei Änderung der Länderliste)"
echo ""
fi
# Aktive Sperren
local ban_count=0
if [[ -d "$STATE_DIR" ]]; then
@@ -923,6 +1075,7 @@ show_status() {
local reason_tag=""
[[ "$s_reason" == "subdomain-flood" ]] && reason_tag=" (Subdomain-Flood)"
[[ "$s_reason" == "dns-flood-watchlist" ]] && reason_tag=" (DNS-Flood-Watchlist)"
local count_info=""
if [[ -n "$s_count" && "$s_count" != "-" ]]; then
@@ -935,7 +1088,9 @@ show_status() {
local proto_tag=" via ${s_proto}"
if [[ "$s_perm" == "true" ]]; then
if [[ "$s_perm" == "true" && "$s_reason" == "dns-flood-watchlist" ]]; then
echo " 🚫 Gesperrt: $s_ip$s_domain [PERMANENT${count_info}${proto_tag}]${reason_tag}"
elif [[ "$s_perm" == "true" ]]; then
echo " 🚫 Gesperrt: $s_ip$s_domain [PERMANENT, Stufe ${s_level:-?}${count_info}${proto_tag}]${reason_tag}"
elif [[ -n "$s_level" && "$s_level" -gt 0 ]]; then
echo " 🚫 Gesperrt: $s_ip$s_domain [Stufe ${s_level}, $(format_duration "${s_dur:-$BAN_DURATION}"), bis $s_until${count_info}${proto_tag}]${reason_tag}"
@@ -1094,16 +1249,116 @@ stop_blocklist_worker() {
fi
}
# ─── Externer Whitelist-Worker starten ───────────────────────────────────────
start_whitelist_worker() {
if [[ "${EXTERNAL_WHITELIST_ENABLED:-false}" != "true" ]]; then
log "DEBUG" "Externer Whitelist-Worker ist deaktiviert"
return
fi
local worker_script="${SCRIPT_DIR}/external-whitelist-worker.sh"
if [[ ! -f "$worker_script" ]]; then
log "WARN" "Whitelist-Worker Script nicht gefunden: $worker_script"
return
fi
log "INFO" "Starte externen Whitelist-Worker im Hintergrund..."
bash "$worker_script" start &
WHITELIST_WORKER_PID=$!
log "INFO" "Whitelist-Worker gestartet (PID: $WHITELIST_WORKER_PID)"
}
# ─── Externer Whitelist-Worker stoppen ───────────────────────────────────────
stop_whitelist_worker() {
local worker_pid_file="/var/run/adguard-whitelist-worker.pid"
if [[ -f "$worker_pid_file" ]]; then
local wpid
wpid=$(cat "$worker_pid_file")
if kill -0 "$wpid" 2>/dev/null; then
log "INFO" "Stoppe Whitelist-Worker (PID: $wpid)..."
kill "$wpid" 2>/dev/null || true
rm -f "$worker_pid_file"
fi
fi
}
# ─── GeoIP-Worker starten ────────────────────────────────────────────────────
start_geoip_worker() {
if [[ "${GEOIP_ENABLED:-false}" != "true" ]]; then
log "DEBUG" "GeoIP-Worker ist deaktiviert"
return
fi
local worker_script="${SCRIPT_DIR}/geoip-worker.sh"
if [[ ! -f "$worker_script" ]]; then
log "WARN" "GeoIP-Worker Script nicht gefunden: $worker_script"
return
fi
log "INFO" "Starte GeoIP-Worker im Hintergrund..."
bash "$worker_script" start &
GEOIP_WORKER_PID=$!
log "INFO" "GeoIP-Worker gestartet (PID: $GEOIP_WORKER_PID)"
}
# ─── GeoIP-Worker stoppen ────────────────────────────────────────────────────
stop_geoip_worker() {
local worker_pid_file="/var/run/adguard-geoip-worker.pid"
if [[ -f "$worker_pid_file" ]]; then
local wpid
wpid=$(cat "$worker_pid_file")
if kill -0 "$wpid" 2>/dev/null; then
log "INFO" "Stoppe GeoIP-Worker (PID: $wpid)..."
kill "$wpid" 2>/dev/null || true
rm -f "$worker_pid_file"
fi
fi
}
# ─── Offense-Cleanup-Worker starten ──────────────────────────────────────────
start_offense_cleanup_worker() {
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" != "true" ]]; then
log "DEBUG" "Offense-Cleanup-Worker ist deaktiviert (Progressive Sperren inaktiv)"
return
fi
local worker_script="${SCRIPT_DIR}/offense-cleanup-worker.sh"
if [[ ! -f "$worker_script" ]]; then
log "WARN" "Offense-Cleanup-Worker Script nicht gefunden: $worker_script"
return
fi
log "INFO" "Starte Offense-Cleanup-Worker im Hintergrund (nice 19, idle I/O)..."
nice -n 19 ionice -c 3 bash "$worker_script" start &
OFFENSE_CLEANUP_WORKER_PID=$!
log "INFO" "Offense-Cleanup-Worker gestartet (PID: $OFFENSE_CLEANUP_WORKER_PID)"
}
# ─── Offense-Cleanup-Worker stoppen ──────────────────────────────────────────
stop_offense_cleanup_worker() {
local worker_pid_file="/var/run/adguard-offense-cleanup-worker.pid"
if [[ -f "$worker_pid_file" ]]; then
local wpid
wpid=$(cat "$worker_pid_file")
if kill -0 "$wpid" 2>/dev/null; then
log "INFO" "Stoppe Offense-Cleanup-Worker (PID: $wpid)..."
kill "$wpid" 2>/dev/null || true
rm -f "$worker_pid_file"
fi
fi
}
# ─── Hauptschleife ──────────────────────────────────────────────────────────
main_loop() {
log "INFO" "═══════════════════════════════════════════════════════════"
log "INFO" "AdGuard Shield v${VERSION} gestartet"
log "INFO" "AdGuard Shield ${VERSION} gestartet"
log "INFO" " Limit: ${RATE_LIMIT_MAX_REQUESTS} Anfragen pro ${RATE_LIMIT_WINDOW}s"
log "INFO" " Sperrdauer: $(format_duration "${BAN_DURATION}")"
log "INFO" " Prüfintervall: ${CHECK_INTERVAL}s"
log "INFO" " Dry-Run: ${DRY_RUN}"
log "INFO" " Whitelist: ${WHITELIST}"
log "INFO" " Externe Blocklist: ${EXTERNAL_BLOCKLIST_ENABLED:-false}"
log "INFO" " Externe Whitelist: ${EXTERNAL_WHITELIST_ENABLED:-false}"
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" ]]; then
log "INFO" " Progressive Sperren: AKTIV (×${PROGRESSIVE_BAN_MULTIPLIER:-2}, Max-Stufe: ${PROGRESSIVE_BAN_MAX_LEVEL:-0}, Reset: $(format_duration "${PROGRESSIVE_BAN_RESET_AFTER:-86400}"))"
else
@@ -1114,11 +1369,24 @@ main_loop() {
else
log "INFO" " Subdomain-Flood-Schutz: deaktiviert"
fi
if [[ "${DNS_FLOOD_WATCHLIST_ENABLED:-false}" == "true" ]]; then
log "INFO" " DNS-Flood-Watchlist: AKTIV (Domains: ${DNS_FLOOD_WATCHLIST:-<keine>})"
else
log "INFO" " DNS-Flood-Watchlist: deaktiviert"
fi
if [[ "${ABUSEIPDB_ENABLED:-false}" == "true" ]]; then
log "INFO" " AbuseIPDB Reporting: AKTIV (Kategorien: ${ABUSEIPDB_CATEGORIES:-4})"
else
log "INFO" " AbuseIPDB Reporting: deaktiviert"
fi
if [[ "${GEOIP_ENABLED:-false}" == "true" ]]; then
log "INFO" " GeoIP-Filter: AKTIV (Modus: ${GEOIP_MODE:-blocklist}, Länder: ${GEOIP_COUNTRIES:-<keine>})"
else
log "INFO" " GeoIP-Filter: deaktiviert"
fi
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" ]]; then
log "INFO" " Offense-Cleanup: AKTIV (Reset: $(format_duration "${PROGRESSIVE_BAN_RESET_AFTER:-86400}"), Prüfintervall: 1h)"
fi
log "INFO" "═══════════════════════════════════════════════════════════"
# Service-Start-Benachrichtigung senden
@@ -1129,6 +1397,15 @@ main_loop() {
# Blocklist-Worker als Hintergrundprozess starten
start_blocklist_worker
# Whitelist-Worker als Hintergrundprozess starten
start_whitelist_worker
# GeoIP-Worker als Hintergrundprozess starten
start_geoip_worker
# Offense-Cleanup-Worker als Hintergrundprozess starten
start_offense_cleanup_worker
while true; do
# Abgelaufene Sperren prüfen
check_expired_bans
@@ -1150,7 +1427,7 @@ trap cleanup SIGTERM SIGINT SIGHUP
# ─── Kommandozeilen-Argumente ────────────────────────────────────────────────
case "${1:-start}" in
start)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] AdGuard Shield v${VERSION} wird gestartet..."
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] AdGuard Shield ${VERSION} wird gestartet..."
check_dependencies
check_already_running
init_directories
@@ -1195,6 +1472,74 @@ case "${1:-start}" in
echo "Blocklist-Worker nicht gefunden"
fi
;;
whitelist-status)
init_directories
_worker_script="${SCRIPT_DIR}/external-whitelist-worker.sh"
if [[ -f "$_worker_script" ]]; then
bash "$_worker_script" status
else
echo "Whitelist-Worker nicht gefunden"
fi
;;
whitelist-sync)
init_directories
_worker_script="${SCRIPT_DIR}/external-whitelist-worker.sh"
if [[ -f "$_worker_script" ]]; then
bash "$_worker_script" sync
else
echo "Whitelist-Worker nicht gefunden"
fi
;;
whitelist-flush)
init_directories
_worker_script="${SCRIPT_DIR}/external-whitelist-worker.sh"
if [[ -f "$_worker_script" ]]; then
bash "$_worker_script" flush
else
echo "Whitelist-Worker nicht gefunden"
fi
;;
geoip-status)
init_directories
_worker_script="${SCRIPT_DIR}/geoip-worker.sh"
if [[ -f "$_worker_script" ]]; then
bash "$_worker_script" status
else
echo "GeoIP-Worker nicht gefunden"
fi
;;
geoip-sync)
init_directories
setup_iptables_chain
_worker_script="${SCRIPT_DIR}/geoip-worker.sh"
if [[ -f "$_worker_script" ]]; then
bash "$_worker_script" sync
else
echo "GeoIP-Worker nicht gefunden"
fi
;;
geoip-flush)
init_directories
_worker_script="${SCRIPT_DIR}/geoip-worker.sh"
if [[ -f "$_worker_script" ]]; then
bash "$_worker_script" flush
else
echo "GeoIP-Worker nicht gefunden"
fi
;;
geoip-lookup)
if [[ -z "${2:-}" ]]; then
echo "Nutzung: $0 geoip-lookup <IP-Adresse>" >&2
exit 1
fi
init_directories
_worker_script="${SCRIPT_DIR}/geoip-worker.sh"
if [[ -f "$_worker_script" ]]; then
bash "$_worker_script" lookup "$2"
else
echo "GeoIP-Worker nicht gefunden"
fi
;;
status)
init_directories
show_status
@@ -1250,7 +1595,7 @@ case "${1:-start}" in
;;
*)
cat << USAGE
AdGuard Shield v${VERSION}
AdGuard Shield ${VERSION}
Service-Steuerung (empfohlen):
sudo systemctl start adguard-shield
@@ -1258,7 +1603,7 @@ Service-Steuerung (empfohlen):
sudo systemctl restart adguard-shield
sudo systemctl status adguard-shield
Nutzung: $0 {status|history|flush|unban|reset-offenses|test|dry-run|blocklist-status|blocklist-sync|blocklist-flush}
Nutzung: $0 {status|history|flush|unban|reset-offenses|test|dry-run|blocklist-status|blocklist-sync|blocklist-flush|whitelist-status|whitelist-sync|whitelist-flush|geoip-status|geoip-sync|geoip-flush|geoip-lookup}
Verwaltungsbefehle:
status Zeigt aktive Sperren, Regeln und Wiederholungstäter
@@ -1271,6 +1616,13 @@ Verwaltungsbefehle:
blocklist-status Zeigt Status der externen Blocklisten
blocklist-sync Einmalige Synchronisation der externen Blocklisten
blocklist-flush Entfernt alle Sperren der externen Blocklisten
whitelist-status Zeigt Status der externen Whitelisten
whitelist-sync Einmalige Synchronisation der externen Whitelisten
whitelist-flush Entfernt alle aufgelösten Whitelist-IPs
geoip-status Zeigt Status der GeoIP-Länderfilter
geoip-sync Einmalige GeoIP-Prüfung aller aktiven Clients
geoip-flush Alle GeoIP-Sperren aufheben
geoip-lookup IP GeoIP-Lookup für eine einzelne IP-Adresse
Interne Befehle (nicht direkt verwenden — nur über systemd):
start Startet den Monitor im Vordergrund

View File

@@ -1,126 +0,0 @@
# Webhook-Benachrichtigungen
Das Tool kann beim Starten und Stoppen des Services sowie bei Sperren und Entsperrungen Benachrichtigungen an verschiedene Dienste senden.
## Aktivierung
In der Konfiguration (`adguard-shield.conf`):
```bash
NOTIFY_ENABLED=true
NOTIFY_TYPE="<typ>"
NOTIFY_WEBHOOK_URL="<url>"
```
## Ntfy
```bash
NOTIFY_ENABLED=true
NOTIFY_TYPE="ntfy"
NTFY_SERVER_URL="https://ntfy.sh"
NTFY_TOPIC="adguard-shield"
NTFY_TOKEN=""
NTFY_PRIORITY="4"
```
> **Hinweis:** Bei Ntfy wird `NOTIFY_WEBHOOK_URL` nicht benötigt Server-URL und Topic werden separat konfiguriert.
**Eigene Ntfy-Instanz:**
```bash
NTFY_SERVER_URL="https://ntfy.mein-server.de"
NTFY_TOPIC="dns-security"
NTFY_TOKEN="tk_mein_geheimer_token"
```
**Prioritäten:**
| Wert | Bedeutung |
|------|-----------|
| 1 | Minimum |
| 2 | Niedrig |
| 3 | Standard |
| 4 | Hoch |
| 5 | Maximum |
**Token erstellen (Self-hosted):**
1. Ntfy Web-UI → Benutzer/Tokens
2. Token kopieren und in `NTFY_TOKEN` eintragen
3. Bei ntfy.sh: Account erstellen → Access Token generieren
## Discord
```bash
NOTIFY_ENABLED=true
NOTIFY_TYPE="discord"
NOTIFY_WEBHOOK_URL="https://discord.com/api/webhooks/xxx/yyy"
```
**Webhook erstellen:**
1. Discord Server → Servereinstellungen → Integrationen → Webhooks
2. Neuer Webhook → URL kopieren
## Gotify
```bash
NOTIFY_ENABLED=true
NOTIFY_TYPE="gotify"
NOTIFY_WEBHOOK_URL="https://gotify.example.com/message?token=xxx"
```
**Token erstellen:**
1. Gotify Web-UI → Apps → App erstellen
2. Token kopieren und in die URL einfügen
## Slack
```bash
NOTIFY_ENABLED=true
NOTIFY_TYPE="slack"
NOTIFY_WEBHOOK_URL="https://hooks.slack.com/services/xxx/yyy/zzz"
```
**Webhook erstellen:**
1. Slack App → Incoming Webhooks aktivieren
2. Webhook-URL kopieren
## Generic (eigener Endpoint)
```bash
NOTIFY_ENABLED=true
NOTIFY_TYPE="generic"
NOTIFY_WEBHOOK_URL="https://your-server.com/webhook"
```
Sendet einen POST mit JSON-Body:
```json
{
"message": "🚫 AdGuard Shield: Client 192.168.1.50 gesperrt ...",
"action": "ban",
"client": "192.168.1.50",
"domain": "microsoft.com"
}
```
## Benachrichtigungen und externe Blocklisten
Bei Sperren aus der **externen Blocklist** werden Benachrichtigungen separat über `EXTERNAL_BLOCKLIST_NOTIFY` gesteuert — unabhängig von `NOTIFY_ENABLED`.
| Parameter | Standard | Beschreibung |
|-----------|----------|--------------|
| `EXTERNAL_BLOCKLIST_NOTIFY` | `false` | Benachrichtigungen bei Blocklist-Sperren aktivieren |
> **Wichtig:** Bei großen Listen `EXTERNAL_BLOCKLIST_NOTIFY=false` belassen. Beim ersten Sync (oder nach einem `blocklist-flush`) werden alle IPs der Liste auf einmal gesperrt — mit `true` würde das zu einer Nachrichten-Flut im Notification-Channel führen. Nur auf `true` setzen, wenn die Liste sehr klein ist.
## Beispiel-Nachrichten
**Service gestartet:**
> 🟢 AdGuard Shield v0.4.0 wurde gestartet.
**Service gestoppt:**
> 🔴 AdGuard Shield v0.4.0 wurde gestoppt.
**Sperre:**
> 🚫 AdGuard Shield: Client **192.168.1.50** gesperrt (45x microsoft.com in 60s). Sperre für 3600s.
**Entsperrung:**
> ✅ AdGuard Shield: Client **192.168.1.50** wurde entsperrt.

15
docs/README.md Normal file
View File

@@ -0,0 +1,15 @@
# Dokumentation
Hier findest du die vollständige Dokumentation zu AdGuard Shield.
## Inhaltsverzeichnis
| Dokument | Beschreibung |
|---|---|
| [Architektur & Funktionsweise](architektur.md) | Überblick über den Systemaufbau, Datenfluss und die internen Komponenten |
| [Befehle & Nutzung](befehle.md) | Alle verfügbaren Befehle des Installers, des Hauptskripts und des Watchdogs |
| [Konfiguration](konfiguration.md) | Beschreibung aller Konfigurationsparameter in `adguard-shield.conf` |
| [Webhook-Benachrichtigungen](benachrichtigungen.md) | Einrichtung von Push-Benachrichtigungen über Telegram, Discord, Gotify u.a. |
| [E-Mail Report](report.md) | Konfiguration des automatischen Statistik-Reports per E-Mail |
| [Update-Anleitung](update.md) | Schritt-für-Schritt-Anleitung zum Aktualisieren einer bestehenden Installation |
| [Tipps & Troubleshooting](tipps-und-troubleshooting.md) | Best Practices, häufige Probleme und deren Lösungen |

View File

@@ -56,6 +56,17 @@
> **Hinweis:** Die Subdomain-Flood-Erkennung hat ein eigenes Zeitfenster (`SUBDOMAIN_FLOOD_WINDOW`) und einen eigenen Schwellwert (`SUBDOMAIN_FLOOD_MAX_UNIQUE`), unabhängig von den Rate-Limit-Einstellungen.
### DNS-Flood-Watchlist-Sperre
1. Client `10.0.0.42` fragt `microsoft.com` 35x in 60 Sekunden an
2. Monitor erkennt: 35 > 30 (Limit überschritten)
3. Domain `microsoft.com` steht auf der DNS-Flood-Watchlist → **sofortige permanente Sperre**
4. Progressive-Ban-Stufe wird ignoriert — kein stufenweises Hochstufen
5. IP wird an AbuseIPDB gemeldet (falls aktiviert)
6. Permanente Sperre bleibt bis zur manuellen Freigabe aktiv
> **Hinweis:** Die Watchlist greift sowohl bei normalen Rate-Limit-Verstößen als auch bei Subdomain-Flood-Erkennungen. Subdomains werden automatisch erkannt: `foo.microsoft.com` matcht den Watchlist-Eintrag `microsoft.com`.
## iptables Strategie
Das Tool erstellt eine eigene Chain `ADGUARD_SHIELD`:
@@ -132,17 +143,26 @@ Das ermöglicht:
├── adguard-shield.sh # Haupt-Monitor-Script
├── adguard-shield.conf # Konfiguration (chmod 600)
├── adguard-shield.conf.old # Backup der Konfig nach Update
├── adguard-shield-watchdog.sh # Watchdog Health-Check-Script
├── iptables-helper.sh # iptables Verwaltung
├── external-blocklist-worker.sh # Externer Blocklist-Worker
── unban-expired.sh # Cron-basiertes Entsperren
── external-whitelist-worker.sh # Externer Whitelist-Worker (DNS-Auflösung)
├── geoip-worker.sh # GeoIP-Länderfilter-Worker
├── offense-cleanup-worker.sh # Aufräumen abgelaufener Offense-Zähler (nice 19, idle I/O)
├── unban-expired.sh # Cron-basiertes Entsperren
└── geoip/ # Auto-Download MaxMind GeoLite2 DB (optional)
/etc/systemd/system/
── adguard-shield.service # systemd Service (Autostart aktiv)
── adguard-shield.service # systemd Service (Autostart aktiv)
├── adguard-shield-watchdog.service # systemd Watchdog-Unit (oneshot)
└── adguard-shield-watchdog.timer # systemd Timer (alle 5 Min.)
/var/lib/adguard-shield/
├── *.ban # State-Dateien aktiver Sperren
├── *.offenses # Offense-Zähler (Progressive Sperren)
── external-blocklist/ # Cache für externe Blocklisten
── external-blocklist/ # Cache für externe Blocklisten
├── external-whitelist/ # Cache für externe Whitelisten + aufgelöste IPs
└── geoip-cache/ # Cache für GeoIP-Lookups (24h)
/var/log/
├── adguard-shield.log # Laufzeit-Log
@@ -155,8 +175,8 @@ Der Installer (`install.sh`) bietet ein interaktives Menü und folgende Funktion
| Befehl | Beschreibung |
|--------|--------------|
| `install` | Vollständige Neuinstallation (Abhängigkeiten, Dateien, Konfiguration, Service) |
| `update` | Update mit automatischer Konfigurations-Migration und Service-Neustart |
| `install` | Vollständige Neuinstallation (Abhängigkeiten, Dateien, Konfiguration, Service, Watchdog) |
| `update` | Update mit automatischer Konfigurations-Migration, Watchdog-Aktivierung und Service-Neustart |
| `uninstall` | Deinstallation mit optionalem Behalten der Konfiguration |
| `status` | Installationsstatus, Version und Service-Status anzeigen |
| `--help` | Hilfe und Befehlsübersicht |
@@ -201,8 +221,10 @@ ZEITSTEMPEL | AKTION | CLIENT-IP | DOMAIN
|-------|----------|
| `rate-limit` | Automatische Sperre wegen Limit-Überschreitung |
| `subdomain-flood` | Sperre wegen zu vieler eindeutiger Subdomains einer Basisdomain |
| `dns-flood-watchlist` | Sofortige permanente Sperre + AbuseIPDB-Meldung (Domain auf der Watchlist) |
| `dry-run` | Im Dry-Run erkannt (nicht wirklich gesperrt) |
| `dry-run (subdomain-flood)` | Subdomain-Flood im Dry-Run erkannt |
| `dry-run (dns-flood-watchlist)` | Watchlist-Treffer im Dry-Run erkannt |
| `expired` | Automatisch entsperrt nach Ablauf der Sperrdauer |
| `expired-cron` | Entsperrt durch den Cron-Job (`unban-expired.sh`) |
| `manual` | Manuell entsperrt per `unban`-Befehl |

View File

@@ -26,7 +26,7 @@ sudo bash install.sh --help
## Uninstaller (eigenständig)
Ab Version 0.6 wird bei der Installation ein eigenständiger Uninstaller nach `/opt/adguard-shield/uninstall.sh` kopiert. Die Deinstallation kann damit **ohne die originalen Installationsdateien** durchgeführt werden:
Ab Version 0.5.2 wird bei der Installation ein eigenständiger Uninstaller nach `/opt/adguard-shield/uninstall.sh` kopiert. Die Deinstallation kann damit **ohne die originalen Installationsdateien** durchgeführt werden:
```bash
# Direkt aus dem Installationsverzeichnis — kein install.sh benötigt
@@ -42,8 +42,9 @@ Beim Update passiert automatisch:
2. Die bestehende Konfiguration wird als `adguard-shield.conf.old` gesichert
3. Neue Konfigurationsparameter werden automatisch zur bestehenden Konfig hinzugefügt
4. Bestehende Einstellungen bleiben **immer** erhalten
5. Der systemd Service wird per `daemon-reload` neu geladen
6. Der Service wird automatisch neu gestartet (falls er lief)
5. Der systemd Service und Watchdog-Timer werden per `daemon-reload` neu geladen
6. Der Watchdog-Timer wird automatisch aktiviert (falls noch nicht aktiv)
7. Der Service wird automatisch neu gestartet (falls er lief)
### API-Verbindungstest nach Installation
@@ -86,6 +87,31 @@ sudo systemctl disable adguard-shield
> **Hinweis:** Der Service wird bei der Installation automatisch für den Autostart beim Booten aktiviert. Nach einem Update wird der Service automatisch neu gestartet — ein manueller Neustart ist nicht nötig.
## Watchdog (automatischer Health Check)
Der Watchdog prüft alle 5 Minuten ob der Hauptservice läuft und startet ihn bei Bedarf automatisch neu. Er wird als systemd Timer betrieben und bei der Installation automatisch aktiviert.
```bash
# Watchdog-Status
sudo systemctl status adguard-shield-watchdog.timer
# Nächste geplante Ausführung anzeigen
sudo systemctl list-timers adguard-shield-watchdog.timer
# Watchdog aktivieren / deaktivieren
sudo systemctl enable adguard-shield-watchdog.timer
sudo systemctl disable adguard-shield-watchdog.timer
# Watchdog starten / stoppen
sudo systemctl start adguard-shield-watchdog.timer
sudo systemctl stop adguard-shield-watchdog.timer
# Watchdog-Logs anzeigen
sudo journalctl -u adguard-shield-watchdog.service --no-pager -n 20
```
> **Hinweis:** Der Watchdog sendet automatisch Benachrichtigungen (falls `NOTIFY_ENABLED=true`), wenn er den Service wiederbeleben muss oder die Recovery fehlschlägt.
## Monitor — Verwaltungsbefehle
Die folgenden Befehle dienen der **Verwaltung und Diagnose** und können jederzeit ausgeführt werden, auch während der Service läuft:
@@ -158,6 +184,40 @@ sudo /opt/adguard-shield/iptables-helper.sh save
sudo /opt/adguard-shield/iptables-helper.sh restore
```
## Externer Whitelist-Worker
Der Whitelist-Worker löst Domains aus externen Listen regelmäßig per DNS auf und stellt die IPs als dynamische Whitelist bereit:
```bash
# Status anzeigen (aufgelöste IPs, konfigurierte Listen)
sudo /opt/adguard-shield/adguard-shield.sh whitelist-status
# Einmalige Synchronisation (z.B. nach Konfigurationsänderung)
sudo /opt/adguard-shield/adguard-shield.sh whitelist-sync
# Alle aufgelösten Whitelist-IPs entfernen
sudo /opt/adguard-shield/adguard-shield.sh whitelist-flush
```
Der Worker kann auch standalone gesteuert werden:
```bash
# Worker manuell starten (normalerweise automatisch per Hauptscript)
sudo /opt/adguard-shield/external-whitelist-worker.sh start
# Worker stoppen
sudo /opt/adguard-shield/external-whitelist-worker.sh stop
# Einmalige Synchronisation
sudo /opt/adguard-shield/external-whitelist-worker.sh sync
# Status anzeigen
sudo /opt/adguard-shield/external-whitelist-worker.sh status
# Aufgelöste IPs entfernen
sudo /opt/adguard-shield/external-whitelist-worker.sh flush
```
## Externer Blocklist-Worker
Der Worker kann auch standalone gesteuert werden:
@@ -179,6 +239,69 @@ sudo /opt/adguard-shield/external-blocklist-worker.sh status
sudo /opt/adguard-shield/external-blocklist-worker.sh flush
```
## GeoIP-Worker (Länderfilter)
Der GeoIP-Worker prüft Client-IPs auf ihr Herkunftsland und sperrt/erlaubt sie basierend auf der Konfiguration:
```bash
# GeoIP-Status anzeigen (Modus, Länder, aktive Sperren, verfügbare Tools)
sudo /opt/adguard-shield/adguard-shield.sh geoip-status
# Einmalige GeoIP-Prüfung aller aktiven Clients
sudo /opt/adguard-shield/adguard-shield.sh geoip-sync
# Alle GeoIP-Sperren aufheben
sudo /opt/adguard-shield/adguard-shield.sh geoip-flush
# GeoIP-Lookup für eine einzelne IP
sudo /opt/adguard-shield/adguard-shield.sh geoip-lookup 8.8.8.8
```
Der Worker kann auch standalone gesteuert werden:
```bash
# Worker manuell starten (normalerweise automatisch per Hauptscript)
sudo /opt/adguard-shield/geoip-worker.sh start
# Worker stoppen
sudo /opt/adguard-shield/geoip-worker.sh stop
# Einmalige Synchronisation
sudo /opt/adguard-shield/geoip-worker.sh sync
# Status anzeigen
sudo /opt/adguard-shield/geoip-worker.sh status
# IP nachschlagen
sudo /opt/adguard-shield/geoip-worker.sh lookup 1.2.3.4
# Alle GeoIP-Sperren aufheben
sudo /opt/adguard-shield/geoip-worker.sh flush
# GeoIP-Lookup-Cache leeren
sudo /opt/adguard-shield/geoip-worker.sh flush-cache
```
## Offense-Cleanup-Worker
Der Offense-Cleanup-Worker räumt abgelaufene Offense-Zähler (progressive Sperren) automatisch auf. Er startet automatisch mit dem Hauptservice, wenn progressive Sperren aktiviert sind, und prüft stündlich ob Zähler aufgeräumt werden können. Der Worker läuft mit niedrigster CPU- und I/O-Priorität (`nice 19`, `ionice idle`), um den DNS-Dienst nicht zu beeinträchtigen.
Der Worker kann auch standalone gesteuert werden:
```bash
# Worker manuell starten (normalerweise automatisch per Hauptscript)
sudo /opt/adguard-shield/offense-cleanup-worker.sh start
# Worker stoppen
sudo /opt/adguard-shield/offense-cleanup-worker.sh stop
# Einmaliger Cleanup-Durchlauf
sudo /opt/adguard-shield/offense-cleanup-worker.sh run-once
# Status anzeigen (aktive/abgelaufene Zähler)
sudo /opt/adguard-shield/offense-cleanup-worker.sh status
```
## E-Mail Report
```bash

221
docs/benachrichtigungen.md Normal file
View File

@@ -0,0 +1,221 @@
# Webhook-Benachrichtigungen
Das Tool kann beim Starten und Stoppen des Services sowie bei Sperren und Entsperrungen Benachrichtigungen an verschiedene Dienste senden.
## Aktivierung
In der Konfiguration (`adguard-shield.conf`):
```bash
NOTIFY_ENABLED=true
NOTIFY_TYPE="<typ>"
NOTIFY_WEBHOOK_URL="<url>"
```
## Ntfy
```bash
NOTIFY_ENABLED=true
NOTIFY_TYPE="ntfy"
NTFY_SERVER_URL="https://ntfy.sh"
NTFY_TOPIC="adguard-shield"
NTFY_TOKEN=""
NTFY_PRIORITY="4"
```
> **Hinweis:** Bei Ntfy wird `NOTIFY_WEBHOOK_URL` nicht benötigt Server-URL und Topic werden separat konfiguriert.
**Eigene Ntfy-Instanz:**
```bash
NTFY_SERVER_URL="https://ntfy.mein-server.de"
NTFY_TOPIC="dns-security"
NTFY_TOKEN="tk_mein_geheimer_token"
```
**Prioritäten:**
| Wert | Bedeutung |
|------|-----------|
| 1 | Minimum |
| 2 | Niedrig |
| 3 | Standard |
| 4 | Hoch |
| 5 | Maximum |
**Token erstellen (Self-hosted):**
1. Ntfy Web-UI → Benutzer/Tokens
2. Token kopieren und in `NTFY_TOKEN` eintragen
3. Bei ntfy.sh: Account erstellen → Access Token generieren
## Discord
```bash
NOTIFY_ENABLED=true
NOTIFY_TYPE="discord"
NOTIFY_WEBHOOK_URL="https://discord.com/api/webhooks/xxx/yyy"
```
**Webhook erstellen:**
1. Discord Server → Servereinstellungen → Integrationen → Webhooks
2. Neuer Webhook → URL kopieren
## Gotify
```bash
NOTIFY_ENABLED=true
NOTIFY_TYPE="gotify"
NOTIFY_WEBHOOK_URL="https://gotify.example.com/message?token=xxx"
```
**Token erstellen:**
1. Gotify Web-UI → Apps → App erstellen
2. Token kopieren und in die URL einfügen
## Slack
```bash
NOTIFY_ENABLED=true
NOTIFY_TYPE="slack"
NOTIFY_WEBHOOK_URL="https://hooks.slack.com/services/xxx/yyy/zzz"
```
**Webhook erstellen:**
1. Slack App → Incoming Webhooks aktivieren
2. Webhook-URL kopieren
## Generic (eigener Endpoint)
```bash
NOTIFY_ENABLED=true
NOTIFY_TYPE="generic"
NOTIFY_WEBHOOK_URL="https://your-server.com/webhook"
```
Sendet einen POST mit JSON-Body:
```json
{
"message": "🚫 AdGuard Shield Ban auf dns1\n---\nIP: 192.168.1.50\nHostname: client.local\nGrund: 45x microsoft.com in 60s via DNS, Rate-Limit\nDauer: 1h 0m\n\nWhois: https://www.whois.com/whois/192.168.1.50\nAbuseIPDB: https://www.abuseipdb.com/check/192.168.1.50",
"action": "ban",
"client": "192.168.1.50",
"domain": "microsoft.com"
}
```
## Benachrichtigungen und externe Blocklisten
Bei Sperren aus der **externen Blocklist** werden Benachrichtigungen separat über `EXTERNAL_BLOCKLIST_NOTIFY` gesteuert — unabhängig von `NOTIFY_ENABLED`.
| Parameter | Standard | Beschreibung |
|-----------|----------|--------------|
| `EXTERNAL_BLOCKLIST_NOTIFY` | `false` | Benachrichtigungen bei Blocklist-Sperren aktivieren |
> **Wichtig:** Bei großen Listen `EXTERNAL_BLOCKLIST_NOTIFY=false` belassen. Beim ersten Sync (oder nach einem `blocklist-flush`) werden alle IPs der Liste auf einmal gesperrt — mit `true` würde das zu einer Nachrichten-Flut im Notification-Channel führen. Nur auf `true` setzen, wenn die Liste sehr klein ist.
## Beispiel-Nachrichten
### Service gestartet
**Überschrift:** ✅ AdGuard Shield
> 🟢 AdGuard Shield v0.9.0 wurde auf dns1 gestartet.
### Service gestoppt
**Überschrift:** 🚨 🛡️ AdGuard Shield
> 🔴 AdGuard Shield v0.9.0 wurde auf dns1 gestoppt.
### Watchdog — Service wiederhergestellt
**Überschrift:** 🔄 AdGuard Shield Watchdog
> 🔄 AdGuard Shield Watchdog auf dns1
> ---
> Der Service war ausgefallen und wurde automatisch neu gestartet.
> Versuch: 1
### Watchdog — Recovery fehlgeschlagen
**Überschrift:** 🚨 AdGuard Shield Watchdog
> 🚨 AdGuard Shield Watchdog auf dns1
> ---
> Der Service konnte NICHT automatisch neu gestartet werden!
> Manuelles Eingreifen erforderlich.
> Fehlversuche: 1
> Letzter Fehler: (systemd Statusausgabe)
### Sperre (Ban)
**Überschrift:** 🚨 🛡️ AdGuard Shield
> 🚫 AdGuard Shield Ban auf dns1
> ---
> IP: 95.71.42.116
> Hostname: example-host.provider.net
> Grund: 153x radioportal.techniverse.net in 60s via DNS, Rate-Limit
> Dauer: 1h 0m [Stufe 1/5]
>
> Whois: https://www.whois.com/whois/95.71.42.116
> AbuseIPDB: https://www.abuseipdb.com/check/95.71.42.116
Bei permanenter Sperre mit aktiviertem AbuseIPDB-Reporting erscheint zusätzlich:
> 🚫 AdGuard Shield Ban auf dns1
> ⚠️ IP wurde an AbuseIPDB gemeldet
> ---
> IP: 95.71.42.116
> Hostname: example-host.provider.net
> Grund: 153x radioportal.techniverse.net in 60s via DNS, Rate-Limit
> Dauer: PERMANENT [Stufe 5/5]
>
> Whois: https://www.whois.com/whois/95.71.42.116
> AbuseIPDB: https://www.abuseipdb.com/check/95.71.42.116
Bei DNS-Flood-Watchlist-Treffer (sofort permanent, ohne Stufe):
> 🚫 AdGuard Shield Ban auf dns1
> ⚠️ IP wurde an AbuseIPDB gemeldet
> ---
> IP: 95.71.42.116
> Hostname: example-host.provider.net
> Grund: 45x microsoft.com in 60s via DNS, DNS-Flood-Watchlist
> Dauer: PERMANENT
>
> Whois: https://www.whois.com/whois/95.71.42.116
> AbuseIPDB: https://www.abuseipdb.com/check/95.71.42.116
### Entsperrung (Unban)
**Überschrift:** ✅ AdGuard Shield
> ✅ AdGuard Shield Freigabe auf dns1
> ---
> IP: 95.71.42.116
> Hostname: example-host.provider.net
>
> AbuseIPDB: https://www.abuseipdb.com/check/95.71.42.116
### Externe Blocklist Sperre
**Überschrift:** 🚨 🛡️ AdGuard Shield
> 🚫 AdGuard Shield Ban auf dns1 (Externe Blocklist)
> ---
> IP: 203.0.113.50
> Hostname: bad-actor.example.com
>
> Whois: https://www.whois.com/whois/203.0.113.50
> AbuseIPDB: https://www.abuseipdb.com/check/203.0.113.50
### Externe Blocklist Entsperrung
**Überschrift:** ✅ AdGuard Shield
> ✅ AdGuard Shield Freigabe auf dns1 (Externe Blocklist)
> ---
> IP: 203.0.113.50
> Hostname: bad-actor.example.com
>
> AbuseIPDB: https://www.abuseipdb.com/check/203.0.113.50
### Hinweise
- Der **Hostname** der IP wird automatisch per Reverse-DNS aufgelöst (`dig`, `host` oder `getent`). Ist kein PTR-Record vorhanden, wird `(unbekannt)` angezeigt.
- Der **Servername** (`dns1` in den Beispielen) wird dynamisch über `$(hostname)` ermittelt und zeigt, auf welchem Server das Ereignis stattfand.
- Die **Überschrift** unterscheidet sich je nach Aktion:
- 🚨 🛡️ bei Sperren und Service-Stopp
- ✅ bei Freigaben und Service-Start
- Bei **permanenten Sperren** mit aktiviertem AbuseIPDB-Reporting wird ein Hinweis eingeblendet, dass die IP an AbuseIPDB gemeldet wurde.

View File

@@ -70,6 +70,34 @@ xk9z3a.microsoft.com
> **Tipp:** Der Schwellwert `SUBDOMAIN_FLOOD_MAX_UNIQUE` sollte hoch genug sein, um legitime Clients nicht zu stören (z.B. CDNs nutzen oft viele Subdomains). Ein Wert von 50100 ist in den meisten Fällen sinnvoll.
### DNS-Flood-Watchlist
Domains bei denen eine Rate-Limit-Überschreitung **sofort** zu einer **permanenten Sperre** und einer **AbuseIPDB-Meldung** führt — ohne progressive Eskalation. Ideal für bekannte Angriffsziele, die regelmäßig geflutet werden (z.B. `microsoft.com`, `google.com`).
| Parameter | Standard | Beschreibung |
|-----------|----------|--------------|
| `DNS_FLOOD_WATCHLIST_ENABLED` | `false` | DNS-Flood-Watchlist aktivieren |
| `DNS_FLOOD_WATCHLIST` | *(leer)* | Überwachte Domains, kommagetrennt (z.B. `"microsoft.com,google.com"`) |
#### Wie funktioniert die Watchlist?
1. Die reguläre Rate-Limit-Prüfung erkennt, dass ein Client mehr als `RATE_LIMIT_MAX_REQUESTS` Anfragen für eine Domain gestellt hat
2. Zusätzlich wird geprüft, ob die angefragte Domain in der Watchlist steht (inkl. Subdomains: `foo.microsoft.com` matcht `microsoft.com`)
3. Trifft beides zu → **sofortige permanente Sperre** + **AbuseIPDB-Meldung** (falls aktiviert)
Die Watchlist greift sowohl bei normalen Rate-Limit-Verstößen als auch bei Subdomain-Flood-Erkennungen.
#### Beispiel
```bash
DNS_FLOOD_WATCHLIST_ENABLED=true
DNS_FLOOD_WATCHLIST="microsoft.com,google.com,apple.com"
```
→ Ein Client der `35x foo.microsoft.com` in 60s abfragt (bei `RATE_LIMIT_MAX_REQUESTS=30`) wird **sofort permanent** gesperrt und an AbuseIPDB gemeldet.
> **Hinweis:** Damit die AbuseIPDB-Meldung funktioniert, muss `ABUSEIPDB_ENABLED=true` und ein gültiger `ABUSEIPDB_API_KEY` konfiguriert sein. Ohne AbuseIPDB-Konfiguration wird nur permanent gesperrt.
### Sperr-Einstellungen
| Parameter | Standard | Beschreibung |
@@ -100,7 +128,7 @@ Wiederholungstäter werden wie bei fail2ban stufenweise länger gesperrt. Wird e
| 4. Mal | 4 | 8 Stunden | 3600 × 8 |
| 5. Mal | 5 | **PERMANENT** | Max-Stufe erreicht |
> **Hinweis:** Der Offense-Zähler wird automatisch zurückgesetzt, wenn eine IP für den konfigurierten Zeitraum (`PROGRESSIVE_BAN_RESET_AFTER`) kein erneutes Vergehen begeht. Permanente Sperren werden **nicht** automatisch aufgehoben sie müssen manuell mit `unban` oder `flush` entfernt werden.
> **Hinweis:** Abgelaufene Offense-Zähler werden automatisch vom **Offense-Cleanup-Worker** aufgeräumt, der stündlich prüft, ob das letzte Vergehen einer IP länger als `PROGRESSIVE_BAN_RESET_AFTER` zurückliegt. Der Worker startet automatisch zusammen mit dem Hauptservice, wenn progressive Sperren aktiviert sind. Er läuft mit niedrigster CPU- und I/O-Priorität (`nice 19`, `ionice idle`), sodass andere Dienste nicht beeinträchtigt werden. Manuelles Zurücksetzen ist jederzeit mit `reset-offenses` möglich. Permanente Sperren werden **nicht** automatisch aufgehoben sie müssen manuell mit `unban` oder `flush` entfernt werden.
### Logging
@@ -139,6 +167,7 @@ Regelmäßige Statistik-Reports per E-Mail. Voraussetzung ist ein funktionierend
| `REPORT_EMAIL_FROM` | `adguard-shield@hostname` | E-Mail-Absender |
| `REPORT_FORMAT` | `html` | Format: `html` oder `txt` |
| `REPORT_MAIL_CMD` | `msmtp` | Mail-Befehl (`msmtp`, `sendmail`, `mail`) |
| `REPORT_BUSIEST_DAY_RANGE` | `30` | Zeitraum in Tagen für „Aktivster Tag“. `30` = letzte 30 Tage. `0` = nur Berichtszeitraum (altes Verhalten) |
> Siehe [E-Mail Report Dokumentation](report.md) für Details zu Inhalten, Templates und Befehlen.
@@ -149,6 +178,60 @@ Regelmäßige Statistik-Reports per E-Mail. Voraussetzung ist ein funktionierend
| `STATE_DIR` | `/var/lib/adguard-shield` | Verzeichnis für State-Dateien |
| `PID_FILE` | `/var/run/adguard-shield.pid` | PID-Datei |
| `DRY_RUN` | `false` | Testmodus — nur loggen, nicht sperren |
### Externe Whitelist
Ermöglicht das Einbinden externer Whitelist-Dateien mit Domains und IP-Adressen. Der Worker löst Domains regelmäßig per DNS auf — ideal für DynDNS-Einträge mit wechselnden IP-Adressen. Aufgelöste IPs werden automatisch zur Whitelist hinzugefügt und bei jeder Prüfung aktualisiert.
| Parameter | Standard | Beschreibung |
|-----------|----------|--------------|
| `EXTERNAL_WHITELIST_ENABLED` | `false` | Aktiviert den externen Whitelist-Worker |
| `EXTERNAL_WHITELIST_URLS` | *(leer)* | URL(s) zu Whitelist-Textdateien (kommagetrennt). Unterstützt IPv4, IPv6, CIDR und Hostnamen |
| `EXTERNAL_WHITELIST_INTERVAL` | `300` | Prüfintervall in Sekunden (300 = 5 Min.). Bei DynDNS-Einträgen ggf. kürzer wählen |
| `EXTERNAL_WHITELIST_CACHE_DIR` | `/var/lib/adguard-shield/external-whitelist` | Lokaler Cache für heruntergeladene Listen und aufgelöste IPs |
#### Externe Whitelist einrichten
1. Erstelle eine Textdatei auf einem Webserver. Pro Zeile ein Eintrag — Domain, IPv4, IPv6 oder CIDR:
```text
# Domains (werden regelmäßig per DNS aufgelöst — ideal für DynDNS)
mein-router.dyndns.org
homeserver.example.com
# Feste IPs
192.168.1.100
10.0.0.0/24
2001:db8::1
# Kommentare und Inline-Kommentare werden unterstützt
192.168.1.200 # Backup-Server
```
2. Aktiviere die Whitelist in der Konfiguration:
```bash
EXTERNAL_WHITELIST_ENABLED=true
EXTERNAL_WHITELIST_URLS="https://example.com/whitelist.txt"
EXTERNAL_WHITELIST_INTERVAL=300
```
3. Mehrere Listen können kommagetrennt angegeben werden:
```bash
EXTERNAL_WHITELIST_URLS="https://example.com/trusted.txt,https://other.com/whitelist.txt"
```
4. Service neustarten:
```bash
sudo systemctl restart adguard-shield
```
> **Hinweis:** Da Domains bei jedem Prüfintervall neu aufgelöst werden, eignet sich diese Funktion besonders für DynDNS-Einträge. Ändert sich die IP eines DynDNS-Hostnamens, wird die neue IP beim nächsten Sync automatisch erkannt und in die Whitelist aufgenommen.
> **Wichtig:** Wird eine bereits gesperrte IP durch eine Whitelist-Aktualisierung gewhitelistet, wird sie **automatisch entsperrt**.
### Externe Blocklist
Ermöglicht das Einbinden externer Blocklisten, die IPv4-Adressen, IPv6-Adressen und Hostnamen enthalten können. Der Worker läuft als Hintergrundprozess, prüft periodisch auf Änderungen und löst Hostnamen automatisch über den lokalen DNS-Resolver auf.
@@ -196,10 +279,97 @@ sudo systemctl restart adguard-shield
Der Report an AbuseIPDB enthält (auf Englisch):
- **Bei Rate-Limit:** `DNS flooding on our DNS server: 100x microsoft.com in 60s. Banned by Adguard Shield 🔗 https://tnvs.de/as`
- **Bei Rate-Limit / DNS-Flood-Watchlist:** `DNS flooding on our DNS server: 100x microsoft.com in 60s. Banned by Adguard Shield 🔗 https://tnvs.de/as`
- **Bei Subdomain-Flood:** `DNS flooding on our DNS server: 85x *.microsoft.com in 60s (random subdomain attack). Banned by Adguard Shield 🔗 https://tnvs.de/as`
Die Kategorie `4` (DDoS Attack) wird standardmäßig verwendet. Weitere Kategorien können kommagetrennt angegeben werden (z.B. `"4,15"`).
### GeoIP-basierte Länderfilter
Ermöglicht das Sperren oder Erlauben von DNS-Anfragen basierend auf dem Herkunftsland der Client-IP. Unterstützt zwei Modi:
- **Blocklist-Modus:** Nur die gelisteten Länder werden gesperrt (alle anderen erlaubt)
- **Allowlist-Modus:** Nur die gelisteten Länder werden erlaubt (alle anderen gesperrt)
| Parameter | Standard | Beschreibung |
|-----------|----------|--------------|
| `GEOIP_ENABLED` | `false` | GeoIP-Filter aktivieren |
| `GEOIP_MODE` | `blocklist` | Modus: `blocklist` oder `allowlist` |
| `GEOIP_COUNTRIES` | *(leer)* | ISO 3166-1 Alpha-2 Ländercodes (kommagetrennt), z.B. `CN,RU,KP,IR` |
| `GEOIP_CHECK_INTERVAL` | `0` | Prüfintervall in Sekunden (`0` = nutzt `CHECK_INTERVAL`) |
| `GEOIP_NOTIFY` | `true` | Benachrichtigungen bei GeoIP-Sperren senden |
| `GEOIP_SKIP_PRIVATE` | `true` | Private/lokale IPs von der GeoIP-Prüfung ausnehmen |
| `GEOIP_LICENSE_KEY` | *(leer)* | MaxMind License-Key für automatischen DB-Download (kostenlos) |
| `GEOIP_MMDB_PATH` | *(leer)* | Manueller Pfad zur MaxMind GeoLite2 Datenbank (überschreibt Auto-Download) |
#### Voraussetzungen
Es muss mindestens eines der folgenden GeoIP-Tools installiert sein:
1. **Automatischer MaxMind-Download** (empfohlen):
```bash
# Kostenlosen Account erstellen: https://www.maxmind.com/en/geolite2/signup
# License-Key generieren und in adguard-shield.conf eintragen:
GEOIP_LICENSE_KEY="dein_license_key_hier"
```
Die GeoLite2-Country-Datenbank wird automatisch heruntergeladen und alle 24 Stunden aktualisiert.
Es wird zusätzlich `mmdbinspect` oder `mmdblookup` benötigt:
```bash
sudo apt install mmdb-bin # für mmdblookup
```
2. **geoiplookup** (einfachster Einstieg, weniger genau):
```bash
sudo apt install geoip-bin geoip-database
```
3. **Manueller MaxMind-Pfad** (eigene Datenbank):
```bash
# mmdbinspect oder mmdblookup installieren
# Datenbank manuell herunterladen: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data
GEOIP_MMDB_PATH="/usr/share/GeoIP/GeoLite2-Country.mmdb"
```
> **Priorität:** `GEOIP_MMDB_PATH` (manuell) → Auto-Download-DB → `geoiplookup` (Legacy-Fallback)
#### Beispiel: Bestimmte Länder sperren (Blocklist)
```bash
GEOIP_ENABLED=true
GEOIP_MODE="blocklist"
GEOIP_COUNTRIES="CN,RU,KP,IR"
GEOIP_LICENSE_KEY="dein_maxmind_license_key" # optional, für Auto-Download
```
→ Alle Anfragen aus China, Russland, Nordkorea und Iran werden permanent gesperrt.
#### Beispiel: Nur bestimmte Länder erlauben (Allowlist)
```bash
GEOIP_ENABLED=true
GEOIP_MODE="allowlist"
GEOIP_COUNTRIES="DE,AT,CH"
```
→ Nur Anfragen aus Deutschland, Österreich und der Schweiz werden erlaubt. Alle anderen Länder werden gesperrt.
> **Hinweis:** Private IP-Adressen (10.x.x.x, 192.168.x.x, etc.) und Whitelist-IPs werden niemals durch GeoIP gesperrt. GeoIP-Sperren sind **immer permanent**.
> **Auto-Unban:** Wird ein Land aus `GEOIP_COUNTRIES` entfernt oder der Modus (`GEOIP_MODE`) geändert, werden die nicht mehr zutreffenden Sperren beim nächsten Sync **automatisch aufgehoben**. Dasselbe gilt, wenn GeoIP komplett deaktiviert wird (`GEOIP_ENABLED=false`).
> **Tipp:** GeoIP-Lookups werden für 24 Stunden gecacht. Mit `geoip-flush-cache` kann der Cache manuell geleert werden.
> **Auto-Download:** Ist `GEOIP_LICENSE_KEY` gesetzt, wird die GeoLite2-Country-Datenbank automatisch nach `<INSTALL_DIR>/geoip/` heruntergeladen und alle 24 Stunden aktualisiert. Bei einem Update wird der Download im Hintergrund durchgeführt — der Worker läuft während des Downloads normal weiter. Ein manuell gesetzter `GEOIP_MMDB_PATH` hat immer Vorrang vor der automatisch heruntergeladenen Datenbank.
#### GeoIP-Befehle
| Befehl | Beschreibung |
|--------|--------------|
| `adguard-shield.sh geoip-status` | Zeigt GeoIP-Status, aktive Sperren und verfügbare Tools |
| `adguard-shield.sh geoip-sync` | Einmalige GeoIP-Prüfung aller aktiven Clients |
| `adguard-shield.sh geoip-flush` | Alle GeoIP-Sperren aufheben |
| `adguard-shield.sh geoip-lookup <IP>` | GeoIP-Lookup einer einzelnen IP-Adresse |
#### Externe Blocklist einrichten
1. Erstelle eine Textdatei auf einem Webserver. Pro Zeile ein Eintrag — IPv4, IPv6, CIDR oder Hostname:
@@ -338,3 +508,7 @@ Beispiel:
```
WHITELIST="127.0.0.1,::1,192.168.1.1,192.168.1.10,fd00::1"
```
### Externe Whitelist für dynamische IPs
Für Clients mit wechselnden IP-Adressen (z.B. DynDNS) kann eine **externe Whitelist** genutzt werden. Der Whitelist-Worker löst Domains regelmäßig per DNS auf und fügt die aktuellen IPs automatisch zur Whitelist hinzu. Siehe [Externe Whitelist](#externe-whitelist) für die Konfiguration.

View File

@@ -42,6 +42,7 @@ sudo /opt/adguard-shield/report-generator.sh install
| `REPORT_EMAIL_FROM` | `adguard-shield@hostname` | E-Mail-Absender |
| `REPORT_FORMAT` | `html` | Format: `html` oder `txt` |
| `REPORT_MAIL_CMD` | `msmtp` | Mail-Befehl (`msmtp`, `sendmail`, `mail`) |
| `REPORT_BUSIEST_DAY_RANGE` | `30` | Zeitraum in Tagen für „Aktivster Tag“ (0 = Berichtszeitraum) |
### Versandintervalle
@@ -56,7 +57,26 @@ sudo /opt/adguard-shield/report-generator.sh install
Der Report enthält folgende Statistiken:
### Übersicht
### Zeitraum-Schnellübersicht *(immer ganz oben)*
Eine Vergleichstabelle mit Live-Zahlen für vier feste Zeitfenster unabhängig vom konfigurierten `REPORT_INTERVAL`:
| Zeitraum | Sperren | Entsperrungen | Eindeutige IPs | Permanent gebannt |
|----------|---------|---------------|----------------|-------------------|
| Heute *(nur nach 20:00 Uhr)* | … | … | … | … |
| Gestern | … | … | … | … |
| Letzte 7 Tage | … | … | … | … |
| Letzte 14 Tage | … | … | … | … |
| Letzte 30 Tage | … | … | … | … |
Im HTML-Format wird **Gestern** grün hervorgehoben, **Heute** blau (erscheint nur ab 20:00 Uhr).
- **Gestern** umfasst exakt 00:00:00 23:59:59 des gestrigen Tages.
- **Heute** umfasst den laufenden Tag von 00:00:00 bis zum Zeitpunkt der Reportgenerierung und wird nur eingeblendet, wenn der Report nach 20:00 Uhr erstellt wird.
Die übrigen Zeiträume laufen vom Starttag 00:00 Uhr bis zum Zeitpunkt der Reportgenerierung.
> **Hinweis:** Die AbuseIPDB-Meldungen werden in der Schnellübersicht nicht mehr separat ausgewiesen, da sie immer mit einer Permanentsperre korrelieren der Wert „Permanent gebannt" ist daher ausreichend. Die Gesamtanzahl der AbuseIPDB-Reports im Berichtszeitraum ist weiterhin in der allgemeinen Übersicht sichtbar.
### Übersicht (Berichtszeitraum)
- Gesamtzahl der Sperren und Entsperrungen
- Anzahl eindeutiger gesperrter IPs
- Permanente Sperren
@@ -67,7 +87,7 @@ Der Report enthält folgende Statistiken:
- Rate-Limit Sperren
- Subdomain-Flood Sperren
- Externe Blocklist Sperren
- Aktivster Tag im Berichtszeitraum
- Aktivster Tag wird über einen konfigurierbaren Zeitraum ermittelt (Standard: letzte 30 Tage, `REPORT_BUSIEST_DAY_RANGE`). Zeigt zusätzlich die Anzahl der Sperren an diesem Tag. Bei `REPORT_BUSIEST_DAY_RANGE=0` wird nur der Berichtszeitraum betrachtet.
### Top 10 Listen
- **Auffälligste IPs** — Die 10 IPs mit den meisten Sperren (mit Balkendiagramm im HTML-Format)
@@ -156,7 +176,8 @@ Die Templates verwenden Platzhalter (z.B. `{{TOTAL_BANS}}`, `{{TOP10_IPS_TABLE}}
| `{{RATELIMIT_BANS}}` | Rate-Limit Sperren |
| `{{SUBDOMAIN_FLOOD_BANS}}` | Subdomain-Flood Sperren |
| `{{EXTERNAL_BLOCKLIST_BANS}}` | Externe Blocklist Sperren |
| `{{BUSIEST_DAY}}` | Aktivster Tag |
| `{{BUSIEST_DAY}}` | Aktivster Tag (Datum + Anzahl Sperren) |
| `{{BUSIEST_DAY_LABEL}}` | Dynamisches Label für den aktivsten Tag (z.B. „Aktivster Tag (30 Tage)“) |
| `{{TOP10_IPS_TABLE}}` | Top 10 IPs (HTML-Tabelle) |
| `{{TOP10_IPS_TEXT}}` | Top 10 IPs (Text-Tabelle) |
| `{{TOP10_DOMAINS_TABLE}}` | Top 10 Domains (HTML-Tabelle) |

View File

@@ -193,6 +193,37 @@ sudo rm -f /var/run/adguard-shield.pid
sudo systemctl start adguard-shield
```
### Service ist ausgefallen und startet nicht mehr
Wenn systemd das Restart-Limit erreicht hat (z.B. `"Start request repeated too quickly"`), hilft der **Watchdog** — er prüft alle 5 Minuten ob der Service läuft und startet ihn automatisch neu.
**Watchdog-Status prüfen:**
```bash
# Timer-Status anzeigen
sudo systemctl status adguard-shield-watchdog.timer
# Letzte Watchdog-Ausführungen anzeigen
sudo systemctl list-timers adguard-shield-watchdog.timer
# Watchdog-Logs prüfen
sudo journalctl -u adguard-shield-watchdog.service --no-pager -n 20
```
**Manuelles Recovery (sofort):**
```bash
# systemd-Fehlerzähler zurücksetzen und Service starten
sudo systemctl reset-failed adguard-shield.service
sudo systemctl start adguard-shield.service
```
**Watchdog nachträglich aktivieren:**
```bash
sudo systemctl enable adguard-shield-watchdog.timer
sudo systemctl start adguard-shield-watchdog.timer
```
> **Hinweis:** Der Watchdog sendet automatisch eine Benachrichtigung (falls `NOTIFY_ENABLED=true`), wenn er den Service wiederbeleben muss oder die Recovery fehlschlägt.
## Update durchführen
```bash
@@ -229,9 +260,13 @@ Oder manuell:
```bash
sudo systemctl stop adguard-shield
sudo systemctl disable adguard-shield
sudo systemctl stop adguard-shield-watchdog.timer
sudo systemctl disable adguard-shield-watchdog.timer
sudo /opt/adguard-shield/iptables-helper.sh remove
sudo rm -rf /opt/adguard-shield
sudo rm -f /etc/systemd/system/adguard-shield.service
sudo rm -f /etc/systemd/system/adguard-shield-watchdog.service
sudo rm -f /etc/systemd/system/adguard-shield-watchdog.timer
sudo systemctl daemon-reload
```

View File

@@ -35,8 +35,9 @@ Das Update-Script macht automatisch folgendes:
2. **Scripts aktualisieren** — Alle `.sh`-Dateien werden nach `/opt/adguard-shield/` kopiert
3. **Konfigurations-Migration** — Neue Parameter werden automatisch zur bestehenden Konfiguration hinzugefügt, bestehende Einstellungen bleiben **unverändert**
4. **Backup erstellen** — Die alte Konfiguration wird als `adguard-shield.conf.old` gesichert
5. **Service aktualisieren** — Die systemd Service-Datei wird aktualisiert und `daemon-reload` ausgeführt
6. **Service neustarten** — Der Service wird automatisch neu gestartet (falls er vorher lief)
5. **Service aktualisieren** — Die systemd Service-Datei und Watchdog-Dateien werden aktualisiert und `daemon-reload` ausgeführt
6. **Watchdog aktivieren** — Der Watchdog-Timer wird automatisch aktiviert (falls noch nicht aktiv)
7. **Service neustarten** — Der Service wird automatisch neu gestartet (falls er vorher lief)
### 3. Neue Parameter prüfen (optional)

View File

@@ -82,6 +82,13 @@ is_whitelisted() {
return 0
fi
done
# Externe Whitelist prüfen (aufgelöste IPs aus dem Whitelist-Worker)
local ext_wl_file="${EXTERNAL_WHITELIST_CACHE_DIR:-/var/lib/adguard-shield/external-whitelist}/resolved_ips.txt"
if [[ -f "$ext_wl_file" ]] && grep -qxF "$ip" "$ext_wl_file" 2>/dev/null; then
return 0
fi
return 1
}
@@ -203,6 +210,27 @@ unban_ip() {
fi
}
# ─── Hostname-Auflösung ──────────────────────────────────────────────────────
# Versucht den Hostnamen einer IP per Reverse-DNS aufzulösen
resolve_hostname() {
local ip="$1"
local hostname=""
if command -v dig &>/dev/null; then
hostname=$(dig +short -x "$ip" 2>/dev/null | head -1 | sed 's/\.$//')
fi
if [[ -z "$hostname" ]] && command -v host &>/dev/null; then
hostname=$(host "$ip" 2>/dev/null | awk '/domain name pointer/ {print $NF}' | sed 's/\.$//' | head -1)
fi
if [[ -z "$hostname" ]] && command -v getent &>/dev/null; then
hostname=$(getent hosts "$ip" 2>/dev/null | awk '{print $2}' | head -1)
fi
echo "${hostname:-(unbekannt)}"
}
# ─── Benachrichtigung ────────────────────────────────────────────────────────
send_notification() {
local action="$1"
@@ -213,45 +241,79 @@ send_notification() {
return
fi
local title
local message
local my_hostname
my_hostname=$(hostname)
local client_hostname
client_hostname=$(resolve_hostname "$ip")
if [[ "$action" == "ban" ]]; then
message="🚫 Externe Blocklist: IP **$ip** gesperrt."
title="🚨 🛡️ AdGuard Shield"
message="🚫 AdGuard Shield Ban auf ${my_hostname} (Externe Blocklist)
---
IP: ${ip}
Hostname: ${client_hostname}
Whois: https://www.whois.com/whois/${ip}
AbuseIPDB: https://www.abuseipdb.com/check/${ip}"
else
message="✅ Externe Blocklist: IP **$ip** entsperrt (aus Liste entfernt)."
title="✅ AdGuard Shield"
message="✅ AdGuard Shield Freigabe auf ${my_hostname} (Externe Blocklist)
---
IP: ${ip}
Hostname: ${client_hostname}
AbuseIPDB: https://www.abuseipdb.com/check/${ip}"
fi
case "${NOTIFY_TYPE:-generic}" in
discord)
local json_payload
json_payload=$(jq -nc --arg msg "$message" '{content: $msg}')
curl -s -H "Content-Type: application/json" \
-d "{\"content\": \"$message\"}" \
-d "$json_payload" \
"$NOTIFY_WEBHOOK_URL" &>/dev/null &
;;
slack)
local json_payload
json_payload=$(jq -nc --arg msg "$message" '{text: $msg}')
curl -s -H "Content-Type: application/json" \
-d "{\"text\": \"$message\"}" \
-d "$json_payload" \
"$NOTIFY_WEBHOOK_URL" &>/dev/null &
;;
gotify)
curl -s -X POST "$NOTIFY_WEBHOOK_URL" \
-F "title=AdGuard Shield - Externe Blocklist" \
-F "message=$message" \
-F "title=${title}" \
-F "message=${message}" \
-F "priority=5" &>/dev/null &
;;
ntfy)
local ntfy_url="${NTFY_SERVER_URL:-https://ntfy.sh}"
local tags="rotating_light,blocklist"
[[ "$action" != "ban" ]] && tags="white_check_mark,blocklist"
# Ntfy fügt Emojis über Tags hinzu → Titel ohne führende Emojis setzen
local ntfy_title
case "$action" in
ban) ntfy_title="🛡️ AdGuard Shield" ;;
*) ntfy_title="AdGuard Shield" ;;
esac
local -a curl_args=(
-s -X POST "${ntfy_url}/${NTFY_TOPIC}"
-H "Title: AdGuard Shield - Externe Blocklist"
-H "Title: ${ntfy_title}"
-H "Priority: ${NTFY_PRIORITY:-3}"
-H "Tags: rotating_light,blocklist"
-d "$(echo "$message" | sed 's/\*\*//g')"
-H "Tags: ${tags}"
-d "${message}"
)
[[ -n "${NTFY_TOKEN:-}" ]] && curl_args+=(-H "Authorization: Bearer ${NTFY_TOKEN}")
curl "${curl_args[@]}" &>/dev/null &
;;
generic)
local json_payload
json_payload=$(jq -nc --arg msg "$message" --arg act "$action" --arg cl "$ip" \
'{message: $msg, action: $act, client: $cl, source: "external-blocklist"}')
curl -s -H "Content-Type: application/json" \
-d "{\"message\": \"$message\", \"action\": \"$action\", \"client\": \"$ip\", \"source\": \"external-blocklist\"}" \
-d "$json_payload" \
"$NOTIFY_WEBHOOK_URL" &>/dev/null &
;;
esac
@@ -439,8 +501,8 @@ parse_blocklist_ips() {
log "WARN" "Eintrag übersprungen (ungültige IPv6-Adresse oder IP:Port): $line"
fi
elif [[ "$line" =~ ^[0-9] ]]; then
# ── IPv4 ──────────────────────────────────────────────────────────
elif [[ "$line" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(/[0-9]+)?$ ]]; then
# ── IPv4 (nur Ziffern, Punkte und optionaler CIDR-Suffix) ────────
[[ "$line" == "0.0.0.0"* ]] && continue
if _is_valid_ipv4 "$line"; then
echo "$line"

View File

@@ -0,0 +1,532 @@
#!/bin/bash
###############################################################################
# AdGuard Shield - Externer Whitelist-Worker
# Lädt externe Whitelist-Dateien herunter, löst Domains zu IPs auf und
# stellt diese dem Hauptscript als dynamische Whitelist zur Verfügung.
# Ideal für DynDNS-Domains mit wechselnden IP-Adressen.
# Wird als Hintergrundprozess vom Hauptscript gestartet.
#
# Autor: Patrick Asmus
# E-Mail: support@techniverse.net
# Datum: 2026-04-04
# Lizenz: MIT
###############################################################################
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/adguard-shield.conf"
# ─── Konfiguration laden ───────────────────────────────────────────────────────
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "FEHLER: Konfigurationsdatei nicht gefunden: $CONFIG_FILE" >&2
exit 1
fi
# shellcheck source=adguard-shield.conf
source "$CONFIG_FILE"
# ─── Standardwerte ────────────────────────────────────────────────────────────
EXTERNAL_WHITELIST_CACHE_DIR="${EXTERNAL_WHITELIST_CACHE_DIR:-/var/lib/adguard-shield/external-whitelist}"
EXTERNAL_WHITELIST_RESOLVED_FILE="${EXTERNAL_WHITELIST_CACHE_DIR}/resolved_ips.txt"
# ─── Worker PID-File ──────────────────────────────────────────────────────────
WORKER_PID_FILE="/var/run/adguard-whitelist-worker.pid"
# ─── Logging (eigene Funktion, nutzt gleiche Log-Datei) ───────────────────────
declare -A LOG_LEVELS=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3)
log() {
local level="$1"
shift
local message="$*"
local configured_level="${LOG_LEVEL:-INFO}"
if [[ ${LOG_LEVELS[$level]:-1} -ge ${LOG_LEVELS[$configured_level]:-1} ]]; then
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
local log_entry="[$timestamp] [$level] [WHITELIST-WORKER] $message"
echo "$log_entry" | tee -a "$LOG_FILE" >&2
fi
}
# ─── Verzeichnisse erstellen ──────────────────────────────────────────────────
init_directories() {
mkdir -p "$EXTERNAL_WHITELIST_CACHE_DIR"
mkdir -p "$(dirname "$LOG_FILE")"
}
# ─── Eintrag-Validierung ─────────────────────────────────────────────────────
# Prüft IPv4-Adresse mit optionalem CIDR
_is_valid_ipv4() {
local ip="$1" addr="$1" prefix=""
if [[ "$ip" == */* ]]; then
addr="${ip%/*}"
prefix="${ip#*/}"
{ [[ "$prefix" =~ ^[0-9]+$ ]] && [[ "$prefix" -le 32 ]]; } || return 1
fi
local IFS='.'
read -ra _octets <<< "$addr"
[[ ${#_octets[@]} -eq 4 ]] || return 1
local o
for o in "${_octets[@]}"; do
[[ "$o" =~ ^[0-9]+$ ]] || return 1
[[ "$o" -le 255 ]] || return 1
done
return 0
}
# Prüft IPv6-Adresse mit optionalem CIDR
_is_valid_ipv6() {
local ip="$1" addr="$1"
if [[ "$ip" == */* ]]; then
addr="${ip%/*}"
local prefix="${ip#*/}"
{ [[ "$prefix" =~ ^[0-9]+$ ]] && [[ "$prefix" -le 128 ]]; } || return 1
fi
[[ "$addr" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9] ]] && return 1
[[ "$addr" == *:* ]] || return 1
[[ "$addr" =~ ^[0-9a-fA-F:\.]+$ ]] || return 1
return 0
}
# Prüft ob ein Hostname syntaktisch plausibel ist
_is_valid_hostname() {
local host="$1"
host="${host%.}" # trailing dot entfernen
[[ -z "$host" ]] && return 1
[[ ${#host} -gt 253 ]] && return 1
[[ "$host" =~ ^[a-zA-Z0-9._-]+$ ]] || return 1
[[ "$host" =~ ^[.\-] ]] && return 1
[[ "$host" == *.* ]] || return 1
return 0
}
# ─── Externe Whitelist herunterladen ─────────────────────────────────────────
download_whitelist() {
local url="$1"
local index="$2"
local cache_file="${EXTERNAL_WHITELIST_CACHE_DIR}/whitelist_${index}.txt"
local etag_file="${EXTERNAL_WHITELIST_CACHE_DIR}/whitelist_${index}.etag"
local tmp_file="${EXTERNAL_WHITELIST_CACHE_DIR}/whitelist_${index}.tmp"
log "DEBUG" "Prüfe externe Whitelist: $url"
local -a curl_args=(
-s
-L
--connect-timeout 10
--max-time 30
-o "$tmp_file"
-w "%{http_code}"
)
if [[ -f "$etag_file" ]]; then
local stored_etag
stored_etag=$(cat "$etag_file")
curl_args+=(-H "If-None-Match: ${stored_etag}")
fi
local http_code
http_code=$(curl "${curl_args[@]}" -D "${tmp_file}.headers" "$url" 2>/dev/null) || {
log "WARN" "Fehler beim Download der Whitelist: $url"
rm -f "$tmp_file" "${tmp_file}.headers"
return 1
}
if [[ "$http_code" == "304" ]]; then
log "DEBUG" "Whitelist nicht geändert (HTTP 304): $url"
rm -f "$tmp_file" "${tmp_file}.headers"
# Auch bei 304 müssen wir DNS neu auflösen (dynamische IPs!)
return 0
fi
if [[ "$http_code" != "200" ]]; then
log "WARN" "Whitelist Download fehlgeschlagen (HTTP $http_code): $url"
rm -f "$tmp_file" "${tmp_file}.headers"
return 1
fi
if [[ -f "${tmp_file}.headers" ]]; then
local new_etag
new_etag=$(grep -i '^etag:' "${tmp_file}.headers" | head -1 | sed 's/^[^:]*: *//;s/\r$//')
if [[ -n "$new_etag" ]]; then
echo "$new_etag" > "$etag_file"
fi
fi
rm -f "${tmp_file}.headers"
if [[ -f "$cache_file" ]]; then
if diff -q "$tmp_file" "$cache_file" &>/dev/null; then
log "DEBUG" "Whitelist Inhalt unverändert: $url"
rm -f "$tmp_file"
return 0
fi
fi
mv "$tmp_file" "$cache_file"
log "INFO" "Whitelist aktualisiert: $url"
return 0
}
# ─── Einträge aus Whitelist-Datei parsen und IPs auflösen ───────────────────
# Gibt pro Zeile eine IP-Adresse aus (aufgelöste Domains + direkte IPs)
parse_whitelist_entries() {
local cache_file="$1"
[[ -f "$cache_file" ]] || return
while IFS= read -r line; do
line="${line%$'\r'}"
line="${line#$'\xef\xbb\xbf'}"
[[ -z "$line" ]] && continue
[[ "$line" =~ ^[[:space:]]*# ]] && continue
line=$(echo "$line" | xargs)
line=$(echo "$line" | sed 's/[[:space:]]*[#;].*$//' | xargs)
[[ -z "$line" ]] && continue
# URLs ablehnen
if [[ "$line" =~ ^[a-zA-Z][a-zA-Z0-9+.-]*:// ]]; then
log "WARN" "Whitelist-Eintrag übersprungen (URL nicht erlaubt): $line"
continue
fi
# Hosts-Datei-Format erkennen
if [[ "$line" =~ ^[^[:space:]]+[[:space:]]+[^[:space:]] ]]; then
local _first="${line%% *}"
local _rest="${line#* }"
local _second="${_rest%% *}"
if [[ "$_first" == "0.0.0.0" || "$_first" =~ ^127\. ||
"$_first" == "::1" || "$_first" == "::0" ||
"$_first" == "::" ]]; then
log "DEBUG" "Whitelist Hosts-Format erkannt, extrahiere: $_second"
line="$_second"
else
log "WARN" "Whitelist-Eintrag übersprungen (unbekanntes Format): $line"
continue
fi
fi
# Klassifizieren und validieren
if [[ "$line" == *:* ]]; then
# IPv6
if _is_valid_ipv6 "$line"; then
echo "$line"
else
log "WARN" "Whitelist-Eintrag übersprungen (ungültige IPv6): $line"
fi
elif [[ "$line" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(/[0-9]+)?$ ]]; then
# IPv4 (nur Ziffern, Punkte und optionaler CIDR-Suffix)
[[ "$line" == "0.0.0.0"* ]] && continue
if _is_valid_ipv4 "$line"; then
echo "$line"
else
log "WARN" "Whitelist-Eintrag übersprungen (ungültige IPv4): $line"
fi
else
# Hostname → DNS-Auflösung (wird bei jedem Durchlauf neu aufgelöst!)
if ! _is_valid_hostname "$line"; then
log "WARN" "Whitelist-Eintrag übersprungen (kein gültiger Hostname): $line"
continue
fi
local resolved
resolved=$(getent ahosts "$line" 2>/dev/null | awk '{print $1}' | sort -u) || resolved=""
if [[ -z "$resolved" ]]; then
log "WARN" "Whitelist-Hostname konnte nicht aufgelöst werden: $line"
continue
fi
local resolved_count=0
while IFS= read -r resolved_ip; do
[[ -z "$resolved_ip" ]] && continue
[[ "$resolved_ip" == "0.0.0.0" ]] && continue
[[ "$resolved_ip" == "::" ]] && continue
[[ "$resolved_ip" == "::0" ]] && continue
echo "$resolved_ip"
resolved_count=$((resolved_count + 1))
done <<< "$resolved"
if [[ $resolved_count -gt 0 ]]; then
log "DEBUG" "Whitelist-Hostname aufgelöst: $line$resolved_count IP(s)"
else
log "WARN" "Whitelist-Hostname lieferte nur ungültige Adressen: $line"
fi
fi
done < "$cache_file"
}
# ─── Whitelisten synchronisieren ─────────────────────────────────────────────
sync_whitelists() {
# Alle URLs herunterladen
IFS=',' read -ra urls <<< "$EXTERNAL_WHITELIST_URLS"
local index=0
for url in "${urls[@]}"; do
url=$(echo "$url" | xargs)
[[ -z "$url" ]] && continue
download_whitelist "$url" "$index" || true
index=$((index + 1))
done
# Alle Einträge aus Cache-Dateien parsen und IPs auflösen
local all_ips_file="${EXTERNAL_WHITELIST_CACHE_DIR}/.all_ips.tmp"
> "$all_ips_file"
for cache_file in "${EXTERNAL_WHITELIST_CACHE_DIR}"/whitelist_*.txt; do
[[ -f "$cache_file" ]] || continue
parse_whitelist_entries "$cache_file" >> "$all_ips_file"
done
# Duplikate entfernen und in die resolved-Datei schreiben
local unique_count
sort -u "$all_ips_file" > "${EXTERNAL_WHITELIST_RESOLVED_FILE}.tmp"
mv "${EXTERNAL_WHITELIST_RESOLVED_FILE}.tmp" "$EXTERNAL_WHITELIST_RESOLVED_FILE"
unique_count=$(wc -l < "$EXTERNAL_WHITELIST_RESOLVED_FILE" | xargs)
rm -f "$all_ips_file"
log "DEBUG" "Externe Whitelist: $unique_count eindeutige IPs aufgelöst"
# Prüfe ob gesperrte IPs jetzt auf der Whitelist stehen und entsperrt werden müssen
check_banned_whitelist_ips
}
# ─── Gesperrte IPs prüfen die jetzt gewhitelistet sind ──────────────────────
# Wenn eine IP nach einer Whitelist-Aktualisierung nun auf der externen
# Whitelist steht, wird sie automatisch entsperrt.
check_banned_whitelist_ips() {
local state_dir="${STATE_DIR:-/var/lib/adguard-shield}"
[[ -d "$state_dir" ]] || return
[[ -f "$EXTERNAL_WHITELIST_RESOLVED_FILE" ]] || return
for state_file in "${state_dir}"/*.ban "${state_dir}"/ext_*.ban; do
[[ -f "$state_file" ]] || continue
local client_ip
client_ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2)
[[ -z "$client_ip" ]] && continue
if grep -qxF "$client_ip" "$EXTERNAL_WHITELIST_RESOLVED_FILE" 2>/dev/null; then
log "INFO" "Gesperrte IP $client_ip ist jetzt auf externer Whitelist entsperre automatisch"
# iptables-Regel entfernen
if [[ "$client_ip" == *:* ]]; then
ip6tables -D "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true
else
iptables -D "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true
fi
rm -f "$state_file"
# Ban-History Eintrag
if [[ -f "${BAN_HISTORY_FILE:-/var/log/adguard-shield-bans.log}" ]]; then
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
printf "%-19s | %-6s | %-39s | %-30s | %-8s | %-10s | %-10s | %s\n" \
"$timestamp" "UNBAN" "$client_ip" "-" "-" "-" "-" "external-whitelist" \
>> "${BAN_HISTORY_FILE:-/var/log/adguard-shield-bans.log}"
fi
fi
done
}
# ─── PID-Management ──────────────────────────────────────────────────────────
write_pid() {
echo $$ > "$WORKER_PID_FILE"
}
cleanup() {
log "INFO" "Externer Whitelist-Worker wird beendet..."
rm -f "$WORKER_PID_FILE"
exit 0
}
check_already_running() {
if [[ -f "$WORKER_PID_FILE" ]]; then
local old_pid
old_pid=$(cat "$WORKER_PID_FILE")
if kill -0 "$old_pid" 2>/dev/null; then
log "DEBUG" "Whitelist-Worker läuft bereits (PID: $old_pid)"
return 1
else
rm -f "$WORKER_PID_FILE"
fi
fi
return 0
}
# ─── Status anzeigen ─────────────────────────────────────────────────────────
show_status() {
echo "═══════════════════════════════════════════════════════════════"
echo " Externer Whitelist-Worker - Status"
echo "═══════════════════════════════════════════════════════════════"
echo ""
if [[ "$EXTERNAL_WHITELIST_ENABLED" != "true" ]]; then
echo " ⚠️ Externer Whitelist-Worker ist deaktiviert"
echo " Aktivieren: EXTERNAL_WHITELIST_ENABLED=true in $CONFIG_FILE"
echo ""
return
fi
# Worker-Prozess Status
if [[ -f "$WORKER_PID_FILE" ]]; then
local pid
pid=$(cat "$WORKER_PID_FILE")
if kill -0 "$pid" 2>/dev/null; then
echo " ✅ Worker läuft (PID: $pid)"
else
echo " ❌ Worker nicht aktiv (veraltete PID-Datei)"
fi
else
echo " ❌ Worker nicht aktiv"
fi
echo ""
# Konfigurierte URLs
echo " Konfigurierte Whitelisten:"
IFS=',' read -ra urls <<< "$EXTERNAL_WHITELIST_URLS"
local index=0
for url in "${urls[@]}"; do
url=$(echo "$url" | xargs)
[[ -z "$url" ]] && continue
local cache_file="${EXTERNAL_WHITELIST_CACHE_DIR}/whitelist_${index}.txt"
if [[ -f "$cache_file" ]]; then
local entry_count
entry_count=$(grep -cv '^\s*#\|^\s*$' "$cache_file" 2>/dev/null || echo "0")
local last_modified
last_modified=$(date -r "$cache_file" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "unbekannt")
echo " [$index] $url"
echo " Einträge: $entry_count | Zuletzt aktualisiert: $last_modified"
else
echo " [$index] $url (noch nicht heruntergeladen)"
fi
index=$((index + 1))
done
echo ""
# Aufgelöste IPs
if [[ -f "$EXTERNAL_WHITELIST_RESOLVED_FILE" ]]; then
local resolved_count
resolved_count=$(wc -l < "$EXTERNAL_WHITELIST_RESOLVED_FILE" | xargs)
local last_resolved
last_resolved=$(date -r "$EXTERNAL_WHITELIST_RESOLVED_FILE" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "unbekannt")
echo " Aufgelöste IPs: $resolved_count"
echo " Letzte Auflösung: $last_resolved"
if [[ "$resolved_count" -gt 0 && "$resolved_count" -le 20 ]]; then
echo ""
echo " Aktuelle IPs:"
while IFS= read -r ip; do
echo "$ip"
done < "$EXTERNAL_WHITELIST_RESOLVED_FILE"
elif [[ "$resolved_count" -gt 20 ]]; then
echo ""
echo " Erste 20 IPs:"
head -20 "$EXTERNAL_WHITELIST_RESOLVED_FILE" | while IFS= read -r ip; do
echo "$ip"
done
echo " ... ($((resolved_count - 20)) weitere)"
fi
else
echo " Aufgelöste IPs: 0 (noch keine Synchronisation durchgeführt)"
fi
echo ""
echo " Prüfintervall: ${EXTERNAL_WHITELIST_INTERVAL}s"
echo ""
echo "═══════════════════════════════════════════════════════════════"
}
# ─── Einmalig synchronisieren ────────────────────────────────────────────────
run_once() {
init_directories
if [[ -z "${EXTERNAL_WHITELIST_URLS:-}" ]]; then
log "ERROR" "Keine externen Whitelist-URLs konfiguriert (EXTERNAL_WHITELIST_URLS)"
exit 1
fi
log "INFO" "Einmalige Whitelist-Synchronisation..."
sync_whitelists
log "INFO" "Whitelist-Synchronisation abgeschlossen"
}
# ─── Hauptschleife ──────────────────────────────────────────────────────────
main_loop() {
init_directories
if [[ -z "${EXTERNAL_WHITELIST_URLS:-}" ]]; then
log "ERROR" "Keine externen Whitelist-URLs konfiguriert (EXTERNAL_WHITELIST_URLS)"
exit 1
fi
log "INFO" "═══════════════════════════════════════════════════════════"
log "INFO" "Externer Whitelist-Worker gestartet"
log "INFO" " URLs: ${EXTERNAL_WHITELIST_URLS}"
log "INFO" " Prüfintervall: ${EXTERNAL_WHITELIST_INTERVAL}s"
log "INFO" "═══════════════════════════════════════════════════════════"
while true; do
sync_whitelists
sleep "$EXTERNAL_WHITELIST_INTERVAL"
done
}
# ─── Signal-Handler ──────────────────────────────────────────────────────────
trap cleanup SIGTERM SIGINT SIGHUP
# ─── Kommandozeilen-Argumente ────────────────────────────────────────────────
case "${1:-start}" in
start)
if ! check_already_running; then
exit 0
fi
write_pid
main_loop
;;
stop)
if [[ -f "$WORKER_PID_FILE" ]]; then
kill "$(cat "$WORKER_PID_FILE")" 2>/dev/null || true
rm -f "$WORKER_PID_FILE"
echo "Whitelist-Worker gestoppt"
else
echo "Whitelist-Worker läuft nicht"
fi
;;
sync)
run_once
;;
status)
init_directories
show_status
;;
flush)
init_directories
echo "Entferne aufgelöste externe Whitelist-IPs..."
rm -f "$EXTERNAL_WHITELIST_RESOLVED_FILE"
echo "Externe Whitelist-IPs entfernt"
;;
*)
cat << USAGE
AdGuard Shield - Externer Whitelist-Worker
Nutzung: $0 {start|stop|sync|status|flush}
Befehle:
start Startet den Worker (Dauerbetrieb)
stop Stoppt den Worker
sync Einmalige Synchronisation (DNS-Auflösung)
status Zeigt Status und aufgelöste IPs
flush Entfernt alle aufgelösten Whitelist-IPs
Konfiguration: $CONFIG_FILE
USAGE
exit 0
;;
esac

932
geoip-worker.sh Normal file
View File

@@ -0,0 +1,932 @@
#!/bin/bash
###############################################################################
# AdGuard Shield - GeoIP Worker
# Prüft Client-IPs auf Herkunftsland und sperrt/erlaubt basierend auf Konfig.
# Wird als Hintergrundprozess vom Hauptscript gestartet.
#
# Autor: Patrick Asmus
# E-Mail: support@techniverse.net
# Lizenz: MIT
###############################################################################
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/adguard-shield.conf"
# ─── Konfiguration laden ───────────────────────────────────────────────────────
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "FEHLER: Konfigurationsdatei nicht gefunden: $CONFIG_FILE" >&2
exit 1
fi
# shellcheck source=adguard-shield.conf
source "$CONFIG_FILE"
# ─── Worker PID-File ──────────────────────────────────────────────────────────
WORKER_PID_FILE="/var/run/adguard-geoip-worker.pid"
# ─── GeoIP Cache ──────────────────────────────────────────────────────────────
GEOIP_CACHE_DIR="${STATE_DIR}/geoip-cache"
# ─── MaxMind Auto-Download Verzeichnis ────────────────────────────────────────
GEOIP_DB_DIR="${SCRIPT_DIR}/geoip"
GEOIP_AUTO_DB="${GEOIP_DB_DIR}/GeoLite2-Country.mmdb"
GEOIP_DB_UPDATE_INTERVAL=86400 # 24 Stunden (fest)
# ─── Logging (eigene Funktion, nutzt gleiche Log-Datei) ───────────────────────
declare -A LOG_LEVELS=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3)
log() {
local level="$1"
shift
local message="$*"
local configured_level="${LOG_LEVEL:-INFO}"
if [[ ${LOG_LEVELS[$level]:-1} -ge ${LOG_LEVELS[$configured_level]:-1} ]]; then
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
local log_entry="[$timestamp] [$level] [GEOIP-WORKER] $message"
echo "$log_entry" | tee -a "$LOG_FILE" >&2
fi
}
# ─── Ban-History ─────────────────────────────────────────────────────────────
log_ban_history() {
local action="$1"
local client_ip="$2"
local country="${3:-}"
local reason="${4:-geoip}"
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
if [[ ! -f "$BAN_HISTORY_FILE" ]]; then
echo "# AdGuard Shield - Ban History" > "$BAN_HISTORY_FILE"
echo "# Format: ZEITSTEMPEL | AKTION | CLIENT-IP | DOMAIN | ANFRAGEN | SPERRDAUER | PROTOKOLL | GRUND" >> "$BAN_HISTORY_FILE"
echo "#──────────────────────────────────────────────────────────────────────────────────────────────────" >> "$BAN_HISTORY_FILE"
fi
local duration="permanent"
printf "%-19s | %-6s | %-39s | %-30s | %-8s | %-10s | %-10s | %s\n" \
"$timestamp" "$action" "$client_ip" "Land: ${country:-?}" "-" "$duration" "-" "$reason" \
>> "$BAN_HISTORY_FILE"
}
# ─── Verzeichnisse erstellen ──────────────────────────────────────────────────
init_directories() {
mkdir -p "$GEOIP_CACHE_DIR"
mkdir -p "$GEOIP_DB_DIR"
mkdir -p "$STATE_DIR"
mkdir -p "$(dirname "$LOG_FILE")"
}
# ─── Private IP-Adressen erkennen ────────────────────────────────────────────
is_private_ip() {
local ip="$1"
# IPv6 Loopback und Link-Local
if [[ "$ip" == "::1" || "$ip" == fe80:* || "$ip" == fc00:* || "$ip" == fd00:* ]]; then
return 0
fi
# IPv4 private Bereiche
if [[ "$ip" =~ ^10\. || "$ip" =~ ^172\.(1[6-9]|2[0-9]|3[0-1])\. || "$ip" =~ ^192\.168\. || "$ip" =~ ^127\. || "$ip" == "0.0.0.0" ]]; then
return 0
fi
# IPv4 CGNAT
if [[ "$ip" =~ ^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\. ]]; then
return 0
fi
return 1
}
# ─── Whitelist Prüfung ───────────────────────────────────────────────────────
is_whitelisted() {
local ip="$1"
IFS=',' read -ra wl_entries <<< "$WHITELIST"
for entry in "${wl_entries[@]}"; do
entry=$(echo "$entry" | xargs) # trim
if [[ "$ip" == "$entry" ]]; then
return 0
fi
done
# Externe Whitelist prüfen
local ext_wl_file="${EXTERNAL_WHITELIST_CACHE_DIR:-/var/lib/adguard-shield/external-whitelist}/resolved_ips.txt"
if [[ -f "$ext_wl_file" ]] && grep -qxF "$ip" "$ext_wl_file" 2>/dev/null; then
return 0
fi
return 1
}
# ─── MaxMind GeoLite2 Auto-Download & Update ─────────────────────────────────
# Lädt die GeoLite2-Country.mmdb herunter, wenn GEOIP_LICENSE_KEY gesetzt ist
# und kein eigener GEOIP_MMDB_PATH angegeben wurde.
# Aktualisiert automatisch alle 24 Stunden.
update_maxmind_db() {
local license_key="${GEOIP_LICENSE_KEY:-}"
# Kein License-Key → nichts zu tun
if [[ -z "$license_key" ]]; then
return 0
fi
# User hat eigenen Pfad gesetzt → kein Auto-Download
if [[ -n "${GEOIP_MMDB_PATH:-}" ]]; then
return 0
fi
# Prüfen ob Update nötig (alle 24h)
if [[ -f "$GEOIP_AUTO_DB" ]]; then
local db_age
db_age=$(( $(date '+%s') - $(stat -c '%Y' "$GEOIP_AUTO_DB" 2>/dev/null || stat -f '%m' "$GEOIP_AUTO_DB" 2>/dev/null || echo "0") ))
if [[ "$db_age" -lt "$GEOIP_DB_UPDATE_INTERVAL" ]]; then
log "DEBUG" "MaxMind DB ist aktuell (Alter: $((db_age / 3600))h, nächstes Update in $(( (GEOIP_DB_UPDATE_INTERVAL - db_age) / 3600 ))h)"
return 0
fi
log "INFO" "MaxMind DB ist älter als 24h starte Update..."
else
log "INFO" "MaxMind DB nicht vorhanden starte Erstdownload..."
fi
# Download-URL zusammenbauen (MaxMind Permalink)
local download_url="https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=${license_key}&suffix=tar.gz"
local tmp_file="${GEOIP_DB_DIR}/GeoLite2-Country.tar.gz"
local tmp_extract="${GEOIP_DB_DIR}/extract_tmp"
# Herunterladen
local http_code
http_code=$(curl -s -o "$tmp_file" -w "%{http_code}" \
--connect-timeout 10 \
--max-time 60 \
"$download_url" 2>/dev/null) || true
if [[ "$http_code" != "200" ]]; then
rm -f "$tmp_file"
case "$http_code" in
401) log "ERROR" "MaxMind Download fehlgeschlagen: Ungültiger License-Key (HTTP 401)" ;;
403) log "ERROR" "MaxMind Download fehlgeschlagen: Zugriff verweigert (HTTP 403) License-Key prüfen" ;;
*) log "ERROR" "MaxMind Download fehlgeschlagen (HTTP ${http_code:-timeout})" ;;
esac
return 1
fi
# Entpacken
rm -rf "$tmp_extract"
mkdir -p "$tmp_extract"
if ! tar -xzf "$tmp_file" -C "$tmp_extract" 2>/dev/null; then
log "ERROR" "MaxMind DB: tar-Archiv konnte nicht entpackt werden"
rm -f "$tmp_file"
rm -rf "$tmp_extract"
return 1
fi
# .mmdb Datei finden und verschieben
local mmdb_file
mmdb_file=$(find "$tmp_extract" -name 'GeoLite2-Country.mmdb' -type f 2>/dev/null | head -1)
if [[ -z "$mmdb_file" || ! -f "$mmdb_file" ]]; then
log "ERROR" "MaxMind DB: GeoLite2-Country.mmdb nicht im Archiv gefunden"
rm -f "$tmp_file"
rm -rf "$tmp_extract"
return 1
fi
mv "$mmdb_file" "$GEOIP_AUTO_DB"
rm -f "$tmp_file"
rm -rf "$tmp_extract"
log "INFO" "MaxMind GeoLite2-Country DB erfolgreich aktualisiert: $GEOIP_AUTO_DB"
return 0
}
# ─── Effektiven MMDB-Pfad ermitteln ──────────────────────────────────────────
# Priorität: GEOIP_MMDB_PATH (User) > Auto-Download > leer (Fallback auf geoiplookup)
resolve_mmdb_path() {
# User hat eigenen Pfad gesetzt
if [[ -n "${GEOIP_MMDB_PATH:-}" && -f "${GEOIP_MMDB_PATH:-}" ]]; then
echo "$GEOIP_MMDB_PATH"
return 0
fi
# Auto-Download DB vorhanden
if [[ -f "$GEOIP_AUTO_DB" ]]; then
echo "$GEOIP_AUTO_DB"
return 0
fi
# Kein MMDB verfügbar
echo ""
return 1
}
# ─── GeoIP Lookup ────────────────────────────────────────────────────────────
# Gibt den ISO 3166-1 Alpha-2 Ländercode zurück (z.B. "DE", "US", "CN")
# Nutzt Cache um wiederholte Lookups zu vermeiden
geoip_lookup() {
local ip="$1"
local cache_file="${GEOIP_CACHE_DIR}/${ip//[:\/]/_}.country"
# Cache prüfen (max 24 Stunden alt)
if [[ -f "$cache_file" ]]; then
local cache_age
cache_age=$(( $(date '+%s') - $(stat -c '%Y' "$cache_file" 2>/dev/null || stat -f '%m' "$cache_file" 2>/dev/null || echo "0") ))
if [[ "$cache_age" -lt 86400 ]]; then
cat "$cache_file"
return 0
fi
fi
local country_code=""
# Effektiven MMDB-Pfad ermitteln (User-Pfad oder Auto-Download)
local effective_mmdb
effective_mmdb=$(resolve_mmdb_path 2>/dev/null) || true
# Methode 1: MaxMind mmdbinspect (bevorzugt, genauer)
if [[ -n "$effective_mmdb" && -f "$effective_mmdb" ]] && command -v mmdbinspect &>/dev/null; then
country_code=$(mmdbinspect -db "$effective_mmdb" -ip "$ip" 2>/dev/null \
| jq -r '.[0].Records[0].Record.country.iso_code // empty' 2>/dev/null || true)
fi
# Methode 2: geoiplookup (GeoIP Legacy)
if [[ -z "$country_code" ]] && command -v geoiplookup &>/dev/null; then
if [[ "$ip" == *:* ]]; then
# IPv6
if command -v geoiplookup6 &>/dev/null; then
country_code=$(geoiplookup6 "$ip" 2>/dev/null \
| grep -oP '(?<=: )[A-Z]{2}(?=,)' | head -1 || true)
fi
else
# IPv4
country_code=$(geoiplookup "$ip" 2>/dev/null \
| grep -oP '(?<=: )[A-Z]{2}(?=,)' | head -1 || true)
fi
fi
# Methode 3: mmdblookup (libmaxminddb)
if [[ -z "$country_code" && -n "$effective_mmdb" && -f "$effective_mmdb" ]] && command -v mmdblookup &>/dev/null; then
country_code=$(mmdblookup --file "$effective_mmdb" --ip "$ip" country iso_code 2>/dev/null \
| grep -oP '"[A-Z]{2}"' | tr -d '"' | head -1 || true)
fi
if [[ -n "$country_code" ]]; then
echo "$country_code" > "$cache_file"
echo "$country_code"
return 0
fi
# Unbekannt nicht cachen (könnte temporärer Fehler sein)
echo ""
return 1
}
# ─── GeoIP Prüfung: Soll eine IP gesperrt werden? ────────────────────────────
# Return 0 = sperren, Return 1 = erlauben
should_block_by_geoip() {
local country_code="$1"
local mode="${GEOIP_MODE:-blocklist}"
local countries="${GEOIP_COUNTRIES:-}"
[[ -z "$country_code" || -z "$countries" ]] && return 1
# Länder-Liste in Array umwandeln
IFS=',' read -ra country_list <<< "$countries"
local found=false
for c in "${country_list[@]}"; do
c=$(echo "$c" | xargs | tr '[:lower:]' '[:upper:]') # trim + uppercase
if [[ "$country_code" == "$c" ]]; then
found=true
break
fi
done
if [[ "$mode" == "blocklist" ]]; then
# Blocklist-Modus: Sperren wenn Land in der Liste
[[ "$found" == "true" ]] && return 0 || return 1
elif [[ "$mode" == "allowlist" ]]; then
# Allowlist-Modus: Sperren wenn Land NICHT in der Liste
[[ "$found" == "true" ]] && return 1 || return 0
fi
return 1
}
# ─── IP via iptables sperren ─────────────────────────────────────────────────
ban_ip_geoip() {
local client_ip="$1"
local country_code="$2"
local mode="${GEOIP_MODE:-blocklist}"
# Prüfen ob bereits gesperrt
local state_file="${STATE_DIR}/${client_ip//[:\/]/_}.ban"
if [[ -f "$state_file" ]]; then
log "DEBUG" "GeoIP: $client_ip ist bereits gesperrt"
return 0
fi
# GeoIP-Sperren sind immer permanent
local ban_until=0
local ban_until_display="PERMANENT"
local reason_text
if [[ "$mode" == "blocklist" ]]; then
reason_text="geoip-blocklist (Land: $country_code)"
else
reason_text="geoip-allowlist (Land: $country_code)"
fi
log "WARN" "GeoIP SPERRE: $client_ip (Land: $country_code, Modus: $mode) PERMANENT"
# iptables Regel setzen
if [[ "$client_ip" == *:* ]]; then
ip6tables -I "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true
else
iptables -I "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true
fi
# State-Datei erstellen
cat > "$state_file" << EOF
CLIENT_IP=$client_ip
DOMAIN=GeoIP:${country_code}
COUNT=-
BAN_TIME=$(date '+%Y-%m-%d %H:%M:%S')
BAN_UNTIL_EPOCH=0
BAN_UNTIL=PERMANENT
BAN_DURATION=0
OFFENSE_LEVEL=0
IS_PERMANENT=true
REASON=geoip
PROTOCOL=-
GEOIP_COUNTRY=$country_code
GEOIP_MODE=$mode
EOF
# Ban-History
log_ban_history "BAN" "$client_ip" "$country_code" "$reason_text"
# Benachrichtigung senden
if [[ "${GEOIP_NOTIFY:-true}" == "true" && "${NOTIFY_ENABLED:-false}" == "true" ]]; then
send_geoip_notification "ban" "$client_ip" "$country_code" "PERMANENT" "$mode"
fi
}
# ─── GeoIP Benachrichtigung ──────────────────────────────────────────────────
send_geoip_notification() {
local action="$1"
local client_ip="$2"
local country_code="$3"
local duration="${4:-PERMANENT}"
local mode="${5:-blocklist}"
local my_hostname
my_hostname=$(hostname)
local title="🌍 🛡️ AdGuard Shield"
local mode_label
[[ "$mode" == "blocklist" ]] && mode_label="Blocklist" || mode_label="Allowlist"
local message="🌍 AdGuard Shield GeoIP-Sperre auf ${my_hostname}
---
IP: ${client_ip}
Land: ${country_code}
Modus: ${mode_label}
Dauer: ${duration}
Whois: https://www.whois.com/whois/${client_ip}
AbuseIPDB: https://www.abuseipdb.com/check/${client_ip}"
case "${NOTIFY_TYPE:-}" in
discord)
local json_payload
json_payload=$(jq -nc --arg msg "$message" '{content: $msg}')
curl -s -H "Content-Type: application/json" \
-d "$json_payload" \
"$NOTIFY_WEBHOOK_URL" &>/dev/null &
;;
slack)
local json_payload
json_payload=$(jq -nc --arg msg "$message" '{text: $msg}')
curl -s -H "Content-Type: application/json" \
-d "$json_payload" \
"$NOTIFY_WEBHOOK_URL" &>/dev/null &
;;
gotify)
local clean_message
clean_message=$(echo "$message" | sed 's/\*\*//g')
curl -s -X POST "$NOTIFY_WEBHOOK_URL" \
-F "title=${title}" \
-F "message=${clean_message}" \
-F "priority=5" &>/dev/null &
;;
ntfy)
local ntfy_url="${NTFY_SERVER_URL:-https://ntfy.sh}"
local -a curl_args=(
-s -X POST
"${ntfy_url}/${NTFY_TOPIC}"
-H "Title: 🛡️ AdGuard Shield GeoIP"
-H "Priority: ${NTFY_PRIORITY:-4}"
-H "Tags: globe_with_meridians,ban"
-d "$message"
)
[[ -n "${NTFY_TOKEN:-}" ]] && curl_args+=(-H "Authorization: Bearer ${NTFY_TOKEN}")
curl "${curl_args[@]}" &>/dev/null &
;;
generic)
local json_payload
json_payload=$(jq -nc --arg msg "$message" --arg cl "$client_ip" --arg cc "$country_code" \
'{message: $msg, action: "geoip-ban", client: $cl, country: $cc}')
curl -s -H "Content-Type: application/json" \
-d "$json_payload" \
"$NOTIFY_WEBHOOK_URL" &>/dev/null &
;;
esac
}
# ─── iptables Chain Setup ────────────────────────────────────────────────────
setup_iptables_chain() {
if ! iptables -n -L "$IPTABLES_CHAIN" &>/dev/null; then
iptables -N "$IPTABLES_CHAIN"
for port in $BLOCKED_PORTS; do
iptables -I INPUT -p tcp --dport "$port" -j "$IPTABLES_CHAIN"
iptables -I INPUT -p udp --dport "$port" -j "$IPTABLES_CHAIN"
done
fi
if ! ip6tables -n -L "$IPTABLES_CHAIN" &>/dev/null; then
ip6tables -N "$IPTABLES_CHAIN"
for port in $BLOCKED_PORTS; do
ip6tables -I INPUT -p tcp --dport "$port" -j "$IPTABLES_CHAIN"
ip6tables -I INPUT -p udp --dport "$port" -j "$IPTABLES_CHAIN"
done
fi
}
# ─── GeoIP-Tools Verfügbarkeit prüfen ────────────────────────────────────────
check_geoip_tools() {
# Effektiven MMDB-Pfad ermitteln
local effective_mmdb
effective_mmdb=$(resolve_mmdb_path 2>/dev/null) || true
# mmdbinspect + MMDB
if [[ -n "$effective_mmdb" && -f "$effective_mmdb" ]]; then
if command -v mmdbinspect &>/dev/null; then
echo "mmdbinspect"
return 0
elif command -v mmdblookup &>/dev/null; then
echo "mmdblookup"
return 0
fi
fi
# geoiplookup (Legacy GeoIP)
if command -v geoiplookup &>/dev/null; then
echo "geoiplookup"
return 0
fi
echo "none"
return 1
}
# ─── Client-IPs aus AdGuard API extrahieren ──────────────────────────────────
get_active_clients() {
local response
response=$(curl -s -u "${ADGUARD_USER}:${ADGUARD_PASS}" \
--connect-timeout 5 \
--max-time 10 \
-k "${ADGUARD_URL}/control/querylog?limit=${API_QUERY_LIMIT:-500}&response_status=all" 2>/dev/null)
if [[ -z "$response" || "$response" == "null" ]]; then
log "ERROR" "Keine Antwort von AdGuard Home API"
return 1
fi
# Eindeutige Client-IPs extrahieren
echo "$response" | jq -r '.data // [] | [.[].client // .[].client_info.ip] | unique | .[]' 2>/dev/null | sort -u
}
# ─── Auto-Unban: GeoIP-Sperren aufheben bei Konfigurationsänderung ────────────
# Prüft alle bestehenden GeoIP-Sperren und hebt sie auf, wenn:
# - Das Land nicht mehr in GEOIP_COUNTRIES steht
# - Der Modus gewechselt wurde (blocklist ↔ allowlist)
# - GeoIP deaktiviert wurde
auto_unban_geoip() {
local unban_count=0
for f in "${STATE_DIR}"/*.ban; do
[[ -f "$f" ]] || continue
local reason
reason=$(grep '^REASON=' "$f" | cut -d= -f2 || true)
[[ "$reason" != "geoip" ]] && continue
local client_ip country_code old_mode
client_ip=$(grep '^CLIENT_IP=' "$f" | cut -d= -f2 || true)
country_code=$(grep '^GEOIP_COUNTRY=' "$f" | cut -d= -f2 || true)
old_mode=$(grep '^GEOIP_MODE=' "$f" | cut -d= -f2 || true)
local should_unban=false
# GeoIP deaktiviert → alle GeoIP-Sperren aufheben
if [[ "${GEOIP_ENABLED:-false}" != "true" ]]; then
should_unban=true
# Modus gewechselt → alle GeoIP-Sperren aufheben und neu prüfen
elif [[ -n "$old_mode" && "$old_mode" != "${GEOIP_MODE:-blocklist}" ]]; then
should_unban=true
# Prüfen ob das Land nach aktueller Konfiguration noch gesperrt sein soll
elif [[ -n "$country_code" ]] && ! should_block_by_geoip "$country_code"; then
should_unban=true
fi
if [[ "$should_unban" == "true" ]]; then
log "INFO" "GeoIP Auto-Unban: $client_ip (Land: ${country_code:-?}, war: ${old_mode:-?})"
# iptables Regel entfernen
if [[ "$client_ip" == *:* ]]; then
ip6tables -D "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true
else
iptables -D "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true
fi
rm -f "$f"
log_ban_history "UNBAN" "$client_ip" "$country_code" "geoip-auto-unban"
unban_count=$((unban_count + 1))
fi
done
if [[ $unban_count -gt 0 ]]; then
log "INFO" "GeoIP Auto-Unban: $unban_count Sperren aufgehoben (Länderliste/Modus geändert)"
fi
}
# ─── Einmaliger GeoIP-Sync ──────────────────────────────────────────────────
sync_geoip() {
# Auto-Unban zuerst: bestehende Sperren prüfen, die nicht mehr zur Config passen
auto_unban_geoip
if [[ "${GEOIP_ENABLED:-false}" != "true" ]]; then
log "INFO" "GeoIP ist deaktiviert"
return 0
fi
# MaxMind DB automatisch herunterladen/aktualisieren (falls License-Key gesetzt)
update_maxmind_db || true
local countries="${GEOIP_COUNTRIES:-}"
if [[ -z "$countries" ]]; then
log "WARN" "GeoIP: Keine Länder konfiguriert (GEOIP_COUNTRIES ist leer)"
return 0
fi
local tool
tool=$(check_geoip_tools) || {
log "ERROR" "GeoIP: Kein GeoIP-Tool verfügbar. Installiere geoip-bin oder mmdbinspect."
return 1
}
log "INFO" "GeoIP-Sync gestartet (Tool: $tool, Modus: ${GEOIP_MODE:-blocklist}, Länder: $countries)"
# Client-IPs aus der API holen
local clients
clients=$(get_active_clients) || {
log "ERROR" "GeoIP: Konnte aktive Clients nicht ermitteln"
return 1
}
local checked=0
local blocked=0
local skipped=0
while IFS= read -r client_ip; do
[[ -z "$client_ip" || "$client_ip" == "null" ]] && continue
# Private IPs überspringen
if [[ "${GEOIP_SKIP_PRIVATE:-true}" == "true" ]] && is_private_ip "$client_ip"; then
log "DEBUG" "GeoIP: Private IP übersprungen: $client_ip"
skipped=$((skipped + 1))
continue
fi
# Whitelist prüfen
if is_whitelisted "$client_ip"; then
log "DEBUG" "GeoIP: Whitelisted IP übersprungen: $client_ip"
skipped=$((skipped + 1))
continue
fi
# Bereits gesperrt?
local state_file="${STATE_DIR}/${client_ip//[:\/]/_}.ban"
if [[ -f "$state_file" ]]; then
skipped=$((skipped + 1))
continue
fi
checked=$((checked + 1))
# GeoIP Lookup
local country_code
country_code=$(geoip_lookup "$client_ip") || true
if [[ -z "$country_code" ]]; then
log "DEBUG" "GeoIP: Kein Ergebnis für $client_ip"
continue
fi
log "DEBUG" "GeoIP: $client_ip$country_code"
# Prüfen ob gesperrt werden soll
if should_block_by_geoip "$country_code"; then
ban_ip_geoip "$client_ip" "$country_code"
blocked=$((blocked + 1))
fi
done <<< "$clients"
log "INFO" "GeoIP-Sync abgeschlossen: $checked geprüft, $blocked gesperrt, $skipped übersprungen"
}
# ─── Worker-Hauptschleife ────────────────────────────────────────────────────
start_worker() {
if [[ "${GEOIP_ENABLED:-false}" != "true" ]]; then
log "DEBUG" "GeoIP-Worker ist deaktiviert"
return 0
fi
# PID schreiben
echo $$ > "$WORKER_PID_FILE"
trap 'rm -f "$WORKER_PID_FILE"; exit 0' SIGTERM SIGINT SIGHUP
local interval="${GEOIP_CHECK_INTERVAL:-0}"
[[ "$interval" -le 0 ]] && interval="${CHECK_INTERVAL:-10}"
log "INFO" "GeoIP-Worker gestartet (PID: $$, Intervall: ${interval}s)"
# Beim Start: MaxMind DB herunterladen/aktualisieren (falls License-Key gesetzt)
update_maxmind_db || true
while true; do
sync_geoip
sleep "$interval"
done
}
# ─── Status anzeigen ─────────────────────────────────────────────────────────
show_status() {
echo "═══════════════════════════════════════════════════════════════"
echo " AdGuard Shield - GeoIP Status"
echo "═══════════════════════════════════════════════════════════════"
echo ""
if [[ "${GEOIP_ENABLED:-false}" != "true" ]]; then
echo " GeoIP ist deaktiviert"
echo ""
return
fi
echo " Modus: ${GEOIP_MODE:-blocklist}"
echo " Länder: ${GEOIP_COUNTRIES:-<keine>}"
echo " Sperrdauer: PERMANENT (Auto-Unban bei Änderung der Länderliste)"
echo " Private IPs überspringen: ${GEOIP_SKIP_PRIVATE:-true}"
echo ""
# MaxMind DB Info
local eff_mmdb
eff_mmdb=$(resolve_mmdb_path)
if [[ -n "${GEOIP_MMDB_PATH:-}" ]]; then
echo " MMDB-Pfad: ${GEOIP_MMDB_PATH} (manuell konfiguriert)"
elif [[ -n "${GEOIP_LICENSE_KEY:-}" ]]; then
echo " MMDB-Pfad: ${GEOIP_AUTO_DB} (Auto-Download)"
if [[ -f "${GEOIP_AUTO_DB}" ]]; then
local db_age db_age_h
db_age=$(( $(date +%s) - $(stat -c %Y "${GEOIP_AUTO_DB}" 2>/dev/null || echo 0) ))
db_age_h=$(( db_age / 3600 ))
echo " DB-Alter: ${db_age_h}h (Update alle 24h)"
else
echo " DB-Status: ⚠️ Noch nicht heruntergeladen"
fi
elif [[ -n "$eff_mmdb" ]]; then
echo " MMDB-Pfad: ${eff_mmdb}"
else
echo " MMDB-Pfad: <nicht konfiguriert> (Fallback auf geoiplookup)"
fi
echo " License-Key: $(if [[ -n "${GEOIP_LICENSE_KEY:-}" ]]; then echo "✅ konfiguriert"; else echo "❌ nicht gesetzt (kein Auto-Download)"; fi)"
echo ""
# GeoIP Tools prüfen
echo " GeoIP Tools:"
local tool
tool=$(check_geoip_tools 2>/dev/null) || tool="none"
case "$tool" in
mmdbinspect) echo " ✅ mmdbinspect mit MaxMind DB" ;;
mmdblookup) echo " ✅ mmdblookup mit MaxMind DB" ;;
geoiplookup) echo " ✅ geoiplookup (Legacy GeoIP)" ;;
none) echo " ❌ Kein GeoIP-Tool gefunden!" ;;
esac
echo ""
# Worker-Status
if [[ -f "$WORKER_PID_FILE" ]]; then
local wpid
wpid=$(cat "$WORKER_PID_FILE")
if kill -0 "$wpid" 2>/dev/null; then
echo " Worker: Läuft (PID: $wpid)"
else
echo " Worker: Abgestürzt (PID: $wpid existiert nicht mehr)"
fi
else
echo " Worker: Nicht gestartet"
fi
echo ""
# GeoIP-Sperren anzeigen
local geoip_bans=0
if [[ -d "$STATE_DIR" ]]; then
for f in "${STATE_DIR}"/*.ban; do
[[ -f "$f" ]] || continue
local reason
reason=$(grep '^REASON=' "$f" | cut -d= -f2 || true)
if [[ "$reason" == "geoip" ]]; then
geoip_bans=$((geoip_bans + 1))
local s_ip s_country s_until
s_ip=$(grep '^CLIENT_IP=' "$f" | cut -d= -f2 || true)
s_country=$(grep '^GEOIP_COUNTRY=' "$f" | cut -d= -f2 || true)
s_until=$(grep '^BAN_UNTIL=' "$f" | cut -d= -f2 || true)
echo " 🌍 $s_ip → Land: ${s_country:-?} (bis: ${s_until:-?})"
fi
done
fi
if [[ $geoip_bans -eq 0 ]]; then
echo " Keine aktiven GeoIP-Sperren"
else
echo ""
echo " Gesamt: $geoip_bans aktive GeoIP-Sperren"
fi
# Cache-Statistik
if [[ -d "$GEOIP_CACHE_DIR" ]]; then
local cache_count
cache_count=$(find "$GEOIP_CACHE_DIR" -name '*.country' -type f 2>/dev/null | wc -l)
echo ""
echo " Cache: $cache_count IP-Lookups zwischengespeichert"
fi
echo ""
echo "═══════════════════════════════════════════════════════════════"
}
# ─── Einzelne IP nachschlagen ────────────────────────────────────────────────
lookup_ip() {
local ip="$1"
local eff_mmdb
eff_mmdb=$(resolve_mmdb_path)
local tool
tool=$(check_geoip_tools 2>/dev/null) || tool="none"
if [[ "$tool" == "none" ]]; then
echo "❌ Kein GeoIP-Tool verfügbar."
echo " Installiere geoip-bin: sudo apt install geoip-bin geoip-database"
echo " Oder mmdbinspect mit MaxMind GeoLite2 DB"
return 1
fi
local country_code
country_code=$(geoip_lookup "$ip") || true
if [[ -z "$country_code" ]]; then
echo "IP: $ip → Land: unbekannt (kein GeoIP-Ergebnis)"
return 1
fi
echo "IP: $ip → Land: $country_code (Tool: $tool)"
[[ -n "$eff_mmdb" ]] && echo " MMDB: $eff_mmdb"
# Prüfen ob diese IP gesperrt werden würde
if [[ "${GEOIP_ENABLED:-false}" == "true" && -n "${GEOIP_COUNTRIES:-}" ]]; then
if should_block_by_geoip "$country_code"; then
echo "→ Würde GESPERRT werden (Modus: ${GEOIP_MODE:-blocklist}, Länder: ${GEOIP_COUNTRIES})"
else
echo "→ Würde ERLAUBT werden (Modus: ${GEOIP_MODE:-blocklist}, Länder: ${GEOIP_COUNTRIES})"
fi
fi
}
# ─── Cache leeren ────────────────────────────────────────────────────────────
flush_cache() {
if [[ -d "$GEOIP_CACHE_DIR" ]]; then
local count
count=$(find "$GEOIP_CACHE_DIR" -name '*.country' -type f 2>/dev/null | wc -l)
rm -f "${GEOIP_CACHE_DIR}"/*.country 2>/dev/null || true
echo "✅ GeoIP-Cache geleert ($count Einträge entfernt)"
log "INFO" "GeoIP-Cache geleert ($count Einträge)"
else
echo " GeoIP-Cache-Verzeichnis existiert nicht"
fi
}
# ─── GeoIP-Sperren aufheben ─────────────────────────────────────────────────
flush_geoip_bans() {
local count=0
if [[ -d "$STATE_DIR" ]]; then
for f in "${STATE_DIR}"/*.ban; do
[[ -f "$f" ]] || continue
local reason
reason=$(grep '^REASON=' "$f" | cut -d= -f2 || true)
if [[ "$reason" == "geoip" ]]; then
local client_ip
client_ip=$(grep '^CLIENT_IP=' "$f" | cut -d= -f2 || true)
# iptables Regel entfernen
if [[ "$client_ip" == *:* ]]; then
ip6tables -D "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true
else
iptables -D "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true
fi
rm -f "$f"
log_ban_history "UNBAN" "$client_ip" "" "geoip-flush"
count=$((count + 1))
fi
done
fi
echo "$count GeoIP-Sperren aufgehoben"
log "INFO" "$count GeoIP-Sperren aufgehoben (flush)"
}
# ─── Hauptprogramm ──────────────────────────────────────────────────────────
case "${1:-help}" in
start)
init_directories
setup_iptables_chain
start_worker
;;
sync)
init_directories
setup_iptables_chain
sync_geoip
;;
status)
init_directories
show_status
;;
lookup)
if [[ -z "${2:-}" ]]; then
echo "Nutzung: $0 lookup <IP-Adresse>" >&2
exit 1
fi
init_directories
lookup_ip "$2"
;;
flush)
init_directories
flush_geoip_bans
;;
flush-cache)
init_directories
flush_cache
;;
stop)
if [[ -f "$WORKER_PID_FILE" ]]; then
local wpid
wpid=$(cat "$WORKER_PID_FILE")
if kill -0 "$wpid" 2>/dev/null; then
kill "$wpid" 2>/dev/null || true
rm -f "$WORKER_PID_FILE"
echo "GeoIP-Worker gestoppt"
else
rm -f "$WORKER_PID_FILE"
echo "GeoIP-Worker war nicht aktiv"
fi
else
echo "GeoIP-Worker läuft nicht"
fi
;;
*)
cat << USAGE
AdGuard Shield - GeoIP Worker
Nutzung: $0 {start|stop|sync|status|lookup|flush|flush-cache}
Befehle:
start Startet den GeoIP-Worker (Hintergrundprozess)
stop Stoppt den GeoIP-Worker
sync Einmalige GeoIP-Prüfung aller aktiven Clients
status Zeigt GeoIP-Status und aktive Sperren
lookup <IP> GeoIP-Lookup für eine einzelne IP
flush Alle GeoIP-Sperren aufheben
flush-cache GeoIP-Lookup-Cache leeren
Konfiguration in: $CONFIG_FILE
GEOIP_ENABLED=true/false
GEOIP_MODE=blocklist/allowlist
GEOIP_COUNTRIES="CN,RU,..."
USAGE
exit 0
;;
esac

View File

@@ -6,7 +6,7 @@
# Lizenz: MIT
###############################################################################
VERSION="v0.5.2"
VERSION="v0.9.0"
set -euo pipefail
@@ -103,6 +103,11 @@ print_help() {
echo -e " ${CYAN}sudo /opt/adguard-shield/adguard-shield.sh test${NC} # API-Verbindung testen"
echo -e " ${CYAN}sudo /opt/adguard-shield/adguard-shield.sh dry-run${NC} # Testmodus (nur loggen)"
echo ""
echo -e "${BOLD}Externe Whitelist-Befehle:${NC}"
echo -e " ${CYAN}sudo /opt/adguard-shield/adguard-shield.sh whitelist-status${NC} # Status der externen Whitelisten"
echo -e " ${CYAN}sudo /opt/adguard-shield/adguard-shield.sh whitelist-sync${NC} # Einmalige Synchronisation"
echo -e " ${CYAN}sudo /opt/adguard-shield/adguard-shield.sh whitelist-flush${NC} # Aufgelöste IPs entfernen"
echo ""
echo -e "${BOLD}iptables-Befehle:${NC}"
echo -e " ${CYAN}sudo /opt/adguard-shield/iptables-helper.sh status${NC} # Firewall-Regeln anzeigen"
echo -e " ${CYAN}sudo /opt/adguard-shield/iptables-helper.sh ban IP${NC} # IP manuell sperren"
@@ -120,11 +125,25 @@ print_help() {
echo -e " ${CYAN}sudo /opt/adguard-shield/report-generator.sh install${NC} # Cron-Job einrichten"
echo -e " ${CYAN}sudo /opt/adguard-shield/report-generator.sh remove${NC} # Cron-Job entfernen"
echo ""
echo -e "${BOLD}Watchdog-Befehle:${NC}"
echo -e " ${CYAN}sudo systemctl status adguard-shield-watchdog.timer${NC} # Watchdog-Status"
echo -e " ${CYAN}sudo systemctl list-timers adguard-shield-watchdog.timer${NC} # Nächste Ausführung"
echo -e " ${CYAN}sudo systemctl enable adguard-shield-watchdog.timer${NC} # Watchdog aktivieren"
echo -e " ${CYAN}sudo systemctl disable adguard-shield-watchdog.timer${NC} # Watchdog deaktivieren"
echo -e " ${CYAN}sudo journalctl -u adguard-shield-watchdog.service${NC} # Watchdog-Logs"
echo ""
echo -e "${BOLD}GeoIP-Befehle:${NC}"
echo -e " ${CYAN}sudo /opt/adguard-shield/adguard-shield.sh geoip-status${NC} # GeoIP-Status anzeigen"
echo -e " ${CYAN}sudo /opt/adguard-shield/adguard-shield.sh geoip-sync${NC} # Einmalige GeoIP-Prüfung"
echo -e " ${CYAN}sudo /opt/adguard-shield/adguard-shield.sh geoip-flush${NC} # Alle GeoIP-Sperren aufheben"
echo -e " ${CYAN}sudo /opt/adguard-shield/adguard-shield.sh geoip-lookup IP${NC} # GeoIP-Lookup einer IP"
echo ""
echo -e "${BOLD}Voraussetzungen:${NC}"
echo " - Linux Server (Debian/Ubuntu empfohlen)"
echo " - Root-Zugriff (sudo)"
echo " - AdGuard Home installiert und erreichbar"
echo " - Pakete: curl, jq, iptables, gawk (werden bei Installation automatisch installiert)"
echo " - GeoIP (optional): geoip-bin + geoip-database oder MaxMind GeoLite2 DB"
echo ""
echo -e "${BOLD}Dokumentation:${NC}"
echo " https://git.techniverse.net/scriptos/adguard-shield"
@@ -241,8 +260,12 @@ install_files() {
cp "$SCRIPT_DIR/iptables-helper.sh" "$INSTALL_DIR/"
cp "$SCRIPT_DIR/unban-expired.sh" "$INSTALL_DIR/"
cp "$SCRIPT_DIR/external-blocklist-worker.sh" "$INSTALL_DIR/"
cp "$SCRIPT_DIR/external-whitelist-worker.sh" "$INSTALL_DIR/"
cp "$SCRIPT_DIR/report-generator.sh" "$INSTALL_DIR/"
cp "$SCRIPT_DIR/adguard-shield-watchdog.sh" "$INSTALL_DIR/"
cp "$SCRIPT_DIR/uninstall.sh" "$INSTALL_DIR/"
cp "$SCRIPT_DIR/geoip-worker.sh" "$INSTALL_DIR/"
cp "$SCRIPT_DIR/offense-cleanup-worker.sh" "$INSTALL_DIR/"
# Templates kopieren
mkdir -p "$INSTALL_DIR/templates"
@@ -254,8 +277,12 @@ install_files() {
chmod +x "$INSTALL_DIR/iptables-helper.sh"
chmod +x "$INSTALL_DIR/unban-expired.sh"
chmod +x "$INSTALL_DIR/external-blocklist-worker.sh"
chmod +x "$INSTALL_DIR/external-whitelist-worker.sh"
chmod +x "$INSTALL_DIR/report-generator.sh"
chmod +x "$INSTALL_DIR/adguard-shield-watchdog.sh"
chmod +x "$INSTALL_DIR/uninstall.sh"
chmod +x "$INSTALL_DIR/geoip-worker.sh"
chmod +x "$INSTALL_DIR/offense-cleanup-worker.sh"
echo -e " ✅ Dateien installiert"
echo ""
@@ -349,18 +376,22 @@ install_service() {
echo -e "${YELLOW}Installiere systemd Service...${NC}"
cp "$SCRIPT_DIR/adguard-shield.service" "$SERVICE_FILE"
cp "$SCRIPT_DIR/adguard-shield-watchdog.service" /etc/systemd/system/adguard-shield-watchdog.service
cp "$SCRIPT_DIR/adguard-shield-watchdog.timer" /etc/systemd/system/adguard-shield-watchdog.timer
systemctl daemon-reload
echo -e " ✅ Service-Datei installiert"
echo -e " ✅ Service-Dateien installiert (inkl. Watchdog)"
echo ""
# Interaktiv: Autostart beim Booten?
read -rep " Soll AdGuard Shield beim Booten automatisch starten? [J/n]: " autostart
if [[ "${autostart,,}" != "n" ]]; then
systemctl enable adguard-shield.service
echo -e " ✅ Autostart aktiviert"
systemctl enable adguard-shield-watchdog.timer
echo -e " ✅ Autostart aktiviert (inkl. Watchdog-Timer)"
else
systemctl disable adguard-shield.service 2>/dev/null || true
systemctl disable adguard-shield-watchdog.timer 2>/dev/null || true
echo -e " Autostart nicht aktiviert"
echo -e " ${YELLOW}Später aktivieren mit: sudo systemctl enable adguard-shield${NC}"
fi
@@ -493,6 +524,15 @@ print_summary() {
echo " Konfiguration: $INSTALL_DIR/adguard-shield.conf"
echo " Service: adguard-shield.service ($svc_status)"
echo " Autostart: $autostart_status"
# Watchdog-Status
local watchdog_status="deaktiviert"
if systemctl is-active adguard-shield-watchdog.timer &>/dev/null 2>&1; then
watchdog_status="aktiv ✅"
elif systemctl is-enabled adguard-shield-watchdog.timer &>/dev/null 2>&1; then
watchdog_status="aktiviert (Timer nicht gestartet)"
fi
echo " Watchdog: $watchdog_status"
echo " Log-Datei: /var/log/adguard-shield.log"
echo ""
echo " Nützliche Befehle:"
@@ -572,6 +612,15 @@ do_status() {
echo -e " ❌ Konfiguration: fehlt!"
fi
# Watchdog-Status
if systemctl is-active adguard-shield-watchdog.timer &>/dev/null 2>&1; then
echo -e " ✅ Watchdog-Timer: aktiv"
elif systemctl is-enabled adguard-shield-watchdog.timer &>/dev/null 2>&1; then
echo -e " ⚠️ Watchdog-Timer: aktiviert aber nicht gestartet"
else
echo -e " ❌ Watchdog-Timer: nicht installiert/deaktiviert"
fi
echo ""
}
@@ -611,7 +660,8 @@ do_install() {
read -rep " Soll der AdGuard Shield Service jetzt gestartet werden? [J/n]: " start_now
if [[ "${start_now,,}" != "n" ]]; then
systemctl start adguard-shield
echo -e " ✅ Service gestartet"
systemctl start adguard-shield-watchdog.timer 2>/dev/null || true
echo -e " ✅ Service gestartet (inkl. Watchdog-Timer)"
else
echo -e " Service nicht gestartet"
echo -e " ${YELLOW}Später starten mit: sudo systemctl start adguard-shield${NC}"
@@ -644,18 +694,28 @@ do_update() {
# Service-Datei aktualisieren
echo -e "${YELLOW}Aktualisiere systemd Service...${NC}"
cp "$SCRIPT_DIR/adguard-shield.service" "$SERVICE_FILE"
cp "$SCRIPT_DIR/adguard-shield-watchdog.service" /etc/systemd/system/adguard-shield-watchdog.service
cp "$SCRIPT_DIR/adguard-shield-watchdog.timer" /etc/systemd/system/adguard-shield-watchdog.timer
systemctl daemon-reload
echo -e " ✅ Service-Datei aktualisiert"
echo -e " ✅ Service-Dateien aktualisiert (inkl. Watchdog)"
echo ""
# Interaktiv: Autostart beim Booten?
if systemctl is-enabled adguard-shield &>/dev/null; then
echo -e " Autostart ist bereits aktiviert"
# Watchdog-Timer auch aktivieren falls noch nicht aktiv
if ! systemctl is-enabled adguard-shield-watchdog.timer &>/dev/null 2>&1; then
systemctl enable adguard-shield-watchdog.timer
systemctl start adguard-shield-watchdog.timer
echo -e " ✅ Watchdog-Timer aktiviert"
fi
else
read -rep " Soll AdGuard Shield beim Booten automatisch starten? [J/n]: " autostart
if [[ "${autostart,,}" != "n" ]]; then
systemctl enable adguard-shield.service
echo -e " ✅ Autostart aktiviert"
systemctl enable adguard-shield-watchdog.timer
systemctl start adguard-shield-watchdog.timer
echo -e " ✅ Autostart aktiviert (inkl. Watchdog-Timer)"
else
echo -e " Autostart bleibt deaktiviert"
fi
@@ -751,8 +811,14 @@ do_uninstall() {
rm -f "$INSTALL_DIR/iptables-helper.sh"
rm -f "$INSTALL_DIR/unban-expired.sh"
rm -f "$INSTALL_DIR/external-blocklist-worker.sh"
rm -f "$INSTALL_DIR/external-whitelist-worker.sh"
rm -f "$INSTALL_DIR/offense-cleanup-worker.sh"
rm -f "$INSTALL_DIR/geoip-worker.sh"
rm -f "$INSTALL_DIR/report-generator.sh"
rm -f "$INSTALL_DIR/adguard-shield-watchdog.sh"
rm -f "$INSTALL_DIR/uninstall.sh"
rm -rf "$INSTALL_DIR/templates"
rm -rf "$INSTALL_DIR/geoip"
echo " ✅ Scripts entfernt (Konfiguration und Logs behalten)"
else
rm -rf "$INSTALL_DIR"

268
offense-cleanup-worker.sh Normal file
View File

@@ -0,0 +1,268 @@
#!/bin/bash
###############################################################################
# AdGuard Shield - Offense-Cleanup-Worker
# Räumt abgelaufene Offense-Zähler (progressive Sperren) automatisch auf.
# Entfernt .offenses-Dateien, deren letztes Vergehen länger als
# PROGRESSIVE_BAN_RESET_AFTER zurückliegt.
# Wird als Hintergrundprozess vom Hauptscript gestartet.
#
# Autor: Patrick Asmus
# E-Mail: support@techniverse.net
# Datum: 2026-04-16
# Lizenz: MIT
###############################################################################
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/adguard-shield.conf"
# ─── Konfiguration laden ───────────────────────────────────────────────────────
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "FEHLER: Konfigurationsdatei nicht gefunden: $CONFIG_FILE" >&2
exit 1
fi
# shellcheck source=adguard-shield.conf
source "$CONFIG_FILE"
# ─── Niedrigste Priorität setzen (CPU + I/O) ─────────────────────────────────
# Stellt sicher, dass der Worker auch bei manuellem Start nie andere Dienste
# verdrängt. nice 19 = niedrigste CPU-Priorität, ionice idle = nur bei freier I/O.
renice -n 19 $$ >/dev/null 2>&1 || true
ionice -c 3 -p $$ >/dev/null 2>&1 || true
# ─── Worker PID-File ──────────────────────────────────────────────────────────
WORKER_PID_FILE="/var/run/adguard-offense-cleanup-worker.pid"
# ─── Prüfintervall ───────────────────────────────────────────────────────────
# Prüft einmal pro Stunde das ist völlig ausreichend für diese Aufgabe
OFFENSE_CLEANUP_INTERVAL=3600
# ─── Logging (eigene Funktion, nutzt gleiche Log-Datei) ───────────────────────
declare -A LOG_LEVELS=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3)
log() {
local level="$1"
shift
local message="$*"
local configured_level="${LOG_LEVEL:-INFO}"
if [[ ${LOG_LEVELS[$level]:-1} -ge ${LOG_LEVELS[$configured_level]:-1} ]]; then
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
local log_entry="[$timestamp] [$level] [OFFENSE-CLEANUP] $message"
echo "$log_entry" | tee -a "$LOG_FILE" >&2
fi
}
# ─── Hilfsfunktionen ─────────────────────────────────────────────────────────
format_duration() {
local seconds="$1"
if [[ "$seconds" -eq 0 ]]; then
echo "PERMANENT"
return
fi
if [[ "$seconds" -ge 86400 ]]; then
echo "$((seconds / 86400))d $((seconds % 86400 / 3600))h"
elif [[ "$seconds" -ge 3600 ]]; then
echo "$((seconds / 3600))h $((seconds % 3600 / 60))m"
elif [[ "$seconds" -ge 60 ]]; then
echo "$((seconds / 60))m $((seconds % 60))s"
else
echo "${seconds}s"
fi
}
# ─── Verzeichnisse erstellen ──────────────────────────────────────────────────
init_directories() {
mkdir -p "${STATE_DIR}"
mkdir -p "$(dirname "$LOG_FILE")"
}
# ─── Abgelaufene Offense-Zähler aufräumen ────────────────────────────────────
cleanup_expired_offenses() {
local reset_after="${PROGRESSIVE_BAN_RESET_AFTER:-86400}"
local now
now=$(date '+%s')
local cleaned=0
local batch_count=0
for offense_file in "${STATE_DIR}"/*.offenses; do
[[ -f "$offense_file" ]] || continue
local last_offense_epoch client_ip offense_level
last_offense_epoch=$(grep '^LAST_OFFENSE_EPOCH=' "$offense_file" | cut -d= -f2 || true)
client_ip=$(grep '^CLIENT_IP=' "$offense_file" | cut -d= -f2 || true)
offense_level=$(grep '^OFFENSE_LEVEL=' "$offense_file" | cut -d= -f2 || true)
# Kein Zeitstempel vorhanden → überspringen
if [[ -z "$last_offense_epoch" ]]; then
log "DEBUG" "Offense-Datei ohne Zeitstempel übersprungen: $offense_file"
continue
fi
local elapsed=$((now - last_offense_epoch))
if [[ $elapsed -gt $reset_after ]]; then
log "INFO" "Offense-Zähler abgelaufen: $client_ip (Stufe $offense_level, letztes Vergehen vor $(format_duration $elapsed)) → entfernt"
rm -f "$offense_file"
cleaned=$((cleaned + 1))
fi
# Alle 10 Dateien kurz pausieren, um I/O-Bursts zu vermeiden
batch_count=$((batch_count + 1))
if (( batch_count % 10 == 0 )); then
sleep 0.1
fi
done
if [[ $cleaned -gt 0 ]]; then
log "INFO" "Offense-Cleanup: $cleaned abgelaufene Zähler entfernt"
else
log "DEBUG" "Offense-Cleanup: keine abgelaufenen Zähler gefunden"
fi
}
# ─── PID-Management ──────────────────────────────────────────────────────────
write_pid() {
echo $$ > "$WORKER_PID_FILE"
}
cleanup() {
log "INFO" "Offense-Cleanup-Worker wird beendet..."
rm -f "$WORKER_PID_FILE"
exit 0
}
check_already_running() {
if [[ -f "$WORKER_PID_FILE" ]]; then
local old_pid
old_pid=$(cat "$WORKER_PID_FILE")
if kill -0 "$old_pid" 2>/dev/null; then
log "DEBUG" "Offense-Cleanup-Worker läuft bereits (PID: $old_pid)"
return 1
else
rm -f "$WORKER_PID_FILE"
fi
fi
return 0
}
# ─── Status anzeigen ─────────────────────────────────────────────────────────
show_status() {
echo "═══════════════════════════════════════════════════════════════"
echo " Offense-Cleanup-Worker - Status"
echo "═══════════════════════════════════════════════════════════════"
echo ""
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" != "true" ]]; then
echo " ⚠️ Progressive Sperren sind deaktiviert"
echo " Aktivieren: PROGRESSIVE_BAN_ENABLED=true in $CONFIG_FILE"
echo ""
return
fi
# Worker-Prozess Status
if [[ -f "$WORKER_PID_FILE" ]]; then
local pid
pid=$(cat "$WORKER_PID_FILE")
if kill -0 "$pid" 2>/dev/null; then
echo " 🟢 Worker läuft (PID: $pid)"
else
echo " 🔴 Worker nicht aktiv (veraltete PID-Datei)"
fi
else
echo " 🔴 Worker nicht aktiv"
fi
echo ""
echo " Reset-Zeitraum: $(format_duration "${PROGRESSIVE_BAN_RESET_AFTER:-86400}")"
echo " Prüfintervall: $(format_duration "$OFFENSE_CLEANUP_INTERVAL")"
# Aktuelle Offense-Dateien zählen
local total=0
local expired=0
local now
now=$(date '+%s')
local reset_after="${PROGRESSIVE_BAN_RESET_AFTER:-86400}"
for offense_file in "${STATE_DIR}"/*.offenses; do
[[ -f "$offense_file" ]] || continue
total=$((total + 1))
local last_epoch
last_epoch=$(grep '^LAST_OFFENSE_EPOCH=' "$offense_file" | cut -d= -f2 || true)
if [[ -n "$last_epoch" && $((now - last_epoch)) -gt $reset_after ]]; then
expired=$((expired + 1))
fi
done
echo ""
echo " Offense-Zähler gesamt: $total"
echo " Davon abgelaufen: $expired"
echo ""
echo "═══════════════════════════════════════════════════════════════"
}
# ─── Hauptschleife ──────────────────────────────────────────────────────────
main_loop() {
init_directories
log "INFO" "═══════════════════════════════════════════════════════════"
log "INFO" "Offense-Cleanup-Worker gestartet"
log "INFO" " Reset-Zeitraum: $(format_duration "${PROGRESSIVE_BAN_RESET_AFTER:-86400}")"
log "INFO" " Prüfintervall: $(format_duration "$OFFENSE_CLEANUP_INTERVAL")"
log "INFO" "═══════════════════════════════════════════════════════════"
while true; do
cleanup_expired_offenses
sleep "$OFFENSE_CLEANUP_INTERVAL"
done
}
# ─── Signal-Handler ──────────────────────────────────────────────────────────
trap cleanup SIGTERM SIGINT SIGHUP
# ─── Kommandozeilen-Argumente ────────────────────────────────────────────────
case "${1:-start}" in
start)
if ! check_already_running; then
exit 0
fi
write_pid
main_loop
;;
stop)
if [[ -f "$WORKER_PID_FILE" ]]; then
kill "$(cat "$WORKER_PID_FILE")" 2>/dev/null || true
rm -f "$WORKER_PID_FILE"
echo "Offense-Cleanup-Worker gestoppt"
else
echo "Offense-Cleanup-Worker läuft nicht"
fi
;;
run-once)
init_directories
log "INFO" "Einmaliger Offense-Cleanup..."
cleanup_expired_offenses
log "INFO" "Cleanup abgeschlossen"
;;
status)
init_directories
show_status
;;
*)
cat << USAGE
AdGuard Shield - Offense-Cleanup-Worker
Nutzung: $0 {start|stop|run-once|status}
Befehle:
start Startet den Worker (Dauerbetrieb)
stop Stoppt den Worker
run-once Einmaliger Cleanup-Durchlauf
status Zeigt Status und aktuelle Offense-Zähler
Konfiguration: $CONFIG_FILE
USAGE
;;
esac

View File

@@ -41,6 +41,7 @@ REPORT_EMAIL_TO="${REPORT_EMAIL_TO:-}"
REPORT_EMAIL_FROM="${REPORT_EMAIL_FROM:-adguard-shield@$(hostname -f 2>/dev/null || hostname)}"
REPORT_FORMAT="${REPORT_FORMAT:-html}"
REPORT_MAIL_CMD="${REPORT_MAIL_CMD:-msmtp}"
REPORT_BUSIEST_DAY_RANGE="${REPORT_BUSIEST_DAY_RANGE:-30}"
BAN_HISTORY_FILE="${BAN_HISTORY_FILE:-/var/log/adguard-shield-bans.log}"
BAN_HISTORY_RETENTION_DAYS="${BAN_HISTORY_RETENTION_DAYS:-0}"
STATE_DIR="${STATE_DIR:-/var/lib/adguard-shield}"
@@ -65,6 +66,49 @@ log() {
echo "[$timestamp] [$level] [REPORT] $message" | tee -a "$LOG_FILE" >&2
}
# ─── Versionsnummern vergleichen ──────────────────────────────────────────────
# Gibt 0 zurück, wenn $1 > $2 (semver-Vergleich, v-Präfix wird ignoriert)
version_gt() {
local v1="${1#v}"
local v2="${2#v}"
[[ "$v1" == "$v2" ]] && return 1
local IFS='.' i a b
read -ra ver1 <<< "$v1"
read -ra ver2 <<< "$v2"
local max_len=$(( ${#ver1[@]} > ${#ver2[@]} ? ${#ver1[@]} : ${#ver2[@]} ))
for ((i=0; i<max_len; i++)); do
a="${ver1[i]:-0}"
b="${ver2[i]:-0}"
[[ "$((10#${a}))" -gt "$((10#${b}))" ]] && return 0
[[ "$((10#${a}))" -lt "$((10#${b}))" ]] && return 1
done
return 1
}
# ─── Versionsprüfung gegen Gitea-Releases ─────────────────────────────────────
check_for_update() {
UPDATE_NOTICE_HTML=""
UPDATE_NOTICE_TXT=""
[[ "$VERSION" == "unknown" ]] && return
local latest_tag
latest_tag=$(curl -sf --max-time 5 \
"https://git.techniverse.net/api/v1/repos/scriptos/adguard-shield/releases?limit=1&page=1" \
2>/dev/null | grep -o '"tag_name":"[^"]*"' | head -1 | cut -d'"' -f4 || true)
[[ -z "$latest_tag" ]] && return
if version_gt "$latest_tag" "$VERSION"; then
UPDATE_NOTICE_HTML='<div class="update-notice">🆕 Update verfügbar: <strong>'"${latest_tag}"'</strong> · <a href="https://git.techniverse.net/scriptos/adguard-shield/releases">Jetzt aktualisieren →</a></div>'
UPDATE_NOTICE_TXT=" ⚠ Neue Version verfügbar: ${latest_tag}
Update: https://git.techniverse.net/scriptos/adguard-shield/releases
"
fi
}
# ─── Berichtszeitraum berechnen ───────────────────────────────────────────────
# Gibt Epoch-Wert für heute 00:00:00 (Mitternacht) zurück
@@ -135,40 +179,58 @@ get_period_end_epoch() {
echo $((today_midnight - 1))
}
# ─── History-Cache (einmaliges Einlesen der Ban-History) ─────────────────────
# Die Datei wird genau einmal mit awk geparst; alle Funktionen lesen danach
# nur noch aus diesem In-Memory-Cache keine date-Subprozesse pro Zeile mehr.
#
# Cache-Format pro Zeile (Pipe-separiert, alle Felder getrimmt):
# EPOCH|TIMESTAMP|ACTION|IP|DOMAIN|COUNT|DURATION|PROTOCOL|REASON
HISTORY_CACHE=""
HISTORY_CACHE_LOADED=false
_load_history_cache() {
[[ "$HISTORY_CACHE_LOADED" == "true" ]] && return
HISTORY_CACHE_LOADED=true
[[ ! -f "$BAN_HISTORY_FILE" ]] && return
HISTORY_CACHE=$(awk '
/^#/ || /^[[:space:]]*$/ { next }
{
n = split($0, f, "|")
if (n < 2) next
ts = f[1]; gsub(/^[[:space:]]+|[[:space:]]+$/, "", ts)
if (length(ts) < 19) next
ep = mktime(substr(ts,1,4) " " substr(ts,6,2) " " substr(ts,9,2) " " \
substr(ts,12,2) " " substr(ts,15,2) " " substr(ts,18,2))
if (ep < 0) next
for (i = 1; i <= n; i++) gsub(/^[[:space:]]+|[[:space:]]+$/, "", f[i])
print ep "|" f[1] "|" f[2] "|" f[3] "|" f[4] "|" f[5] "|" f[6] "|" f[7] "|" f[8]
}
' "$BAN_HISTORY_FILE")
}
# ─── Ban-History filtern nach Zeitraum ────────────────────────────────────────
# Gibt nur Zeilen zurück, deren Zeitstempel im Berichtszeitraum liegen
# Gibt nur Zeilen zurück, deren Zeitstempel im Berichtszeitraum liegen.
# Liest intern aus dem Cache keine erneuten date-Subprozesse.
filter_history_by_period() {
local start_epoch="$1"
local end_epoch="$2"
if [[ ! -f "$BAN_HISTORY_FILE" ]]; then
return
fi
[[ ! -f "$BAN_HISTORY_FILE" ]] && return
_load_history_cache
[[ -z "$HISTORY_CACHE" ]] && return
while IFS= read -r line; do
# Kommentare und leere Zeilen überspringen
[[ "$line" =~ ^#.*$ || -z "$line" ]] && continue
# Zeitstempel extrahieren (erstes Feld, Format: YYYY-MM-DD HH:MM:SS)
local timestamp
timestamp=$(echo "$line" | awk -F'|' '{print $1}' | xargs)
if [[ -z "$timestamp" ]]; then
continue
fi
# Timestamp in Epoch umwandeln
local line_epoch
line_epoch=$(date -d "$timestamp" '+%s' 2>/dev/null || date -j -f '%Y-%m-%d %H:%M:%S' "$timestamp" '+%s' 2>/dev/null || echo "0")
if [[ "$line_epoch" -ge "$start_epoch" && "$line_epoch" -le "$end_epoch" ]]; then
echo "$line"
fi
done < "$BAN_HISTORY_FILE"
# Aus dem Cache filtern und im Original-Format ausgeben (Abwärtskompatibilität)
echo "$HISTORY_CACHE" | awk -F'|' -v s="$start_epoch" -v e="$end_epoch" '
$1 >= s && $1 <= e {
printf "%-19s | %-6s | %-39s | %-30s | %-8s | %-10s | %-10s | %s\n",
$2, $3, $4, $5, $6, $7, $8, $9
}
'
}
# ─── Ban-History bereinigen ────────────────────────────────────────────────────
# Entfernt Einträge älter als BAN_HISTORY_RETENTION_DAYS (0 = deaktiviert)
# Entfernt Einträge älter als BAN_HISTORY_RETENTION_DAYS (0 = deaktiviert).
# Nutzt einen einzelnen awk-Durchlauf mit mktime() kein date-Subprocess pro Zeile.
cleanup_ban_history() {
[[ ! -f "$BAN_HISTORY_FILE" ]] && return
[[ "$BAN_HISTORY_RETENTION_DAYS" == "0" || -z "$BAN_HISTORY_RETENTION_DAYS" ]] && return
@@ -178,36 +240,69 @@ cleanup_ban_history() {
[[ -z "$cutoff_epoch" ]] && return
local tmp_file="${BAN_HISTORY_FILE}.tmp"
local removed=0
local lines_before lines_after
lines_before=$(wc -l < "$BAN_HISTORY_FILE")
while IFS= read -r line; do
# Header-Zeilen immer beibehalten
if [[ "$line" =~ ^#.*$ || -z "$line" ]]; then
echo "$line"
continue
fi
awk -v cutoff="$cutoff_epoch" '
/^#/ || /^[[:space:]]*$/ { print; next }
{
n = split($0, f, "|")
if (n < 2) { print; next }
ts = f[1]; gsub(/^[[:space:]]+|[[:space:]]+$/, "", ts)
if (length(ts) < 19) { print; next }
ep = mktime(substr(ts,1,4) " " substr(ts,6,2) " " substr(ts,9,2) " " \
substr(ts,12,2) " " substr(ts,15,2) " " substr(ts,18,2))
if (ep >= cutoff) print
}
' "$BAN_HISTORY_FILE" > "$tmp_file"
local timestamp
timestamp=$(echo "$line" | awk -F'|' '{print $1}' | xargs)
local line_epoch
line_epoch=$(date -d "$timestamp" '+%s' 2>/dev/null || echo "0")
if [[ "$line_epoch" -ge "$cutoff_epoch" ]]; then
echo "$line"
else
((removed++)) || true
fi
done < "$BAN_HISTORY_FILE" > "$tmp_file"
lines_after=$(wc -l < "$tmp_file")
local removed=$(( lines_before - lines_after ))
if [[ $removed -gt 0 ]]; then
mv "$tmp_file" "$BAN_HISTORY_FILE"
# Cache invalidieren, damit Folgeaufrufe die bereinigte Datei neu lesen
HISTORY_CACHE=""
HISTORY_CACHE_LOADED=false
log "INFO" "Ban-History bereinigt: $removed Einträge älter als ${BAN_HISTORY_RETENTION_DAYS} Tage entfernt"
else
rm -f "$tmp_file"
fi
}
# ─── Statistiken für beliebigen Zeitraum berechnen ──────────────────────────
# Gibt "bans|unbans|unique_ips|permanent" für einen Epochen-Bereich zurück.
# Liest direkt aus dem Cache in einem einzigen awk-Durchlauf.
get_stats_for_epoch_range() {
local start_epoch="$1"
local end_epoch="$2"
_load_history_cache
if [[ -z "$HISTORY_CACHE" ]]; then
echo "0|0|0|0"
return
fi
echo "$HISTORY_CACHE" | awk -F'|' -v s="$start_epoch" -v e="$end_epoch" '
$1 >= s && $1 <= e {
if ($3 == "BAN") {
bans++
ip_seen[$4] = 1
if (tolower($7) ~ /permanent/) perm++
} else if ($3 == "UNBAN") {
unbans++
}
}
END {
for (ip in ip_seen) unique++
print (bans+0) "|" (unbans+0) "|" (unique+0) "|" (perm+0)
}
'
}
# ─── Statistiken berechnen ────────────────────────────────────────────────────
# Liest die Ban-History genau einmal aus dem Cache und berechnet alle
# Kennzahlen in einem einzigen awk-Durchlauf keine Subprozesse pro Zeile.
calculate_stats() {
# Ban-History bereinigen (falls Retention konfiguriert)
cleanup_ban_history
@@ -217,11 +312,10 @@ calculate_stats() {
local end_epoch
end_epoch=$(get_period_end_epoch)
local filtered_data
filtered_data=$(filter_history_by_period "$start_epoch" "$end_epoch")
_load_history_cache
# Wenn keine Daten vorhanden, Standardwerte
if [[ -z "$filtered_data" ]]; then
# Wenn keine History-Datei vorhanden, Standardwerte setzen
if [[ -z "$HISTORY_CACHE" ]]; then
TOTAL_BANS=0
TOTAL_UNBANS=0
UNIQUE_IPS=0
@@ -232,6 +326,7 @@ calculate_stats() {
SUBDOMAIN_FLOOD_BANS=0
EXTERNAL_BLOCKLIST_BANS=0
BUSIEST_DAY=""
BUSIEST_DAY_LABEL="Aktivster Tag"
TOP10_IPS=""
TOP10_DOMAINS=""
PROTOCOL_STATS=""
@@ -239,17 +334,105 @@ calculate_stats() {
return
fi
# Gesamtzahl Sperren
TOTAL_BANS=$(echo "$filtered_data" | grep -c '| BAN ' || echo "0")
# Einen einzigen awk-Pass über den Cache: alle Statistiken auf einmal
# Busiest-Day-Bereich berechnen (konfigurierbar, Standard: 30 Tage)
local busiest_start_epoch
if [[ "$REPORT_BUSIEST_DAY_RANGE" == "0" || -z "$REPORT_BUSIEST_DAY_RANGE" ]]; then
busiest_start_epoch="$start_epoch"
else
local today_midnight
today_midnight=$(get_today_midnight)
busiest_start_epoch=$((today_midnight - REPORT_BUSIEST_DAY_RANGE * 86400))
fi
# Gesamtzahl Entsperrungen
TOTAL_UNBANS=$(echo "$filtered_data" | grep -c '| UNBAN ' || echo "0")
local awk_result
awk_result=$(echo "$HISTORY_CACHE" | awk -F'|' -v s="$start_epoch" -v e="$end_epoch" -v bs="$busiest_start_epoch" '
$1 >= s && $1 <= e {
action = $3
if (action == "BAN") {
bans++
ip_count[$4]++
ip_seen[$4] = 1
dom = $5
if (dom != "" && dom != "-") dom_count[dom]++
proto = $8
if (proto == "" || proto == "-") proto = "unbekannt"
proto_count[proto]++
if (tolower($7) ~ /permanent/) perm++
rsn = tolower($9)
if (rsn ~ /rate.limit/) rl++
if (rsn ~ /subdomain.flood/) sf++
if (rsn ~ /external.blocklist/) eb++
# Zirkulärer Puffer für die letzten 10 Sperren
recent[bans % 10] = $2 "|" $3 "|" $4 "|" $5 "|" $6 "|" $7 "|" $8 "|" $9
} else if (action == "UNBAN") {
unbans++
}
}
# Aktivster Tag: separater Zeitraum (konfigurierbar, z.B. letzte 30 Tage)
$1 >= bs && $1 <= e && $3 == "BAN" {
bday = substr($2, 1, 10)
bday_count[bday]++
}
END {
for (ip in ip_seen) unique++
busiest = ""; max_d = 0
for (d in bday_count) {
if (bday_count[d] > max_d) { max_d = bday_count[d]; busiest = d; busiest_cnt = bday_count[d] }
}
print "BANS=" (bans+0)
print "UNBANS=" (unbans+0)
print "UNIQUE=" (unique+0)
print "PERM=" (perm+0)
print "RL=" (rl+0)
print "SF=" (sf+0)
print "EB=" (eb+0)
print "BUSIEST=" busiest
print "BUSIEST_CNT=" (busiest_cnt+0)
for (ip in ip_count) print "IP\t" ip_count[ip] "\t" ip
for (d in dom_count) print "DOMAIN\t" dom_count[d] "\t" d
for (p in proto_count) print "PROTO\t" proto_count[p] "\t" p
n = (bans < 10) ? bans : 10
for (i = 0; i < n; i++) {
idx = (bans - i) % 10
print "RECENT\t" recent[idx]
}
}
')
# Eindeutige IPs (nur BAN-Einträge)
UNIQUE_IPS=$(echo "$filtered_data" | grep '| BAN ' | awk -F'|' '{print $3}' | xargs -I{} echo {} | sort -u | wc -l | xargs)
# Einfache Kennzahlen aus dem awk-Ergebnis extrahieren
TOTAL_BANS=$( echo "$awk_result" | awk -F= '$1=="BANS" {print $2; exit}')
TOTAL_UNBANS=$( echo "$awk_result" | awk -F= '$1=="UNBANS" {print $2; exit}')
UNIQUE_IPS=$( echo "$awk_result" | awk -F= '$1=="UNIQUE" {print $2; exit}')
PERMANENT_BANS=$(echo "$awk_result" | awk -F= '$1=="PERM" {print $2; exit}')
RATELIMIT_BANS=$( echo "$awk_result" | awk -F= '$1=="RL" {print $2; exit}')
SUBDOMAIN_FLOOD_BANS=$( echo "$awk_result" | awk -F= '$1=="SF" {print $2; exit}')
EXTERNAL_BLOCKLIST_BANS=$(echo "$awk_result" | awk -F= '$1=="EB" {print $2; exit}')
# Permanente Sperren (Dauer enthält "PERMANENT" oder "permanent")
PERMANENT_BANS=$(echo "$filtered_data" | grep '| BAN ' | awk -F'|' '{print $6}' | grep -ic 'permanent' || echo "0")
local busiest_raw
busiest_raw=$(echo "$awk_result" | awk -F= '$1=="BUSIEST" {print $2; exit}')
local busiest_cnt
busiest_cnt=$(echo "$awk_result" | awk -F= '$1=="BUSIEST_CNT" {print $2; exit}')
if [[ -n "$busiest_raw" ]]; then
local busiest_formatted
busiest_formatted=$(date -d "$busiest_raw" '+%d.%m.%Y' 2>/dev/null || echo "$busiest_raw")
BUSIEST_DAY="${busiest_formatted} (${busiest_cnt})"
else
BUSIEST_DAY=""
fi
# Dynamisches Label für den aktivsten Tag
if [[ "$REPORT_BUSIEST_DAY_RANGE" == "0" || -z "$REPORT_BUSIEST_DAY_RANGE" ]]; then
BUSIEST_DAY_LABEL="Aktivster Tag"
else
BUSIEST_DAY_LABEL="Aktivster Tag (${REPORT_BUSIEST_DAY_RANGE} Tage)"
fi
# Top-Listen: Tab-getrennte Felder sortieren und in das erwartete Format bringen
TOP10_IPS=$( echo "$awk_result" | awk -F'\t' '$1=="IP" {print $2 " " $3}' | sort -rn | head -10)
TOP10_DOMAINS=$(echo "$awk_result" | awk -F'\t' '$1=="DOMAIN" {print $2 " " $3}' | sort -rn | head -10)
PROTOCOL_STATS=$(echo "$awk_result" | awk -F'\t' '$1=="PROTO" {print $2 " " $3}' | sort -rn)
RECENT_BANS=$( echo "$awk_result" | awk -F'\t' '$1=="RECENT" {print $2}')
# Aktuell aktive Sperren (aus State-Dateien)
ACTIVE_BANS=0
@@ -259,41 +442,21 @@ calculate_stats() {
done
fi
# AbuseIPDB Reports (suche in Log-Datei)
# AbuseIPDB Reports zeitraum-gefiltert aus der Logdatei via awk+mktime
ABUSEIPDB_REPORTS=0
if [[ -f "$LOG_FILE" ]]; then
local abuseipdb_start_date
abuseipdb_start_date=$(date -d "@$start_epoch" '+%Y-%m-%d' 2>/dev/null || date -r "$start_epoch" '+%Y-%m-%d')
ABUSEIPDB_REPORTS=$(grep -c "AbuseIPDB: IP .* erfolgreich gemeldet" "$LOG_FILE" 2>/dev/null | head -1 || echo "0")
ABUSEIPDB_REPORTS=$(grep "AbuseIPDB:.*erfolgreich gemeldet" "$LOG_FILE" 2>/dev/null | \
awk -v s="$start_epoch" -v e="$end_epoch" '
{
ts = substr($0, 2, 19)
if (ts !~ /^[0-9]{4}/) next
ep = mktime(substr(ts,1,4) " " substr(ts,6,2) " " substr(ts,9,2) " " \
substr(ts,12,2) " " substr(ts,15,2) " " substr(ts,18,2))
if (ep >= s && ep <= e) count++
}
END { print count+0 }
' || echo "0")
fi
# Angriffsarten
RATELIMIT_BANS=$(echo "$filtered_data" | grep '| BAN ' | awk -F'|' '{print $8}' | grep -ic 'rate-limit' || echo "0")
SUBDOMAIN_FLOOD_BANS=$(echo "$filtered_data" | grep '| BAN ' | awk -F'|' '{print $8}' | grep -ic 'subdomain-flood' || echo "0")
EXTERNAL_BLOCKLIST_BANS=$(echo "$filtered_data" | grep '| BAN ' | awk -F'|' '{print $8}' | grep -ic 'external-blocklist' || echo "0")
# Aktivster Tag
BUSIEST_DAY=$(echo "$filtered_data" | grep '| BAN ' | awk -F'|' '{print $1}' | xargs -I{} echo {} | awk '{print $1}' | sort | uniq -c | sort -rn | head -1 | awk '{print $2}' || echo "")
if [[ -z "$BUSIEST_DAY" ]]; then
BUSIEST_DAY=""
else
# Datum in DE-Format umwandeln
local busiest_formatted
busiest_formatted=$(date -d "$BUSIEST_DAY" '+%d.%m.%Y' 2>/dev/null || echo "$BUSIEST_DAY")
BUSIEST_DAY="$busiest_formatted"
fi
# Top 10 IPs (nach Häufigkeit der Sperren)
TOP10_IPS=$(echo "$filtered_data" | grep '| BAN ' | awk -F'|' '{print $3}' | xargs -I{} echo {} | sort | uniq -c | sort -rn | head -10)
# Top 10 Domains (nach Häufigkeit)
TOP10_DOMAINS=$(echo "$filtered_data" | grep '| BAN ' | awk -F'|' '{print $4}' | xargs -I{} echo {} | sed 's/^-$//' | grep -v '^$' | sort | uniq -c | sort -rn | head -10)
# Protokoll-Verteilung
PROTOCOL_STATS=$(echo "$filtered_data" | grep '| BAN ' | awk -F'|' '{print $7}' | xargs -I{} echo {} | sed 's/^-$/unbekannt/' | grep -v '^$' | sort | uniq -c | sort -rn)
# Letzte 10 Sperren
RECENT_BANS=$(echo "$filtered_data" | grep '| BAN ' | tail -10)
}
# ─── HTML-Tabellen generieren ─────────────────────────────────────────────────
@@ -420,6 +583,65 @@ generate_recent_bans_html() {
echo "$html"
}
# ─── Zeitraum-Schnellübersicht (HTML) ─────────────────────────────────────────
generate_period_overview_html() {
local today_midnight
today_midnight=$(get_today_midnight)
local now
now=$(date '+%s')
local yesterday_start=$((today_midnight - 86400))
local yesterday_end=$((today_midnight - 1))
# Zeiträume: "Label:start_epoch:end_epoch" (Doppelpunkt als Trennzeichen)
local periods=()
# Heute nur nach 20:00 Uhr einblenden
local current_hour
current_hour=$(date '+%H' | sed 's/^0*//')
if [[ "${current_hour:-0}" -ge 20 ]]; then
periods+=("Heute:${today_midnight}:${now}")
fi
periods+=(
"Gestern:${yesterday_start}:${yesterday_end}"
"Letzte 7 Tage:$((today_midnight - 7 * 86400)):${now}"
"Letzte 14 Tage:$((today_midnight - 14 * 86400)):${now}"
"Letzte 30 Tage:$((today_midnight - 30 * 86400)):${now}"
)
local html='<table>'
html+='<tr>'
html+='<th>Zeitraum</th>'
html+='<th>Sperren</th>'
html+='<th>Entsperrt</th>'
html+='<th>Unique IPs</th>'
html+='<th>Dauerhaft gebannt</th>'
html+='</tr>'
for period_def in "${periods[@]}"; do
IFS=':' read -r label start_e end_e <<< "$period_def"
local row_class=""
case "$label" in
Heute) row_class=' class="period-today"' ;;
Gestern) row_class=' class="period-gestern"' ;;
esac
local stats
stats=$(get_stats_for_epoch_range "$start_e" "$end_e")
IFS='|' read -r bans unbans unique perm <<< "$stats"
html+="<tr${row_class}>"
html+="<td><strong>${label}</strong></td>"
html+="<td>${bans}</td>"
html+="<td>${unbans}</td>"
html+="<td>${unique}</td>"
html+="<td>${perm}</td>"
html+="</tr>"
done
html+='</table>'
echo "$html"
}
# ─── TXT-Tabellen generieren ──────────────────────────────────────────────────
generate_top10_ips_txt() {
if [[ -z "$TOP10_IPS" ]]; then
@@ -495,6 +717,46 @@ generate_recent_bans_txt() {
done <<< "$RECENT_BANS"
}
# ─── Zeitraum-Schnellübersicht (TXT) ──────────────────────────────────────────
generate_period_overview_txt() {
local today_midnight
today_midnight=$(get_today_midnight)
local now
now=$(date '+%s')
local yesterday_start=$((today_midnight - 86400))
local yesterday_end=$((today_midnight - 1))
local periods=()
# Heute nur nach 20:00 Uhr einblenden
local current_hour
current_hour=$(date '+%H' | sed 's/^0*//')
if [[ "${current_hour:-0}" -ge 20 ]]; then
periods+=("Heute:${today_midnight}:${now}")
fi
periods+=(
"Gestern:${yesterday_start}:${yesterday_end}"
"Letzte 7 Tage:$((today_midnight - 7 * 86400)):${now}"
"Letzte 14 Tage:$((today_midnight - 14 * 86400)):${now}"
"Letzte 30 Tage:$((today_midnight - 30 * 86400)):${now}"
)
printf " %-15s %-9s %-12s %-14s %-11s\n" \
"Zeitraum" "Sperren" "Entsperrt" "Unique IPs" "Dauerhaft"
printf " %-15s %-9s %-12s %-14s %-11s\n" \
"───────────────" "─────────" "────────────" "──────────────" "───────────"
for period_def in "${periods[@]}"; do
IFS=':' read -r label start_e end_e <<< "$period_def"
local stats
stats=$(get_stats_for_epoch_range "$start_e" "$end_e")
IFS='|' read -r bans unbans unique perm <<< "$stats"
printf " %-15s %-9s %-12s %-14s %-11s\n" \
"$label" "$bans" "$unbans" "$unique" "$perm"
done
}
# ─── Report generieren ────────────────────────────────────────────────────────
generate_report() {
local format="${1:-$REPORT_FORMAT}"
@@ -504,6 +766,9 @@ generate_report() {
# Statistiken berechnen
calculate_stats
# Update-Verfügbarkeit prüfen
check_for_update
local report_period
report_period=$(get_report_period)
local report_date
@@ -530,6 +795,8 @@ generate_report() {
protocol_table=$(generate_protocol_html)
local recent_bans_table
recent_bans_table=$(generate_recent_bans_html)
local period_overview_table
period_overview_table=$(generate_period_overview_html)
# Platzhalter ersetzen
report="${report//\{\{REPORT_PERIOD\}\}/$report_period}"
@@ -546,10 +813,13 @@ generate_report() {
report="${report//\{\{SUBDOMAIN_FLOOD_BANS\}\}/$SUBDOMAIN_FLOOD_BANS}"
report="${report//\{\{EXTERNAL_BLOCKLIST_BANS\}\}/$EXTERNAL_BLOCKLIST_BANS}"
report="${report//\{\{BUSIEST_DAY\}\}/$BUSIEST_DAY}"
report="${report//\{\{BUSIEST_DAY_LABEL\}\}/$BUSIEST_DAY_LABEL}"
report="${report//\{\{TOP10_IPS_TABLE\}\}/$top10_ips_table}"
report="${report//\{\{TOP10_DOMAINS_TABLE\}\}/$top10_domains_table}"
report="${report//\{\{PROTOCOL_TABLE\}\}/$protocol_table}"
report="${report//\{\{RECENT_BANS_TABLE\}\}/$recent_bans_table}"
report="${report//\{\{PERIOD_OVERVIEW_TABLE\}\}/$period_overview_table}"
report="${report//\{\{UPDATE_NOTICE\}\}/$UPDATE_NOTICE_HTML}"
echo "$report"
@@ -572,6 +842,8 @@ generate_report() {
protocol_txt=$(generate_protocol_txt)
local recent_bans_txt
recent_bans_txt=$(generate_recent_bans_txt)
local period_overview_txt
period_overview_txt=$(generate_period_overview_txt)
# Platzhalter ersetzen
report="${report//\{\{REPORT_PERIOD\}\}/$report_period}"
@@ -588,10 +860,13 @@ generate_report() {
report="${report//\{\{SUBDOMAIN_FLOOD_BANS\}\}/$SUBDOMAIN_FLOOD_BANS}"
report="${report//\{\{EXTERNAL_BLOCKLIST_BANS\}\}/$EXTERNAL_BLOCKLIST_BANS}"
report="${report//\{\{BUSIEST_DAY\}\}/$BUSIEST_DAY}"
report="${report//\{\{BUSIEST_DAY_LABEL\}\}/$BUSIEST_DAY_LABEL}"
report="${report//\{\{TOP10_IPS_TEXT\}\}/$top10_ips_txt}"
report="${report//\{\{TOP10_DOMAINS_TEXT\}\}/$top10_domains_txt}"
report="${report//\{\{PROTOCOL_TEXT\}\}/$protocol_txt}"
report="${report//\{\{RECENT_BANS_TEXT\}\}/$recent_bans_txt}"
report="${report//\{\{PERIOD_OVERVIEW_TEXT\}\}/$period_overview_txt}"
report="${report//\{\{UPDATE_NOTICE_TXT\}\}/$UPDATE_NOTICE_TXT}"
echo "$report"
else
@@ -737,6 +1012,7 @@ show_cron_status() {
echo " Empfänger: ${REPORT_EMAIL_TO:-nicht konfiguriert}"
echo " Absender: ${REPORT_EMAIL_FROM}"
echo " Mail-Befehl: ${REPORT_MAIL_CMD}"
echo " Aktivster Tag: letzte ${REPORT_BUSIEST_DAY_RANGE:-30} Tage"
echo ""
if command -v "$REPORT_MAIL_CMD" &>/dev/null; then
@@ -836,6 +1112,11 @@ send_test_email() {
local content_type="text/plain"
[[ "$REPORT_FORMAT" == "html" ]] && content_type="text/html"
local test_update_notice_html
test_update_notice_html='<div style="display:inline-block;margin-top:10px;padding:7px 14px;background:#fff8e1;border:1px solid #ffc107;border-radius:8px;color:#7a5700;font-size:12px;font-weight:600;">🆕 Update verfügbar (Testanzeige): <strong>'"${VERSION}"'</strong> · <a href="https://git.techniverse.net/scriptos/adguard-shield/releases" style="color:#7a5700;font-weight:700;text-decoration:none;">Jetzt aktualisieren →</a></div>'
local test_update_notice_txt
test_update_notice_txt=" ⚠ Neue Version verfügbar (Testanzeige): ${VERSION}\n Update: https://git.techniverse.net/scriptos/adguard-shield/releases\n"
local test_body
if [[ "$REPORT_FORMAT" == "html" ]]; then
test_body=$(cat <<TESTHTML
@@ -861,13 +1142,17 @@ send_test_email() {
</table>
<p style="color:#6c757d;font-size:13px;">Ab jetzt kannst du den automatischen Versand aktivieren mit:<br><code>sudo $(basename "$0") install</code></p>
</div>
<div style="background:#f8f9fc;padding:20px;text-align:center;font-size:12px;color:#6c757d;border-top:1px solid #e8ecf1;">
<a href="https://www.patrick-asmus.de" style="color:#0f3460;text-decoration:none;font-weight:600;">Patrick-Asmus.DE</a>
<div style="background:#f8f9fc;padding:20px;font-size:12px;color:#6c757d;border-top:1px solid #e8ecf1;text-align:center;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span><a href="https://www.patrick-asmus.de" style="color:#0f3460;text-decoration:none;font-weight:600;">Patrick-Asmus.de</a>
<span style="margin:0 8px;color:#ced4da;">|</span>
<a href="https://www.cleveradmin.de" style="color:#0f3460;text-decoration:none;font-weight:600;">CleverAdmin.DE</a>
<a href="https://www.cleveradmin.de" style="color:#0f3460;text-decoration:none;font-weight:600;">CleverAdmin.de</a></span>
<span><a href="https://git.techniverse.net/scriptos/adguard-shield.git" style="color:#0f3460;text-decoration:none;font-weight:600;">AdGuard Shield auf Gitea</a>
<span style="margin:0 8px;color:#ced4da;">|</span>
<a href="https://git.techniverse.net/scriptos/adguard-shield.git" style="color:#0f3460;text-decoration:none;font-weight:600;">AdGuard Shield auf Gitea</a>
<div style="margin-top:8px;font-size:11px;color:#adb5bd;">AdGuard Shield v${VERSION} &middot; ${hostname}</div>
<a href="https://git.techniverse.net/scriptos/adguard-shield/src/branch/main/docs" style="color:#0f3460;text-decoration:none;font-weight:600;">docs</a></span>
</div>
<div style="margin-top:8px;font-size:11px;color:#adb5bd;text-align:center;">AdGuard Shield ${VERSION} &middot; ${hostname}</div>
${test_update_notice_html}
</div>
</div>
</body></html>
@@ -895,12 +1180,14 @@ TESTHTML
Ab jetzt kannst du den automatischen Versand aktivieren mit:
sudo $(basename "$0") install
${test_update_notice_txt}
═══════════════════════════════════════════════════════════════
AdGuard Shield v${VERSION} · ${hostname}
AdGuard Shield ${VERSION} · ${hostname}
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
═══════════════════════════════════════════════════════════════
TESTTXT
)

View File

@@ -210,17 +210,57 @@
}
.footer .links {
margin-top: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.footer .separator {
margin: 0 8px;
color: #ced4da;
}
.version-tag {
display: inline-block;
display: block;
margin-top: 8px;
font-size: 11px;
color: #adb5bd;
}
.update-notice {
display: inline-block;
margin-top: 10px;
padding: 7px 14px;
background: #fff8e1;
border: 1px solid #ffc107;
border-radius: 8px;
color: #7a5700;
font-size: 12px;
font-weight: 600;
}
.update-notice a {
color: #7a5700;
text-decoration: none;
font-weight: 700;
}
.update-notice a:hover {
text-decoration: underline;
}
.period-today td {
background: #eef4ff;
font-weight: 600;
}
.period-today td:first-child {
color: #0f3460;
}
.period-gestern td {
background: #f0faf3;
font-weight: 600;
}
.period-gestern td:first-child {
color: #27ae60;
}
@media (max-width: 700px) {
table { font-size: 12px; }
th, td { padding: 8px 8px; }
}
</style>
</head>
<body>
@@ -234,6 +274,11 @@
<!-- Statistik-Übersicht -->
<div class="content">
<!-- Zeitraum-Schnellübersicht -->
<h2>📅 Zeitraum-Schnellübersicht</h2>
{{PERIOD_OVERVIEW_TABLE}}
<!-- Gesamt-Übersicht des Berichtszeitraums -->
<h2>📊 Übersicht</h2>
<div class="stats-grid">
<div class="stat-card danger">
@@ -279,7 +324,7 @@
</div>
<div class="stat-card success">
<div class="stat-value">{{BUSIEST_DAY}}</div>
<div class="stat-label">Aktivster Tag</div>
<div class="stat-label">{{BUSIEST_DAY_LABEL}}</div>
</div>
</div>
@@ -302,16 +347,23 @@
<!-- Footer -->
<div class="footer">
<div class="links">
<span>
<a href="https://www.patrick-asmus.de">Patrick-Asmus.de</a>
<span class="separator">|</span>
<a href="https://www.cleveradmin.de">CleverAdmin.de</a>
</span>
<span>
<a href="https://git.techniverse.net/scriptos/adguard-shield.git">AdGuard Shield auf Gitea</a>
<span class="separator">|</span>
<a href="https://git.techniverse.net/scriptos/adguard-shield/src/branch/main/docs">docs</a>
</span>
</div>
<br>
Dieser Report wurde automatisch von <strong>AdGuard Shield</strong> generiert.<br>
Generiert am: {{REPORT_DATE}}
<div class="links">
<a href="https://www.patrick-asmus.de">Patrick-Asmus.DE</a>
<span class="separator">|</span>
<a href="https://www.cleveradmin.de">CleverAdmin.DE</a>
<span class="separator">|</span>
<a href="https://git.techniverse.net/scriptos/adguard-shield.git">AdGuard Shield auf Gitea</a>
</div>
<div class="version-tag">AdGuard Shield {{VERSION}} · {{HOSTNAME}}</div>
{{UPDATE_NOTICE}}
</div>
</div>
</body>

View File

@@ -7,7 +7,13 @@
Host: {{HOSTNAME}}
───────────────────────────────────────────────────────────────
📊 ÜBERSICHT
<EFBFBD> ZEITRAUM-SCHNELLÜBERSICHT
───────────────────────────────────────────────────────────────
{{PERIOD_OVERVIEW_TEXT}}
───────────────────────────────────────────────────────────────
📊 ÜBERSICHT (Berichtszeitraum)
───────────────────────────────────────────────────────────────
Sperren gesamt: {{TOTAL_BANS}}
@@ -24,7 +30,7 @@
Rate-Limit Sperren: {{RATELIMIT_BANS}}
Subdomain-Flood Sperren: {{SUBDOMAIN_FLOOD_BANS}}
Externe Blocklist: {{EXTERNAL_BLOCKLIST_BANS}}
Aktivster Tag: {{BUSIEST_DAY}}
{{BUSIEST_DAY_LABEL}}: {{BUSIEST_DAY}}
───────────────────────────────────────────────────────────────
🏴‍☠️ TOP 10 AUFFÄLLIGSTE IPs
@@ -50,6 +56,7 @@
{{RECENT_BANS_TEXT}}
{{UPDATE_NOTICE_TXT}}
═══════════════════════════════════════════════════════════════
Dieser Report wurde automatisch von AdGuard Shield generiert.
AdGuard Shield {{VERSION}}
@@ -57,4 +64,5 @@
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
═══════════════════════════════════════════════════════════════

View File

@@ -13,6 +13,8 @@
# INSTALL_DIR ergibt sich aus dem Verzeichnis, in dem dieses Script liegt
INSTALL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SERVICE_FILE="/etc/systemd/system/adguard-shield.service"
WATCHDOG_SERVICE_FILE="/etc/systemd/system/adguard-shield-watchdog.service"
WATCHDOG_TIMER_FILE="/etc/systemd/system/adguard-shield-watchdog.timer"
# Farben
RED='\033[0;31m'
@@ -79,6 +81,16 @@ do_uninstall() {
fi
echo ""
# Watchdog-Timer stoppen und deaktivieren
if systemctl is-active adguard-shield-watchdog.timer &>/dev/null 2>&1; then
systemctl stop adguard-shield-watchdog.timer
echo " ✅ Watchdog-Timer gestoppt"
fi
if systemctl is-enabled adguard-shield-watchdog.timer &>/dev/null 2>&1; then
systemctl disable adguard-shield-watchdog.timer
echo " ✅ Watchdog-Timer deaktiviert"
fi
# Service stoppen und deaktivieren
if systemctl is-active adguard-shield &>/dev/null; then
systemctl stop adguard-shield
@@ -90,9 +102,13 @@ do_uninstall() {
fi
if [[ -f "$SERVICE_FILE" ]]; then
rm -f "$SERVICE_FILE"
systemctl daemon-reload
echo " ✅ Service-Datei entfernt"
fi
rm -f "$WATCHDOG_SERVICE_FILE" "$WATCHDOG_TIMER_FILE"
if [[ -f "$WATCHDOG_SERVICE_FILE" ]] || [[ -f "$WATCHDOG_TIMER_FILE" ]]; then
echo " ✅ Watchdog-Dateien entfernt"
fi
systemctl daemon-reload
# iptables Chain aufräumen
if [[ -f "$INSTALL_DIR/iptables-helper.sh" ]]; then
@@ -106,9 +122,14 @@ do_uninstall() {
rm -f "$INSTALL_DIR/iptables-helper.sh"
rm -f "$INSTALL_DIR/unban-expired.sh"
rm -f "$INSTALL_DIR/external-blocklist-worker.sh"
rm -f "$INSTALL_DIR/external-whitelist-worker.sh"
rm -f "$INSTALL_DIR/offense-cleanup-worker.sh"
rm -f "$INSTALL_DIR/report-generator.sh"
rm -f "$INSTALL_DIR/adguard-shield-watchdog.sh"
rm -f "$INSTALL_DIR/geoip-worker.sh"
rm -f "$INSTALL_DIR/uninstall.sh"
rm -rf "$INSTALL_DIR/templates"
rm -rf "$INSTALL_DIR/geoip"
echo " ✅ Scripts entfernt (Konfiguration und Logs behalten)"
echo ""
echo -e "${YELLOW} Konfiguration verbleibt in: $INSTALL_DIR/adguard-shield.conf${NC}"