feat: Offense-Cleanup-Worker für automatisches Aufräumen abgelaufener Offense-Zähler

This commit is contained in:
2026-04-14 21:01:51 +02:00
parent df15a587ee
commit 0264e1e896
9 changed files with 330 additions and 6 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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"

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

@@ -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

View File

@@ -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"