From 0d1f7db43b1ee46970d8febb8df2b476a5d35cc3 Mon Sep 17 00:00:00 2001 From: Patrick Asmus Date: Thu, 30 Apr 2026 15:39:26 +0200 Subject: [PATCH] feat: Migration auf sqlite3 --- README.md | 4 +- adguard-shield.conf | 2 +- adguard-shield.sh | 298 ++++++-------- db.sh | 641 ++++++++++++++++++++++++++++++ docs/architektur.md | 87 ++-- docs/befehle.md | 8 +- docs/konfiguration.md | 4 +- docs/report.md | 2 +- docs/tipps-und-troubleshooting.md | 4 +- docs/update.md | 9 +- external-blocklist-worker.sh | 110 ++--- external-whitelist-worker.sh | 95 ++--- geoip-worker.sh | 138 +++---- install.sh | 100 ++++- offense-cleanup-worker.sh | 62 +-- report-generator.sh | 239 ++--------- unban-expired.sh | 59 +-- uninstall.sh | 1 + 18 files changed, 1118 insertions(+), 745 deletions(-) create mode 100644 db.sh diff --git a/README.md b/README.md index 1a7cb9f..bcf2a6d 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Das schützt klassische DNS-Anfragen genauso wie DoH, DoT und DoQ, ohne deine be - Linux-Server mit AdGuard Home - Root-Zugriff per `sudo` - Erreichbare AdGuard Home Web-API, standardmäßig `http://127.0.0.1:3000` -- `curl`, `jq`, `iptables`, `gawk` und `systemd` +- `curl`, `jq`, `iptables`, `gawk`, `sqlite3` und `systemd` Die benötigten Pakete werden vom Installer automatisch installiert. @@ -81,7 +81,7 @@ sudo systemctl status adguard-shield ```bash sudo bash install.sh # Interaktives Menü sudo bash install.sh install # Direkt installieren -sudo bash install.sh update # Update inkl. Konfigurations-Migration +sudo bash install.sh update # Update inkl. Konfig- & Datenbank-Migration sudo bash install.sh status # Installationsstatus prüfen sudo bash /opt/adguard-shield/uninstall.sh ``` diff --git a/adguard-shield.conf b/adguard-shield.conf index ffc2032..5266996 100644 --- a/adguard-shield.conf +++ b/adguard-shield.conf @@ -100,7 +100,7 @@ GEOIP_LICENSE_KEY="" # MaxMind GeoLite2 Key (optional, für Auto- GEOIP_MMDB_PATH="" # Manueller DB-Pfad (optional, hat Vorrang) # --- Erweiterte Einstellungen --- -STATE_DIR="/var/lib/adguard-shield" +STATE_DIR="/var/lib/adguard-shield" # SQLite-DB: ${STATE_DIR}/adguard-shield.db PID_FILE="/var/run/adguard-shield.pid" API_QUERY_LIMIT=500 # API-Einträge pro Abfrage (max 5000) DRY_RUN=false # true = nur loggen, nicht sperren diff --git a/adguard-shield.sh b/adguard-shield.sh index 72cbc6b..557c737 100644 --- a/adguard-shield.sh +++ b/adguard-shield.sh @@ -8,7 +8,7 @@ # Lizenz: MIT ############################################################################### -VERSION="v0.9.0" +VERSION="v1.0.0" set -euo pipefail @@ -26,10 +26,14 @@ fi # shellcheck source=adguard-shield.conf source "$CONFIG_FILE" +# ─── Datenbank-Bibliothek laden ─────────────────────────────────────────────── +# shellcheck source=db.sh +source "${SCRIPT_DIR}/db.sh" + # ─── Abhängigkeiten prüfen ──────────────────────────────────────────────────── check_dependencies() { local missing=() - for cmd in curl jq iptables ip6tables date; do + for cmd in curl jq iptables ip6tables date sqlite3; do if ! command -v "$cmd" &>/dev/null; then missing+=("$cmd") fi @@ -79,15 +83,6 @@ log_ban_history() { local reason="${5:-}" local duration="${6:-}" local protocol="${7:-}" - local timestamp - timestamp="$(date '+%Y-%m-%d %H:%M:%S')" - - # Header schreiben falls Datei neu ist - if [[ ! -f "$BAN_HISTORY_FILE" ]]; then - echo "# AdGuard Shield - Ban History" > "$BAN_HISTORY_FILE" - echo "# Format: ZEITSTEMPEL | AKTION | CLIENT-IP | DOMAIN | ANFRAGEN | SPERRDAUER | PROTOKOLL | GRUND" >> "$BAN_HISTORY_FILE" - echo "#──────────────────────────────────────────────────────────────────────────────────────────────────" >> "$BAN_HISTORY_FILE" - fi if [[ -z "$duration" && "$action" == "BAN" ]]; then duration="${BAN_DURATION}s" @@ -95,65 +90,20 @@ log_ban_history() { [[ -z "$duration" ]] && duration="-" [[ -z "$protocol" ]] && protocol="-" - printf "%-19s | %-6s | %-39s | %-30s | %-8s | %-10s | %-10s | %s\n" \ - "$timestamp" "$action" "$client_ip" "${domain:--}" "${count:--}" "$duration" "$protocol" "${reason:-rate-limit}" \ - >> "$BAN_HISTORY_FILE" + db_history_add "$action" "$client_ip" "${domain:--}" "${count:--}" "${reason:-rate-limit}" "$duration" "$protocol" } # ─── Progressive Ban (Recidive) ───────────────────────────────────────────── -# Liest die aktuelle Offense-Stufe einer IP aus der Offense-Datei get_offense_level() { local client_ip="$1" - local offense_file="${STATE_DIR}/${client_ip//[:\/]/_}.offenses" - - if [[ ! -f "$offense_file" ]]; then - echo "0" - return - fi - - local level last_offense now reset_after - level=$(grep '^OFFENSE_LEVEL=' "$offense_file" | cut -d= -f2 || true) - last_offense=$(grep '^LAST_OFFENSE_EPOCH=' "$offense_file" | cut -d= -f2 || true) - now=$(date '+%s') - reset_after="${PROGRESSIVE_BAN_RESET_AFTER:-86400}" - - # Prüfen ob der Zähler abgelaufen ist (Reset nach Zeitraum ohne Vergehen) - if [[ -n "$last_offense" && $((now - last_offense)) -gt "$reset_after" ]]; then - log "INFO" "Progressive Ban: Offense-Zähler für $client_ip zurückgesetzt (>${reset_after}s ohne Vergehen)" - rm -f "$offense_file" - echo "0" - return - fi - - echo "${level:-0}" + local level + level=$(db_offense_get_level "$client_ip" "${PROGRESSIVE_BAN_RESET_AFTER:-86400}") + echo "$level" } -# Erhöht die Offense-Stufe einer IP und gibt die neue Stufe zurück increment_offense_level() { local client_ip="$1" - local offense_file="${STATE_DIR}/${client_ip//[:\/]/_}.offenses" - local current_level - current_level=$(get_offense_level "$client_ip") - local new_level=$((current_level + 1)) - local now - now=$(date '+%s') - local now_readable - now_readable=$(date '+%Y-%m-%d %H:%M:%S') - - # Erstes Vergehen merken (bevor Datei überschrieben wird) - local first_offense - first_offense=$(grep '^FIRST_OFFENSE=' "$offense_file" 2>/dev/null | cut -d= -f2 || true) - [[ -z "$first_offense" ]] && first_offense="$now_readable" - - cat > "$offense_file" << EOF -CLIENT_IP=$client_ip -OFFENSE_LEVEL=$new_level -LAST_OFFENSE_EPOCH=$now -LAST_OFFENSE=$now_readable -FIRST_OFFENSE=$first_offense -EOF - - echo "$new_level" + db_offense_increment "$client_ip" } # Berechnet die Sperrdauer basierend auf der Offense-Stufe @@ -207,11 +157,9 @@ format_duration() { fi } -# Setzt den Offense-Zähler einer IP zurück reset_offense_level() { local client_ip="$1" - local offense_file="${STATE_DIR}/${client_ip//[:\/]/_}.offenses" - rm -f "$offense_file" + db_offense_delete "$client_ip" } # ─── Protokoll-Erkennung ───────────────────────────────────────────────────── @@ -307,12 +255,23 @@ report_to_abuseipdb() { fi } -# ─── Verzeichnisse erstellen ────────────────────────────────────────────────── +# ─── Verzeichnisse und Datenbank erstellen ─────────────────────────────────── init_directories() { mkdir -p "$STATE_DIR" mkdir -p "$(dirname "$LOG_FILE")" mkdir -p "$(dirname "$PID_FILE")" - mkdir -p "$(dirname "$BAN_HISTORY_FILE")" + + db_init + + # Migration von Flat-Files (einmalig beim ersten Start nach Update) + if [[ ! -f "$_DB_MIGRATION_MARKER" ]]; then + local migrated + migrated=$(db_migrate_from_files) + if [[ "${migrated:-0}" -gt 0 ]]; then + log "INFO" "SQLite-Migration abgeschlossen: $migrated Eintraege migriert" + log "INFO" "Backup der alten Dateien: ${STATE_DIR}/.backup_pre_sqlite/" + fi + fi } # ─── PID-Management ────────────────────────────────────────────────────────── @@ -354,15 +313,14 @@ is_whitelisted() { local ip="$1" IFS=',' read -ra wl_entries <<< "$WHITELIST" for entry in "${wl_entries[@]}"; do - entry=$(echo "$entry" | xargs) # trim + entry=$(echo "$entry" | xargs) if [[ "$ip" == "$entry" ]]; then return 0 fi done - # Externe Whitelist prüfen (aufgelöste IPs aus dem Whitelist-Worker) - local ext_wl_file="${EXTERNAL_WHITELIST_CACHE_DIR:-/var/lib/adguard-shield/external-whitelist}/resolved_ips.txt" - if [[ -f "$ext_wl_file" ]] && grep -qxF "$ip" "$ext_wl_file" 2>/dev/null; then + # Externe Whitelist prüfen (SQLite) + if db_whitelist_contains "$ip"; then return 0 fi @@ -431,8 +389,7 @@ ban_client() { local protocol="${6:-DNS}" # Prüfen ob bereits gesperrt - local state_file="${STATE_DIR}/${client_ip//[:\/]/_}.ban" - if [[ -f "$state_file" ]]; then + if db_ban_exists "$client_ip"; then log "DEBUG" "Client $client_ip ist bereits gesperrt" return 0 fi @@ -496,20 +453,10 @@ ban_client() { iptables -I "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true fi - # State speichern - cat > "$state_file" << EOF -CLIENT_IP=$client_ip -DOMAIN=$domain -COUNT=$count -BAN_TIME=$(date '+%Y-%m-%d %H:%M:%S') -BAN_UNTIL_EPOCH=$ban_until -BAN_UNTIL=$ban_until_display -BAN_DURATION=${effective_duration} -OFFENSE_LEVEL=$offense_level -IS_PERMANENT=$is_permanent -REASON=$reason -PROTOCOL=$protocol -EOF + # State in Datenbank speichern + local perm_int=0 + [[ "$is_permanent" == "true" ]] && perm_int=1 + db_ban_insert "$client_ip" "$domain" "$count" "$(date '+%Y-%m-%d %H:%M:%S')" "$ban_until" "$effective_duration" "$offense_level" "$perm_int" "$reason" "$protocol" "monitor" # Ban-History Eintrag local history_duration="${duration_display}" @@ -531,16 +478,17 @@ EOF unban_client() { local client_ip="$1" local reason="${2:-expired}" - local state_file="${STATE_DIR}/${client_ip//[:\/]/_}.ban" - # Domain und Protokoll aus State lesen bevor wir löschen + # Domain und Protokoll aus DB lesen bevor wir loeschen + local ban_data + ban_data=$(db_ban_get "$client_ip") local domain="-" local protocol="-" - if [[ -f "$state_file" ]]; then - domain=$(grep '^DOMAIN=' "$state_file" | cut -d= -f2 || true) - protocol=$(grep '^PROTOCOL=' "$state_file" | cut -d= -f2 || true) + if [[ -n "$ban_data" ]]; then + IFS='|' read -r _ b_domain _ _ _ _ _ _ _ b_protocol _ _ _ <<< "$ban_data" + domain="${b_domain:--}" + protocol="${b_protocol:--}" fi - [[ -z "$protocol" ]] && protocol="-" log "INFO" "ENTSPERRE Client: $client_ip ($reason)" @@ -550,9 +498,8 @@ unban_client() { iptables -D "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true fi - rm -f "$state_file" + db_ban_delete "$client_ip" - # Ban-History Eintrag log_ban_history "UNBAN" "$client_ip" "$domain" "-" "$reason" "-" "$protocol" if [[ "$NOTIFY_ENABLED" == "true" ]]; then @@ -562,29 +509,14 @@ unban_client() { # ─── Abgelaufene Sperren aufheben ─────────────────────────────────────────── check_expired_bans() { - local now - now=$(date '+%s') + local expired_ips + expired_ips=$(db_ban_get_expired) + [[ -z "$expired_ips" ]] && return - for state_file in "${STATE_DIR}"/*.ban; do - [[ -f "$state_file" ]] || continue - - local ban_until_epoch - ban_until_epoch=$(grep '^BAN_UNTIL_EPOCH=' "$state_file" | cut -d= -f2 || true) - local client_ip - client_ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2 || true) - local is_permanent - is_permanent=$(grep '^IS_PERMANENT=' "$state_file" | cut -d= -f2 || true) - - # Permanente Sperren nicht automatisch aufheben - if [[ "$is_permanent" == "true" || "$ban_until_epoch" == "0" ]]; then - log "DEBUG" "Client $client_ip ist permanent gesperrt – überspringe" - continue - fi - - if [[ -n "$ban_until_epoch" && "$now" -ge "$ban_until_epoch" ]]; then - unban_client "$client_ip" "expired" - fi - done + while IFS= read -r client_ip; do + [[ -z "$client_ip" ]] && continue + unban_client "$client_ip" "expired" + done <<< "$expired_ips" } # ─── Benachrichtigungen ───────────────────────────────────────────────────── @@ -995,8 +927,7 @@ analyze_subdomain_flood() { fi # Prüfen ob bereits gesperrt - local state_file="${STATE_DIR}/${client//[:\/]/_}.ban" - if [[ -f "$state_file" ]]; then + if db_ban_exists "$client"; then log "DEBUG" "Client $client ist bereits gesperrt (Subdomain-Flood übersprungen)" continue fi @@ -1054,22 +985,15 @@ show_status() { echo "" fi - # Aktive Sperren + # Aktive Sperren aus Datenbank local ban_count=0 - if [[ -d "$STATE_DIR" ]]; then - for state_file in "${STATE_DIR}"/*.ban; do - [[ -f "$state_file" ]] || continue + local all_bans + all_bans=$(db_ban_get_all) + + if [[ -n "$all_bans" ]]; then + while IFS='|' read -r s_ip s_domain s_count s_ban_time s_ban_until_epoch s_dur s_level s_perm_int s_reason s_proto s_source s_geoip_country s_geoip_mode; do + [[ -z "$s_ip" ]] && continue ban_count=$((ban_count + 1)) - local s_ip s_domain s_level s_perm s_dur s_until s_reason s_count s_proto - s_ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2 || true) - s_domain=$(grep '^DOMAIN=' "$state_file" | cut -d= -f2 || true) - s_level=$(grep '^OFFENSE_LEVEL=' "$state_file" | cut -d= -f2 || true) - s_perm=$(grep '^IS_PERMANENT=' "$state_file" | cut -d= -f2 || true) - s_dur=$(grep '^BAN_DURATION=' "$state_file" | cut -d= -f2 || true) - s_until=$(grep '^BAN_UNTIL=' "$state_file" | cut -d= -f2 || true) - s_reason=$(grep '^REASON=' "$state_file" | cut -d= -f2 || true) - s_count=$(grep '^COUNT=' "$state_file" | cut -d= -f2 || true) - s_proto=$(grep '^PROTOCOL=' "$state_file" | cut -d= -f2 || true) s_reason="${s_reason:-rate-limit}" s_proto="${s_proto:-?}" @@ -1078,7 +1002,7 @@ show_status() { [[ "$s_reason" == "dns-flood-watchlist" ]] && reason_tag=" (DNS-Flood-Watchlist)" local count_info="" - if [[ -n "$s_count" && "$s_count" != "-" ]]; then + if [[ -n "$s_count" && "$s_count" != "0" && "$s_count" != "-" ]]; then if [[ "$s_reason" == "subdomain-flood" ]]; then count_info=", ${s_count} Subdomains" else @@ -1088,16 +1012,23 @@ show_status() { local proto_tag=" via ${s_proto}" - if [[ "$s_perm" == "true" && "$s_reason" == "dns-flood-watchlist" ]]; then + local s_until_display + if [[ "$s_ban_until_epoch" == "0" || "$s_perm_int" == "1" ]]; then + s_until_display="PERMANENT" + else + s_until_display=$(date -d "@$s_ban_until_epoch" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || date -r "$s_ban_until_epoch" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "?") + fi + + if [[ "$s_perm_int" == "1" && "$s_reason" == "dns-flood-watchlist" ]]; then echo " 🚫 Gesperrt: $s_ip → $s_domain [PERMANENT${count_info}${proto_tag}]${reason_tag}" - elif [[ "$s_perm" == "true" ]]; then + elif [[ "$s_perm_int" == "1" ]]; then echo " 🚫 Gesperrt: $s_ip → $s_domain [PERMANENT, Stufe ${s_level:-?}${count_info}${proto_tag}]${reason_tag}" elif [[ -n "$s_level" && "$s_level" -gt 0 ]]; then - echo " 🚫 Gesperrt: $s_ip → $s_domain [Stufe ${s_level}, $(format_duration "${s_dur:-$BAN_DURATION}"), bis $s_until${count_info}${proto_tag}]${reason_tag}" + echo " 🚫 Gesperrt: $s_ip → $s_domain [Stufe ${s_level}, $(format_duration "${s_dur:-$BAN_DURATION}"), bis $s_until_display${count_info}${proto_tag}]${reason_tag}" else - echo " 🚫 Gesperrt: $s_ip → $s_domain [bis $s_until${count_info}${proto_tag}]${reason_tag}" + echo " 🚫 Gesperrt: $s_ip → $s_domain [bis $s_until_display${count_info}${proto_tag}]${reason_tag}" fi - done + done <<< "$all_bans" fi echo "" @@ -1108,24 +1039,25 @@ show_status() { fi # Offense-Informationen anzeigen (Wiederholungstäter) - if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" && -d "$STATE_DIR" ]]; then + if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" ]]; then + local offense_data + offense_data=$(db_offense_get_all) local offense_count=0 local offense_output="" - for offense_file in "${STATE_DIR}"/*.offenses; do - [[ -f "$offense_file" ]] || continue - local o_ip o_level o_last - o_ip=$(grep '^CLIENT_IP=' "$offense_file" | cut -d= -f2 || true) - o_level=$(grep '^OFFENSE_LEVEL=' "$offense_file" | cut -d= -f2 || true) - o_last=$(grep '^LAST_OFFENSE=' "$offense_file" | cut -d= -f2 || true) - offense_count=$((offense_count + 1)) - local next_dur - next_dur=$(calculate_ban_duration "$((o_level + 1))") - if [[ "$next_dur" -eq 0 ]]; then - offense_output+=" ⚠ $o_ip: Stufe $o_level (letztes Vergehen: $o_last) → nächste Sperre: PERMANENT\n" - else - offense_output+=" ⚠ $o_ip: Stufe $o_level (letztes Vergehen: $o_last) → nächste Sperre: $(format_duration "$next_dur")\n" - fi - done + + if [[ -n "$offense_data" ]]; then + while IFS='|' read -r o_ip o_level o_last_epoch o_last o_first; do + [[ -z "$o_ip" ]] && continue + offense_count=$((offense_count + 1)) + local next_dur + next_dur=$(calculate_ban_duration "$((o_level + 1))") + if [[ "$next_dur" -eq 0 ]]; then + offense_output+=" ⚠ $o_ip: Stufe $o_level (letztes Vergehen: $o_last) → nächste Sperre: PERMANENT\n" + else + offense_output+=" ⚠ $o_ip: Stufe $o_level (letztes Vergehen: $o_last) → nächste Sperre: $(format_duration "$next_dur")\n" + fi + done <<< "$offense_data" + fi if [[ $offense_count -gt 0 ]]; then echo "" @@ -1156,29 +1088,34 @@ show_history() { echo "═══════════════════════════════════════════════════════════════" echo "" - if [[ ! -f "$BAN_HISTORY_FILE" ]]; then + local total + total=$(db_history_count) + + if [[ "${total:-0}" -eq 0 ]]; then echo " Noch keine History vorhanden." - echo " Datei: $BAN_HISTORY_FILE" echo "" return fi - # Header zeigen - head -3 "$BAN_HISTORY_FILE" | sed 's/^/ /' + echo " # Format: ZEITSTEMPEL | AKTION | CLIENT-IP | DOMAIN | ANFRAGEN | SPERRDAUER | PROTOKOLL | GRUND" + echo " #──────────────────────────────────────────────────────────────────────────────────────────────────" echo "" - # Letzte N Einträge (ohne Header-Zeilen) - grep -v '^#' "$BAN_HISTORY_FILE" | tail -n "$lines" | sed 's/^/ /' + local recent + recent=$(db_history_get_recent "$lines") + if [[ -n "$recent" ]]; then + while IFS='|' read -r ts action ip domain count duration protocol reason; do + printf " %-19s | %-6s | %-39s | %-30s | %-8s | %-10s | %-10s | %s\n" \ + "$ts" "$action" "$ip" "$domain" "$count" "$duration" "$protocol" "$reason" + done <<< "$recent" + fi echo "" - local total - total=$(grep -vc '^#' "$BAN_HISTORY_FILE" 2>/dev/null || echo "0") - local bans - bans=$(grep -c '| BAN ' "$BAN_HISTORY_FILE" 2>/dev/null || echo "0") - local unbans - unbans=$(grep -c '| UNBAN ' "$BAN_HISTORY_FILE" 2>/dev/null || echo "0") + local bans unbans + bans=$(db_history_count_by_action "BAN") + unbans=$(db_history_count_by_action "UNBAN") echo " Gesamt: $total Einträge ($bans Sperren, $unbans Entsperrungen)" - echo " Datei: $BAN_HISTORY_FILE" + echo " Datenbank: $DB_FILE" echo "" echo "═══════════════════════════════════════════════════════════════" } @@ -1187,12 +1124,14 @@ show_history() { flush_all_bans() { log "INFO" "Alle Sperren werden aufgehoben..." - for state_file in "${STATE_DIR}"/*.ban; do - [[ -f "$state_file" ]] || continue - local client_ip - client_ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2 || true) - unban_client "$client_ip" "manual-flush" - done + local all_ips + all_ips=$(db_query "SELECT client_ip FROM active_bans;") + if [[ -n "$all_ips" ]]; then + while IFS= read -r client_ip; do + [[ -z "$client_ip" ]] && continue + unban_client "$client_ip" "manual-flush" + done <<< "$all_ips" + fi # Chain leeren iptables -F "$IPTABLES_CHAIN" 2>/dev/null || true @@ -1203,15 +1142,8 @@ flush_all_bans() { # ─── Alle Offense-Zähler zurücksetzen ──────────────────────────────────────── flush_all_offenses() { - local count=0 - for offense_file in "${STATE_DIR}"/*.offenses; do - [[ -f "$offense_file" ]] || continue - local o_ip - o_ip=$(grep '^CLIENT_IP=' "$offense_file" | cut -d= -f2 || true) - log "INFO" "Offense-Zähler zurückgesetzt: $o_ip" - rm -f "$offense_file" - count=$((count + 1)) - done + local count + count=$(db_offense_delete_all) log "INFO" "$count Offense-Zähler zurückgesetzt" echo "$count Offense-Zähler zurückgesetzt" } @@ -1630,7 +1562,7 @@ Interne Befehle (nicht direkt verwenden — nur über systemd): Konfiguration: $CONFIG_FILE Log-Datei: $LOG_FILE -Ban-History: $BAN_HISTORY_FILE +Datenbank: $DB_FILE State: $STATE_DIR USAGE diff --git a/db.sh b/db.sh new file mode 100644 index 0000000..6583ea4 --- /dev/null +++ b/db.sh @@ -0,0 +1,641 @@ +#!/bin/bash +############################################################################### +# AdGuard Shield - SQLite Datenbank-Bibliothek +# Zentrale Datenbankfunktionen fuer alle Scripte. +# Wird per "source db.sh" eingebunden. +# +# Autor: Patrick Asmus +# E-Mail: support@techniverse.net +# Lizenz: MIT +############################################################################### + +DB_FILE="${STATE_DIR}/adguard-shield.db" +DB_SCHEMA_VERSION=1 +_DB_MIGRATION_MARKER="${STATE_DIR}/.migration_v1_complete" + +# ─── SQL-Wert escapen (Single Quotes verdoppeln) ──────────────────────────── +_db_escape() { + echo "${1//\'/\'\'}" +} + +# ─── SQL ausfuehren (INSERT/UPDATE/DELETE) ─────────────────────────────────── +db_exec() { + sqlite3 "$DB_FILE" < 0 AND is_permanent = 0 AND ban_until_epoch <= $now;" +} + +db_ban_get_expired_by_source() { + local source=$(_db_escape "$1") + local now + now=$(date '+%s') + db_query "SELECT client_ip FROM active_bans WHERE source='$source' AND ban_until_epoch > 0 AND is_permanent = 0 AND ban_until_epoch <= $now;" +} + +db_ban_get_by_source() { + local source=$(_db_escape "$1") + db_query "SELECT client_ip FROM active_bans WHERE source='$source';" +} + +db_ban_count() { + db_query "SELECT COUNT(*) FROM active_bans;" +} + +db_ban_count_by_source() { + local source=$(_db_escape "$1") + db_query "SELECT COUNT(*) FROM active_bans WHERE source='$source';" +} + +db_ban_get_all() { + db_query "SELECT client_ip, domain, count, ban_time, ban_until_epoch, ban_duration, offense_level, is_permanent, reason, protocol, source, geoip_country, geoip_mode FROM active_bans ORDER BY created_at DESC;" +} + +db_ban_get_by_reason() { + local reason=$(_db_escape "$1") + db_query "SELECT client_ip, domain, count, ban_time, ban_until_epoch, ban_duration, offense_level, is_permanent, reason, protocol, source, geoip_country, geoip_mode FROM active_bans WHERE reason='$reason';" +} + +# ─── Offense-Funktionen ───────────────────────────────────────────────────── + +db_offense_get_level() { + local ip=$(_db_escape "$1") + local reset_after="${2:-86400}" + local now + now=$(date '+%s') + + local row + row=$(db_query "SELECT offense_level, last_offense_epoch FROM offense_tracking WHERE client_ip='$ip' LIMIT 1;") + + if [[ -z "$row" ]]; then + echo "0" + return + fi + + local level last_epoch + IFS='|' read -r level last_epoch <<< "$row" + + if [[ -n "$last_epoch" && $((now - last_epoch)) -gt "$reset_after" ]]; then + db_exec "DELETE FROM offense_tracking WHERE client_ip='$ip';" + echo "0" + return + fi + + echo "${level:-0}" +} + +db_offense_increment() { + local ip=$(_db_escape "$1") + local current_level + current_level=$(db_offense_get_level "$1" "${PROGRESSIVE_BAN_RESET_AFTER:-86400}") + local new_level=$((current_level + 1)) + local now + now=$(date '+%s') + local now_readable + now_readable=$(date '+%Y-%m-%d %H:%M:%S') + + local first_offense + first_offense=$(db_query "SELECT first_offense FROM offense_tracking WHERE client_ip='$ip' LIMIT 1;") + [[ -z "$first_offense" ]] && first_offense="$now_readable" + + db_exec "INSERT OR REPLACE INTO offense_tracking (client_ip, offense_level, last_offense_epoch, last_offense, first_offense, updated_at) VALUES ('$ip', $new_level, $now, '$now_readable', '$first_offense', '$now_readable');" + + echo "$new_level" +} + +db_offense_delete() { + local ip=$(_db_escape "$1") + db_exec "DELETE FROM offense_tracking WHERE client_ip='$ip';" +} + +db_offense_delete_all() { + local count + count=$(db_query "SELECT COUNT(*) FROM offense_tracking;") + db_exec "DELETE FROM offense_tracking;" + echo "${count:-0}" +} + +db_offense_delete_expired() { + local reset_after="${1:-86400}" + local now + now=$(date '+%s') + local cutoff=$((now - reset_after)) + + local expired + expired=$(db_query "SELECT client_ip, offense_level, last_offense_epoch FROM offense_tracking WHERE last_offense_epoch <= $cutoff;") + local count=0 + if [[ -n "$expired" ]]; then + count=$(echo "$expired" | wc -l) + db_exec "DELETE FROM offense_tracking WHERE last_offense_epoch <= $cutoff;" + fi + echo "$count" +} + +db_offense_get_all() { + db_query "SELECT client_ip, offense_level, last_offense_epoch, last_offense, first_offense FROM offense_tracking ORDER BY last_offense_epoch DESC;" +} + +db_offense_count() { + db_query "SELECT COUNT(*) FROM offense_tracking;" +} + +db_offense_count_expired() { + local reset_after="${1:-86400}" + local now + now=$(date '+%s') + local cutoff=$((now - reset_after)) + db_query "SELECT COUNT(*) FROM offense_tracking WHERE last_offense_epoch <= $cutoff;" +} + +# ─── Ban-History-Funktionen ───────────────────────────────────────────────── + +db_history_add() { + local action=$(_db_escape "$1") + local client_ip=$(_db_escape "$2") + local domain=$(_db_escape "${3:--}") + local count=$(_db_escape "${4:--}") + local reason=$(_db_escape "${5:--}") + local duration=$(_db_escape "${6:--}") + local protocol=$(_db_escape "${7:--}") + local now_epoch + now_epoch=$(date '+%s') + local now_text + now_text=$(date '+%Y-%m-%d %H:%M:%S') + + db_exec "INSERT INTO ban_history (timestamp_epoch, timestamp_text, action, client_ip, domain, count, duration, protocol, reason) VALUES ($now_epoch, '$now_text', '$action', '$client_ip', '$domain', '$count', '$duration', '$protocol', '$reason');" +} + +db_history_cleanup() { + local retention_days="${1:-0}" + [[ "$retention_days" == "0" || -z "$retention_days" ]] && return + + local cutoff_epoch + cutoff_epoch=$(date -d "-${retention_days} days" '+%s' 2>/dev/null) + [[ -z "$cutoff_epoch" ]] && return + + local before after removed + before=$(db_query "SELECT COUNT(*) FROM ban_history;") + db_exec "DELETE FROM ban_history WHERE timestamp_epoch < $cutoff_epoch;" + after=$(db_query "SELECT COUNT(*) FROM ban_history;") + removed=$((before - after)) + echo "$removed" +} + +db_history_get_recent() { + local limit="${1:-50}" + db_query "SELECT timestamp_text, action, client_ip, domain, count, duration, protocol, reason FROM ban_history ORDER BY id DESC LIMIT $limit;" +} + +db_history_count() { + db_query "SELECT COUNT(*) FROM ban_history;" +} + +db_history_count_by_action() { + local action=$(_db_escape "$1") + db_query "SELECT COUNT(*) FROM ban_history WHERE action='$action';" +} + +db_history_stats_for_range() { + local start_epoch="$1" + local end_epoch="$2" + + db_query "SELECT + COALESCE(SUM(CASE WHEN action='BAN' THEN 1 ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN action='UNBAN' THEN 1 ELSE 0 END), 0), + COALESCE(COUNT(DISTINCT CASE WHEN action='BAN' THEN client_ip END), 0), + COALESCE(SUM(CASE WHEN action='BAN' AND (duration LIKE '%PERMANENT%' OR duration LIKE '%permanent%') THEN 1 ELSE 0 END), 0) + FROM ban_history + WHERE timestamp_epoch >= $start_epoch AND timestamp_epoch <= $end_epoch;" +} + +db_history_report_stats() { + local start_epoch="$1" + local end_epoch="$2" + local busiest_start="$3" + + db_query "SELECT + COALESCE(SUM(CASE WHEN action='BAN' THEN 1 ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN action='UNBAN' THEN 1 ELSE 0 END), 0), + COALESCE(COUNT(DISTINCT CASE WHEN action='BAN' THEN client_ip END), 0), + COALESCE(SUM(CASE WHEN action='BAN' AND (duration LIKE '%PERMANENT%' OR duration LIKE '%permanent%') THEN 1 ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN action='BAN' AND reason LIKE '%rate%limit%' THEN 1 ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN action='BAN' AND reason LIKE '%subdomain%flood%' THEN 1 ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN action='BAN' AND reason LIKE '%external%blocklist%' THEN 1 ELSE 0 END), 0) + FROM ban_history + WHERE timestamp_epoch >= $start_epoch AND timestamp_epoch <= $end_epoch;" +} + +db_history_busiest_day() { + local start_epoch="$1" + local end_epoch="$2" + + db_query "SELECT substr(timestamp_text, 1, 10), COUNT(*) + FROM ban_history + WHERE action='BAN' AND timestamp_epoch >= $start_epoch AND timestamp_epoch <= $end_epoch + GROUP BY substr(timestamp_text, 1, 10) + ORDER BY COUNT(*) DESC + LIMIT 1;" +} + +db_history_top_ips() { + local start_epoch="$1" + local end_epoch="$2" + local limit="${3:-10}" + + db_query "SELECT COUNT(*), client_ip + FROM ban_history + WHERE action='BAN' AND timestamp_epoch >= $start_epoch AND timestamp_epoch <= $end_epoch + GROUP BY client_ip + ORDER BY COUNT(*) DESC + LIMIT $limit;" +} + +db_history_top_domains() { + local start_epoch="$1" + local end_epoch="$2" + local limit="${3:-10}" + + db_query "SELECT COUNT(*), domain + FROM ban_history + WHERE action='BAN' AND domain != '-' AND domain != '' AND timestamp_epoch >= $start_epoch AND timestamp_epoch <= $end_epoch + GROUP BY domain + ORDER BY COUNT(*) DESC + LIMIT $limit;" +} + +db_history_protocol_stats() { + local start_epoch="$1" + local end_epoch="$2" + + db_query "SELECT COUNT(*), COALESCE(NULLIF(protocol, ''), 'unbekannt') + FROM ban_history + WHERE action='BAN' AND timestamp_epoch >= $start_epoch AND timestamp_epoch <= $end_epoch + GROUP BY COALESCE(NULLIF(protocol, ''), 'unbekannt') + ORDER BY COUNT(*) DESC;" +} + +db_history_recent_bans() { + local start_epoch="$1" + local end_epoch="$2" + local limit="${3:-10}" + + db_query "SELECT timestamp_text, action, client_ip, domain, count, duration, protocol, reason + FROM ban_history + WHERE action='BAN' AND timestamp_epoch >= $start_epoch AND timestamp_epoch <= $end_epoch + ORDER BY id DESC + LIMIT $limit;" +} + +# ─── Whitelist-Funktionen ─────────────────────────────────────────────────── + +db_whitelist_contains() { + local ip=$(_db_escape "$1") + local result + result=$(db_query "SELECT 1 FROM whitelist_cache WHERE ip_address='$ip' LIMIT 1;") + [[ -n "$result" ]] +} + +db_whitelist_sync() { + local source=$(_db_escape "${1:-external}") + local tmp_sql="" + tmp_sql="BEGIN TRANSACTION; DELETE FROM whitelist_cache;" + while IFS= read -r ip; do + [[ -z "$ip" ]] && continue + local safe_ip=$(_db_escape "$ip") + tmp_sql+=" INSERT OR IGNORE INTO whitelist_cache (ip_address, source) VALUES ('$safe_ip', '$source');" + done + tmp_sql+=" COMMIT;" + db_exec "$tmp_sql" +} + +db_whitelist_count() { + db_query "SELECT COUNT(*) FROM whitelist_cache;" +} + +db_whitelist_get_all() { + db_query "SELECT ip_address FROM whitelist_cache;" +} + +db_whitelist_clear() { + db_exec "DELETE FROM whitelist_cache;" +} + +# ─── Migration von Flat-Files ─────────────────────────────────────────────── + +db_migrate_from_files() { + # Bereits migriert? + if [[ -f "$_DB_MIGRATION_MARKER" ]]; then + return 0 + fi + + local migrated=0 + local backup_dir="${STATE_DIR}/.backup_pre_sqlite" + + # ─── .ban-Dateien migrieren ────────────────────────────────────────── + local ban_sql="BEGIN TRANSACTION;" + local ban_count=0 + + for state_file in "${STATE_DIR}"/*.ban "${STATE_DIR}"/ext_*.ban; do + [[ -f "$state_file" ]] || continue + local basename_f + basename_f=$(basename "$state_file") + + local s_ip s_domain s_count s_ban_time s_ban_until_epoch s_ban_duration + local s_offense_level s_is_permanent s_reason s_protocol s_source + local s_geoip_country s_geoip_mode + + s_ip=$(grep '^CLIENT_IP=' "$state_file" 2>/dev/null | cut -d= -f2 || true) + [[ -z "$s_ip" ]] && continue + + s_domain=$(grep '^DOMAIN=' "$state_file" 2>/dev/null | cut -d= -f2 || true) + s_count=$(grep '^COUNT=' "$state_file" 2>/dev/null | cut -d= -f2 || true) + s_ban_time=$(grep '^BAN_TIME=' "$state_file" 2>/dev/null | cut -d= -f2 || true) + s_ban_until_epoch=$(grep '^BAN_UNTIL_EPOCH=' "$state_file" 2>/dev/null | cut -d= -f2 || true) + s_ban_duration=$(grep '^BAN_DURATION=' "$state_file" 2>/dev/null | cut -d= -f2 || true) + s_offense_level=$(grep '^OFFENSE_LEVEL=' "$state_file" 2>/dev/null | cut -d= -f2 || true) + s_is_permanent=$(grep '^IS_PERMANENT=' "$state_file" 2>/dev/null | cut -d= -f2 || true) + s_reason=$(grep '^REASON=' "$state_file" 2>/dev/null | cut -d= -f2 || true) + s_protocol=$(grep '^PROTOCOL=' "$state_file" 2>/dev/null | cut -d= -f2 || true) + s_geoip_country=$(grep '^GEOIP_COUNTRY=' "$state_file" 2>/dev/null | cut -d= -f2 || true) + s_geoip_mode=$(grep '^GEOIP_MODE=' "$state_file" 2>/dev/null | cut -d= -f2 || true) + + # Source bestimmen + if [[ "$basename_f" == ext_* ]]; then + s_source="external-blocklist" + elif [[ "$s_reason" == "geoip" ]]; then + s_source="geoip" + else + s_source="monitor" + fi + + # Boolean zu Integer + local perm_int=0 + [[ "$s_is_permanent" == "true" ]] && perm_int=1 + + s_ip=$(_db_escape "$s_ip") + s_domain=$(_db_escape "${s_domain:--}") + s_ban_time=$(_db_escape "${s_ban_time:-}") + s_reason=$(_db_escape "${s_reason:-rate-limit}") + s_protocol=$(_db_escape "${s_protocol:-DNS}") + s_geoip_country=$(_db_escape "${s_geoip_country:-}") + s_geoip_mode=$(_db_escape "${s_geoip_mode:-}") + + ban_sql+=" INSERT OR IGNORE INTO active_bans (client_ip, domain, count, ban_time, ban_until_epoch, ban_duration, offense_level, is_permanent, reason, protocol, source, geoip_country, geoip_mode) VALUES ('$s_ip', '$s_domain', ${s_count:-0}, '$s_ban_time', ${s_ban_until_epoch:-0}, ${s_ban_duration:-0}, ${s_offense_level:-0}, $perm_int, '$s_reason', '$s_protocol', '$s_source', '$s_geoip_country', '$s_geoip_mode');" + ban_count=$((ban_count + 1)) + done + ban_sql+=" COMMIT;" + + if [[ $ban_count -gt 0 ]]; then + db_exec "$ban_sql" + migrated=$((migrated + ban_count)) + fi + + # ─── .offenses-Dateien migrieren ───────────────────────────────────── + local offense_sql="BEGIN TRANSACTION;" + local offense_count=0 + + for offense_file in "${STATE_DIR}"/*.offenses; do + [[ -f "$offense_file" ]] || continue + + local o_ip o_level o_last_epoch o_last o_first + o_ip=$(grep '^CLIENT_IP=' "$offense_file" 2>/dev/null | cut -d= -f2 || true) + [[ -z "$o_ip" ]] && continue + + o_level=$(grep '^OFFENSE_LEVEL=' "$offense_file" 2>/dev/null | cut -d= -f2 || true) + o_last_epoch=$(grep '^LAST_OFFENSE_EPOCH=' "$offense_file" 2>/dev/null | cut -d= -f2 || true) + o_last=$(grep '^LAST_OFFENSE=' "$offense_file" 2>/dev/null | cut -d= -f2 || true) + o_first=$(grep '^FIRST_OFFENSE=' "$offense_file" 2>/dev/null | cut -d= -f2 || true) + + o_ip=$(_db_escape "$o_ip") + o_last=$(_db_escape "${o_last:-}") + o_first=$(_db_escape "${o_first:-}") + + offense_sql+=" INSERT OR IGNORE INTO offense_tracking (client_ip, offense_level, last_offense_epoch, last_offense, first_offense) VALUES ('$o_ip', ${o_level:-0}, ${o_last_epoch:-0}, '$o_last', '$o_first');" + offense_count=$((offense_count + 1)) + done + offense_sql+=" COMMIT;" + + if [[ $offense_count -gt 0 ]]; then + db_exec "$offense_sql" + migrated=$((migrated + offense_count)) + fi + + # ─── Ban-History-Log migrieren ─────────────────────────────────────── + local history_count=0 + if [[ -f "$BAN_HISTORY_FILE" ]]; then + local history_sql + history_sql=$(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]) + # Single quotes escapen + gsub(/'\''/, "'\'''\''", f[1]) + gsub(/'\''/, "'\'''\''", f[2]) + gsub(/'\''/, "'\'''\''", f[3]) + gsub(/'\''/, "'\'''\''", f[4]) + gsub(/'\''/, "'\'''\''", f[5]) + gsub(/'\''/, "'\'''\''", f[6]) + gsub(/'\''/, "'\'''\''", f[7]) + gsub(/'\''/, "'\'''\''", f[8]) + printf "INSERT INTO ban_history (timestamp_epoch, timestamp_text, action, client_ip, domain, count, duration, protocol, reason) VALUES (%d, '\''%s'\'', '\''%s'\'', '\''%s'\'', '\''%s'\'', '\''%s'\'', '\''%s'\'', '\''%s'\'', '\''%s'\'');\n", \ + ep, f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8] + count++ + } + END { print "-- migrated " count+0 " history entries" } + ' "$BAN_HISTORY_FILE") + + if [[ -n "$history_sql" ]]; then + echo "BEGIN TRANSACTION; $history_sql COMMIT;" | sqlite3 "$DB_FILE" 2>/dev/null + history_count=$(echo "$history_sql" | grep -c '^INSERT' || true) + migrated=$((migrated + history_count)) + fi + fi + + # ─── Whitelist-Cache migrieren ─────────────────────────────────────── + local wl_file="${EXTERNAL_WHITELIST_CACHE_DIR:-/var/lib/adguard-shield/external-whitelist}/resolved_ips.txt" + local wl_count=0 + if [[ -f "$wl_file" ]]; then + local wl_sql="BEGIN TRANSACTION;" + while IFS= read -r ip; do + [[ -z "$ip" ]] && continue + local safe_ip=$(_db_escape "$ip") + wl_sql+=" INSERT OR IGNORE INTO whitelist_cache (ip_address, source) VALUES ('$safe_ip', 'external');" + wl_count=$((wl_count + 1)) + done < "$wl_file" + wl_sql+=" COMMIT;" + + if [[ $wl_count -gt 0 ]]; then + db_exec "$wl_sql" + migrated=$((migrated + wl_count)) + fi + fi + + # ─── Alte Dateien in Backup verschieben ────────────────────────────── + if [[ $migrated -gt 0 ]]; then + mkdir -p "$backup_dir" + + for f in "${STATE_DIR}"/*.ban "${STATE_DIR}"/ext_*.ban; do + [[ -f "$f" ]] || continue + mv "$f" "$backup_dir/" 2>/dev/null || true + done + + for f in "${STATE_DIR}"/*.offenses; do + [[ -f "$f" ]] || continue + mv "$f" "$backup_dir/" 2>/dev/null || true + done + + if [[ -f "$BAN_HISTORY_FILE" ]]; then + cp "$BAN_HISTORY_FILE" "${backup_dir}/adguard-shield-bans.log.bak" 2>/dev/null || true + fi + + if [[ -f "$wl_file" ]]; then + cp "$wl_file" "${backup_dir}/resolved_ips.txt.bak" 2>/dev/null || true + fi + fi + + # Migrations-Marker setzen + echo "migrated_at=$(date '+%Y-%m-%d %H:%M:%S')" > "$_DB_MIGRATION_MARKER" + echo "bans=$ban_count" >> "$_DB_MIGRATION_MARKER" + echo "offenses=$offense_count" >> "$_DB_MIGRATION_MARKER" + echo "history=$history_count" >> "$_DB_MIGRATION_MARKER" + echo "whitelist=$wl_count" >> "$_DB_MIGRATION_MARKER" + + echo "$migrated" +} diff --git a/docs/architektur.md b/docs/architektur.md index ee68854..b3adbae 100644 --- a/docs/architektur.md +++ b/docs/architektur.md @@ -93,48 +93,36 @@ ADGUARD_SHIELD Chain - Kann komplett geflusht werden ohne andere Regeln zu beeinflussen - Einfaches Debugging per `iptables -L ADGUARD_SHIELD` -## State-Management +## State-Management (SQLite) -Jede aktive Sperre wird als Datei gespeichert: +Alle Laufzeitdaten werden in einer zentralen SQLite-Datenbank gespeichert: ``` -/var/lib/adguard-shield/192.168.1.50.ban +/var/lib/adguard-shield/adguard-shield.db ``` -Inhalt: -``` -CLIENT_IP=192.168.1.50 -DOMAIN=microsoft.com -COUNT=45 -BAN_TIME=2026-03-03 14:30:00 -BAN_UNTIL_EPOCH=1741012200 -BAN_UNTIL=2026-03-03 15:30:00 -BAN_DURATION=3600 -OFFENSE_LEVEL=1 -IS_PERMANENT=false -REASON=rate-limit -``` +Die Datenbank enthält folgende Tabellen: -Zusätzlich wird für jede IP ein Offense-Tracker gespeichert: +| Tabelle | Beschreibung | +|---------|--------------| +| `active_bans` | Aktive Sperren (IP, Domain, Sperrdauer, Offense-Level, Grund, Quelle, GeoIP) | +| `offense_tracking` | Offense-Zähler für progressive Sperren (Level, letztes/erstes Vergehen) | +| `ban_history` | Vollständige Ban-History (alle Sperren und Entsperrungen) | +| `whitelist_cache` | Cache der aufgelösten externen Whitelist-IPs | +| `schema_version` | Datenbank-Schema-Version für zukünftige Migrationen | -``` -/var/lib/adguard-shield/192.168.1.50.offenses -``` +**Vorteile gegenüber Flat-Files:** +- Schnellere Abfragen, besonders bei vielen aktiven Sperren +- Atomare Transaktionen — kein Datenverlust bei Stromausfall +- WAL-Modus für parallelen Lese-/Schreibzugriff +- Indexierte Suche nach IP, Zeitstempel, Quelle und Aktion +- Kompakte Speicherung statt tausender Einzeldateien -Inhalt: -``` -CLIENT_IP=192.168.1.50 -OFFENSE_LEVEL=2 -LAST_OFFENSE_EPOCH=1741008600 -LAST_OFFENSE=2026-03-03 14:30:00 -FIRST_OFFENSE=2026-03-03 12:15:00 -``` +Die zentrale Datenbankbibliothek (`db.sh`) wird von allen Scripts per `source db.sh` eingebunden und stellt typisierte Funktionen für alle Tabellen bereit (z.B. `db_ban_insert`, `db_offense_get_level`, `db_history_add`). -Das ermöglicht: -- Persistenz über Script-Neustarts hinweg -- Statusabfragen jederzeit möglich -- Automatisches Aufräumen per Cron-Job -- Progressive Sperrzeiten über mehrere Ban-Zyklen hinweg +### Migration von Flat-Files + +Beim Update auf die SQLite-Version werden bestehende Flat-File-Daten (`.ban`, `.offenses`, Ban-History-Log, Whitelist-Cache) automatisch in die Datenbank migriert. Die alten Dateien werden als Backup nach `/var/lib/adguard-shield/.backup_pre_sqlite/` verschoben. Die Migration läuft einmalig beim Update und zeigt den Fortschritt im Terminal an. ## Dateistruktur nach Installation @@ -149,6 +137,7 @@ Das ermöglicht: ├── external-whitelist-worker.sh # Externer Whitelist-Worker (DNS-Auflösung) ├── geoip-worker.sh # GeoIP-Länderfilter-Worker ├── offense-cleanup-worker.sh # Aufräumen abgelaufener Offense-Zähler (nice 19, idle I/O) +├── db.sh # SQLite Datenbank-Bibliothek (wird von allen Scripts eingebunden) ├── unban-expired.sh # Cron-basiertes Entsperren └── geoip/ # Auto-Download MaxMind GeoLite2 DB (optional) @@ -158,15 +147,16 @@ Das ermöglicht: └── adguard-shield-watchdog.timer # systemd Timer (alle 5 Min.) /var/lib/adguard-shield/ -├── *.ban # State-Dateien aktiver Sperren -├── *.offenses # Offense-Zähler (Progressive Sperren) +├── adguard-shield.db # SQLite-Datenbank (Bans, Offenses, History, Whitelist-Cache) +├── .migration_v1_complete # Marker: Flat-File-Migration abgeschlossen +├── .backup_pre_sqlite/ # Backup der alten Flat-Files nach Migration ├── external-blocklist/ # Cache für externe Blocklisten -├── external-whitelist/ # Cache für externe Whitelisten + aufgelöste IPs +├── external-whitelist/ # Cache für externe Whitelisten └── geoip-cache/ # Cache für GeoIP-Lookups (24h) /var/log/ ├── adguard-shield.log # Laufzeit-Log -└── adguard-shield-bans.log # Ban-History (alle Sperren/Entsperrungen) +└── adguard-shield-bans.log # Ban-History (Legacy, wird nach Migration nicht mehr geschrieben) ``` ## Installer-Architektur @@ -176,7 +166,7 @@ Der Installer (`install.sh`) bietet ein interaktives Menü und folgende Funktion | Befehl | Beschreibung | |--------|--------------| | `install` | Vollständige Neuinstallation (Abhängigkeiten, Dateien, Konfiguration, Service, Watchdog) | -| `update` | Update mit automatischer Konfigurations-Migration, Watchdog-Aktivierung und Service-Neustart | +| `update` | Update mit automatischer Konfigurations-Migration, Datenbank-Migration, Watchdog-Aktivierung und Service-Neustart | | `uninstall` | Deinstallation mit optionalem Behalten der Konfiguration | | `status` | Installationsstatus, Version und Service-Status anzeigen | | `--help` | Hilfe und Befehlsübersicht | @@ -206,15 +196,20 @@ Der Installer (`install.sh`) bietet ein interaktives Menü und folgende Funktion ## Ban-History -Jede Sperre und Entsperrung wird dauerhaft in der Ban-History protokolliert (`/var/log/adguard-shield-bans.log`). Das ermöglicht eine lückenlose Nachvollziehbarkeit, auch nachdem State-Dateien bereits gelöscht wurden. +Jede Sperre und Entsperrung wird dauerhaft in der SQLite-Datenbank protokolliert (Tabelle `ban_history`). Das ermöglicht eine lückenlose Nachvollziehbarkeit mit indexierter Suche nach IP, Zeitstempel und Aktion. -**Format:** -``` -ZEITSTEMPEL | AKTION | CLIENT-IP | DOMAIN | ANFRAGEN | SPERRDAUER | GRUND -2026-03-03 14:30:12 | BAN | 192.168.1.50 | microsoft.com | 45 | 3600s | rate-limit -2026-03-03 15:30:12 | UNBAN | 192.168.1.50 | microsoft.com | - | - | expired -2026-03-03 16:10:33 | UNBAN | 10.0.0.25 | telemetry.example.com | - | - | manual -``` +**Gespeicherte Felder pro Eintrag:** +| Feld | Beschreibung | +|------|--------------| +| `timestamp_epoch` | Unix-Zeitstempel | +| `timestamp_text` | Lesbarer Zeitstempel | +| `action` | `BAN` oder `UNBAN` | +| `client_ip` | Betroffene IP-Adresse | +| `domain` | Angefragte Domain | +| `count` | Anzahl der Anfragen | +| `duration` | Sperrdauer | +| `protocol` | Verwendetes DNS-Protokoll | +| `reason` | Sperrgrund | **Mögliche Gründe (GRUND-Spalte):** | Grund | Bedeutung | diff --git a/docs/befehle.md b/docs/befehle.md index 385cc1b..7c35f49 100644 --- a/docs/befehle.md +++ b/docs/befehle.md @@ -42,9 +42,10 @@ Beim Update passiert automatisch: 2. Die bestehende Konfiguration wird als `adguard-shield.conf.old` gesichert 3. Neue Konfigurationsparameter werden automatisch zur bestehenden Konfig hinzugefügt 4. Bestehende Einstellungen bleiben **immer** erhalten -5. Der systemd Service und Watchdog-Timer werden per `daemon-reload` neu geladen -6. Der Watchdog-Timer wird automatisch aktiviert (falls noch nicht aktiv) -7. Der Service wird automatisch neu gestartet (falls er lief) +5. Bestehende Flat-File-Daten werden einmalig (mit einem Update kommend von einer v0.9.0 oder älter) in die SQLite-Datenbank migriert (mit Fortschrittsanzeige und Backup) +6. Der systemd Service und Watchdog-Timer werden per `daemon-reload` neu geladen +7. Der Watchdog-Timer wird automatisch aktiviert (falls noch nicht aktiv) +8. Der Service wird automatisch neu gestartet (falls er lief) ### API-Verbindungstest nach Installation @@ -66,6 +67,7 @@ Folgende Pakete werden bei der Installation automatisch installiert (via `apt`): - `iptables` — Firewall-Regeln für IP-Sperren - `gawk` — Textverarbeitung - `systemd` — Service-Management +- `sqlite3` — Datenbank für State-Management, Ban-History und Offense-Tracking ## systemd Service diff --git a/docs/konfiguration.md b/docs/konfiguration.md index e28e4cc..5a3dae9 100644 --- a/docs/konfiguration.md +++ b/docs/konfiguration.md @@ -137,7 +137,7 @@ Wiederholungstäter werden wie bei fail2ban stufenweise länger gesperrt. Wird e | `LOG_FILE` | `/var/log/adguard-shield.log` | Pfad zur Log-Datei | | `LOG_LEVEL` | `INFO` | Log-Level: `DEBUG`, `INFO`, `WARN`, `ERROR` | | `LOG_MAX_SIZE_MB` | `50` | Max. Log-Größe bevor rotiert wird | -| `BAN_HISTORY_FILE` | `/var/log/adguard-shield-bans.log` | Datei für die Ban-History (alle Sperren/Entsperrungen) | +| `BAN_HISTORY_FILE` | `/var/log/adguard-shield-bans.log` | Legacy: Pfad zur alten Ban-History-Datei (wird bei der SQLite-Migration als Quelle verwendet). Neue Einträge werden direkt in die SQLite-Datenbank geschrieben. | | `BAN_HISTORY_RETENTION_DAYS` | `0` | Aufbewahrungsdauer der Ban-History in Tagen. `0` = unbegrenzt (niemals löschen). Alte Einträge werden beim nächsten Report automatisch entfernt. | ### Benachrichtigungen @@ -175,7 +175,7 @@ Regelmäßige Statistik-Reports per E-Mail. Voraussetzung ist ein funktionierend | Parameter | Standard | Beschreibung | |-----------|----------|--------------| -| `STATE_DIR` | `/var/lib/adguard-shield` | Verzeichnis für State-Dateien | +| `STATE_DIR` | `/var/lib/adguard-shield` | Verzeichnis für die SQLite-Datenbank (`adguard-shield.db`) und Caches | | `PID_FILE` | `/var/run/adguard-shield.pid` | PID-Datei | | `DRY_RUN` | `false` | Testmodus — nur loggen, nicht sperren | diff --git a/docs/report.md b/docs/report.md index 79e3cc0..502e225 100644 --- a/docs/report.md +++ b/docs/report.md @@ -259,7 +259,7 @@ Die Test-Mail enthält eine Übersicht der aktuellen Konfiguration und bestätig ### Report enthält keine Daten -Der Report basiert auf der Ban-History-Datei (`/var/log/adguard-shield-bans.log`). Wenn keine Sperren im Berichtszeitraum vorhanden sind, zeigt der Report „Keine Daten" an. +Der Report basiert auf der Ban-History in der SQLite-Datenbank (`/var/lib/adguard-shield/adguard-shield.db`). Wenn keine Sperren im Berichtszeitraum vorhanden sind, zeigt der Report „Keine Daten" an. ### Cron-Job wird nicht ausgeführt diff --git a/docs/tipps-und-troubleshooting.md b/docs/tipps-und-troubleshooting.md index 23f0663..9813669 100644 --- a/docs/tipps-und-troubleshooting.md +++ b/docs/tipps-und-troubleshooting.md @@ -149,7 +149,7 @@ Wenn eine IP die maximale Stufe der progressiven Sperren erreicht hat, wird sie ### Sperren überleben Reboot nicht -Das ist normal — iptables-Regeln sind flüchtig. Der **Service** erstellt die Chain beim Start automatisch neu. Aktive Sperren aus dem State-Verzeichnis werden aber nicht automatisch wiederhergestellt. +Das ist normal — iptables-Regeln sind flüchtig. Der **Service** erstellt die Chain beim Start automatisch neu. Aktive Sperren aus der SQLite-Datenbank werden aber nicht automatisch als iptables-Regeln wiederhergestellt. **Optionen:** - `iptables-persistent` installieren (`apt install iptables-persistent`) @@ -240,6 +240,7 @@ sudo bash install.sh update - Konfiguration wird als `adguard-shield.conf.old` gesichert - Neue Konfigurationsparameter werden automatisch zur bestehenden Konfig ergänzt - Bestehende Einstellungen bleiben erhalten +- Bestehende Flat-File-Daten werden einmalig in die SQLite-Datenbank migriert (mit Fortschrittsanzeige) - Service wird per `daemon-reload` neu geladen und automatisch neu gestartet ## Deinstallation @@ -281,5 +282,6 @@ Folgende Pakete werden für den Betrieb benötigt und bei der Installation autom | `iptables` | Firewall-Regeln (IPv4 + IPv6) | | `gawk` | Textverarbeitung in Scripts | | `systemd` | Service-Management und Autostart | +| `sqlite3` | Datenbank für State-Management, Ban-History und Offense-Tracking | Diese werden bei `sudo bash install.sh install` automatisch geprüft und bei Bedarf über den Paketmanager (`apt`, `dnf`, `yum`, `pacman`) nachinstalliert. diff --git a/docs/update.md b/docs/update.md index 0636259..aa84a6a 100644 --- a/docs/update.md +++ b/docs/update.md @@ -31,13 +31,14 @@ sudo bash install.sh update Das Update-Script macht automatisch folgendes: -1. **Abhängigkeiten prüfen** — Fehlende Pakete werden nachinstalliert +1. **Abhängigkeiten prüfen** — Fehlende Pakete (inkl. `sqlite3`) werden nachinstalliert 2. **Scripts aktualisieren** — Alle `.sh`-Dateien werden nach `/opt/adguard-shield/` kopiert 3. **Konfigurations-Migration** — Neue Parameter werden automatisch zur bestehenden Konfiguration hinzugefügt, bestehende Einstellungen bleiben **unverändert** 4. **Backup erstellen** — Die alte Konfiguration wird als `adguard-shield.conf.old` gesichert -5. **Service aktualisieren** — Die systemd Service-Datei und Watchdog-Dateien werden aktualisiert und `daemon-reload` ausgeführt -6. **Watchdog aktivieren** — Der Watchdog-Timer wird automatisch aktiviert (falls noch nicht aktiv) -7. **Service neustarten** — Der Service wird automatisch neu gestartet (falls er vorher lief) +5. **Datenbank-Migration (in der v1.0.0)** — Bestehende Flat-File-Daten (`.ban`, `.offenses`, Ban-History-Log) werden einmalig in die SQLite-Datenbank migriert. Die alten Dateien werden als Backup gesichert. Der Fortschritt und das Ergebnis werden im Terminal angezeigt. +6. **Service aktualisieren** — Die systemd Service-Datei und Watchdog-Dateien werden aktualisiert und `daemon-reload` ausgeführt +7. **Watchdog aktivieren** — Der Watchdog-Timer wird automatisch aktiviert (falls noch nicht aktiv) +8. **Service neustarten** — Der Service wird automatisch neu gestartet (falls er vorher lief) ### 3. Neue Parameter prüfen (optional) diff --git a/external-blocklist-worker.sh b/external-blocklist-worker.sh index 6613a3e..ebc3d53 100644 --- a/external-blocklist-worker.sh +++ b/external-blocklist-worker.sh @@ -23,6 +23,9 @@ fi # shellcheck source=adguard-shield.conf source "$CONFIG_FILE" +# shellcheck source=db.sh +source "${SCRIPT_DIR}/db.sh" + # ─── Worker PID-File ────────────────────────────────────────────────────────── WORKER_PID_FILE="/var/run/adguard-blocklist-worker.pid" @@ -48,21 +51,11 @@ log_ban_history() { local action="$1" local client_ip="$2" local reason="${3:-external-blocklist}" - local timestamp - timestamp="$(date '+%Y-%m-%d %H:%M:%S')" - - if [[ ! -f "$BAN_HISTORY_FILE" ]]; then - echo "# AdGuard Shield - Ban History" > "$BAN_HISTORY_FILE" - echo "# Format: ZEITSTEMPEL | AKTION | CLIENT-IP | DOMAIN | ANFRAGEN | SPERRDAUER | PROTOKOLL | GRUND" >> "$BAN_HISTORY_FILE" - echo "#──────────────────────────────────────────────────────────────────────────────────────────────────" >> "$BAN_HISTORY_FILE" - fi local duration="permanent" [[ "$EXTERNAL_BLOCKLIST_BAN_DURATION" -gt 0 ]] && duration="${EXTERNAL_BLOCKLIST_BAN_DURATION}s" - printf "%-19s | %-6s | %-39s | %-30s | %-8s | %-10s | %-10s | %s\n" \ - "$timestamp" "$action" "$client_ip" "-" "-" "$duration" "-" "$reason" \ - >> "$BAN_HISTORY_FILE" + db_history_add "$action" "$client_ip" "-" "-" "$reason" "$duration" "-" } # ─── Verzeichnisse erstellen ────────────────────────────────────────────────── @@ -70,6 +63,7 @@ init_directories() { mkdir -p "$EXTERNAL_BLOCKLIST_CACHE_DIR" mkdir -p "$STATE_DIR" mkdir -p "$(dirname "$LOG_FILE")" + db_init } # ─── Whitelist Prüfung ─────────────────────────────────────────────────────── @@ -77,15 +71,13 @@ is_whitelisted() { local ip="$1" IFS=',' read -ra wl_entries <<< "$WHITELIST" for entry in "${wl_entries[@]}"; do - entry=$(echo "$entry" | xargs) # trim + entry=$(echo "$entry" | xargs) if [[ "$ip" == "$entry" ]]; then return 0 fi done - # Externe Whitelist prüfen (aufgelöste IPs aus dem Whitelist-Worker) - local ext_wl_file="${EXTERNAL_WHITELIST_CACHE_DIR:-/var/lib/adguard-shield/external-whitelist}/resolved_ips.txt" - if [[ -f "$ext_wl_file" ]] && grep -qxF "$ip" "$ext_wl_file" 2>/dev/null; then + if db_whitelist_contains "$ip"; then return 0 fi @@ -118,11 +110,10 @@ setup_iptables_chain() { # ─── IP sperren ────────────────────────────────────────────────────────────── ban_ip() { local ip="$1" - local state_file="${STATE_DIR}/ext_${ip//[:\/]/_}.ban" # Bereits gesperrt? - if [[ -f "$state_file" ]]; then - # iptables-Regel prüfen und ggf. nachziehen (z.B. nach Neustart verloren gegangen) + if db_ban_exists "$ip"; then + # iptables-Regel pruefen und ggf. nachziehen 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 @@ -132,14 +123,7 @@ ban_ip() { 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 - - # Nicht auch vom Hauptscript gesperrt? (State-Datei ohne ext_ Prefix) - local main_state_file="${STATE_DIR}/${ip//[:\/]/_}.ban" - if [[ -f "$main_state_file" ]]; then - log "DEBUG" "IP $ip bereits vom Rate-Limiter gesperrt - überspringe" + log "DEBUG" "IP $ip bereits gesperrt" return 0 fi @@ -151,36 +135,24 @@ ban_ip() { log "WARN" "SPERRE IP (externe Blocklist): $ip" - # iptables-Regel setzen if [[ "$ip" == *:* ]]; then ip6tables -I "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null || true else iptables -I "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null || true fi - # State speichern local ban_until_epoch="0" - local ban_until_display="permanent" + local is_permanent=1 if [[ "$EXTERNAL_BLOCKLIST_BAN_DURATION" -gt 0 ]]; then ban_until_epoch=$(date -d "+${EXTERNAL_BLOCKLIST_BAN_DURATION} seconds" '+%s' 2>/dev/null \ || date -v "+${EXTERNAL_BLOCKLIST_BAN_DURATION}S" '+%s') - ban_until_display=$(date -d "@$ban_until_epoch" '+%Y-%m-%d %H:%M:%S' 2>/dev/null \ - || date -r "$ban_until_epoch" '+%Y-%m-%d %H:%M:%S') + is_permanent=0 fi - cat > "$state_file" << EOF -CLIENT_IP=$ip -DOMAIN=- -COUNT=- -BAN_TIME=$(date '+%Y-%m-%d %H:%M:%S') -BAN_UNTIL_EPOCH=$ban_until_epoch -BAN_UNTIL=$ban_until_display -SOURCE=external-blocklist -EOF + db_ban_insert "$ip" "-" "0" "$(date '+%Y-%m-%d %H:%M:%S')" "$ban_until_epoch" "${EXTERNAL_BLOCKLIST_BAN_DURATION:-0}" "0" "$is_permanent" "external-blocklist" "-" "external-blocklist" log_ban_history "BAN" "$ip" "external-blocklist" - # Benachrichtigung senden (nur wenn EXTERNAL_BLOCKLIST_NOTIFY=true) if [[ "$NOTIFY_ENABLED" == "true" && "${EXTERNAL_BLOCKLIST_NOTIFY:-false}" == "true" ]]; then send_notification "ban" "$ip" fi @@ -190,9 +162,8 @@ EOF unban_ip() { local ip="$1" local reason="${2:-external-blocklist-removed}" - local state_file="${STATE_DIR}/ext_${ip//[:\/]/_}.ban" - [[ -f "$state_file" ]] || return 0 + db_ban_exists "$ip" || return 0 log "INFO" "ENTSPERRE IP (externe Blocklist entfernt): $ip" @@ -202,7 +173,7 @@ unban_ip() { iptables -D "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null || true fi - rm -f "$state_file" + db_ban_delete "$ip" log_ban_history "UNBAN" "$ip" "$reason" if [[ "$NOTIFY_ENABLED" == "true" && "${EXTERNAL_BLOCKLIST_NOTIFY:-false}" == "true" ]]; then @@ -542,31 +513,21 @@ parse_blocklist_ips() { # ─── Aktuelle externe Sperren ermitteln ────────────────────────────────────── get_currently_banned_external_ips() { - for state_file in "${STATE_DIR}"/ext_*.ban; do - [[ -f "$state_file" ]] || continue - grep '^CLIENT_IP=' "$state_file" | cut -d= -f2 - done + db_ban_get_by_source "external-blocklist" } # ─── Abgelaufene externe Sperren prüfen ───────────────────────────────────── check_expired_external_bans() { [[ "$EXTERNAL_BLOCKLIST_BAN_DURATION" -gt 0 ]] || return - local now - now=$(date '+%s') + local expired_ips + expired_ips=$(db_ban_get_expired_by_source "external-blocklist") + [[ -z "$expired_ips" ]] && return - for state_file in "${STATE_DIR}"/ext_*.ban; do - [[ -f "$state_file" ]] || continue - - local ban_until_epoch - ban_until_epoch=$(grep '^BAN_UNTIL_EPOCH=' "$state_file" | cut -d= -f2) - local client_ip - client_ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2) - - if [[ -n "$ban_until_epoch" && "$ban_until_epoch" -gt 0 && "$now" -ge "$ban_until_epoch" ]]; then - unban_ip "$client_ip" "external-blocklist-expired" - fi - done + while IFS= read -r client_ip; do + [[ -z "$client_ip" ]] && continue + unban_ip "$client_ip" "external-blocklist-expired" + done <<< "$expired_ips" } # ─── Blocklisten synchronisieren ───────────────────────────────────────────── @@ -615,9 +576,8 @@ sync_blocklists() { continue fi - local _state_file_before="${STATE_DIR}/ext_${ip//[:/]/_}.ban" local _was_new=false - [[ ! -f "$_state_file_before" ]] && _was_new=true + db_ban_exists "$ip" || _was_new=true ban_ip "$ip" [[ "$_was_new" == "true" ]] && new_bans=$((new_bans + 1)) @@ -729,12 +689,9 @@ show_status() { echo "" # Aktive externe Sperren - local ext_ban_count=0 - for state_file in "${STATE_DIR}"/ext_*.ban; do - [[ -f "$state_file" ]] || continue - ext_ban_count=$((ext_ban_count + 1)) - done - echo " Aktive Sperren (externe Blocklist): $ext_ban_count" + local ext_ban_count + ext_ban_count=$(db_ban_count_by_source "external-blocklist") + echo " Aktive Sperren (externe Blocklist): ${ext_ban_count:-0}" echo "" echo " Prüfintervall: ${EXTERNAL_BLOCKLIST_INTERVAL}s" @@ -817,11 +774,14 @@ case "${1:-start}" in flush) init_directories echo "Entferne alle externen Blocklist-Sperren..." - for state_file in "${STATE_DIR}"/ext_*.ban; do - [[ -f "$state_file" ]] || continue - _ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2) - unban_ip "$_ip" "manual-flush" - done + local flush_ips + flush_ips=$(db_ban_get_by_source "external-blocklist") + if [[ -n "$flush_ips" ]]; then + while IFS= read -r _ip; do + [[ -z "$_ip" ]] && continue + unban_ip "$_ip" "manual-flush" + done <<< "$flush_ips" + fi echo "Alle externen Blocklist-Sperren aufgehoben" ;; *) diff --git a/external-whitelist-worker.sh b/external-whitelist-worker.sh index 83734bf..101f832 100644 --- a/external-whitelist-worker.sh +++ b/external-whitelist-worker.sh @@ -25,9 +25,11 @@ fi # shellcheck source=adguard-shield.conf source "$CONFIG_FILE" +# shellcheck source=db.sh +source "${SCRIPT_DIR}/db.sh" + # ─── Standardwerte ──────────────────────────────────────────────────────────── EXTERNAL_WHITELIST_CACHE_DIR="${EXTERNAL_WHITELIST_CACHE_DIR:-/var/lib/adguard-shield/external-whitelist}" -EXTERNAL_WHITELIST_RESOLVED_FILE="${EXTERNAL_WHITELIST_CACHE_DIR}/resolved_ips.txt" # ─── Worker PID-File ────────────────────────────────────────────────────────── WORKER_PID_FILE="/var/run/adguard-whitelist-worker.pid" @@ -53,6 +55,7 @@ log() { init_directories() { mkdir -p "$EXTERNAL_WHITELIST_CACHE_DIR" mkdir -p "$(dirname "$LOG_FILE")" + db_init } # ─── Eintrag-Validierung ───────────────────────────────────────────────────── @@ -271,7 +274,7 @@ sync_whitelists() { index=$((index + 1)) done - # Alle Einträge aus Cache-Dateien parsen und IPs auflösen + # Alle Eintraege aus Cache-Dateien parsen und IPs aufloesen local all_ips_file="${EXTERNAL_WHITELIST_CACHE_DIR}/.all_ips.tmp" > "$all_ips_file" @@ -280,56 +283,42 @@ sync_whitelists() { parse_whitelist_entries "$cache_file" >> "$all_ips_file" done - # Duplikate entfernen und in die resolved-Datei schreiben + # Duplikate entfernen und in SQLite-Whitelist schreiben (atomar) + local unique_file="${EXTERNAL_WHITELIST_CACHE_DIR}/.all_ips_unique.tmp" + sort -u "$all_ips_file" > "$unique_file" local unique_count - sort -u "$all_ips_file" > "${EXTERNAL_WHITELIST_RESOLVED_FILE}.tmp" - mv "${EXTERNAL_WHITELIST_RESOLVED_FILE}.tmp" "$EXTERNAL_WHITELIST_RESOLVED_FILE" - unique_count=$(wc -l < "$EXTERNAL_WHITELIST_RESOLVED_FILE" | xargs) + unique_count=$(wc -l < "$unique_file" | xargs) - rm -f "$all_ips_file" + db_whitelist_sync "external" < "$unique_file" + + rm -f "$all_ips_file" "$unique_file" log "DEBUG" "Externe Whitelist: $unique_count eindeutige IPs aufgelöst" - # Prüfe ob gesperrte IPs jetzt auf der Whitelist stehen und entsperrt werden müssen + # Pruefen ob gesperrte IPs jetzt auf der Whitelist stehen check_banned_whitelist_ips } # ─── Gesperrte IPs prüfen die jetzt gewhitelistet sind ────────────────────── -# Wenn eine IP nach einer Whitelist-Aktualisierung nun auf der externen -# Whitelist steht, wird sie automatisch entsperrt. check_banned_whitelist_ips() { - local state_dir="${STATE_DIR:-/var/lib/adguard-shield}" - [[ -d "$state_dir" ]] || return - [[ -f "$EXTERNAL_WHITELIST_RESOLVED_FILE" ]] || return + # Alle gesperrten IPs pruefen, ob sie jetzt auf der Whitelist stehen + local banned_ips + banned_ips=$(db_query "SELECT a.client_ip FROM active_bans a INNER JOIN whitelist_cache w ON a.client_ip = w.ip_address;") + [[ -z "$banned_ips" ]] && return - for state_file in "${state_dir}"/*.ban "${state_dir}"/ext_*.ban; do - [[ -f "$state_file" ]] || continue - local client_ip - client_ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2) + while IFS= read -r client_ip; do [[ -z "$client_ip" ]] && continue + log "INFO" "Gesperrte IP $client_ip ist jetzt auf externer Whitelist – entsperre automatisch" - if grep -qxF "$client_ip" "$EXTERNAL_WHITELIST_RESOLVED_FILE" 2>/dev/null; then - log "INFO" "Gesperrte IP $client_ip ist jetzt auf externer Whitelist – entsperre automatisch" - - # iptables-Regel entfernen - if [[ "$client_ip" == *:* ]]; then - ip6tables -D "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true - else - iptables -D "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true - fi - - rm -f "$state_file" - - # Ban-History Eintrag - if [[ -f "${BAN_HISTORY_FILE:-/var/log/adguard-shield-bans.log}" ]]; then - local timestamp - timestamp="$(date '+%Y-%m-%d %H:%M:%S')" - printf "%-19s | %-6s | %-39s | %-30s | %-8s | %-10s | %-10s | %s\n" \ - "$timestamp" "UNBAN" "$client_ip" "-" "-" "-" "-" "external-whitelist" \ - >> "${BAN_HISTORY_FILE:-/var/log/adguard-shield-bans.log}" - fi + if [[ "$client_ip" == *:* ]]; then + ip6tables -D "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true + else + iptables -D "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true fi - done + + db_ban_delete "$client_ip" + db_history_add "UNBAN" "$client_ip" "-" "-" "external-whitelist" "-" "-" + done <<< "$banned_ips" } # ─── PID-Management ────────────────────────────────────────────────────────── @@ -409,27 +398,29 @@ show_status() { echo "" - # Aufgelöste IPs - if [[ -f "$EXTERNAL_WHITELIST_RESOLVED_FILE" ]]; then - local resolved_count - resolved_count=$(wc -l < "$EXTERNAL_WHITELIST_RESOLVED_FILE" | xargs) - local last_resolved - last_resolved=$(date -r "$EXTERNAL_WHITELIST_RESOLVED_FILE" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "unbekannt") - echo " Aufgelöste IPs: $resolved_count" - echo " Letzte Auflösung: $last_resolved" + # Aufgelöste IPs aus Datenbank + local resolved_count + resolved_count=$(db_whitelist_count) - if [[ "$resolved_count" -gt 0 && "$resolved_count" -le 20 ]]; then + if [[ "${resolved_count:-0}" -gt 0 ]]; then + echo " Aufgelöste IPs: $resolved_count" + + if [[ "$resolved_count" -le 20 ]]; then echo "" echo " Aktuelle IPs:" + local all_wl_ips + all_wl_ips=$(db_whitelist_get_all) while IFS= read -r ip; do echo " ✅ $ip" - done < "$EXTERNAL_WHITELIST_RESOLVED_FILE" - elif [[ "$resolved_count" -gt 20 ]]; then + done <<< "$all_wl_ips" + else echo "" echo " Erste 20 IPs:" - head -20 "$EXTERNAL_WHITELIST_RESOLVED_FILE" | while IFS= read -r ip; do + local first_wl_ips + first_wl_ips=$(db_query "SELECT ip_address FROM whitelist_cache LIMIT 20;") + while IFS= read -r ip; do echo " ✅ $ip" - done + done <<< "$first_wl_ips" echo " ... ($((resolved_count - 20)) weitere)" fi else @@ -508,7 +499,7 @@ case "${1:-start}" in flush) init_directories echo "Entferne aufgelöste externe Whitelist-IPs..." - rm -f "$EXTERNAL_WHITELIST_RESOLVED_FILE" + db_whitelist_clear echo "Externe Whitelist-IPs entfernt" ;; *) diff --git a/geoip-worker.sh b/geoip-worker.sh index e2f63ff..62c92bb 100644 --- a/geoip-worker.sh +++ b/geoip-worker.sh @@ -22,6 +22,9 @@ fi # shellcheck source=adguard-shield.conf source "$CONFIG_FILE" +# shellcheck source=db.sh +source "${SCRIPT_DIR}/db.sh" + # ─── Worker PID-File ────────────────────────────────────────────────────────── WORKER_PID_FILE="/var/run/adguard-geoip-worker.pid" @@ -56,20 +59,8 @@ log_ban_history() { local client_ip="$2" local country="${3:-}" local reason="${4:-geoip}" - local timestamp - timestamp="$(date '+%Y-%m-%d %H:%M:%S')" - if [[ ! -f "$BAN_HISTORY_FILE" ]]; then - echo "# AdGuard Shield - Ban History" > "$BAN_HISTORY_FILE" - echo "# Format: ZEITSTEMPEL | AKTION | CLIENT-IP | DOMAIN | ANFRAGEN | SPERRDAUER | PROTOKOLL | GRUND" >> "$BAN_HISTORY_FILE" - echo "#──────────────────────────────────────────────────────────────────────────────────────────────────" >> "$BAN_HISTORY_FILE" - fi - - local duration="permanent" - - printf "%-19s | %-6s | %-39s | %-30s | %-8s | %-10s | %-10s | %s\n" \ - "$timestamp" "$action" "$client_ip" "Land: ${country:-?}" "-" "$duration" "-" "$reason" \ - >> "$BAN_HISTORY_FILE" + db_history_add "$action" "$client_ip" "Land: ${country:-?}" "-" "$reason" "permanent" "-" } # ─── Verzeichnisse erstellen ────────────────────────────────────────────────── @@ -78,6 +69,7 @@ init_directories() { mkdir -p "$GEOIP_DB_DIR" mkdir -p "$STATE_DIR" mkdir -p "$(dirname "$LOG_FILE")" + db_init } # ─── Private IP-Adressen erkennen ──────────────────────────────────────────── @@ -107,15 +99,13 @@ is_whitelisted() { local ip="$1" IFS=',' read -ra wl_entries <<< "$WHITELIST" for entry in "${wl_entries[@]}"; do - entry=$(echo "$entry" | xargs) # trim + entry=$(echo "$entry" | xargs) if [[ "$ip" == "$entry" ]]; then return 0 fi done - # Externe Whitelist prüfen - local ext_wl_file="${EXTERNAL_WHITELIST_CACHE_DIR:-/var/lib/adguard-shield/external-whitelist}/resolved_ips.txt" - if [[ -f "$ext_wl_file" ]] && grep -qxF "$ip" "$ext_wl_file" 2>/dev/null; then + if db_whitelist_contains "$ip"; then return 0 fi @@ -324,8 +314,7 @@ ban_ip_geoip() { local mode="${GEOIP_MODE:-blocklist}" # Prüfen ob bereits gesperrt - local state_file="${STATE_DIR}/${client_ip//[:\/]/_}.ban" - if [[ -f "$state_file" ]]; then + if db_ban_exists "$client_ip"; then log "DEBUG" "GeoIP: $client_ip ist bereits gesperrt" return 0 fi @@ -350,22 +339,8 @@ ban_ip_geoip() { iptables -I "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true fi - # State-Datei erstellen - cat > "$state_file" << EOF -CLIENT_IP=$client_ip -DOMAIN=GeoIP:${country_code} -COUNT=- -BAN_TIME=$(date '+%Y-%m-%d %H:%M:%S') -BAN_UNTIL_EPOCH=0 -BAN_UNTIL=PERMANENT -BAN_DURATION=0 -OFFENSE_LEVEL=0 -IS_PERMANENT=true -REASON=geoip -PROTOCOL=- -GEOIP_COUNTRY=$country_code -GEOIP_MODE=$mode -EOF + # State in Datenbank speichern + db_ban_insert "$client_ip" "GeoIP:${country_code}" "0" "$(date '+%Y-%m-%d %H:%M:%S')" "0" "0" "0" "1" "geoip" "-" "geoip" "$country_code" "$mode" # Ban-History log_ban_history "BAN" "$client_ip" "$country_code" "$reason_text" @@ -516,47 +491,37 @@ get_active_clients() { # - GeoIP deaktiviert wurde auto_unban_geoip() { local unban_count=0 + local geoip_bans + geoip_bans=$(db_ban_get_by_reason "geoip") + [[ -z "$geoip_bans" ]] && return - for f in "${STATE_DIR}"/*.ban; do - [[ -f "$f" ]] || continue - - local reason - reason=$(grep '^REASON=' "$f" | cut -d= -f2 || true) - [[ "$reason" != "geoip" ]] && continue - - local client_ip country_code old_mode - client_ip=$(grep '^CLIENT_IP=' "$f" | cut -d= -f2 || true) - country_code=$(grep '^GEOIP_COUNTRY=' "$f" | cut -d= -f2 || true) - old_mode=$(grep '^GEOIP_MODE=' "$f" | cut -d= -f2 || true) + while IFS='|' read -r client_ip domain count ban_time ban_until_epoch ban_duration offense_level is_permanent reason protocol source geoip_country geoip_mode; do + [[ -z "$client_ip" ]] && continue local should_unban=false - # GeoIP deaktiviert → alle GeoIP-Sperren aufheben if [[ "${GEOIP_ENABLED:-false}" != "true" ]]; then should_unban=true - # Modus gewechselt → alle GeoIP-Sperren aufheben und neu prüfen - elif [[ -n "$old_mode" && "$old_mode" != "${GEOIP_MODE:-blocklist}" ]]; then + elif [[ -n "$geoip_mode" && "$geoip_mode" != "${GEOIP_MODE:-blocklist}" ]]; then should_unban=true - # Prüfen ob das Land nach aktueller Konfiguration noch gesperrt sein soll - elif [[ -n "$country_code" ]] && ! should_block_by_geoip "$country_code"; then + elif [[ -n "$geoip_country" ]] && ! should_block_by_geoip "$geoip_country"; then should_unban=true fi if [[ "$should_unban" == "true" ]]; then - log "INFO" "GeoIP Auto-Unban: $client_ip (Land: ${country_code:-?}, war: ${old_mode:-?})" + log "INFO" "GeoIP Auto-Unban: $client_ip (Land: ${geoip_country:-?}, war: ${geoip_mode:-?})" - # iptables Regel entfernen if [[ "$client_ip" == *:* ]]; then ip6tables -D "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true else iptables -D "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true fi - rm -f "$f" - log_ban_history "UNBAN" "$client_ip" "$country_code" "geoip-auto-unban" + db_ban_delete "$client_ip" + log_ban_history "UNBAN" "$client_ip" "$geoip_country" "geoip-auto-unban" unban_count=$((unban_count + 1)) fi - done + done <<< "$geoip_bans" if [[ $unban_count -gt 0 ]]; then log "INFO" "GeoIP Auto-Unban: $unban_count Sperren aufgehoben (Länderliste/Modus geändert)" @@ -618,8 +583,7 @@ sync_geoip() { fi # Bereits gesperrt? - local state_file="${STATE_DIR}/${client_ip//[:\/]/_}.ban" - if [[ -f "$state_file" ]]; then + if db_ban_exists "$client_ip"; then skipped=$((skipped + 1)) continue fi @@ -741,21 +705,20 @@ show_status() { echo "" # GeoIP-Sperren anzeigen + local geoip_bans_data + geoip_bans_data=$(db_ban_get_by_reason "geoip") local geoip_bans=0 - if [[ -d "$STATE_DIR" ]]; then - for f in "${STATE_DIR}"/*.ban; do - [[ -f "$f" ]] || continue - local reason - reason=$(grep '^REASON=' "$f" | cut -d= -f2 || true) - if [[ "$reason" == "geoip" ]]; then - geoip_bans=$((geoip_bans + 1)) - local s_ip s_country s_until - s_ip=$(grep '^CLIENT_IP=' "$f" | cut -d= -f2 || true) - s_country=$(grep '^GEOIP_COUNTRY=' "$f" | cut -d= -f2 || true) - s_until=$(grep '^BAN_UNTIL=' "$f" | cut -d= -f2 || true) - echo " 🌍 $s_ip → Land: ${s_country:-?} (bis: ${s_until:-?})" + + if [[ -n "$geoip_bans_data" ]]; then + while IFS='|' read -r s_ip s_domain _ _ s_ban_until_epoch _ _ s_perm_int _ _ _ s_country _; do + [[ -z "$s_ip" ]] && continue + geoip_bans=$((geoip_bans + 1)) + local s_until_display="PERMANENT" + if [[ "$s_ban_until_epoch" != "0" && "$s_perm_int" != "1" ]]; then + s_until_display=$(date -d "@$s_ban_until_epoch" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "?") fi - done + echo " 🌍 $s_ip → Land: ${s_country:-?} (bis: $s_until_display)" + done <<< "$geoip_bans_data" fi if [[ $geoip_bans -eq 0 ]]; then @@ -831,27 +794,24 @@ flush_cache() { # ─── GeoIP-Sperren aufheben ───────────────────────────────────────────────── flush_geoip_bans() { local count=0 - if [[ -d "$STATE_DIR" ]]; then - for f in "${STATE_DIR}"/*.ban; do - [[ -f "$f" ]] || continue - local reason - reason=$(grep '^REASON=' "$f" | cut -d= -f2 || true) - if [[ "$reason" == "geoip" ]]; then - local client_ip - client_ip=$(grep '^CLIENT_IP=' "$f" | cut -d= -f2 || true) + local geoip_ips + geoip_ips=$(db_query "SELECT client_ip FROM active_bans WHERE reason='geoip';") - # iptables Regel entfernen - if [[ "$client_ip" == *:* ]]; then - ip6tables -D "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true - else - iptables -D "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true - fi + if [[ -n "$geoip_ips" ]]; then + while IFS= read -r client_ip; do + [[ -z "$client_ip" ]] && continue - rm -f "$f" - log_ban_history "UNBAN" "$client_ip" "" "geoip-flush" - count=$((count + 1)) + # iptables Regel entfernen + if [[ "$client_ip" == *:* ]]; then + ip6tables -D "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true + else + iptables -D "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true fi - done + + db_ban_delete "$client_ip" + log_ban_history "UNBAN" "$client_ip" "" "geoip-flush" + count=$((count + 1)) + done <<< "$geoip_ips" fi echo "✅ $count GeoIP-Sperren aufgehoben" diff --git a/install.sh b/install.sh index 89049fb..b7c63ba 100644 --- a/install.sh +++ b/install.sh @@ -6,7 +6,7 @@ # Lizenz: MIT ############################################################################### -VERSION="v0.9.0" +VERSION="v1.0.0" set -euo pipefail @@ -65,6 +65,7 @@ print_help() { echo -e " Aktualisiert alle Scripts, führt eine automatische" echo -e " Konfigurations-Migration durch (neue Parameter werden" echo -e " hinzugefügt, bestehende Einstellungen bleiben erhalten)," + echo -e " migriert bestehende Daten nach SQLite (einmalig)" echo -e " und startet den Service automatisch neu." echo "" echo -e " ${GREEN}uninstall${NC} Vollständige Deinstallation" @@ -142,7 +143,7 @@ print_help() { echo " - Linux Server (Debian/Ubuntu empfohlen)" echo " - Root-Zugriff (sudo)" echo " - AdGuard Home installiert und erreichbar" - echo " - Pakete: curl, jq, iptables, gawk (werden bei Installation automatisch installiert)" + echo " - Pakete: curl, jq, iptables, gawk, sqlite3 (werden bei Installation automatisch installiert)" echo " - GeoIP (optional): geoip-bin + geoip-database oder MaxMind GeoLite2 DB" echo "" echo -e "${BOLD}Dokumentation:${NC}" @@ -197,9 +198,10 @@ check_dependencies() { [ip6tables]="iptables" [gawk]="gawk" [systemctl]="systemd" + [sqlite3]="sqlite3" ) - for cmd in curl jq iptables ip6tables gawk systemctl; do + for cmd in curl jq iptables ip6tables gawk systemctl sqlite3; do if command -v "$cmd" &>/dev/null; then echo -e " ✅ $cmd" else @@ -266,6 +268,7 @@ install_files() { cp "$SCRIPT_DIR/uninstall.sh" "$INSTALL_DIR/" cp "$SCRIPT_DIR/geoip-worker.sh" "$INSTALL_DIR/" cp "$SCRIPT_DIR/offense-cleanup-worker.sh" "$INSTALL_DIR/" + cp "$SCRIPT_DIR/db.sh" "$INSTALL_DIR/" # Templates kopieren mkdir -p "$INSTALL_DIR/templates" @@ -283,6 +286,7 @@ install_files() { chmod +x "$INSTALL_DIR/uninstall.sh" chmod +x "$INSTALL_DIR/geoip-worker.sh" chmod +x "$INSTALL_DIR/offense-cleanup-worker.sh" + chmod +x "$INSTALL_DIR/db.sh" echo -e " ✅ Dateien installiert" echo "" @@ -671,6 +675,92 @@ do_install() { print_summary } +# ─── SQLite-Datenbank-Migration ────────────────────────────────────────────── +# Migriert bestehende Flat-File-Daten (*.ban, *.offenses, History-Log) nach SQLite. +# Läuft synchron im Vordergrund mit sichtbarer Fortschrittsanzeige. +migrate_database() { + echo -e "${YELLOW}Prüfe Datenbank-Migration...${NC}" + + # Konfiguration laden für STATE_DIR und BAN_HISTORY_FILE + local conf="$INSTALL_DIR/adguard-shield.conf" + if [[ ! -f "$conf" ]]; then + echo -e " ${RED}Konfiguration nicht gefunden — Migration übersprungen${NC}" + echo "" + return 0 + fi + + # Nur die benötigten Variablen aus der Konfig laden + STATE_DIR=$(grep '^STATE_DIR=' "$conf" | cut -d= -f2 | tr -d '"') + STATE_DIR="${STATE_DIR:-/var/lib/adguard-shield}" + BAN_HISTORY_FILE=$(grep '^BAN_HISTORY_FILE=' "$conf" | cut -d= -f2 | tr -d '"') + BAN_HISTORY_FILE="${BAN_HISTORY_FILE:-/var/log/adguard-shield-bans.log}" + export STATE_DIR BAN_HISTORY_FILE + + # db.sh aus dem Installationsverzeichnis laden + source "$INSTALL_DIR/db.sh" + + # Datenbank initialisieren (Schema anlegen falls nötig) + db_init + + # Prüfen ob Migration bereits durchgeführt wurde + if [[ -f "$_DB_MIGRATION_MARKER" ]]; then + echo -e " ✅ Datenbank ist aktuell — Migration bereits abgeschlossen" + echo "" + return 0 + fi + + # Prüfen ob überhaupt Flat-Files vorhanden sind + local has_files=false + for f in "${STATE_DIR}"/*.ban "${STATE_DIR}"/ext_*.ban "${STATE_DIR}"/*.offenses; do + if [[ -f "$f" ]]; then + has_files=true + break + fi + done + if [[ "$has_files" == "false" && ! -f "$BAN_HISTORY_FILE" ]]; then + # Keine alten Daten vorhanden — Marker setzen und fertig + echo "migrated_at=$(date '+%Y-%m-%d %H:%M:%S')" > "$_DB_MIGRATION_MARKER" + echo "bans=0" >> "$_DB_MIGRATION_MARKER" + echo "offenses=0" >> "$_DB_MIGRATION_MARKER" + echo "history=0" >> "$_DB_MIGRATION_MARKER" + echo "whitelist=0" >> "$_DB_MIGRATION_MARKER" + echo -e " ✅ Keine bestehenden Daten gefunden — Datenbank bereit" + echo "" + return 0 + fi + + echo -e " ${CYAN}Migriere bestehende Daten nach SQLite...${NC}" + echo "" + + local migrated + migrated=$(db_migrate_from_files) + + if [[ "${migrated:-0}" -gt 0 ]]; then + # Details aus dem Marker lesen + local m_bans m_offenses m_history m_whitelist + m_bans=$(grep '^bans=' "$_DB_MIGRATION_MARKER" 2>/dev/null | cut -d= -f2) + m_offenses=$(grep '^offenses=' "$_DB_MIGRATION_MARKER" 2>/dev/null | cut -d= -f2) + m_history=$(grep '^history=' "$_DB_MIGRATION_MARKER" 2>/dev/null | cut -d= -f2) + m_whitelist=$(grep '^whitelist=' "$_DB_MIGRATION_MARKER" 2>/dev/null | cut -d= -f2) + + echo -e " ${GREEN}═══════════════════════════════════════════════════════════${NC}" + echo -e " ${GREEN} SQLite-Migration erfolgreich abgeschlossen!${NC}" + echo -e " ${GREEN}═══════════════════════════════════════════════════════════${NC}" + echo "" + echo -e " Migrierte Einträge gesamt: ${BOLD}${migrated}${NC}" + [[ "${m_bans:-0}" -gt 0 ]] && echo -e " • Aktive Bans: ${m_bans}" + [[ "${m_offenses:-0}" -gt 0 ]] && echo -e " • Offense-Tracking: ${m_offenses}" + [[ "${m_history:-0}" -gt 0 ]] && echo -e " • Ban-History: ${m_history}" + [[ "${m_whitelist:-0}" -gt 0 ]] && echo -e " • Whitelist-Cache: ${m_whitelist}" + echo "" + echo -e " 📦 Backup der alten Dateien: ${STATE_DIR}/.backup_pre_sqlite/" + echo -e " 📂 Neue Datenbank: ${STATE_DIR}/adguard-shield.db" + else + echo -e " ✅ Migration abgeschlossen — keine Daten zum Migrieren" + fi + echo "" +} + # ─── Update ────────────────────────────────────────────────────────────────── do_update() { check_root @@ -691,6 +781,9 @@ do_update() { # Konfigurations-Migration durchführen migrate_config + # SQLite-Datenbank-Migration durchführen + migrate_database + # Service-Datei aktualisieren echo -e "${YELLOW}Aktualisiere systemd Service...${NC}" cp "$SCRIPT_DIR/adguard-shield.service" "$SERVICE_FILE" @@ -816,6 +909,7 @@ do_uninstall() { 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/db.sh" rm -f "$INSTALL_DIR/uninstall.sh" rm -rf "$INSTALL_DIR/templates" rm -rf "$INSTALL_DIR/geoip" diff --git a/offense-cleanup-worker.sh b/offense-cleanup-worker.sh index ff40f9e..d1904cd 100644 --- a/offense-cleanup-worker.sh +++ b/offense-cleanup-worker.sh @@ -24,6 +24,8 @@ if [[ ! -f "$CONFIG_FILE" ]]; then fi # shellcheck source=adguard-shield.conf source "$CONFIG_FILE" +# shellcheck source=db.sh +source "${SCRIPT_DIR}/db.sh" # ─── Niedrigste Priorität setzen (CPU + I/O) ───────────────────────────────── # Stellt sicher, dass der Worker auch bei manuellem Start nie andere Dienste @@ -77,6 +79,7 @@ format_duration() { init_directories() { mkdir -p "${STATE_DIR}" mkdir -p "$(dirname "$LOG_FILE")" + db_init } # ─── Abgelaufene Offense-Zähler aufräumen ──────────────────────────────────── @@ -84,39 +87,23 @@ cleanup_expired_offenses() { local reset_after="${PROGRESSIVE_BAN_RESET_AFTER:-86400}" local now now=$(date '+%s') - local cleaned=0 + local cutoff=$((now - reset_after)) - local batch_count=0 - for offense_file in "${STATE_DIR}"/*.offenses; do - [[ -f "$offense_file" ]] || continue + local expired_rows + expired_rows=$(db_query "SELECT client_ip, offense_level, last_offense_epoch FROM offense_tracking WHERE last_offense_epoch <= $cutoff;") - 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 + if [[ -n "$expired_rows" ]]; then + while IFS='|' read -r client_ip offense_level last_epoch; do + [[ -z "$client_ip" ]] && continue + local elapsed=$((now - last_epoch)) 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 <<< "$expired_rows" + fi - # Alle 10 Dateien kurz pausieren, um I/O-Bursts zu vermeiden - batch_count=$((batch_count + 1)) - if (( batch_count % 10 == 0 )); then - sleep 0.1 - fi - done + local cleaned + cleaned=$(db_offense_delete_expired "$reset_after") - if [[ $cleaned -gt 0 ]]; then + if [[ "$cleaned" -gt 0 ]]; then log "INFO" "Offense-Cleanup: $cleaned abgelaufene Zähler entfernt" else log "DEBUG" "Offense-Cleanup: keine abgelaufenen Zähler gefunden" @@ -179,22 +166,11 @@ show_status() { 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 + local total + total=$(db_offense_count) + local expired + expired=$(db_offense_count_expired "$reset_after") echo "" echo " Offense-Zähler gesamt: $total" diff --git a/report-generator.sh b/report-generator.sh index 5fdb8e2..9f94979 100644 --- a/report-generator.sh +++ b/report-generator.sh @@ -32,6 +32,8 @@ if [[ ! -f "$CONFIG_FILE" ]]; then fi # shellcheck source=adguard-shield.conf source "$CONFIG_FILE" +# shellcheck source=db.sh +source "${SCRIPT_DIR}/db.sh" # ─── Standardwerte ──────────────────────────────────────────────────────────── REPORT_ENABLED="${REPORT_ENABLED:-false}" @@ -179,143 +181,48 @@ get_period_end_epoch() { echo $((today_midnight - 1)) } -# ─── History-Cache (einmaliges Einlesen der Ban-History) ───────────────────── -# Die Datei wird genau einmal mit awk geparst; alle Funktionen lesen danach -# nur noch aus diesem In-Memory-Cache – keine date-Subprozesse pro Zeile mehr. -# -# Cache-Format pro Zeile (Pipe-separiert, alle Felder getrimmt): -# EPOCH|TIMESTAMP|ACTION|IP|DOMAIN|COUNT|DURATION|PROTOCOL|REASON -HISTORY_CACHE="" -HISTORY_CACHE_LOADED=false - -_load_history_cache() { - [[ "$HISTORY_CACHE_LOADED" == "true" ]] && return - HISTORY_CACHE_LOADED=true - [[ ! -f "$BAN_HISTORY_FILE" ]] && return - HISTORY_CACHE=$(awk ' - /^#/ || /^[[:space:]]*$/ { next } - { - n = split($0, f, "|") - if (n < 2) next - ts = f[1]; gsub(/^[[:space:]]+|[[:space:]]+$/, "", ts) - if (length(ts) < 19) next - ep = mktime(substr(ts,1,4) " " substr(ts,6,2) " " substr(ts,9,2) " " \ - substr(ts,12,2) " " substr(ts,15,2) " " substr(ts,18,2)) - if (ep < 0) next - for (i = 1; i <= n; i++) gsub(/^[[:space:]]+|[[:space:]]+$/, "", f[i]) - print ep "|" f[1] "|" f[2] "|" f[3] "|" f[4] "|" f[5] "|" f[6] "|" f[7] "|" f[8] - } - ' "$BAN_HISTORY_FILE") -} - -# ─── Ban-History filtern nach Zeitraum ──────────────────────────────────────── -# Gibt nur Zeilen zurück, deren Zeitstempel im Berichtszeitraum liegen. -# Liest intern aus dem Cache – keine erneuten date-Subprozesse. -filter_history_by_period() { - local start_epoch="$1" - local end_epoch="$2" - - [[ ! -f "$BAN_HISTORY_FILE" ]] && return - _load_history_cache - [[ -z "$HISTORY_CACHE" ]] && return - - # 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). -# 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 - local cutoff_epoch - cutoff_epoch=$(date -d "-${BAN_HISTORY_RETENTION_DAYS} days" '+%s' 2>/dev/null) - [[ -z "$cutoff_epoch" ]] && return - - local tmp_file="${BAN_HISTORY_FILE}.tmp" - local lines_before lines_after - lines_before=$(wc -l < "$BAN_HISTORY_FILE") - - 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" - - 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 + local removed + removed=$(db_history_cleanup "$BAN_HISTORY_RETENTION_DAYS") + if [[ "${removed:-0}" -gt 0 ]]; then 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 + local result + result=$(db_history_stats_for_range "$start_epoch" "$end_epoch") + if [[ -z "$result" ]]; 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) - } - ' + echo "$result" } # ─── 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 + # Datenbank initialisieren + db_init + local start_epoch start_epoch=$(get_period_start_epoch) local end_epoch end_epoch=$(get_period_end_epoch) - _load_history_cache + local total_history + total_history=$(db_history_count) - # Wenn keine History-Datei vorhanden, Standardwerte setzen - if [[ -z "$HISTORY_CACHE" ]]; then + if [[ "${total_history:-0}" -eq 0 ]]; then TOTAL_BANS=0 TOTAL_UNBANS=0 UNIQUE_IPS=0 @@ -334,8 +241,12 @@ calculate_stats() { return fi - # Einen einzigen awk-Pass über den Cache: alle Statistiken auf einmal - # Busiest-Day-Bereich berechnen (konfigurierbar, Standard: 30 Tage) + # Haupt-Statistiken per SQL + local stats_row + stats_row=$(db_history_report_stats "$start_epoch" "$end_epoch") + IFS='|' read -r TOTAL_BANS TOTAL_UNBANS UNIQUE_IPS PERMANENT_BANS RATELIMIT_BANS SUBDOMAIN_FLOOD_BANS EXTERNAL_BLOCKLIST_BANS <<< "$stats_row" + + # Busiest-Day-Bereich berechnen local busiest_start_epoch if [[ "$REPORT_BUSIEST_DAY_RANGE" == "0" || -z "$REPORT_BUSIEST_DAY_RANGE" ]]; then busiest_start_epoch="$start_epoch" @@ -345,75 +256,11 @@ calculate_stats() { busiest_start_epoch=$((today_midnight - REPORT_BUSIEST_DAY_RANGE * 86400)) fi - local awk_result - awk_result=$(echo "$HISTORY_CACHE" | awk -F'|' -v s="$start_epoch" -v e="$end_epoch" -v bs="$busiest_start_epoch" ' - $1 >= s && $1 <= e { - action = $3 - if (action == "BAN") { - bans++ - ip_count[$4]++ - ip_seen[$4] = 1 - dom = $5 - if (dom != "" && dom != "-") dom_count[dom]++ - proto = $8 - if (proto == "" || proto == "-") proto = "unbekannt" - proto_count[proto]++ - if (tolower($7) ~ /permanent/) perm++ - rsn = tolower($9) - if (rsn ~ /rate.limit/) rl++ - if (rsn ~ /subdomain.flood/) sf++ - if (rsn ~ /external.blocklist/) eb++ - # Zirkulärer Puffer für die letzten 10 Sperren - recent[bans % 10] = $2 "|" $3 "|" $4 "|" $5 "|" $6 "|" $7 "|" $8 "|" $9 - } else if (action == "UNBAN") { - unbans++ - } - } - # Aktivster Tag: separater Zeitraum (konfigurierbar, z.B. letzte 30 Tage) - $1 >= bs && $1 <= e && $3 == "BAN" { - bday = substr($2, 1, 10) - bday_count[bday]++ - } - END { - for (ip in ip_seen) unique++ - busiest = ""; max_d = 0 - for (d in bday_count) { - if (bday_count[d] > max_d) { max_d = bday_count[d]; busiest = d; busiest_cnt = bday_count[d] } - } - print "BANS=" (bans+0) - print "UNBANS=" (unbans+0) - print "UNIQUE=" (unique+0) - print "PERM=" (perm+0) - print "RL=" (rl+0) - print "SF=" (sf+0) - print "EB=" (eb+0) - print "BUSIEST=" busiest - print "BUSIEST_CNT=" (busiest_cnt+0) - for (ip in ip_count) print "IP\t" ip_count[ip] "\t" ip - for (d in dom_count) print "DOMAIN\t" dom_count[d] "\t" d - for (p in proto_count) print "PROTO\t" proto_count[p] "\t" p - n = (bans < 10) ? bans : 10 - for (i = 0; i < n; i++) { - idx = (bans - i) % 10 - print "RECENT\t" recent[idx] - } - } - ') - - # 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}') - - local busiest_raw - busiest_raw=$(echo "$awk_result" | awk -F= '$1=="BUSIEST" {print $2; exit}') - local busiest_cnt - busiest_cnt=$(echo "$awk_result" | awk -F= '$1=="BUSIEST_CNT" {print $2; exit}') - if [[ -n "$busiest_raw" ]]; then + local busiest_row + busiest_row=$(db_history_busiest_day "$busiest_start_epoch" "$end_epoch") + if [[ -n "$busiest_row" ]]; then + local busiest_raw busiest_cnt + IFS='|' read -r busiest_raw busiest_cnt <<< "$busiest_row" local busiest_formatted busiest_formatted=$(date -d "$busiest_raw" '+%d.%m.%Y' 2>/dev/null || echo "$busiest_raw") BUSIEST_DAY="${busiest_formatted} (${busiest_cnt})" @@ -421,28 +268,22 @@ calculate_stats() { BUSIEST_DAY="–" fi - # Dynamisches Label für den aktivsten Tag if [[ "$REPORT_BUSIEST_DAY_RANGE" == "0" || -z "$REPORT_BUSIEST_DAY_RANGE" ]]; then BUSIEST_DAY_LABEL="Aktivster Tag" else BUSIEST_DAY_LABEL="Aktivster Tag (${REPORT_BUSIEST_DAY_RANGE} Tage)" fi - # Top-Listen: Tab-getrennte Felder sortieren und in das erwartete Format bringen - TOP10_IPS=$( echo "$awk_result" | awk -F'\t' '$1=="IP" {print $2 " " $3}' | sort -rn | head -10) - TOP10_DOMAINS=$(echo "$awk_result" | awk -F'\t' '$1=="DOMAIN" {print $2 " " $3}' | sort -rn | head -10) - PROTOCOL_STATS=$(echo "$awk_result" | awk -F'\t' '$1=="PROTO" {print $2 " " $3}' | sort -rn) - RECENT_BANS=$( echo "$awk_result" | awk -F'\t' '$1=="RECENT" {print $2}') + # Top-Listen per SQL (Ausgabe: "count|value" → umformatieren zu "count value") + TOP10_IPS=$(db_history_top_ips "$start_epoch" "$end_epoch" 10 | sed 's/|/ /') + TOP10_DOMAINS=$(db_history_top_domains "$start_epoch" "$end_epoch" 10 | sed 's/|/ /') + PROTOCOL_STATS=$(db_history_protocol_stats "$start_epoch" "$end_epoch" | sed 's/|/ /') + RECENT_BANS=$(db_history_recent_bans "$start_epoch" "$end_epoch" 10) - # Aktuell aktive Sperren (aus State-Dateien) - ACTIVE_BANS=0 - if [[ -d "$STATE_DIR" ]]; then - for f in "${STATE_DIR}"/*.ban; do - [[ -f "$f" ]] && ACTIVE_BANS=$((ACTIVE_BANS + 1)) - done - fi + # Aktuell aktive Sperren aus der Datenbank + ACTIVE_BANS=$(db_ban_count) - # AbuseIPDB Reports – zeitraum-gefiltert aus der Logdatei via awk+mktime + # AbuseIPDB Reports – zeitraum-gefiltert aus der Logdatei ABUSEIPDB_REPORTS=0 if [[ -f "$LOG_FILE" ]]; then ABUSEIPDB_REPORTS=$(grep "AbuseIPDB:.*erfolgreich gemeldet" "$LOG_FILE" 2>/dev/null | \ @@ -1081,12 +922,12 @@ send_test_email() { errors=$((errors + 1)) fi - # 5. Ban-History prüfen - echo -n " 5) Ban-History ... " - if [[ -f "$BAN_HISTORY_FILE" ]]; then - local lines - lines=$(grep -vc '^#' "$BAN_HISTORY_FILE" 2>/dev/null || echo "0") - echo "✅ vorhanden ($lines Einträge)" + # 5. Datenbank prüfen + echo -n " 5) Datenbank ... " + if [[ -f "$DB_FILE" ]]; then + local entries + entries=$(db_history_count 2>/dev/null || echo "0") + echo "✅ vorhanden ($entries History-Einträge)" else echo "⚠️ nicht vorhanden (Report wird leer sein – das ist OK für einen Test)" fi diff --git a/unban-expired.sh b/unban-expired.sh index a45184a..0b6bda9 100644 --- a/unban-expired.sh +++ b/unban-expired.sh @@ -17,52 +17,29 @@ if [[ ! -f "$CONFIG_FILE" ]]; then exit 1 fi source "$CONFIG_FILE" +# shellcheck source=db.sh +source "${SCRIPT_DIR}/db.sh" -BAN_HISTORY_FILE="${BAN_HISTORY_FILE:-/var/log/adguard-shield-bans.log}" LOG_PREFIX="[$(date '+%Y-%m-%d %H:%M:%S')] [UNBAN-TIMER]" -NOW=$(date '+%s') -# History-Eintrag schreiben -log_ban_history() { - local action="$1" - local client_ip="$2" - local domain="${3:-}" - local count="${4:-}" - local reason="${5:-}" - local protocol="${6:-}" - local timestamp - timestamp="$(date '+%Y-%m-%d %H:%M:%S')" - - if [[ ! -f "$BAN_HISTORY_FILE" ]]; then - echo "# AdGuard Shield - Ban History" > "$BAN_HISTORY_FILE" - echo "# Format: ZEITSTEMPEL | AKTION | CLIENT-IP | DOMAIN | ANFRAGEN | SPERRDAUER | PROTOKOLL | GRUND" >> "$BAN_HISTORY_FILE" - echo "#────────────────────────────────────────────────────────────────────────────────────────────────" >> "$BAN_HISTORY_FILE" - fi - - [[ -z "$protocol" ]] && protocol="-" - - printf "%-19s | %-6s | %-39s | %-30s | %-8s | %-10s | %-10s | %s\n" \ - "$timestamp" "$action" "$client_ip" "${domain:--}" "${count:--}" "-" "$protocol" "${reason:-expired}" \ - >> "$BAN_HISTORY_FILE" -} +# Datenbank initialisieren +mkdir -p "${STATE_DIR}" +db_init unban_count=0 -for state_file in "${STATE_DIR}"/*.ban; do - [[ -f "$state_file" ]] || continue +# Abgelaufene Sperren aus der Datenbank abfragen +expired_ips=$(db_ban_get_expired) - ban_until_epoch=$(grep '^BAN_UNTIL_EPOCH=' "$state_file" | cut -d= -f2) - client_ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2) - domain=$(grep '^DOMAIN=' "$state_file" | cut -d= -f2) - is_permanent=$(grep '^IS_PERMANENT=' "$state_file" | cut -d= -f2) - protocol=$(grep '^PROTOCOL=' "$state_file" | cut -d= -f2) +if [[ -n "$expired_ips" ]]; then + while IFS= read -r client_ip; do + [[ -z "$client_ip" ]] && continue - # Permanente Sperren nicht automatisch aufheben - if [[ "$is_permanent" == "true" || "$ban_until_epoch" == "0" ]]; then - continue - fi + # Domain und Protokoll für History-Eintrag holen + local_ban_data=$(db_ban_get "$client_ip") + domain=$(echo "$local_ban_data" | cut -d'|' -f2) + protocol=$(echo "$local_ban_data" | cut -d'|' -f10) - if [[ -n "$ban_until_epoch" && "$NOW" -ge "$ban_until_epoch" ]]; then echo "$LOG_PREFIX Entsperre abgelaufene Sperre: $client_ip" >> "$LOG_FILE" # iptables Regel entfernen @@ -73,12 +50,12 @@ for state_file in "${STATE_DIR}"/*.ban; do fi # Ban-History Eintrag - log_ban_history "UNBAN" "$client_ip" "$domain" "-" "expired-cron" "${protocol:-}" + db_history_add "UNBAN" "$client_ip" "${domain:--}" "-" "expired-cron" "-" "${protocol:-}" - rm -f "$state_file" + db_ban_delete "$client_ip" unban_count=$((unban_count + 1)) - fi -done + done <<< "$expired_ips" +fi if [[ $unban_count -gt 0 ]]; then echo "$LOG_PREFIX $unban_count Sperren aufgehoben" >> "$LOG_FILE" diff --git a/uninstall.sh b/uninstall.sh index 751891e..ba964f7 100644 --- a/uninstall.sh +++ b/uninstall.sh @@ -127,6 +127,7 @@ do_uninstall() { rm -f "$INSTALL_DIR/report-generator.sh" rm -f "$INSTALL_DIR/adguard-shield-watchdog.sh" rm -f "$INSTALL_DIR/geoip-worker.sh" + rm -f "$INSTALL_DIR/db.sh" rm -f "$INSTALL_DIR/uninstall.sh" rm -rf "$INSTALL_DIR/templates" rm -rf "$INSTALL_DIR/geoip"