19 Commits
v0.6.2 ... main

Author SHA1 Message Date
c97e327f0d Merge pull request 'fix: Offense-Cleanup-Worker mit niedrigster CPU/IO-Priorität ausführen' (#16) from v0.8.1 into main
Reviewed-on: #16
2026-04-16 20:23:48 +00:00
c2d6f872f5 Release: Version v0.8.1 2026-04-16 22:22:13 +02:00
633331748f fix: Offense-Cleanup-Worker mit niedrigster CPU/IO-Priorität ausführen 2026-04-16 21:33:47 +02:00
df8b18ae08 Merge pull request 'v0.8.0' (#15) from v0.8.0 into main
Reviewed-on: #15
2026-04-14 19:30:31 +00:00
a79586de94 Merge tag 'v0.8.0' of https://git.techniverse.net/scriptos/adguard-shield into v0.8.0 2026-04-14 21:24:44 +02:00
b42f458d5a Merge pull request 'v0.8.0' (#14) from v0.8.0 into main
Reviewed-on: #14
2026-04-14 19:17:15 +00:00
83075f2782 Release: Version v0.8.0 2026-04-14 21:06:52 +02:00
2a1d8ae975 feat: Offense-Cleanup-Worker für automatisches Aufräumen abgelaufener Offense-Zähler 2026-04-14 21:01:51 +02:00
0da5d01641 update: Konfigurationsdatei aufräumen – Kommentare gekürzt, Verweis auf Doku ergänzt, fehlende Variable EXTERNAL_WHITELIST_CACHE_DIR hinzugefügt 2026-04-14 20:44:06 +02:00
535be66b55 feat: GeoIP-Länderfilter mit MaxMind Auto-Download 2026-04-14 20:30:37 +02:00
2e78b9c14e Merge pull request 'v0.7.1' (#13) from v0.7.1 into main
Reviewed-on: #13
2026-04-12 12:37:09 +00:00
0af79e7a28 Release: Version v0.7.1 2026-04-12 14:35:47 +02:00
606a28ed8e docs: README für docs-Verzeichnis hinzugefügt 2026-04-12 14:28:37 +02:00
77a5ebb144 docs: Projektstruktur entfernt 2026-04-12 14:19:08 +02:00
01a99489ab Hier soll in wenigen Worten drin stehen, was geändert wurde, hinzugefügt wurde oder entfernt wurde. Du kannst Formate wie “fix”, “feat”, “update” und co verwenden. 2026-04-12 14:17:59 +02:00
2200e80f87 Merge pull request 'v0.7.0' (#12) from v0.7.0 into main
Reviewed-on: #12
2026-04-04 18:02:27 +00:00
6bdeb5bc31 Release: Version v0.7.0 2026-04-04 19:59:46 +02:00
5451c01603 Externe Whitelist mit DNS-Auflösung für dynamische IPs (DynDNS) 2026-04-04 19:57:07 +02:00
6daaf67f7c Merge pull request 'v0.6.2' (#11) from v0.6.2 into main
Reviewed-on: #11
2026-03-24 10:31:03 +00:00
20 changed files with 2659 additions and 208 deletions

View File

@@ -23,18 +23,21 @@ Wenn ein Client eine bestimmte Domain zu oft anfragt (z.B. >30x pro Minute), wir
- Automatische Erkennung und Sperre bei Rate-Limit-Verstößen
- **Subdomain-Flood-Erkennung** — erkennt Random-Subdomain-Attacken (z.B. `abc123.microsoft.com`, `xyz456.microsoft.com`, ...)
- **Progressive Sperren (Recidive)** — Wiederholungstäter werden stufenweise länger gesperrt (wie bei fail2ban)
- **Progressive Sperren (Recidive)** — Wiederholungstäter werden stufenweise länger gesperrt (wie bei fail2ban), mit automatischem Cleanup abgelaufener Zähler
- Unterstützt **alle DNS-Protokolle**: DNS (53), DoH (443), DoT (853), DoQ (784/853/8853)
- **IPv4 + IPv6**
- Eigene iptables Chain — greift nicht in bestehende Regeln ein
- Automatisches Entsperren nach konfigurierbarer Dauer
- **Externe Blocklisten** — IP-Adressen von externen Textdateien (URLs) laden und automatisch sperren
- **Externe Whitelisten** — Domains/IPs aus externen Listen laden und automatisch whitelisten (ideal für DynDNS)
- **GeoIP-Länderfilter** — Länder sperren oder erlauben (Blocklist/Allowlist), mit automatischem MaxMind-DB-Download
- **AbuseIPDB Reporting** — permanent gesperrte IPs automatisch an AbuseIPDB melden
- **E-Mail Reports** — periodische Statistik-Reports als HTML oder TXT (täglich, wöchentlich, zweiwöchentlich, monatlich)
- **Ban-History** — lückenlose Protokollierung aller Sperren/Entsperrungen mit Zeitstempel
- Whitelist für vertrauenswürdige IPs
- Dry-Run Modus zum gefahrlosen Testen
- Benachrichtigungen (Discord, Slack, Gotify, Ntfy)
- **Watchdog** — automatischer Health Check alle 5 Minuten mit Recovery und Benachrichtigung bei Service-Ausfall
- systemd Service für dauerhaften Betrieb
## Voraussetzungen
@@ -90,35 +93,19 @@ sudo /opt/adguard-shield/adguard-shield.sh reset-offenses # Offense-Zähler
sudo /opt/adguard-shield/adguard-shield.sh test # API-Verbindung testen
sudo /opt/adguard-shield/adguard-shield.sh blocklist-status # Externe Blocklisten Status
sudo /opt/adguard-shield/adguard-shield.sh blocklist-sync # Blocklisten manuell synchronisieren
sudo /opt/adguard-shield/adguard-shield.sh whitelist-status # Externe Whitelisten Status
sudo /opt/adguard-shield/adguard-shield.sh whitelist-sync # Whitelisten manuell synchronisieren
sudo /opt/adguard-shield/adguard-shield.sh geoip-status # GeoIP-Status anzeigen
sudo /opt/adguard-shield/adguard-shield.sh geoip-sync # GeoIP einmalig prüfen
sudo /opt/adguard-shield/adguard-shield.sh geoip-lookup IP # GeoIP-Lookup einer IP
sudo /opt/adguard-shield/report-generator.sh send # Report jetzt senden
sudo /opt/adguard-shield/report-generator.sh status # Report-Status anzeigen
sudo /opt/adguard-shield/report-generator.sh install # Cron-Job einrichten
sudo journalctl -u adguard-shield -f # Logs live verfolgen
```
## Projektstruktur
```
├── adguard-shield.sh # Haupt-Monitor-Script
├── adguard-shield.conf # Konfiguration
├── adguard-shield.service # systemd Unit
├── external-blocklist-worker.sh # Externer Blocklist-Worker
├── iptables-helper.sh # Manuelle iptables-Verwaltung
├── unban-expired.sh # Cron-basiertes Entsperren
├── report-generator.sh # E-Mail Report Generator
├── install.sh # Installer / Updater
├── uninstall.sh # Uninstaller (wird ins Installationsverzeichnis kopiert)
├── templates/
│ ├── report.html # HTML-Report-Template
│ └── report.txt # TXT-Report-Template
├── README.md
└── docs/
├── architektur.md # Architektur & Funktionsweise
├── konfiguration.md # Alle Parameter erklärt + Konfig-Migration
├── befehle.md # Vollständige Befehlsreferenz inkl. Installer
├── benachrichtigungen.md # Webhook-Setup (Discord, Slack, Gotify, Ntfy)
├── report.md # E-Mail Report Setup & Konfiguration
└── tipps-und-troubleshooting.md
# Watchdog (automatischer Health Check)
sudo systemctl status adguard-shield-watchdog.timer # Watchdog-Status
sudo systemctl list-timers adguard-shield-watchdog.timer # Nächste Ausführung
```
## Dokumentation
@@ -130,7 +117,7 @@ sudo journalctl -u adguard-shield -f # Logs live ver
| [Befehle](docs/befehle.md) | Vollständige Befehlsreferenz für Installer, Monitor, iptables-Helper und systemd |
| [Benachrichtigungen](docs/benachrichtigungen.md) | Setup für Discord, Slack, Gotify, Ntfy |
| [E-Mail Report](docs/report.md) | Periodische Statistik-Reports per E-Mail (HTML/TXT) |
| [Tipps & Troubleshooting](docs/tipps-und-troubleshooting.md) | Best Practices, häufige Probleme, Deinstallation |
| [Tipps & Troubleshooting](docs/tipps-und-troubleshooting.md) | Best Practices, häufige Probleme, Watchdog, Deinstallation |
## Lizenz

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@
# Lizenz: MIT
###############################################################################
VERSION="v0.6.2"
VERSION="v0.8.1"
set -euo pipefail
@@ -329,6 +329,9 @@ cleanup() {
sleep 1
fi
stop_blocklist_worker
stop_whitelist_worker
stop_geoip_worker
stop_offense_cleanup_worker
rm -f "$PID_FILE"
exit 0
}
@@ -356,6 +359,13 @@ is_whitelisted() {
return 0
fi
done
# Externe Whitelist prüfen (aufgelöste IPs aus dem Whitelist-Worker)
local ext_wl_file="${EXTERNAL_WHITELIST_CACHE_DIR:-/var/lib/adguard-shield/external-whitelist}/resolved_ips.txt"
if [[ -f "$ext_wl_file" ]] && grep -qxF "$ip" "$ext_wl_file" 2>/dev/null; then
return 0
fi
return 1
}
@@ -979,6 +989,17 @@ show_status() {
echo ""
fi
# GeoIP-Filter Info
if [[ "${GEOIP_ENABLED:-false}" == "true" ]]; then
local geoip_mode_label
[[ "${GEOIP_MODE:-blocklist}" == "blocklist" ]] && geoip_mode_label="Blocklist" || geoip_mode_label="Allowlist"
echo " 🌍 GeoIP-Filter: AKTIV"
echo " Modus: ${geoip_mode_label}"
echo " Länder: ${GEOIP_COUNTRIES:-<keine>}"
echo " Sperrdauer: PERMANENT (Auto-Unban bei Änderung der Länderliste)"
echo ""
fi
# Aktive Sperren
local ban_count=0
if [[ -d "$STATE_DIR" ]]; then
@@ -1171,6 +1192,105 @@ stop_blocklist_worker() {
fi
}
# ─── Externer Whitelist-Worker starten ───────────────────────────────────────
start_whitelist_worker() {
if [[ "${EXTERNAL_WHITELIST_ENABLED:-false}" != "true" ]]; then
log "DEBUG" "Externer Whitelist-Worker ist deaktiviert"
return
fi
local worker_script="${SCRIPT_DIR}/external-whitelist-worker.sh"
if [[ ! -f "$worker_script" ]]; then
log "WARN" "Whitelist-Worker Script nicht gefunden: $worker_script"
return
fi
log "INFO" "Starte externen Whitelist-Worker im Hintergrund..."
bash "$worker_script" start &
WHITELIST_WORKER_PID=$!
log "INFO" "Whitelist-Worker gestartet (PID: $WHITELIST_WORKER_PID)"
}
# ─── Externer Whitelist-Worker stoppen ───────────────────────────────────────
stop_whitelist_worker() {
local worker_pid_file="/var/run/adguard-whitelist-worker.pid"
if [[ -f "$worker_pid_file" ]]; then
local wpid
wpid=$(cat "$worker_pid_file")
if kill -0 "$wpid" 2>/dev/null; then
log "INFO" "Stoppe Whitelist-Worker (PID: $wpid)..."
kill "$wpid" 2>/dev/null || true
rm -f "$worker_pid_file"
fi
fi
}
# ─── GeoIP-Worker starten ────────────────────────────────────────────────────
start_geoip_worker() {
if [[ "${GEOIP_ENABLED:-false}" != "true" ]]; then
log "DEBUG" "GeoIP-Worker ist deaktiviert"
return
fi
local worker_script="${SCRIPT_DIR}/geoip-worker.sh"
if [[ ! -f "$worker_script" ]]; then
log "WARN" "GeoIP-Worker Script nicht gefunden: $worker_script"
return
fi
log "INFO" "Starte GeoIP-Worker im Hintergrund..."
bash "$worker_script" start &
GEOIP_WORKER_PID=$!
log "INFO" "GeoIP-Worker gestartet (PID: $GEOIP_WORKER_PID)"
}
# ─── GeoIP-Worker stoppen ────────────────────────────────────────────────────
stop_geoip_worker() {
local worker_pid_file="/var/run/adguard-geoip-worker.pid"
if [[ -f "$worker_pid_file" ]]; then
local wpid
wpid=$(cat "$worker_pid_file")
if kill -0 "$wpid" 2>/dev/null; then
log "INFO" "Stoppe GeoIP-Worker (PID: $wpid)..."
kill "$wpid" 2>/dev/null || true
rm -f "$worker_pid_file"
fi
fi
}
# ─── Offense-Cleanup-Worker starten ──────────────────────────────────────────
start_offense_cleanup_worker() {
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" != "true" ]]; then
log "DEBUG" "Offense-Cleanup-Worker ist deaktiviert (Progressive Sperren inaktiv)"
return
fi
local worker_script="${SCRIPT_DIR}/offense-cleanup-worker.sh"
if [[ ! -f "$worker_script" ]]; then
log "WARN" "Offense-Cleanup-Worker Script nicht gefunden: $worker_script"
return
fi
log "INFO" "Starte Offense-Cleanup-Worker im Hintergrund (nice 19, idle I/O)..."
nice -n 19 ionice -c 3 bash "$worker_script" start &
OFFENSE_CLEANUP_WORKER_PID=$!
log "INFO" "Offense-Cleanup-Worker gestartet (PID: $OFFENSE_CLEANUP_WORKER_PID)"
}
# ─── Offense-Cleanup-Worker stoppen ──────────────────────────────────────────
stop_offense_cleanup_worker() {
local worker_pid_file="/var/run/adguard-offense-cleanup-worker.pid"
if [[ -f "$worker_pid_file" ]]; then
local wpid
wpid=$(cat "$worker_pid_file")
if kill -0 "$wpid" 2>/dev/null; then
log "INFO" "Stoppe Offense-Cleanup-Worker (PID: $wpid)..."
kill "$wpid" 2>/dev/null || true
rm -f "$worker_pid_file"
fi
fi
}
# ─── Hauptschleife ──────────────────────────────────────────────────────────
main_loop() {
log "INFO" "═══════════════════════════════════════════════════════════"
@@ -1181,6 +1301,7 @@ main_loop() {
log "INFO" " Dry-Run: ${DRY_RUN}"
log "INFO" " Whitelist: ${WHITELIST}"
log "INFO" " Externe Blocklist: ${EXTERNAL_BLOCKLIST_ENABLED:-false}"
log "INFO" " Externe Whitelist: ${EXTERNAL_WHITELIST_ENABLED:-false}"
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" ]]; then
log "INFO" " Progressive Sperren: AKTIV (×${PROGRESSIVE_BAN_MULTIPLIER:-2}, Max-Stufe: ${PROGRESSIVE_BAN_MAX_LEVEL:-0}, Reset: $(format_duration "${PROGRESSIVE_BAN_RESET_AFTER:-86400}"))"
else
@@ -1196,6 +1317,14 @@ main_loop() {
else
log "INFO" " AbuseIPDB Reporting: deaktiviert"
fi
if [[ "${GEOIP_ENABLED:-false}" == "true" ]]; then
log "INFO" " GeoIP-Filter: AKTIV (Modus: ${GEOIP_MODE:-blocklist}, Länder: ${GEOIP_COUNTRIES:-<keine>})"
else
log "INFO" " GeoIP-Filter: deaktiviert"
fi
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" ]]; then
log "INFO" " Offense-Cleanup: AKTIV (Reset: $(format_duration "${PROGRESSIVE_BAN_RESET_AFTER:-86400}"), Prüfintervall: 1h)"
fi
log "INFO" "═══════════════════════════════════════════════════════════"
# Service-Start-Benachrichtigung senden
@@ -1206,6 +1335,15 @@ main_loop() {
# Blocklist-Worker als Hintergrundprozess starten
start_blocklist_worker
# Whitelist-Worker als Hintergrundprozess starten
start_whitelist_worker
# GeoIP-Worker als Hintergrundprozess starten
start_geoip_worker
# Offense-Cleanup-Worker als Hintergrundprozess starten
start_offense_cleanup_worker
while true; do
# Abgelaufene Sperren prüfen
check_expired_bans
@@ -1272,6 +1410,74 @@ case "${1:-start}" in
echo "Blocklist-Worker nicht gefunden"
fi
;;
whitelist-status)
init_directories
_worker_script="${SCRIPT_DIR}/external-whitelist-worker.sh"
if [[ -f "$_worker_script" ]]; then
bash "$_worker_script" status
else
echo "Whitelist-Worker nicht gefunden"
fi
;;
whitelist-sync)
init_directories
_worker_script="${SCRIPT_DIR}/external-whitelist-worker.sh"
if [[ -f "$_worker_script" ]]; then
bash "$_worker_script" sync
else
echo "Whitelist-Worker nicht gefunden"
fi
;;
whitelist-flush)
init_directories
_worker_script="${SCRIPT_DIR}/external-whitelist-worker.sh"
if [[ -f "$_worker_script" ]]; then
bash "$_worker_script" flush
else
echo "Whitelist-Worker nicht gefunden"
fi
;;
geoip-status)
init_directories
_worker_script="${SCRIPT_DIR}/geoip-worker.sh"
if [[ -f "$_worker_script" ]]; then
bash "$_worker_script" status
else
echo "GeoIP-Worker nicht gefunden"
fi
;;
geoip-sync)
init_directories
setup_iptables_chain
_worker_script="${SCRIPT_DIR}/geoip-worker.sh"
if [[ -f "$_worker_script" ]]; then
bash "$_worker_script" sync
else
echo "GeoIP-Worker nicht gefunden"
fi
;;
geoip-flush)
init_directories
_worker_script="${SCRIPT_DIR}/geoip-worker.sh"
if [[ -f "$_worker_script" ]]; then
bash "$_worker_script" flush
else
echo "GeoIP-Worker nicht gefunden"
fi
;;
geoip-lookup)
if [[ -z "${2:-}" ]]; then
echo "Nutzung: $0 geoip-lookup <IP-Adresse>" >&2
exit 1
fi
init_directories
_worker_script="${SCRIPT_DIR}/geoip-worker.sh"
if [[ -f "$_worker_script" ]]; then
bash "$_worker_script" lookup "$2"
else
echo "GeoIP-Worker nicht gefunden"
fi
;;
status)
init_directories
show_status
@@ -1335,7 +1541,7 @@ Service-Steuerung (empfohlen):
sudo systemctl restart adguard-shield
sudo systemctl status adguard-shield
Nutzung: $0 {status|history|flush|unban|reset-offenses|test|dry-run|blocklist-status|blocklist-sync|blocklist-flush}
Nutzung: $0 {status|history|flush|unban|reset-offenses|test|dry-run|blocklist-status|blocklist-sync|blocklist-flush|whitelist-status|whitelist-sync|whitelist-flush|geoip-status|geoip-sync|geoip-flush|geoip-lookup}
Verwaltungsbefehle:
status Zeigt aktive Sperren, Regeln und Wiederholungstäter
@@ -1348,6 +1554,13 @@ Verwaltungsbefehle:
blocklist-status Zeigt Status der externen Blocklisten
blocklist-sync Einmalige Synchronisation der externen Blocklisten
blocklist-flush Entfernt alle Sperren der externen Blocklisten
whitelist-status Zeigt Status der externen Whitelisten
whitelist-sync Einmalige Synchronisation der externen Whitelisten
whitelist-flush Entfernt alle aufgelösten Whitelist-IPs
geoip-status Zeigt Status der GeoIP-Länderfilter
geoip-sync Einmalige GeoIP-Prüfung aller aktiven Clients
geoip-flush Alle GeoIP-Sperren aufheben
geoip-lookup IP GeoIP-Lookup für eine einzelne IP-Adresse
Interne Befehle (nicht direkt verwenden — nur über systemd):
start Startet den Monitor im Vordergrund

15
docs/README.md Normal file
View File

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

View File

@@ -132,17 +132,26 @@ Das ermöglicht:
├── adguard-shield.sh # Haupt-Monitor-Script
├── adguard-shield.conf # Konfiguration (chmod 600)
├── adguard-shield.conf.old # Backup der Konfig nach Update
├── adguard-shield-watchdog.sh # Watchdog Health-Check-Script
├── iptables-helper.sh # iptables Verwaltung
├── external-blocklist-worker.sh # Externer Blocklist-Worker
── unban-expired.sh # Cron-basiertes Entsperren
── external-whitelist-worker.sh # Externer Whitelist-Worker (DNS-Auflösung)
├── geoip-worker.sh # GeoIP-Länderfilter-Worker
├── offense-cleanup-worker.sh # Aufräumen abgelaufener Offense-Zähler (nice 19, idle I/O)
├── unban-expired.sh # Cron-basiertes Entsperren
└── geoip/ # Auto-Download MaxMind GeoLite2 DB (optional)
/etc/systemd/system/
── adguard-shield.service # systemd Service (Autostart aktiv)
── adguard-shield.service # systemd Service (Autostart aktiv)
├── adguard-shield-watchdog.service # systemd Watchdog-Unit (oneshot)
└── adguard-shield-watchdog.timer # systemd Timer (alle 5 Min.)
/var/lib/adguard-shield/
├── *.ban # State-Dateien aktiver Sperren
├── *.offenses # Offense-Zähler (Progressive Sperren)
── external-blocklist/ # Cache für externe Blocklisten
── external-blocklist/ # Cache für externe Blocklisten
├── external-whitelist/ # Cache für externe Whitelisten + aufgelöste IPs
└── geoip-cache/ # Cache für GeoIP-Lookups (24h)
/var/log/
├── adguard-shield.log # Laufzeit-Log
@@ -155,8 +164,8 @@ Der Installer (`install.sh`) bietet ein interaktives Menü und folgende Funktion
| Befehl | Beschreibung |
|--------|--------------|
| `install` | Vollständige Neuinstallation (Abhängigkeiten, Dateien, Konfiguration, Service) |
| `update` | Update mit automatischer Konfigurations-Migration und Service-Neustart |
| `install` | Vollständige Neuinstallation (Abhängigkeiten, Dateien, Konfiguration, Service, Watchdog) |
| `update` | Update mit automatischer Konfigurations-Migration, Watchdog-Aktivierung und Service-Neustart |
| `uninstall` | Deinstallation mit optionalem Behalten der Konfiguration |
| `status` | Installationsstatus, Version und Service-Status anzeigen |
| `--help` | Hilfe und Befehlsübersicht |

View File

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

View File

@@ -116,12 +116,30 @@ Bei Sperren aus der **externen Blocklist** werden Benachrichtigungen separat üb
### Service gestartet
**Überschrift:** ✅ AdGuard Shield
> 🟢 AdGuard Shield v0.6.2 wurde auf dns1 gestartet.
> 🟢 AdGuard Shield v0.8.1 wurde auf dns1 gestartet.
### Service gestoppt
**Überschrift:** 🚨 🛡️ AdGuard Shield
> 🔴 AdGuard Shield v0.6.2 wurde auf dns1 gestoppt.
> 🔴 AdGuard Shield v0.8.1 wurde auf dns1 gestoppt.
### Watchdog — Service wiederhergestellt
**Überschrift:** 🔄 AdGuard Shield Watchdog
> 🔄 AdGuard Shield Watchdog auf dns1
> ---
> Der Service war ausgefallen und wurde automatisch neu gestartet.
> Versuch: 1
### Watchdog — Recovery fehlgeschlagen
**Überschrift:** 🚨 AdGuard Shield Watchdog
> 🚨 AdGuard Shield Watchdog auf dns1
> ---
> Der Service konnte NICHT automatisch neu gestartet werden!
> Manuelles Eingreifen erforderlich.
> Fehlversuche: 1
> Letzter Fehler: (systemd Statusausgabe)
### Sperre (Ban)
**Überschrift:** 🚨 🛡️ AdGuard Shield

View File

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

View File

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

View File

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

View File

@@ -82,6 +82,13 @@ is_whitelisted() {
return 0
fi
done
# Externe Whitelist prüfen (aufgelöste IPs aus dem Whitelist-Worker)
local ext_wl_file="${EXTERNAL_WHITELIST_CACHE_DIR:-/var/lib/adguard-shield/external-whitelist}/resolved_ips.txt"
if [[ -f "$ext_wl_file" ]] && grep -qxF "$ip" "$ext_wl_file" 2>/dev/null; then
return 0
fi
return 1
}

View File

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

932
geoip-worker.sh Normal file
View File

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

View File

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

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

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

View File

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