21 Commits

Author SHA1 Message Date
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
007c2b01bc Release v0.5.2 2026-03-06 13:13:32 +01:00
fd8388df0b Uninstaller ausgelagert. 2026-03-06 13:12:36 +01:00
db955263ed Email-Reports werden jetzt sauber erzeugt. 2026-03-06 13:04:19 +01:00
4b188193f6 Merge pull request 'v0.5.1' (#5) from v0.5.1 into main
Reviewed-on: #5
2026-03-05 22:03:36 +00:00
ae37610ec0 Release v0.5.1 2026-03-05 23:02:59 +01:00
f685e7eb3e Handling mit dem Sync externer Listen verbessert. 2026-03-05 22:59:51 +01:00
5f631ba858 Merge pull request 'v0.5.0' (#4) from v0.5.0 into main
Reviewed-on: #4
2026-03-05 19:13:10 +00:00
18 changed files with 1092 additions and 314 deletions

View File

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

View File

@@ -78,6 +78,9 @@ 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
@@ -103,12 +106,13 @@ sudo journalctl -u adguard-shield -f # Logs live ver
├── iptables-helper.sh # Manuelle iptables-Verwaltung
├── unban-expired.sh # Cron-basiertes Entsperren
├── report-generator.sh # E-Mail Report Generator
├── install.sh # Installer / Updater / Uninstaller
├── 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/
└── docs/
├── architektur.md # Architektur & Funktionsweise
├── konfiguration.md # Alle Parameter erklärt + Konfig-Migration
├── befehle.md # Vollständige Befehlsreferenz inkl. Installer
@@ -121,12 +125,12 @@ sudo journalctl -u adguard-shield -f # Logs live ver
| 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 |
| [Architektur](docs/architektur.md) | Wie das Tool funktioniert, iptables-Strategie, Konfig-Migration |
| [Konfiguration](docs/konfiguration.md) | Alle Parameter, Ports, Whitelist-Pflege, automatische Migration |
| [Befehle](docs/befehle.md) | Vollständige Befehlsreferenz für Installer, Monitor, iptables-Helper und systemd |
| [Benachrichtigungen](docs/benachrichtigungen.md) | Setup für Discord, Slack, Gotify, Ntfy |
| [E-Mail Report](docs/report.md) | Periodische Statistik-Reports per E-Mail (HTML/TXT) |
| [Tipps & Troubleshooting](docs/tipps-und-troubleshooting.md) | Best Practices, häufige Probleme, Deinstallation |
## Lizenz

View File

@@ -146,6 +146,11 @@ EXTERNAL_BLOCKLIST_BAN_DURATION=0
# Automatisch IPs entsperren die aus der externen Liste entfernt wurden?
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_CACHE_DIR="/var/lib/adguard-shield/external-blocklist"

View File

@@ -5,11 +5,10 @@
#
# Autor: Patrick Asmus
# E-Mail: support@techniverse.net
# Datum: 2026-03-04
# Lizenz: MIT
###############################################################################
VERSION="v0.5.0"
VERSION="v0.6.1"
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() {
@@ -452,7 +475,7 @@ EOF
# 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 +513,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 +555,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
@@ -541,46 +565,92 @@ send_notification() {
local reason_label="Rate-Limit"
[[ "$reason" == "subdomain-flood" ]] && reason_label="Subdomain-Flood"
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
local dur_line
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" && -n "$offense_level" ]]; then
dur_line="**${duration_display}** [Stufe ${offense_level}/${PROGRESSIVE_BAN_MAX_LEVEL:-0}]"
else
dur_line=$(format_duration "${BAN_DURATION}")
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 +659,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 +669,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 +685,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}"
@@ -1097,7 +1174,7 @@ stop_blocklist_worker() {
# ─── 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"
@@ -1150,7 +1227,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
@@ -1250,7 +1327,7 @@ case "${1:-start}" in
;;
*)
cat << USAGE
AdGuard Shield v${VERSION}
AdGuard Shield ${VERSION}
Service-Steuerung (empfohlen):
sudo systemctl start adguard-shield

View File

@@ -1,116 +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"
}
```
## 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.

View File

@@ -14,7 +14,7 @@ sudo bash install.sh install
# Update (mit automatischer Konfigurations-Migration)
sudo bash install.sh update
# Deinstallation
# Deinstallation (delegiert automatisch an den installierten Uninstaller)
sudo bash install.sh uninstall
# Installationsstatus anzeigen
@@ -24,6 +24,17 @@ sudo bash install.sh status
sudo bash install.sh --help
```
## Uninstaller (eigenständig)
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
sudo bash /opt/adguard-shield/uninstall.sh
```
Der Uninstaller kennt seinen Speicherort und leitet daraus automatisch das Installationsverzeichnis ab. `install.sh uninstall` delegiert intern ebenfalls dorthin — beide Wege führen zum selben Ergebnis.
### Update-Verhalten
Beim Update passiert automatisch:

190
docs/benachrichtigungen.md Normal file
View File

@@ -0,0 +1,190 @@
# 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.6.0 wurde auf dns1 gestartet.
### Service gestoppt
**Überschrift:** 🚨 🛡️ AdGuard Shield
> 🔴 AdGuard Shield v0.6.0 wurde auf dns1 gestoppt.
### 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
### 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

@@ -160,6 +160,7 @@ Ermöglicht das Einbinden externer Blocklisten, die IPv4-Adressen, IPv6-Adressen
| `EXTERNAL_BLOCKLIST_INTERVAL` | `300` | Prüfintervall in Sekunden (300 = 5 Min.) |
| `EXTERNAL_BLOCKLIST_BAN_DURATION` | `0` | Sperrdauer in Sekunden (0 = permanent bis IP aus Liste entfernt) |
| `EXTERNAL_BLOCKLIST_AUTO_UNBAN` | `true` | IPs automatisch entsperren wenn aus Liste entfernt |
| `EXTERNAL_BLOCKLIST_NOTIFY` | `false` | Webhook-Benachrichtigungen bei Blocklist-Sperren senden. Bei großen Listen unbedingt auf `false` lassen — beim ersten Sync kommen sonst hunderte Nachrichten auf einmal. |
| `EXTERNAL_BLOCKLIST_CACHE_DIR` | `/var/lib/adguard-shield/external-blocklist` | Lokaler Cache für heruntergeladene Listen |
### AbuseIPDB Reporting
@@ -227,6 +228,28 @@ malware.example.net
> **Hinweis zur Hostname-Auflösung:** Da AdGuard Shield idealerweise auf demselben Host wie der DNS-Resolver läuft, verwendet der Worker automatisch den lokalen Resolver. Hostnamen die bereits von AdGuard geblockt werden (Antwort `0.0.0.0`) werden übersprungen und nicht importiert.
#### Dateiformat der Blocklist
Beim Erstellen eigener Blocklisten müssen zwei Dinge beachtet werden:
- **Zeichenkodierung:** Datei in **UTF-8 ohne BOM** speichern. Dateien mit BOM (z.B. Standard-Einstellung in Notepad++) führen dazu, dass der erste Eintrag als ungültig erkannt wird.
- **Zeilenenden:** Datei mit **Unix-Zeilenenden (LF)** speichern, nicht Windows (CRLF). CRLF-Zeilenenden führen dazu, dass alle Einträge als ungültig abgelehnt werden.
In **Notepad++:** Kodierung → „UTF-8 (ohne BOM)" und Bearbeiten → Zeilenende-Konvertierung → Unix (LF).
In **VS Code:** Unten rechts auf `CRLF` klicken → `LF` auswählen; Zeichenkodierung ebenfalls unten rechts prüfen.
> **Empfehlung:** IP-Adressen und Hostnamen in **getrennten Listen** pflegen. Bei Hostname-Listen löst der Worker jeden Eintrag per DNS auf — das ist langsamer als direkte IP-Listen und liefert je nach DNS-Antwort mehrere IPs pro Eintrag. Getrennte Listen sind außerdem übersichtlicher und einfacher zu pflegen.
#### Synchronisierungsverhalten
Der Worker synchronisiert die Blocklisten:
- **Beim Service-Start:** Der erste Sync läuft **sofort** beim Start — ohne Wartezeit. Danach beginnt erst das periodische Intervall (`EXTERNAL_BLOCKLIST_INTERVAL`).
- **Automatisch im Hintergrund:** Alle `EXTERNAL_BLOCKLIST_INTERVAL` Sekunden (Standard: 300s = 5 Min.) wird geprüft, ob sich die Liste geändert hat. Unveränderte Listen (HTTP 304 oder gleicher Inhalt) werden nicht erneut verarbeitet.
- **Manuell:** `sudo adguard-shield.sh blocklist-sync` erzwingt sofort einen Sync, unabhängig vom laufenden Worker.
> **Nach einem Neustart** (Server oder Service) werden fehlende iptables-Regeln beim nächsten Sync automatisch nachgezogen.
2. Aktiviere die Blocklist in der Konfiguration:
```bash

View File

@@ -56,7 +56,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

View File

@@ -213,11 +213,18 @@ sudo bash install.sh update
## Deinstallation
Ab Version 0.6 gibt es einen eigenständigen Uninstaller im Installationsverzeichnis. Die Deinstallation kann daher jederzeit durchgeführt werden, **ohne die originalen Installationsdateien (install.sh) behalten zu müssen**:
```bash
# Über den Installer (interaktiv mit Menü)
# Empfohlen: direkt aus dem Installationsverzeichnis ausführen
sudo bash /opt/adguard-shield/uninstall.sh
# Alternativ: über den Installer (sofern noch vorhanden)
sudo bash install.sh uninstall
```
Beide Wege sind gleichwertig — `install.sh uninstall` delegiert intern an `/opt/adguard-shield/uninstall.sh`.
Oder manuell:
```bash
sudo systemctl stop adguard-shield

View File

@@ -39,7 +39,7 @@ log() {
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
local log_entry="[$timestamp] [$level] [BLOCKLIST-WORKER] $message"
echo "$log_entry" | tee -a "$LOG_FILE"
echo "$log_entry" | tee -a "$LOG_FILE" >&2
fi
}
@@ -115,6 +115,16 @@ ban_ip() {
# Bereits gesperrt?
if [[ -f "$state_file" ]]; then
# iptables-Regel prüfen und ggf. nachziehen (z.B. nach Neustart verloren gegangen)
if [[ "$ip" == *:* ]]; then
if ! ip6tables -C "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null; then
ip6tables -I "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null || true
fi
else
if ! iptables -C "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null; then
iptables -I "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null || true
fi
fi
log "DEBUG" "IP $ip bereits über externe Blocklist gesperrt"
return 0
fi
@@ -163,8 +173,8 @@ EOF
log_ban_history "BAN" "$ip" "external-blocklist"
# Benachrichtigung senden
if [[ "$NOTIFY_ENABLED" == "true" ]]; then
# Benachrichtigung senden (nur wenn EXTERNAL_BLOCKLIST_NOTIFY=true)
if [[ "$NOTIFY_ENABLED" == "true" && "${EXTERNAL_BLOCKLIST_NOTIFY:-false}" == "true" ]]; then
send_notification "ban" "$ip"
fi
}
@@ -188,57 +198,115 @@ unban_ip() {
rm -f "$state_file"
log_ban_history "UNBAN" "$ip" "$reason"
if [[ "$NOTIFY_ENABLED" == "true" ]]; then
if [[ "$NOTIFY_ENABLED" == "true" && "${EXTERNAL_BLOCKLIST_NOTIFY:-false}" == "true" ]]; then
send_notification "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"
local ip="$2"
[[ -z "${NOTIFY_WEBHOOK_URL:-}" ]] && return
# ntfy benötigt keine NOTIFY_WEBHOOK_URL, alle anderen schon
if [[ "${NOTIFY_TYPE:-generic}" != "ntfy" && -z "${NOTIFY_WEBHOOK_URL:-}" ]]; then
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
@@ -382,6 +450,9 @@ parse_blocklist_ips() {
[[ -f "$cache_file" ]] || return
while IFS= read -r line; do
line="${line%$'\r'}" # Windows-Zeilenenden (CRLF) entfernen
line="${line#$'\xef\xbb\xbf'}" # UTF-8 BOM entfernen (erste Zeile)
# Leerzeilen und Kommentarzeilen überspringen
[[ -z "$line" ]] && continue
[[ "$line" =~ ^[[:space:]]*# ]] && continue
@@ -439,7 +510,7 @@ parse_blocklist_ips() {
continue
fi
local resolved
resolved=$(getent ahosts "$line" 2>/dev/null | awk '{print $1}' | sort -u)
resolved=$(getent ahosts "$line" 2>/dev/null | awk '{print $1}' | sort -u) || resolved=""
if [[ -z "$resolved" ]]; then
log "WARN" "Hostname konnte nicht aufgelöst werden: $line"
continue
@@ -537,8 +608,12 @@ sync_blocklists() {
continue
fi
local _state_file_before="${STATE_DIR}/ext_${ip//[:/]/_}.ban"
local _was_new=false
[[ ! -f "$_state_file_before" ]] && _was_new=true
ban_ip "$ip"
new_bans=$((new_bans + 1))
[[ "$_was_new" == "true" ]] && new_bans=$((new_bans + 1))
done < "$unique_ips_file"
# ─── Entfernte IPs entsperren ────────────────────────────────────────────

View File

@@ -6,7 +6,7 @@
# Lizenz: MIT
###############################################################################
VERSION="v0.5.0"
VERSION="v0.6.1"
set -euo pipefail
@@ -70,6 +70,9 @@ print_help() {
echo -e " ${GREEN}uninstall${NC} Vollständige Deinstallation"
echo -e " Stoppt den Service, entfernt iptables-Regeln und"
echo -e " löscht alle Dateien (optional Konfiguration behalten)."
echo -e " Delegiert automatisch an den im Installationsverzeichnis"
echo -e " liegenden Uninstaller — kein Behalten der Installationsdateien nötig."
echo -e " Direkt ausführbar: ${CYAN}sudo bash $INSTALL_DIR/uninstall.sh${NC}"
echo ""
echo -e " ${GREEN}status${NC} Installationsstatus anzeigen"
echo -e " Zeigt ob AdGuard Shield installiert ist, welche Version"
@@ -139,7 +142,7 @@ show_menu() {
echo -e " ${CYAN}5)${NC} Hilfe — Hilfe & Befehlsübersicht anzeigen"
echo -e " ${CYAN}0)${NC} Beenden"
echo ""
read -rp " Auswahl [0-5]: " choice
read -rep " Auswahl [0-5]: " choice
echo ""
case "$choice" in
@@ -239,6 +242,7 @@ install_files() {
cp "$SCRIPT_DIR/unban-expired.sh" "$INSTALL_DIR/"
cp "$SCRIPT_DIR/external-blocklist-worker.sh" "$INSTALL_DIR/"
cp "$SCRIPT_DIR/report-generator.sh" "$INSTALL_DIR/"
cp "$SCRIPT_DIR/uninstall.sh" "$INSTALL_DIR/"
# Templates kopieren
mkdir -p "$INSTALL_DIR/templates"
@@ -251,6 +255,7 @@ install_files() {
chmod +x "$INSTALL_DIR/unban-expired.sh"
chmod +x "$INSTALL_DIR/external-blocklist-worker.sh"
chmod +x "$INSTALL_DIR/report-generator.sh"
chmod +x "$INSTALL_DIR/uninstall.sh"
echo -e " ✅ Dateien installiert"
echo ""
@@ -350,7 +355,7 @@ install_service() {
echo ""
# Interaktiv: Autostart beim Booten?
read -rp " Soll AdGuard Shield beim Booten automatisch starten? [J/n]: " autostart
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"
@@ -369,17 +374,17 @@ configure() {
local conf="$INSTALL_DIR/adguard-shield.conf"
# AdGuard URL
read -rp " AdGuard Home URL [http://127.0.0.1:3000]: " adguard_url
read -rep " AdGuard Home URL [http://127.0.0.1:3000]: " adguard_url
adguard_url="${adguard_url:-http://127.0.0.1:3000}"
sed -i "s|^ADGUARD_URL=.*|ADGUARD_URL=\"$adguard_url\"|" "$conf"
# Benutzername
read -rp " AdGuard Home Benutzername [admin]: " adguard_user
read -rep " AdGuard Home Benutzername [admin]: " adguard_user
adguard_user="${adguard_user:-admin}"
sed -i "s|^ADGUARD_USER=.*|ADGUARD_USER=\"$adguard_user\"|" "$conf"
# Passwort
read -rsp " AdGuard Home Passwort: " adguard_pass
read -resp " AdGuard Home Passwort: " adguard_pass
echo ""
if [[ -n "$adguard_pass" ]]; then
# Einfache Quotes damit $-Zeichen im Passwort nicht expandiert werden
@@ -387,17 +392,17 @@ configure() {
fi
# Rate Limit
read -rp " Max. Anfragen pro Domain/Client pro Minute [30]: " rate_limit
read -rep " Max. Anfragen pro Domain/Client pro Minute [30]: " rate_limit
rate_limit="${rate_limit:-30}"
sed -i "s|^RATE_LIMIT_MAX_REQUESTS=.*|RATE_LIMIT_MAX_REQUESTS=$rate_limit|" "$conf"
# Sperrdauer
read -rp " Sperrdauer in Sekunden [3600]: " ban_duration
read -rep " Sperrdauer in Sekunden [3600]: " ban_duration
ban_duration="${ban_duration:-3600}"
sed -i "s|^BAN_DURATION=.*|BAN_DURATION=$ban_duration|" "$conf"
# Whitelist
read -rp " Whitelist IPs (kommagetrennt) [127.0.0.1,::1]: " whitelist
read -rep " Whitelist IPs (kommagetrennt) [127.0.0.1,::1]: " whitelist
whitelist="${whitelist:-127.0.0.1,::1}"
sed -i "s|^WHITELIST=.*|WHITELIST=\"$whitelist\"|" "$conf"
@@ -519,6 +524,9 @@ print_summary() {
echo " Hilfe anzeigen:"
echo " sudo bash install.sh --help"
echo ""
echo " Deinstallieren (auch ohne Installationsdateien):"
echo " sudo bash $INSTALL_DIR/uninstall.sh"
echo ""
}
# ─── Status anzeigen ─────────────────────────────────────────────────────────
@@ -575,7 +583,7 @@ do_install() {
if [[ -d "$INSTALL_DIR" ]] && [[ -f "$INSTALL_DIR/adguard-shield.sh" ]]; then
echo -e "${YELLOW}AdGuard Shield ist bereits installiert!${NC}"
echo ""
read -rp " Möchtest du stattdessen ein Update durchführen? [j/N]: " do_upd
read -rep " Möchtest du stattdessen ein Update durchführen? [j/N]: " do_upd
if [[ "${do_upd,,}" == "j" ]]; then
do_update
return
@@ -600,7 +608,7 @@ do_install() {
# Interaktiv: Service jetzt starten?
echo -e "${YELLOW}Service starten:${NC}"
read -rp " Soll der AdGuard Shield Service jetzt gestartet werden? [J/n]: " start_now
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"
@@ -644,7 +652,7 @@ do_update() {
if systemctl is-enabled adguard-shield &>/dev/null; then
echo -e " Autostart ist bereits aktiviert"
else
read -rp " Soll AdGuard Shield beim Booten automatisch starten? [J/n]: " autostart
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"
@@ -661,7 +669,7 @@ do_update() {
fi
if [[ "$service_was_active" == "true" ]]; then
read -rp " Soll der Service jetzt neu gestartet werden? [J/n]: " restart_now
read -rep " Soll der Service jetzt neu gestartet werden? [J/n]: " restart_now
if [[ "${restart_now,,}" != "n" ]]; then
systemctl restart adguard-shield
echo -e " ✅ Service wurde neu gestartet"
@@ -670,7 +678,7 @@ do_update() {
echo -e " ${YELLOW}Bitte manuell neustarten: sudo systemctl restart adguard-shield${NC}"
fi
else
read -rp " Soll der Service jetzt gestartet werden? [J/n]: " start_now
read -rep " Soll der Service jetzt gestartet werden? [J/n]: " start_now
if [[ "${start_now,,}" != "n" ]]; then
systemctl start adguard-shield
echo -e " ✅ Service gestartet"
@@ -705,18 +713,22 @@ do_uninstall() {
exit 1
fi
echo -e "${YELLOW}Deinstalliere AdGuard Shield...${NC}"
# An den im Installationsverzeichnis liegenden Uninstaller delegieren
if [[ -f "$INSTALL_DIR/uninstall.sh" ]]; then
exec bash "$INSTALL_DIR/uninstall.sh"
fi
# Fallback für ältere Installationen ohne uninstall.sh
echo -e "${YELLOW}Deinstalliere AdGuard Shield (Fallback-Modus)...${NC}"
echo ""
# Sicherheitsabfrage
read -rp " Wirklich deinstallieren? [j/N]: " confirm
read -rep " Wirklich deinstallieren? [j/N]: " confirm
if [[ "${confirm,,}" != "j" ]]; then
echo -e "${GREEN}Deinstallation abgebrochen.${NC}"
exit 0
fi
echo ""
# Service stoppen und deaktivieren
if systemctl is-active adguard-shield &>/dev/null; then
systemctl stop adguard-shield
echo " ✅ Service gestoppt"
@@ -729,18 +741,18 @@ do_uninstall() {
systemctl daemon-reload
echo " ✅ Service-Datei entfernt"
# iptables Chain aufräumen
if [[ -f "$INSTALL_DIR/iptables-helper.sh" ]]; then
bash "$INSTALL_DIR/iptables-helper.sh" remove || true
fi
# Dateien entfernen
read -rp " Konfiguration und Logs behalten? [j/N]: " keep
read -rep " Konfiguration und Logs behalten? [j/N]: " keep
if [[ "${keep,,}" == "j" ]]; then
rm -f "$INSTALL_DIR/adguard-shield.sh"
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/report-generator.sh"
rm -rf "$INSTALL_DIR/templates"
echo " ✅ Scripts entfernt (Konfiguration und Logs behalten)"
else
rm -rf "$INSTALL_DIR"
@@ -766,7 +778,7 @@ main() {
do_update
;;
uninstall)
print_header
# print_header wird vom delegierten uninstall.sh übernommen
do_uninstall
;;
status)

View File

@@ -62,35 +62,86 @@ log() {
local message="$*"
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
echo "[$timestamp] [$level] [REPORT] $message" | tee -a "$LOG_FILE"
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
get_today_midnight() {
date -d "today 00:00:00" '+%s' 2>/dev/null || date -v0H -v0M -v0S '+%s'
}
get_report_period() {
local now_epoch
now_epoch=$(date '+%s')
local today_midnight
today_midnight=$(get_today_midnight)
# Ende des Berichtszeitraums ist immer das Ende von gestern (23:59:59)
local end_epoch=$((today_midnight - 1))
local start_epoch
local period_label
case "$REPORT_INTERVAL" in
daily)
start_epoch=$((now_epoch - 86400))
start_epoch=$((today_midnight - 86400))
period_label="Tagesbericht"
;;
weekly)
start_epoch=$((now_epoch - 604800))
start_epoch=$((today_midnight - 7 * 86400))
period_label="Wochenbericht"
;;
biweekly)
start_epoch=$((now_epoch - 1209600))
start_epoch=$((today_midnight - 14 * 86400))
period_label="Zweiwochenbericht"
;;
monthly)
start_epoch=$((now_epoch - 2592000))
start_epoch=$((today_midnight - 30 * 86400))
period_label="Monatsbericht"
;;
*)
start_epoch=$((now_epoch - 604800))
start_epoch=$((today_midnight - 7 * 86400))
period_label="Bericht"
;;
esac
@@ -98,57 +149,87 @@ get_report_period() {
local start_date
start_date=$(date -d "@$start_epoch" '+%d.%m.%Y' 2>/dev/null || date -r "$start_epoch" '+%d.%m.%Y')
local end_date
end_date=$(date '+%d.%m.%Y')
end_date=$(date -d "@$end_epoch" '+%d.%m.%Y' 2>/dev/null || date -r "$end_epoch" '+%d.%m.%Y')
echo "${period_label}: ${start_date} ${end_date}"
if [[ "$REPORT_INTERVAL" == "daily" ]]; then
echo "${period_label}: ${start_date}"
else
echo "${period_label}: ${start_date} ${end_date}"
fi
}
get_period_start_epoch() {
local now_epoch
now_epoch=$(date '+%s')
local today_midnight
today_midnight=$(get_today_midnight)
case "$REPORT_INTERVAL" in
daily) echo $((now_epoch - 86400)) ;;
weekly) echo $((now_epoch - 604800)) ;;
biweekly) echo $((now_epoch - 1209600)) ;;
monthly) echo $((now_epoch - 2592000)) ;;
*) echo $((now_epoch - 604800)) ;;
daily) echo $((today_midnight - 86400)) ;;
weekly) echo $((today_midnight - 7 * 86400)) ;;
biweekly) echo $((today_midnight - 14 * 86400)) ;;
monthly) echo $((today_midnight - 30 * 86400)) ;;
*) echo $((today_midnight - 7 * 86400)) ;;
esac
}
get_period_end_epoch() {
# Ende des Berichtszeitraums = Ende von gestern (heute 00:00:00 minus 1 Sekunde)
local today_midnight
today_midnight=$(get_today_midnight)
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" ]]; 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
@@ -158,48 +239,82 @@ 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
local start_epoch
start_epoch=$(get_period_start_epoch)
local end_epoch
end_epoch=$(get_period_end_epoch)
local filtered_data
filtered_data=$(filter_history_by_period "$start_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
@@ -217,17 +332,80 @@ 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
local awk_result
awk_result=$(echo "$HISTORY_CACHE" | awk -F'|' -v s="$start_epoch" -v e="$end_epoch" '
$1 >= s && $1 <= e {
action = $3
if (action == "BAN") {
bans++
ip_count[$4]++
ip_seen[$4] = 1
day = substr($2, 1, 10)
day_count[day]++
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++
}
}
END {
for (ip in ip_seen) unique++
busiest = ""; max_d = 0
for (d in day_count) {
if (day_count[d] > max_d) { max_d = day_count[d]; busiest = 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
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]
}
}
')
# Gesamtzahl Entsperrungen
TOTAL_UNBANS=$(echo "$filtered_data" | grep -c '| UNBAN ' || echo "0")
# 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}')
# 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)
local busiest_raw
busiest_raw=$(echo "$awk_result" | awk -F= '$1=="BUSIEST" {print $2; exit}')
if [[ -n "$busiest_raw" ]]; then
BUSIEST_DAY=$(date -d "$busiest_raw" '+%d.%m.%Y' 2>/dev/null || echo "$busiest_raw")
else
BUSIEST_DAY=""
fi
# Permanente Sperren (Dauer enthält "PERMANENT" oder "permanent")
PERMANENT_BANS=$(echo "$filtered_data" | grep '| BAN ' | awk -F'|' '{print $6}' | grep -ic 'permanent' || echo "0")
# 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
@@ -237,41 +415,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 ─────────────────────────────────────────────────
@@ -373,6 +531,7 @@ generate_recent_bans_html() {
while IFS='|' read -r timestamp action ip domain count duration protocol reason; do
timestamp=$(echo "$timestamp" | xargs)
ip=$(echo "$ip" | xargs)
[[ -z "$timestamp" && -z "$ip" ]] && continue
domain=$(echo "$domain" | xargs)
reason=$(echo "$reason" | xargs)
[[ "$domain" == "-" ]] && domain=""
@@ -397,6 +556,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
@@ -459,6 +677,7 @@ generate_recent_bans_txt() {
while IFS='|' read -r timestamp action ip domain count duration protocol reason; do
timestamp=$(echo "$timestamp" | xargs)
ip=$(echo "$ip" | xargs)
[[ -z "$timestamp" && -z "$ip" ]] && continue
domain=$(echo "$domain" | xargs)
reason=$(echo "$reason" | xargs)
[[ "$domain" == "-" ]] && domain=""
@@ -471,6 +690,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}"
@@ -480,6 +739,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
@@ -506,6 +768,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}"
@@ -526,6 +790,8 @@ generate_report() {
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"
@@ -548,6 +814,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}"
@@ -568,6 +836,8 @@ generate_report() {
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
@@ -812,6 +1082,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
@@ -837,13 +1112,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>
@@ -871,12 +1150,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">
@@ -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 v{{VERSION}} · {{HOSTNAME}}</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}}
@@ -50,11 +56,13 @@
{{RECENT_BANS_TEXT}}
{{UPDATE_NOTICE_TXT}}
═══════════════════════════════════════════════════════════════
Dieser Report wurde automatisch von AdGuard Shield generiert.
AdGuard Shield v{{VERSION}}
AdGuard Shield {{VERSION}}
Web: https://www.patrick-asmus.de
Blog: https://www.cleveradmin.de
Repo: https://git.techniverse.net/scriptos/adguard-shield.git
Docs: https://git.techniverse.net/scriptos/adguard-shield/src/branch/main/docs
═══════════════════════════════════════════════════════════════

129
uninstall.sh Normal file
View File

@@ -0,0 +1,129 @@
#!/bin/bash
###############################################################################
# AdGuard Shield - Uninstaller
# Autor: Patrick Asmus
# E-Mail: support@techniverse.net
# Lizenz: MIT
#
# Dieses Script befindet sich im Installationsverzeichnis und kann daher
# ohne die originalen Installationsdateien ausgeführt werden:
# sudo bash /opt/adguard-shield/uninstall.sh
###############################################################################
# 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"
# Farben
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
print_header() {
echo ""
echo -e "${BLUE}"
echo " ▄▄▄ ▓█████▄ ▄████ █ ██ ▄▄▄ ██▀███ ▓█████▄ ██████ ██░ ██ ██▓▓█████ ██▓ ▓█████▄ "
echo "▒████▄ ▒██▀ ██▌ ██▒ ▀█▒ ██ ▓██▒▒████▄ ▓██ ▒ ██▒▒██▀ ██▌ ▒██ ▒ ▓██░ ██▒▓██▒▓█ ▀ ▓██▒ ▒██▀ ██▌"
echo "▒██ ▀█▄ ░██ █▌▒██░▄▄▄░▓██ ▒██░▒██ ▀█▄ ▓██ ░▄█ ▒░██ █▌ ░ ▓██▄ ▒██▀▀██░▒██▒▒███ ▒██░ ░██ █▌"
echo "░██▄▄▄▄██ ░▓█▄ ▌░▓█ ██▓▓▓█ ░██░░██▄▄▄▄██ ▒██▀▀█▄ ░▓█▄ ▌ ▒ ██▒░▓█ ░██ ░██░▒▓█ ▄ ▒██░ ░▓█▄ ▌"
echo " ▓█ ▓██▒░▒████▓ ░▒▓███▀▒▒▒█████▓ ▓█ ▓██▒░██▓ ▒██▒░▒████▓ ▒██████▒▒░▓█▒░██▓░██░░▒████▒░██████▒░▒████▓ "
echo " ▒▒ ▓▒█░ ▒▒▓ ▒ ░▒ ▒ ░▒▓▒ ▒ ▒ ▒▒ ▓▒█░░ ▒▓ ░▒▓░ ▒▒▓ ▒ ▒ ▒▓▒ ▒ ░ ▒ ░░▒░▒░▓ ░░ ▒░ ░░ ▒░▓ ░ ▒▒▓ ▒ "
echo " ▒ ▒▒ ░ ░ ▒ ▒ ░ ░ ░░▒░ ░ ░ ▒ ▒▒ ░ ░▒ ░ ▒░ ░ ▒ ▒ ░ ░▒ ░ ░ ▒ ░▒░ ░ ▒ ░ ░ ░ ░░ ░ ▒ ░ ░ ▒ ▒ "
echo " ░ ▒ ░ ░ ░ ░ ░ ░ ░░░ ░ ░ ░ ▒ ░░ ░ ░ ░ ░ ░ ░ ░ ░ ░░ ░ ▒ ░ ░ ░ ░ ░ ░ ░ "
echo " ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ "
echo " ░ ░ ░ "
echo -e "${NC}"
echo -e "${GREEN} Uninstaller${NC}"
echo -e "${BLUE} Autor: Patrick Asmus${NC}"
echo -e
echo -e "${BLUE} E-Mail: support@techniverse.net${NC}"
echo -e "${BLUE} Web: https://www.patrick-asmus.de${NC}"
echo ""
echo -e "${BLUE}───────────────────────────────────────────────────────────────────────────────────────────────────────────────${NC}"
echo ""
echo -e "${BLUE} Repo: https://git.techniverse.net/scriptos/adguard-shield${NC}"
echo ""
echo -e "${BLUE}═══════════════════════════════════════════════════════════════════════════════════════════════════════════════${NC}"
echo ""
}
check_root() {
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}Dieses Script muss als root ausgeführt werden!${NC}" >&2
echo "Bitte mit 'sudo $0' ausführen."
exit 1
fi
}
do_uninstall() {
check_root
# Prüfen ob installiert
if [[ ! -d "$INSTALL_DIR" ]]; then
echo -e "${RED}AdGuard Shield ist nicht installiert (Verzeichnis nicht gefunden: $INSTALL_DIR)!${NC}"
exit 1
fi
echo -e "${YELLOW}Deinstalliere AdGuard Shield aus: ${BOLD}$INSTALL_DIR${NC}"
echo ""
# Sicherheitsabfrage
read -rep " Wirklich deinstallieren? [j/N]: " confirm
if [[ "${confirm,,}" != "j" ]]; then
echo -e "${GREEN}Deinstallation abgebrochen.${NC}"
exit 0
fi
echo ""
# Service stoppen und deaktivieren
if systemctl is-active adguard-shield &>/dev/null; then
systemctl stop adguard-shield
echo " ✅ Service gestoppt"
fi
if systemctl is-enabled adguard-shield &>/dev/null; then
systemctl disable adguard-shield
echo " ✅ Service deaktiviert"
fi
if [[ -f "$SERVICE_FILE" ]]; then
rm -f "$SERVICE_FILE"
systemctl daemon-reload
echo " ✅ Service-Datei entfernt"
fi
# iptables Chain aufräumen
if [[ -f "$INSTALL_DIR/iptables-helper.sh" ]]; then
bash "$INSTALL_DIR/iptables-helper.sh" remove || true
fi
# Dateien entfernen
read -rep " Konfiguration und Logs behalten? [j/N]: " keep
if [[ "${keep,,}" == "j" ]]; then
rm -f "$INSTALL_DIR/adguard-shield.sh"
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/report-generator.sh"
rm -f "$INSTALL_DIR/uninstall.sh"
rm -rf "$INSTALL_DIR/templates"
echo " ✅ Scripts entfernt (Konfiguration und Logs behalten)"
echo ""
echo -e "${YELLOW} Konfiguration verbleibt in: $INSTALL_DIR/adguard-shield.conf${NC}"
echo -e "${YELLOW} Logs verbleiben in: /var/log/adguard-shield*.log${NC}"
else
rm -rf "$INSTALL_DIR"
rm -rf /var/lib/adguard-shield
rm -f /var/log/adguard-shield.log*
rm -f /var/log/adguard-shield-bans.log
echo " ✅ Alles entfernt"
fi
echo ""
echo -e "${GREEN}Deinstallation abgeschlossen.${NC}"
}
print_header
do_uninstall