From 0264e1e896f11989f6229a91a61f89a0450941d1 Mon Sep 17 00:00:00 2001 From: scriptos Date: Tue, 14 Apr 2026 21:01:51 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Offense-Cleanup-Worker=20f=C3=BCr=20aut?= =?UTF-8?q?omatisches=20Aufr=C3=A4umen=20abgelaufener=20Offense-Z=C3=A4hle?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- adguard-shield.conf | 8 +- adguard-shield.sh | 40 ++++++ docs/architektur.md | 1 + docs/befehle.md | 20 +++ docs/konfiguration.md | 2 +- install.sh | 7 ++ offense-cleanup-worker.sh | 255 ++++++++++++++++++++++++++++++++++++++ uninstall.sh | 1 + 9 files changed, 330 insertions(+), 6 deletions(-) create mode 100644 offense-cleanup-worker.sh diff --git a/README.md b/README.md index 7db1893..c757be0 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Wenn ein Client eine bestimmte Domain zu oft anfragt (z.B. >30x pro Minute), wir - 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) +- **Progressive Sperren (Recidive)** — Wiederholungstäter werden stufenweise länger gesperrt (wie bei fail2ban), mit automatischem Cleanup abgelaufener Zähler - 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 diff --git a/adguard-shield.conf b/adguard-shield.conf index 537ebe3..aae7ca0 100644 --- a/adguard-shield.conf +++ b/adguard-shield.conf @@ -21,7 +21,7 @@ SUBDOMAIN_FLOOD_WINDOW=60 # Zeitfenster in Sekunden # --- Sperr-Einstellungen --- BAN_DURATION=3600 # Basis-Sperrdauer in Sekunden IPTABLES_CHAIN="ADGUARD_SHIELD" -BLOCKED_PORTS="53 443 853" # DNS(53), DoH(443), DoT/DoQ(853) +BLOCKED_PORTS="53 443 853" # DNS(53), DoH(443), DoT/DoQ(853) # --- Whitelist --- # IPs die niemals gesperrt werden (kommagetrennt) @@ -88,12 +88,12 @@ ABUSEIPDB_CATEGORIES="4" # 4 = DDoS Attack (siehe abuseipdb.com/categ # 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_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) +GEOIP_LICENSE_KEY="" # MaxMind GeoLite2 Key (optional, für Auto-Download) +GEOIP_MMDB_PATH="" # Manueller DB-Pfad (optional, hat Vorrang) # --- Erweiterte Einstellungen --- STATE_DIR="/var/lib/adguard-shield" diff --git a/adguard-shield.sh b/adguard-shield.sh index ea9f4e6..7eff70f 100644 --- a/adguard-shield.sh +++ b/adguard-shield.sh @@ -331,6 +331,7 @@ cleanup() { stop_blocklist_worker stop_whitelist_worker stop_geoip_worker + stop_offense_cleanup_worker rm -f "$PID_FILE" exit 0 } @@ -1257,6 +1258,39 @@ stop_geoip_worker() { 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..." + 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" "═══════════════════════════════════════════════════════════" @@ -1288,6 +1322,9 @@ main_loop() { 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 @@ -1304,6 +1341,9 @@ main_loop() { # 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 diff --git a/docs/architektur.md b/docs/architektur.md index c3ea517..904613f 100644 --- a/docs/architektur.md +++ b/docs/architektur.md @@ -137,6 +137,7 @@ Das ermöglicht: ├── external-blocklist-worker.sh # Externer Blocklist-Worker ├── external-whitelist-worker.sh # Externer Whitelist-Worker (DNS-Auflösung) ├── geoip-worker.sh # GeoIP-Länderfilter-Worker +├── offense-cleanup-worker.sh # Automatisches Aufräumen abgelaufener Offense-Zähler ├── unban-expired.sh # Cron-basiertes Entsperren └── geoip/ # Auto-Download MaxMind GeoLite2 DB (optional) diff --git a/docs/befehle.md b/docs/befehle.md index 82cda84..22c890d 100644 --- a/docs/befehle.md +++ b/docs/befehle.md @@ -282,6 +282,26 @@ sudo /opt/adguard-shield/geoip-worker.sh flush 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 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 diff --git a/docs/konfiguration.md b/docs/konfiguration.md index 89a7c06..85f0c41 100644 --- a/docs/konfiguration.md +++ b/docs/konfiguration.md @@ -100,7 +100,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. 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 diff --git a/install.sh b/install.sh index 0edc84b..2cf9fef 100644 --- a/install.sh +++ b/install.sh @@ -265,6 +265,7 @@ install_files() { 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" @@ -281,6 +282,7 @@ install_files() { 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 "" @@ -810,8 +812,13 @@ do_uninstall() { 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" diff --git a/offense-cleanup-worker.sh b/offense-cleanup-worker.sh new file mode 100644 index 0000000..b160854 --- /dev/null +++ b/offense-cleanup-worker.sh @@ -0,0 +1,255 @@ +#!/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-14 +# 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-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 + + 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 + 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 diff --git a/uninstall.sh b/uninstall.sh index f734c93..751891e 100644 --- a/uninstall.sh +++ b/uninstall.sh @@ -123,6 +123,7 @@ do_uninstall() { 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"