diff --git a/LICENSE b/LICENSE index fd0c639..5ed467d 100644 --- a/LICENSE +++ b/LICENSE @@ -29,5 +29,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index df7ff0a..beb2573 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,131 @@ -# template_repository +``` + ▄▄▄ ▓█████▄ ▄████ █ ██ ▄▄▄ ██▀███ ▓█████▄ ██████ ██░ ██ ██▓▓█████ ██▓ ▓█████▄ +▒████▄ ▒██▀ ██▌ ██▒ ▀█▒ ██ ▓██▒▒████▄ ▓██ ▒ ██▒▒██▀ ██▌ ▒██ ▒ ▓██░ ██▒▓██▒▓█ ▀ ▓██▒ ▒██▀ ██▌ +▒██ ▀█▄ ░██ █▌▒██░▄▄▄░▓██ ▒██░▒██ ▀█▄ ▓██ ░▄█ ▒░██ █▌ ░ ▓██▄ ▒██▀▀██░▒██▒▒███ ▒██░ ░██ █▌ +░██▄▄▄▄██ ░▓█▄ ▌░▓█ ██▓▓▓█ ░██░░██▄▄▄▄██ ▒██▀▀█▄ ░▓█▄ ▌ ▒ ██▒░▓█ ░██ ░██░▒▓█ ▄ ▒██░ ░▓█▄ ▌ + ▓█ ▓██▒░▒████▓ ░▒▓███▀▒▒▒█████▓ ▓█ ▓██▒░██▓ ▒██▒░▒████▓ ▒██████▒▒░▓█▒░██▓░██░░▒████▒░██████▒░▒████▓ + ▒▒ ▓▒█░ ▒▒▓ ▒ ░▒ ▒ ░▒▓▒ ▒ ▒ ▒▒ ▓▒█░░ ▒▓ ░▒▓░ ▒▒▓ ▒ ▒ ▒▓▒ ▒ ░ ▒ ░░▒░▒░▓ ░░ ▒░ ░░ ▒░▓ ░ ▒▒▓ ▒ + ▒ ▒▒ ░ ░ ▒ ▒ ░ ░ ░░▒░ ░ ░ ▒ ▒▒ ░ ░▒ ░ ▒░ ░ ▒ ▒ ░ ░▒ ░ ░ ▒ ░▒░ ░ ▒ ░ ░ ░ ░░ ░ ▒ ░ ░ ▒ ▒ + ░ ▒ ░ ░ ░ ░ ░ ░ ░░░ ░ ░ ░ ▒ ░░ ░ ░ ░ ░ ░ ░ ░ ░ ░░ ░ ▒ ░ ░ ░ ░ ░ ░ ░ + ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ + ░ ░ ░ +``` +# AdGuard Shield +> **Autor:** Patrick Asmus | **E-Mail:** support@techniverse.net | **Version:** 1.0.0 +Automatischer Schutz für deinen AdGuard Home DNS-Server gegen übermäßige Anfragen einzelner Clients. Überwacht die AdGuard Home API, erkennt Rate-Limit-Verstöße und sperrt missbrauchende Clients per iptables — für alle DNS-Protokolle (DNS, DoH, DoT, DoQ). -Wichtig: Link für Lizenz anpassen. +## Was macht das Tool? +Wenn ein Client eine bestimmte Domain zu oft anfragt (z.B. >30x pro Minute), wird er automatisch auf Firewall-Ebene für alle DNS-Ports gesperrt. Nach einer konfigurierbaren Zeitspanne wird die Sperre automatisch aufgehoben. +## Features + +- Automatische Erkennung und Sperre bei Rate-Limit-Verstößen +- 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 +- **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) +- systemd Service für dauerhaften Betrieb + +## Voraussetzungen + +- Linux Server mit AdGuard Home (bare metal) +- `curl`, `jq`, `iptables` / `ip6tables` +- Root-Zugriff +- AdGuard Home Web-API erreichbar (Standard: Port 3000) + +## Schnellstart + +```bash +# 1. Repository klonen +git clone /tmp/adguard-security +cd /tmp/adguard-security + +# 2. Installer ausführen (fragt interaktiv nach Zugangsdaten & Einstellungen) +sudo bash install.sh install + +# 3. Erst im Dry-Run testen (loggt nur, sperrt nichts) +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh dry-run + +# 4. Wenn alles passt — Service starten +sudo systemctl start adguard-ratelimit +sudo systemctl status adguard-ratelimit +``` + +## Wichtigste Befehle + +```bash +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh status # Aktive Sperren anzeigen +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh history # Ban-History anzeigen +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh unban IP # Einzelne IP entsperren +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh flush # Alle Sperren aufheben +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh test # API-Verbindung testen +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh blocklist-status # Externe Blocklisten Status +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh blocklist-sync # Blocklisten manuell synchronisieren +sudo journalctl -u adguard-ratelimit -f # Logs live verfolgen +``` + +## Projektstruktur + +``` +├── adguard-ratelimit.sh # Haupt-Monitor-Script +├── adguard-ratelimit.conf # Konfiguration +├── adguard-ratelimit.service # systemd Unit +├── external-blocklist-worker.sh # Externer Blocklist-Worker +├── iptables-helper.sh # Manuelle iptables-Verwaltung +├── unban-expired.sh # Cron-basiertes Entsperren +├── install.sh # Installer / Uninstaller +├── README.md +└── doc/ + ├── architektur.md # Architektur & Funktionsweise + ├── konfiguration.md # Alle Parameter erklärt + ├── befehle.md # Vollständige Befehlsreferenz + ├── benachrichtigungen.md # Webhook-Setup (Discord, Slack, Gotify) + └── tipps-und-troubleshooting.md +``` + +## Dokumentation + +| Dokument | Inhalt | +|----------|--------| +| [Architektur](doc/architektur.md) | Wie das Tool funktioniert, iptables-Strategie, Ablauf einer Sperre | +| [Konfiguration](doc/konfiguration.md) | Alle Parameter, Ports, Whitelist-Pflege, externe Blocklisten | +| [Befehle](doc/befehle.md) | Vollständige Befehlsreferenz für Monitor, iptables-Helper und systemd | +| [Benachrichtigungen](doc/benachrichtigungen.md) | Setup für Discord, Slack, Gotify, Ntfy | +| [Tipps & Troubleshooting](doc/tipps-und-troubleshooting.md) | Best Practices, häufige Probleme, Deinstallation | + +## Lizenz + +[MIT](LICENSE) + +--- + +## 👥 Techniverse Community + +Lust auf Austausch rund um Matrix, Selfhosting und andere smarte IT-Lösungen? +In der **Techniverse Community** triffst du Gleichgesinnte, kannst Fragen stellen oder einfach nerdigen Talk genießen. 🚀 + +👉 **[Jetzt der Gruppe auf Matrix beitreten](https://matrix.to/#/#community:techniverse.net)** +~ Direkte Raumadresse: `#community:techniverse.net` + +👉 **[Für lockere Gespräche abseits der Kernthemen komm in den Talkraum](https://matrix.to/#/#talk:techniverse.net)** +~ Direkte Raumadresse: `#talk:techniverse.net` + +Wir freuen uns, wenn du dabei bist! + +--- + +📝 **Blog:** [www.cleveradmin.de](https://www.cleveradmin.de) +🌐 **Webseite:** [www.patrick-asmus.de](https://www.patrick-asmus.de) +📧 **E-Mail:** [support@techniverse.net](mailto:support@techniverse.net)

@@ -13,4 +133,4 @@ Wichtig: Link für Lizenz anpassen.

License License | Matrix Matrix | Mastodon Mastodon -

\ No newline at end of file +

diff --git a/adguard-ratelimit.conf b/adguard-ratelimit.conf new file mode 100644 index 0000000..ed59308 --- /dev/null +++ b/adguard-ratelimit.conf @@ -0,0 +1,114 @@ +############################################################################### +# AdGuard Shield - Konfigurationsdatei +# Schutz vor übermäßigen DNS-Anfragen einzelner Clients +############################################################################### + +# --- AdGuard Home API Einstellungen --- +# URL der AdGuard Home Web-Oberfläche (ohne trailing slash) +ADGUARD_URL="http://127.0.0.1:3000" + +# 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 + +# Zeitfenster in Sekunden (60 = 1 Minute) +RATE_LIMIT_WINDOW=60 + +# Wie oft das Script die Logs prüft (in Sekunden) +CHECK_INTERVAL=10 + +# --- Sperr-Einstellungen --- +# Wie lange ein Client gesperrt wird (in Sekunden, 3600 = 1 Stunde) +BAN_DURATION=3600 + +# iptables Chain-Name für die Sperren +IPTABLES_CHAIN="ADGUARD_RATELIMIT" + +# Welche Ports gesperrt werden sollen (DNS, DoT, DoH, DNSv5/QUIC) +# Port 53 = DNS (UDP + TCP) +# Port 443 = DNS-over-HTTPS (DoH) +# Port 853 = DNS-over-TLS (DoT) / DNS-over-QUIC +# Port 784 = DNS-over-QUIC (alternativ) +# Port 8853 = DNS-over-QUIC (alternativ) +BLOCKED_PORTS="53 443 853 784 8853" + +# --- Whitelist --- +# IP-Adressen die NIEMALS gesperrt werden (kommagetrennt) +# Lokale Netze und wichtige Server hier eintragen +WHITELIST="127.0.0.1,::1" + +# --- Logging --- +# Log-Datei Pfad +LOG_FILE="/var/log/adguard-ratelimit.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) +BAN_HISTORY_FILE="/var/log/adguard-ratelimit-bans.log" + +# --- Benachrichtigungen (optional) --- +# Aktiviert Benachrichtigungen bei Sperren +NOTIFY_ENABLED=false + +# Webhook-URL für Benachrichtigungen (z.B. Discord, Slack, Gotify) +# Discord: https://discord.com/api/webhooks/xxx/yyy +# Gotify: https://gotify.example.com/message?token=xxx +NOTIFY_WEBHOOK_URL="" + +# Benachrichtigungs-Typ: "discord", "slack", "gotify", "ntfy", "generic" +NOTIFY_TYPE="generic" + +# --- Ntfy Einstellungen (nur bei NOTIFY_TYPE="ntfy") --- +# Server-URL der Ntfy-Instanz (ohne trailing slash) +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="" + +# Priorität der Ntfy-Nachrichten (1=min, 3=default, 5=max) +NTFY_PRIORITY="1" + +# --- Externe Blocklist (optional) --- +# Aktiviert den externen Blocklist-Worker +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_AUTO_UNBAN=true + +# Lokaler Cache-Pfad für die heruntergeladene Blocklist +EXTERNAL_BLOCKLIST_CACHE_DIR="/var/lib/adguard-ratelimit/external-blocklist" + +# --- Erweiterte Einstellungen --- +# Pfad zur State-Datei (speichert aktive Sperren) +STATE_DIR="/var/lib/adguard-ratelimit" + +# Pfad zum PID-File +PID_FILE="/var/run/adguard-ratelimit.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 diff --git a/adguard-ratelimit.service b/adguard-ratelimit.service new file mode 100644 index 0000000..0a5e7bb --- /dev/null +++ b/adguard-ratelimit.service @@ -0,0 +1,36 @@ +[Unit] +Description=AdGuard Shield - DNS Rate-Limit Monitor +Documentation=https://github.com/your-repo/adguard-security +After=network.target AdGuardHome.service +Wants=AdGuardHome.service +StartLimitBurst=5 +StartLimitIntervalSec=60 + +[Service] +Type=simple +ExecStart=/opt/adguard-ratelimit/adguard-ratelimit.sh start +ExecStop=/opt/adguard-ratelimit/adguard-ratelimit.sh stop +ExecReload=/bin/kill -HUP $MAINPID + +# Neustart-Verhalten +Restart=on-failure +RestartSec=10 + +# Sicherheits-Hardening +ProtectSystem=full +ReadWritePaths=/var/log /var/lib/adguard-ratelimit /var/lib/adguard-ratelimit/external-blocklist /var/run +ProtectHome=true +NoNewPrivileges=false +PrivateTmp=true + +# iptables benötigt CAP_NET_ADMIN +AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW +CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW + +# Logging +StandardOutput=journal +StandardError=journal +SyslogIdentifier=adguard-ratelimit + +[Install] +WantedBy=multi-user.target diff --git a/adguard-ratelimit.sh b/adguard-ratelimit.sh new file mode 100644 index 0000000..654470d --- /dev/null +++ b/adguard-ratelimit.sh @@ -0,0 +1,711 @@ +#!/bin/bash +############################################################################### +# AdGuard Shield +# Überwacht DNS-Anfragen und sperrt Clients bei Überschreitung des Limits +# +# Autor: Patrick Asmus +# E-Mail: support@techniverse.net +# Datum: 2026-03-03 +# Lizenz: MIT +############################################################################### + +VERSION="1.0.0" + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_FILE="${SCRIPT_DIR}/adguard-ratelimit.conf" + +# ─── Konfiguration laden ─────────────────────────────────────────────────────── +if [[ ! -f "$CONFIG_FILE" ]]; then + echo "FEHLER: Konfigurationsdatei nicht gefunden: $CONFIG_FILE" >&2 + exit 1 +fi +# shellcheck source=adguard-ratelimit.conf +source "$CONFIG_FILE" + +# ─── Abhängigkeiten prüfen ──────────────────────────────────────────────────── +check_dependencies() { + local missing=() + for cmd in curl jq iptables ip6tables date; do + if ! command -v "$cmd" &>/dev/null; then + missing+=("$cmd") + fi + done + if [[ ${#missing[@]} -gt 0 ]]; then + log "ERROR" "Fehlende Abhängigkeiten: ${missing[*]}" + echo "Bitte installieren: sudo apt install ${missing[*]}" >&2 + exit 1 + fi +} + +# ─── Logging ────────────────────────────────────────────────────────────────── +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] $message" + + echo "$log_entry" | tee -a "$LOG_FILE" + + # Log-Rotation prüfen + if [[ -f "$LOG_FILE" ]]; then + local size_kb + size_kb=$(du -k "$LOG_FILE" 2>/dev/null | cut -f1) + local max_kb=$((LOG_MAX_SIZE_MB * 1024)) + if [[ ${size_kb:-0} -gt $max_kb ]]; then + mv "$LOG_FILE" "${LOG_FILE}.old" + log "INFO" "Log-Datei rotiert" + fi + fi + fi +} + +# ─── Ban-History ───────────────────────────────────────────────────────────── +log_ban_history() { + local action="$1" + local client_ip="$2" + local domain="${3:-}" + local count="${4:-}" + local reason="${5:-}" + local timestamp + timestamp="$(date '+%Y-%m-%d %H:%M:%S')" + + # Header schreiben falls Datei neu ist + if [[ ! -f "$BAN_HISTORY_FILE" ]]; then + echo "# AdGuard Shield - Ban History" > "$BAN_HISTORY_FILE" + echo "# Format: ZEITSTEMPEL | AKTION | CLIENT-IP | DOMAIN | ANFRAGEN | SPERRDAUER | GRUND" >> "$BAN_HISTORY_FILE" + echo "#───────────────────────────────────────────────────────────────────────────────" >> "$BAN_HISTORY_FILE" + fi + + local duration="-" + [[ "$action" == "BAN" ]] && duration="${BAN_DURATION}s" + + printf "%-19s | %-6s | %-39s | %-30s | %-8s | %-10s | %s\n" \ + "$timestamp" "$action" "$client_ip" "${domain:--}" "${count:--}" "$duration" "${reason:-rate-limit}" \ + >> "$BAN_HISTORY_FILE" +} + +# ─── Verzeichnisse erstellen ────────────────────────────────────────────────── +init_directories() { + mkdir -p "$STATE_DIR" + mkdir -p "$(dirname "$LOG_FILE")" + mkdir -p "$(dirname "$PID_FILE")" + mkdir -p "$(dirname "$BAN_HISTORY_FILE")" +} + +# ─── PID-Management ────────────────────────────────────────────────────────── +write_pid() { + echo $$ > "$PID_FILE" +} + +cleanup() { + log "INFO" "AdGuard Shield wird beendet..." + stop_blocklist_worker + rm -f "$PID_FILE" + exit 0 +} + +check_already_running() { + if [[ -f "$PID_FILE" ]]; then + local old_pid + old_pid=$(cat "$PID_FILE") + if kill -0 "$old_pid" 2>/dev/null; then + echo "Monitor läuft bereits (PID: $old_pid). Beende." >&2 + exit 1 + else + rm -f "$PID_FILE" + fi + fi +} + +# ─── 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 + return 1 +} + +# ─── iptables Chain Setup ──────────────────────────────────────────────────── +setup_iptables_chain() { + # IPv4 Chain erstellen falls nicht vorhanden + if ! iptables -n -L "$IPTABLES_CHAIN" &>/dev/null; then + log "INFO" "Erstelle iptables Chain: $IPTABLES_CHAIN (IPv4)" + iptables -N "$IPTABLES_CHAIN" + + # Chain in INPUT einhängen für alle relevanten Ports + 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 + + # IPv6 Chain erstellen falls nicht vorhanden + if ! ip6tables -n -L "$IPTABLES_CHAIN" &>/dev/null; then + log "INFO" "Erstelle ip6tables Chain: $IPTABLES_CHAIN (IPv6)" + 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 +} + +# ─── Client sperren ───────────────────────────────────────────────────────── +ban_client() { + local client_ip="$1" + local domain="$2" + local count="$3" + local ban_until + ban_until=$(date -d "+${BAN_DURATION} seconds" '+%s' 2>/dev/null || date -v "+${BAN_DURATION}S" '+%s') + + # Prüfen ob bereits gesperrt + local state_file="${STATE_DIR}/${client_ip//[:\/]/_}.ban" + if [[ -f "$state_file" ]]; then + log "DEBUG" "Client $client_ip ist bereits gesperrt" + return 0 + fi + + if [[ "$DRY_RUN" == "true" ]]; then + log "WARN" "[DRY-RUN] WÜRDE sperren: $client_ip (${count}x $domain in ${RATE_LIMIT_WINDOW}s)" + log_ban_history "DRY" "$client_ip" "$domain" "$count" "dry-run" + return 0 + fi + + log "WARN" "SPERRE Client: $client_ip (${count}x $domain in ${RATE_LIMIT_WINDOW}s) für ${BAN_DURATION}s" + + # IPv4 oder IPv6 erkennen + if [[ "$client_ip" == *:* ]]; then + # IPv6 + ip6tables -I "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true + else + # IPv4 + iptables -I "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true + fi + + # State speichern + cat > "$state_file" << EOF +CLIENT_IP=$client_ip +DOMAIN=$domain +COUNT=$count +BAN_TIME=$(date '+%Y-%m-%d %H:%M:%S') +BAN_UNTIL_EPOCH=$ban_until +BAN_UNTIL=$(date -d "@$ban_until" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || date -r "$ban_until" '+%Y-%m-%d %H:%M:%S') +EOF + + # Ban-History Eintrag + log_ban_history "BAN" "$client_ip" "$domain" "$count" "rate-limit" + + # Benachrichtigung senden + if [[ "$NOTIFY_ENABLED" == "true" ]]; then + send_notification "ban" "$client_ip" "$domain" "$count" + fi +} + +# ─── Client entsperren ────────────────────────────────────────────────────── +unban_client() { + local client_ip="$1" + local reason="${2:-expired}" + local state_file="${STATE_DIR}/${client_ip//[:\/]/_}.ban" + + # Domain aus State lesen bevor wir löschen + local domain="-" + if [[ -f "$state_file" ]]; then + domain=$(grep '^DOMAIN=' "$state_file" | cut -d= -f2) + fi + + log "INFO" "ENTSPERRE Client: $client_ip ($reason)" + + 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 + log_ban_history "UNBAN" "$client_ip" "$domain" "-" "$reason" + + if [[ "$NOTIFY_ENABLED" == "true" ]]; then + send_notification "unban" "$client_ip" "" "" + fi +} + +# ─── Abgelaufene Sperren aufheben ─────────────────────────────────────────── +check_expired_bans() { + local now + now=$(date '+%s') + + for state_file in "${STATE_DIR}"/*.ban; do + [[ -f "$state_file" ]] || continue + + local ban_until_epoch + ban_until_epoch=$(grep '^BAN_UNTIL_EPOCH=' "$state_file" | cut -d= -f2) + local client_ip + client_ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2) + + if [[ -n "$ban_until_epoch" && "$now" -ge "$ban_until_epoch" ]]; then + unban_client "$client_ip" "expired" + fi + done +} + +# ─── Benachrichtigungen ───────────────────────────────────────────────────── +send_notification() { + local action="$1" + local client_ip="$2" + local domain="$3" + local count="$4" + + [[ -z "$NOTIFY_WEBHOOK_URL" ]] && return + + local message + if [[ "$action" == "ban" ]]; then + message="🚫 DNS Rate-Limit: Client **$client_ip** gesperrt (${count}x $domain in ${RATE_LIMIT_WINDOW}s). Sperre für ${BAN_DURATION}s." + else + message="✅ DNS Rate-Limit: Client **$client_ip** wurde entsperrt." + fi + + case "$NOTIFY_TYPE" in + discord) + curl -s -H "Content-Type: application/json" \ + -d "{\"content\": \"$message\"}" \ + "$NOTIFY_WEBHOOK_URL" &>/dev/null & + ;; + slack) + curl -s -H "Content-Type: application/json" \ + -d "{\"text\": \"$message\"}" \ + "$NOTIFY_WEBHOOK_URL" &>/dev/null & + ;; + gotify) + curl -s -X POST "$NOTIFY_WEBHOOK_URL" \ + -F "title=AdGuard Rate-Limit" \ + -F "message=$message" \ + -F "priority=5" &>/dev/null & + ;; + ntfy) + send_ntfy_notification "$action" "$message" + ;; + generic) + curl -s -H "Content-Type: application/json" \ + -d "{\"message\": \"$message\", \"action\": \"$action\", \"client\": \"$client_ip\", \"domain\": \"$domain\"}" \ + "$NOTIFY_WEBHOOK_URL" &>/dev/null & + ;; + esac +} + +# ─── Ntfy Benachrichtigung ─────────────────────────────────────────────────── +send_ntfy_notification() { + local action="$1" + local message="$2" + + if [[ -z "${NTFY_TOPIC:-}" ]]; then + log "WARN" "Ntfy: Kein Topic konfiguriert (NTFY_TOPIC ist leer)" + return 1 + fi + + local ntfy_url="${NTFY_SERVER_URL:-https://ntfy.sh}" + local priority="${NTFY_PRIORITY:-4}" + local title="AdGuard Rate-Limit" + local tags + + if [[ "$action" == "ban" ]]; then + tags="rotating_light,ban" + else + tags="white_check_mark,unban" + fi + + # Markdown-Formatierung entfernen für Ntfy + local clean_message + clean_message=$(echo "$message" | sed 's/\*\*//g') + + local -a curl_args=( + -s + -X POST + "${ntfy_url}/${NTFY_TOPIC}" + -H "Title: ${title}" + -H "Priority: ${priority}" + -H "Tags: ${tags}" + -d "${clean_message}" + ) + + # Token hinzufügen falls konfiguriert + if [[ -n "${NTFY_TOKEN:-}" ]]; then + curl_args+=(-H "Authorization: Bearer ${NTFY_TOKEN}") + fi + + curl "${curl_args[@]}" &>/dev/null & +} + +# ─── AdGuard Home API abfragen ────────────────────────────────────────────── +query_adguard_log() { + local time_from + time_from=$(date -u -d "-${RATE_LIMIT_WINDOW} seconds" '+%Y-%m-%dT%H:%M:%S.000Z' 2>/dev/null \ + || date -u -v "-${RATE_LIMIT_WINDOW}S" '+%Y-%m-%dT%H:%M:%S.000Z') + + local response + response=$(curl -s -u "${ADGUARD_USER}:${ADGUARD_PASS}" \ + --connect-timeout 5 \ + --max-time 10 \ + "${ADGUARD_URL}/control/querylog?limit=${API_QUERY_LIMIT}&response_status=all" 2>/dev/null) + + if [[ -z "$response" || "$response" == "null" ]]; then + log "ERROR" "Keine Antwort von AdGuard Home API" + return 1 + fi + + # Prüfen ob die Antwort gültiges JSON ist + if ! echo "$response" | jq . &>/dev/null; then + log "ERROR" "Ungültige API-Antwort (kein JSON)" + return 1 + fi + + echo "$response" +} + +# ─── Anfragen analysieren ─────────────────────────────────────────────────── +analyze_queries() { + local api_response="$1" + local now_epoch + now_epoch=$(date '+%s') + local window_start=$((now_epoch - RATE_LIMIT_WINDOW)) + + # Extrahiere Client-IP + Domain Paare aus dem Zeitfenster + # und zähle die Häufigkeit pro (client, domain) Kombination + local violations + violations=$(echo "$api_response" | jq -r --argjson window_start "$window_start" ' + .data // [] | + [.[] | + select(.time != null) | + { + client: (.client // .client_info.ip // "unknown"), + domain: (.question.name // "unknown" | rtrimstr(".")), + time_epoch: (.time | split(".")[0] | sub("T"; " ") | sub("Z$"; "") ) + } + ] | + group_by(.client + "|" + .domain) | + map({ + client: .[0].client, + domain: .[0].domain, + count: length + }) | + .[] | + select(.count > 0) | + "\(.client)|\(.domain)|\(.count)" + ' 2>/dev/null) + + if [[ -z "$violations" ]]; then + log "DEBUG" "Keine Anfragen im Zeitfenster gefunden" + return + fi + + # Prüfe jede Kombination gegen das Limit + while IFS='|' read -r client domain count; do + [[ -z "$client" || -z "$domain" || -z "$count" ]] && continue + + log "DEBUG" "Client: $client, Domain: $domain, Anfragen: $count" + + if [[ "$count" -gt "$RATE_LIMIT_MAX_REQUESTS" ]]; then + if is_whitelisted "$client"; then + log "INFO" "Client $client ist auf der Whitelist - keine Sperre (${count}x $domain)" + continue + fi + + ban_client "$client" "$domain" "$count" + fi + done <<< "$violations" +} + +# ─── Status anzeigen ───────────────────────────────────────────────────────── +show_status() { + echo "═══════════════════════════════════════════════════════════════" + echo " AdGuard Home Rate-Limit Monitor - Status" + echo "═══════════════════════════════════════════════════════════════" + echo "" + + # Aktive Sperren + local ban_count=0 + if [[ -d "$STATE_DIR" ]]; then + for state_file in "${STATE_DIR}"/*.ban; do + [[ -f "$state_file" ]] || continue + ban_count=$((ban_count + 1)) + echo " 🚫 Gesperrt:" + while IFS='=' read -r key value; do + printf " %-20s %s\n" "$key:" "$value" + done < "$state_file" + echo "" + done + fi + + if [[ $ban_count -eq 0 ]]; then + echo " ✅ Keine aktiven Sperren" + else + echo " Gesamt: $ban_count aktive Sperren" + fi + + echo "" + + # iptables Regeln anzeigen + echo " iptables Regeln ($IPTABLES_CHAIN):" + if iptables -n -L "$IPTABLES_CHAIN" &>/dev/null; then + iptables -n -L "$IPTABLES_CHAIN" --line-numbers 2>/dev/null | sed 's/^/ /' + else + echo " Chain existiert noch nicht" + fi + + echo "" + echo "═══════════════════════════════════════════════════════════════" +} + +# ─── Ban-History anzeigen ──────────────────────────────────────────────────── +show_history() { + local lines="${1:-50}" + echo "═══════════════════════════════════════════════════════════════" + echo " AdGuard Home Rate-Limit - Ban History (letzte $lines Einträge)" + echo "═══════════════════════════════════════════════════════════════" + echo "" + + if [[ ! -f "$BAN_HISTORY_FILE" ]]; then + echo " Noch keine History vorhanden." + echo " Datei: $BAN_HISTORY_FILE" + echo "" + return + fi + + # Header zeigen + head -3 "$BAN_HISTORY_FILE" | sed 's/^/ /' + echo "" + + # Letzte N Einträge (ohne Header-Zeilen) + grep -v '^#' "$BAN_HISTORY_FILE" | tail -n "$lines" | sed 's/^/ /' + + echo "" + local total + total=$(grep -vc '^#' "$BAN_HISTORY_FILE" 2>/dev/null || echo "0") + local bans + bans=$(grep -c '| BAN ' "$BAN_HISTORY_FILE" 2>/dev/null || echo "0") + local unbans + unbans=$(grep -c '| UNBAN ' "$BAN_HISTORY_FILE" 2>/dev/null || echo "0") + echo " Gesamt: $total Einträge ($bans Sperren, $unbans Entsperrungen)" + echo " Datei: $BAN_HISTORY_FILE" + echo "" + echo "═══════════════════════════════════════════════════════════════" +} + +# ─── Alle Sperren aufheben ────────────────────────────────────────────────── +flush_all_bans() { + log "INFO" "Alle Sperren werden aufgehoben..." + + for state_file in "${STATE_DIR}"/*.ban; do + [[ -f "$state_file" ]] || continue + local client_ip + client_ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2) + unban_client "$client_ip" "manual-flush" + done + + # Chain leeren + iptables -F "$IPTABLES_CHAIN" 2>/dev/null || true + ip6tables -F "$IPTABLES_CHAIN" 2>/dev/null || true + + log "INFO" "Alle Sperren aufgehoben" +} + +# ─── Externer Blocklist-Worker starten ─────────────────────────────────────── +start_blocklist_worker() { + if [[ "${EXTERNAL_BLOCKLIST_ENABLED:-false}" != "true" ]]; then + log "DEBUG" "Externer Blocklist-Worker ist deaktiviert" + return + fi + + local worker_script="${SCRIPT_DIR}/external-blocklist-worker.sh" + if [[ ! -f "$worker_script" ]]; then + log "WARN" "Blocklist-Worker Script nicht gefunden: $worker_script" + return + fi + + log "INFO" "Starte externen Blocklist-Worker im Hintergrund..." + bash "$worker_script" start & + BLOCKLIST_WORKER_PID=$! + log "INFO" "Blocklist-Worker gestartet (PID: $BLOCKLIST_WORKER_PID)" +} + +# ─── Externer Blocklist-Worker stoppen ─────────────────────────────────────── +stop_blocklist_worker() { + local worker_pid_file="/var/run/adguard-blocklist-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 Blocklist-Worker (PID: $wpid)..." + kill "$wpid" 2>/dev/null || true + rm -f "$worker_pid_file" + fi + fi +} + +# ─── Hauptschleife ────────────────────────────────────────────────────────── +main_loop() { + log "INFO" "═══════════════════════════════════════════════════════════" + log "INFO" "AdGuard Shield v${VERSION} gestartet" + log "INFO" " Limit: ${RATE_LIMIT_MAX_REQUESTS} Anfragen pro ${RATE_LIMIT_WINDOW}s" + log "INFO" " Sperrdauer: ${BAN_DURATION}s" + log "INFO" " Prüfintervall: ${CHECK_INTERVAL}s" + log "INFO" " Dry-Run: ${DRY_RUN}" + log "INFO" " Whitelist: ${WHITELIST}" + log "INFO" " Externe Blocklist: ${EXTERNAL_BLOCKLIST_ENABLED:-false}" + log "INFO" "═══════════════════════════════════════════════════════════" + + # Blocklist-Worker als Hintergrundprozess starten + start_blocklist_worker + + while true; do + # Abgelaufene Sperren prüfen + check_expired_bans + + # API abfragen + local api_response + if api_response=$(query_adguard_log); then + analyze_queries "$api_response" + fi + + sleep "$CHECK_INTERVAL" + done +} + +# ─── Signal-Handler ────────────────────────────────────────────────────────── +trap cleanup SIGTERM SIGINT SIGHUP + +# ─── Kommandozeilen-Argumente ──────────────────────────────────────────────── +case "${1:-start}" in + start) + check_dependencies + check_already_running + init_directories + write_pid + setup_iptables_chain + main_loop + ;; + stop) + if [[ -f "$PID_FILE" ]]; then + kill "$(cat "$PID_FILE")" 2>/dev/null || true + rm -f "$PID_FILE" + echo "Monitor gestoppt" + else + echo "Monitor läuft nicht" + fi + ;; + blocklist-status) + init_directories + _worker_script="${SCRIPT_DIR}/external-blocklist-worker.sh" + if [[ -f "$_worker_script" ]]; then + bash "$_worker_script" status + else + echo "Blocklist-Worker nicht gefunden" + fi + ;; + blocklist-sync) + init_directories + setup_iptables_chain + _worker_script="${SCRIPT_DIR}/external-blocklist-worker.sh" + if [[ -f "$_worker_script" ]]; then + bash "$_worker_script" sync + else + echo "Blocklist-Worker nicht gefunden" + fi + ;; + blocklist-flush) + init_directories + _worker_script="${SCRIPT_DIR}/external-blocklist-worker.sh" + if [[ -f "$_worker_script" ]]; then + bash "$_worker_script" flush + else + echo "Blocklist-Worker nicht gefunden" + fi + ;; + status) + init_directories + show_status + ;; + flush) + init_directories + setup_iptables_chain + flush_all_bans + echo "Alle Sperren aufgehoben" + ;; + unban) + if [[ -z "${2:-}" ]]; then + echo "Nutzung: $0 unban " >&2 + exit 1 + fi + init_directories + unban_client "$2" "manual" + echo "Client $2 entsperrt" + ;; + test) + echo "Teste Verbindung zur AdGuard Home API..." + check_dependencies + init_directories + if response=$(query_adguard_log); then + entry_count=$(echo "$response" | jq '.data | length' 2>/dev/null || echo "0") + echo "✅ Verbindung erfolgreich! $entry_count Log-Einträge gefunden." + else + echo "❌ Verbindung fehlgeschlagen! Prüfe URL und Zugangsdaten in $CONFIG_FILE" + exit 1 + fi + ;; + history) + init_directories + show_history "${2:-50}" + ;; + dry-run) + DRY_RUN=true + check_dependencies + check_already_running + init_directories + write_pid + setup_iptables_chain + main_loop + ;; + *) + cat << USAGE +AdGuard Shield v${VERSION} + +Nutzung: $0 {start|stop|status|history|flush|unban|test|dry-run|blocklist-status|blocklist-sync|blocklist-flush} + +Befehle: + start Startet den Monitor (inkl. Blocklist-Worker) + stop Stoppt den Monitor + status Zeigt aktive Sperren und Regeln + history [N] Zeigt die letzten N Ban-Einträge (Standard: 50) + flush Hebt alle Sperren auf + unban IP Entsperrt eine bestimmte IP-Adresse + test Testet die Verbindung zur AdGuard Home API + dry-run Startet im Testmodus (keine echten Sperren) + blocklist-status Zeigt Status der externen Blocklisten + blocklist-sync Einmalige Synchronisation der externen Blocklisten + blocklist-flush Entfernt alle Sperren der externen Blocklisten + +Konfiguration: $CONFIG_FILE +Log-Datei: $LOG_FILE +Ban-History: $BAN_HISTORY_FILE +State: $STATE_DIR + +USAGE + exit 0 + ;; +esac diff --git a/doc/architektur.md b/doc/architektur.md new file mode 100644 index 0000000..98f5f70 --- /dev/null +++ b/doc/architektur.md @@ -0,0 +1,138 @@ +# Architektur & Funktionsweise + +## Überblick + +``` +┌─────────────────────┐ +│ Client Anfragen │ +│ (DNS/DoH/DoT/DoQ) │ +└──────────┬──────────┘ + │ + ▼ +┌─────────────────────┐ ┌──────────────────────┐ +│ AdGuard Home │────▶│ Query Log (API) │ +│ DNS Server │ └──────────┬───────────┘ +└─────────────────────┘ │ + ▼ + ┌──────────────────────┐ + │ adguard-ratelimit.sh │ + │ (Monitor Script) │ + └──────────┬───────────┘ + │ + ┌──────────────┼──────────────┐ + ▼ ▼ ▼ + ┌──────────┐ ┌──────────┐ ┌──────────┐ + │ iptables │ │ Log │ │ Webhook │ + │ DROP │ │ Datei │ │ Notify │ + └──────────┘ └──────────┘ └──────────┘ +``` + +## Ablauf einer Sperre + +1. Client `192.168.1.50` fragt `microsoft.com` 45x in 60 Sekunden an +2. Monitor fragt die AdGuard Home API alle 10 Sekunden ab (`/control/querylog`) +3. Die Anfragen werden pro Client+Domain-Kombination gezählt +4. Monitor erkennt: 45 > 30 (Limit überschritten) +5. Prüfung: Ist der Client auf der Whitelist? → Nein +6. iptables-Regel wird erstellt: `DROP` für `192.168.1.50` auf allen DNS-Ports +7. State-Datei wird angelegt: `/var/lib/adguard-ratelimit/192.168.1.50.ban` +8. Ban-History Eintrag wird in `/var/log/adguard-ratelimit-bans.log` geschrieben +9. Log-Eintrag + optionale Webhook-Benachrichtigung +10. Nach 3600 Sekunden (1 Stunde): automatische Entsperrung + History-Eintrag + +## iptables Strategie + +Das Tool erstellt eine eigene Chain `ADGUARD_RATELIMIT`: + +``` +INPUT Chain + ├── ... (bestehende Regeln bleiben unberührt) + ├── -p tcp --dport 53 → ADGUARD_RATELIMIT + ├── -p udp --dport 53 → ADGUARD_RATELIMIT + ├── -p tcp --dport 443 → ADGUARD_RATELIMIT + ├── -p udp --dport 443 → ADGUARD_RATELIMIT + ├── -p tcp --dport 853 → ADGUARD_RATELIMIT + ├── -p udp --dport 853 → ADGUARD_RATELIMIT + └── ... + +ADGUARD_RATELIMIT Chain + ├── -s 192.168.1.50 → DROP (gesperrter Client) + ├── -s 10.0.0.25 → DROP (gesperrter Client) + └── RETURN (alle anderen passieren) +``` + +**Vorteile der eigenen Chain:** +- Greift nicht in bestehende Firewall-Regeln ein +- Kann komplett geflusht werden ohne andere Regeln zu beeinflussen +- Einfaches Debugging per `iptables -L ADGUARD_RATELIMIT` + +## State-Management + +Jede aktive Sperre wird als Datei gespeichert: + +``` +/var/lib/adguard-ratelimit/192.168.1.50.ban +``` + +Inhalt: +``` +CLIENT_IP=192.168.1.50 +DOMAIN=microsoft.com +COUNT=45 +BAN_TIME=2026-03-03 14:30:00 +BAN_UNTIL_EPOCH=1741012200 +BAN_UNTIL=2026-03-03 15:30:00 +``` + +Das ermöglicht: +- Persistenz über Script-Neustarts hinweg +- Statusabfragen jederzeit möglich +- Automatisches Aufräumen per Cron-Job + +## Dateistruktur nach Installation + +``` +/opt/adguard-ratelimit/ +├── adguard-ratelimit.sh # Haupt-Monitor-Script +├── adguard-ratelimit.conf # Konfiguration (chmod 600) +├── iptables-helper.sh # iptables Verwaltung +└── unban-expired.sh # Cron-basiertes Entsperren + +/etc/systemd/system/ +└── adguard-ratelimit.service + +/var/lib/adguard-ratelimit/ +└── *.ban # State-Dateien aktiver Sperren + +/var/log/ +├── adguard-ratelimit.log # Laufzeit-Log +└── adguard-ratelimit-bans.log # Ban-History (alle Sperren/Entsperrungen) +``` + +## Ban-History + +Jede Sperre und Entsperrung wird dauerhaft in der Ban-History protokolliert (`/var/log/adguard-ratelimit-bans.log`). Das ermöglicht eine lückenlose Nachvollziehbarkeit, auch nachdem State-Dateien bereits gelöscht wurden. + +**Format:** +``` +ZEITSTEMPEL | AKTION | CLIENT-IP | DOMAIN | ANFRAGEN | SPERRDAUER | GRUND +2026-03-03 14:30:12 | BAN | 192.168.1.50 | microsoft.com | 45 | 3600s | rate-limit +2026-03-03 15:30:12 | UNBAN | 192.168.1.50 | microsoft.com | - | - | expired +2026-03-03 16:10:33 | UNBAN | 10.0.0.25 | telemetry.example.com | - | - | manual +``` + +**Mögliche Gründe (GRUND-Spalte):** +| Grund | Bedeutung | +|-------|----------| +| `rate-limit` | Automatische Sperre wegen Limit-Überschreitung | +| `dry-run` | Im Dry-Run erkannt (nicht wirklich gesperrt) | +| `expired` | Automatisch entsperrt nach Ablauf der Sperrdauer | +| `expired-cron` | Entsperrt durch den Cron-Job (`unban-expired.sh`) | +| `manual` | Manuell entsperrt per `unban`-Befehl | +| `manual-flush` | Entsperrt durch `flush`-Befehl (alle Sperren aufgehoben) | + +**History anzeigen:** +```bash +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh history # letzte 50 +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh history 200 # letzte 200 +``` diff --git a/doc/befehle.md b/doc/befehle.md new file mode 100644 index 0000000..8614df7 --- /dev/null +++ b/doc/befehle.md @@ -0,0 +1,134 @@ +# Befehle & Nutzung + +## Monitor (Hauptscript) + +```bash +# Starten +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh start + +# Stoppen +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh stop + +# Status + aktive Sperren anzeigen +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh status + +# Ban-History anzeigen (letzte 50 Einträge) +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh history + +# Ban-History anzeigen (letzte 100 Einträge) +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh history 100 + +# Alle Sperren aufheben +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh flush + +# Einzelne IP entsperren +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh unban 192.168.1.100 + +# API-Verbindung testen +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh test + +# Dry-Run (nur loggen, nichts sperren) +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh dry-run + +# Externe Blocklist - Status anzeigen +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh blocklist-status + +# Externe Blocklist - Einmalige Synchronisation +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh blocklist-sync + +# Externe Blocklist - Alle Sperren der externen Liste aufheben +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh blocklist-flush +``` + +## iptables Helper + +Für die manuelle Verwaltung der Firewall-Regeln: + +```bash +# Chain erstellen +sudo /opt/adguard-ratelimit/iptables-helper.sh create + +# Alle Regeln anzeigen +sudo /opt/adguard-ratelimit/iptables-helper.sh status + +# IP manuell sperren +sudo /opt/adguard-ratelimit/iptables-helper.sh ban 192.168.1.100 + +# IP entsperren +sudo /opt/adguard-ratelimit/iptables-helper.sh unban 192.168.1.100 + +# Alle Regeln leeren +sudo /opt/adguard-ratelimit/iptables-helper.sh flush + +# Chain komplett entfernen +sudo /opt/adguard-ratelimit/iptables-helper.sh remove + +# Regeln speichern / wiederherstellen +sudo /opt/adguard-ratelimit/iptables-helper.sh save +sudo /opt/adguard-ratelimit/iptables-helper.sh restore +``` + +## Externer Blocklist-Worker + +Der Worker kann auch standalone gesteuert werden: + +```bash +# Worker manuell starten (normalerweise automatisch per Hauptscript) +sudo /opt/adguard-ratelimit/external-blocklist-worker.sh start + +# Worker stoppen +sudo /opt/adguard-ratelimit/external-blocklist-worker.sh stop + +# Einmalige Synchronisation (z.B. nach Konfigurationsänderung) +sudo /opt/adguard-ratelimit/external-blocklist-worker.sh sync + +# Status anzeigen +sudo /opt/adguard-ratelimit/external-blocklist-worker.sh status + +# Alle externen Sperren aufheben +sudo /opt/adguard-ratelimit/external-blocklist-worker.sh flush +``` + +## systemd Service + +```bash +# Start / Stop / Restart +sudo systemctl start adguard-ratelimit +sudo systemctl stop adguard-ratelimit +sudo systemctl restart adguard-ratelimit + +# Status +sudo systemctl status adguard-ratelimit + +# Autostart aktivieren / deaktivieren +sudo systemctl enable adguard-ratelimit +sudo systemctl disable adguard-ratelimit +``` + +## Logs + +```bash +# systemd Journal +sudo journalctl -u adguard-ratelimit -f + +# Log-Datei direkt +sudo tail -f /var/log/adguard-ratelimit.log + +# Nur Sperr-Einträge +sudo grep "SPERRE" /var/log/adguard-ratelimit.log + +# Nur Entsperr-Einträge +sudo grep "ENTSPERRE" /var/log/adguard-ratelimit.log +``` + +## Cron-basiertes Entsperren + +Als Alternative oder Ergänzung zum Haupt-Monitor: + +```bash +# Crontab bearbeiten +sudo crontab -e + +# Alle 5 Minuten abgelaufene Sperren prüfen +*/5 * * * * /opt/adguard-ratelimit/unban-expired.sh +``` diff --git a/doc/benachrichtigungen.md b/doc/benachrichtigungen.md new file mode 100644 index 0000000..be1211f --- /dev/null +++ b/doc/benachrichtigungen.md @@ -0,0 +1,110 @@ +# Webhook-Benachrichtigungen + +Das Tool kann bei Sperren und Entsperrungen Benachrichtigungen an verschiedene Dienste senden. + +## Aktivierung + +In der Konfiguration (`adguard-ratelimit.conf`): + +```bash +NOTIFY_ENABLED=true +NOTIFY_TYPE="" +NOTIFY_WEBHOOK_URL="" +``` + +## Ntfy + +```bash +NOTIFY_ENABLED=true +NOTIFY_TYPE="ntfy" +NTFY_SERVER_URL="https://ntfy.sh" +NTFY_TOPIC="adguard-ratelimit" +NTFY_TOKEN="" +NTFY_PRIORITY="4" +``` + +> **Hinweis:** Bei Ntfy wird `NOTIFY_WEBHOOK_URL` nicht benötigt – Server-URL und Topic werden separat konfiguriert. + +**Eigene Ntfy-Instanz:** +```bash +NTFY_SERVER_URL="https://ntfy.mein-server.de" +NTFY_TOPIC="dns-security" +NTFY_TOKEN="tk_mein_geheimer_token" +``` + +**Prioritäten:** +| Wert | Bedeutung | +|------|-----------| +| 1 | Minimum | +| 2 | Niedrig | +| 3 | Standard | +| 4 | Hoch | +| 5 | Maximum | + +**Token erstellen (Self-hosted):** +1. Ntfy Web-UI → Benutzer/Tokens +2. Token kopieren und in `NTFY_TOKEN` eintragen +3. Bei ntfy.sh: Account erstellen → Access Token generieren + +## Discord + +```bash +NOTIFY_ENABLED=true +NOTIFY_TYPE="discord" +NOTIFY_WEBHOOK_URL="https://discord.com/api/webhooks/xxx/yyy" +``` + +**Webhook erstellen:** +1. Discord Server → Servereinstellungen → Integrationen → Webhooks +2. Neuer Webhook → URL kopieren + +## Gotify + +```bash +NOTIFY_ENABLED=true +NOTIFY_TYPE="gotify" +NOTIFY_WEBHOOK_URL="https://gotify.example.com/message?token=xxx" +``` + +**Token erstellen:** +1. Gotify Web-UI → Apps → App erstellen +2. Token kopieren und in die URL einfügen + +## Slack + +```bash +NOTIFY_ENABLED=true +NOTIFY_TYPE="slack" +NOTIFY_WEBHOOK_URL="https://hooks.slack.com/services/xxx/yyy/zzz" +``` + +**Webhook erstellen:** +1. Slack App → Incoming Webhooks aktivieren +2. Webhook-URL kopieren + +## Generic (eigener Endpoint) + +```bash +NOTIFY_ENABLED=true +NOTIFY_TYPE="generic" +NOTIFY_WEBHOOK_URL="https://your-server.com/webhook" +``` + +Sendet einen POST mit JSON-Body: + +```json +{ + "message": "🚫 DNS Rate-Limit: Client 192.168.1.50 gesperrt ...", + "action": "ban", + "client": "192.168.1.50", + "domain": "microsoft.com" +} +``` + +## Beispiel-Nachrichten + +**Sperre:** +> 🚫 DNS Rate-Limit: Client **192.168.1.50** gesperrt (45x microsoft.com in 60s). Sperre für 3600s. + +**Entsperrung:** +> ✅ DNS Rate-Limit: Client **192.168.1.50** wurde entsperrt. diff --git a/doc/konfiguration.md b/doc/konfiguration.md new file mode 100644 index 0000000..bb183d8 --- /dev/null +++ b/doc/konfiguration.md @@ -0,0 +1,130 @@ +# Konfiguration + +Die Konfigurationsdatei liegt nach der Installation unter: + +``` +/opt/adguard-ratelimit/adguard-ratelimit.conf +``` + +## Alle Parameter + +### AdGuard Home API + +| Parameter | Standard | Beschreibung | +|-----------|----------|--------------| +| `ADGUARD_URL` | `http://127.0.0.1:3000` | AdGuard Home Web-UI URL | +| `ADGUARD_USER` | `admin` | API Benutzername | +| `ADGUARD_PASS` | `changeme` | API Passwort | + +### Rate-Limit + +| Parameter | Standard | Beschreibung | +|-----------|----------|--------------| +| `RATE_LIMIT_MAX_REQUESTS` | `30` | Max. Anfragen pro Domain/Client innerhalb des Zeitfensters | +| `RATE_LIMIT_WINDOW` | `60` | Zeitfenster in Sekunden | +| `CHECK_INTERVAL` | `10` | Wie oft die Logs geprüft werden (Sekunden) | +| `API_QUERY_LIMIT` | `500` | Anzahl API-Einträge pro Abfrage (max 5000) | + +### Sperr-Einstellungen + +| Parameter | Standard | Beschreibung | +|-----------|----------|--------------| +| `BAN_DURATION` | `3600` | Sperrdauer in Sekunden (3600 = 1 Stunde) | +| `IPTABLES_CHAIN` | `ADGUARD_RATELIMIT` | Name der iptables Chain | +| `BLOCKED_PORTS` | `53 443 853 784 8853` | Ports die gesperrt werden | +| `WHITELIST` | `127.0.0.1,::1` | IPs die nie gesperrt werden (kommagetrennt) | + +### Logging + +| Parameter | Standard | Beschreibung | +|-----------|----------|--------------| +| `LOG_FILE` | `/var/log/adguard-ratelimit.log` | Pfad zur Log-Datei | +| `LOG_LEVEL` | `INFO` | Log-Level: `DEBUG`, `INFO`, `WARN`, `ERROR` | +| `LOG_MAX_SIZE_MB` | `50` | Max. Log-Größe bevor rotiert wird | +| `BAN_HISTORY_FILE` | `/var/log/adguard-ratelimit-bans.log` | Datei für die Ban-History (alle Sperren/Entsperrungen) | + +### Benachrichtigungen + +| Parameter | Standard | Beschreibung | +|-----------|----------|--------------| +| `NOTIFY_ENABLED` | `false` | Webhook-Benachrichtigungen aktivieren | +| `NOTIFY_WEBHOOK_URL` | *(leer)* | Webhook-URL | +| `NOTIFY_TYPE` | `generic` | Typ: `discord`, `slack`, `gotify`, `generic` | + +### Erweitert + +| Parameter | Standard | Beschreibung | +|-----------|----------|--------------| +| `STATE_DIR` | `/var/lib/adguard-ratelimit` | Verzeichnis für State-Dateien | +| `PID_FILE` | `/var/run/adguard-ratelimit.pid` | PID-Datei | +| `DRY_RUN` | `false` | Testmodus — nur loggen, nicht sperren | +### Externe Blocklist + +Ermöglicht das Einbinden externer IP-Blocklisten (z.B. gehostete Textdateien mit einer IP pro Zeile). Der Worker läuft als Hintergrundprozess und prüft periodisch auf Änderungen. + +| Parameter | Standard | Beschreibung | +|-----------|----------|}--------------| +| `EXTERNAL_BLOCKLIST_ENABLED` | `false` | Aktiviert den externen Blocklist-Worker | +| `EXTERNAL_BLOCKLIST_URLS` | *(leer)* | URL(s) zu Textdateien mit IPs (kommagetrennt) | +| `EXTERNAL_BLOCKLIST_INTERVAL` | `300` | Prüfintervall in Sekunden (300 = 5 Min.) | +| `EXTERNAL_BLOCKLIST_BAN_DURATION` | `0` | Sperrdauer in Sekunden (0 = permanent bis IP aus Liste entfernt) | +| `EXTERNAL_BLOCKLIST_AUTO_UNBAN` | `true` | IPs automatisch entsperren wenn aus Liste entfernt | +| `EXTERNAL_BLOCKLIST_CACHE_DIR` | `/var/lib/adguard-ratelimit/external-blocklist` | Lokaler Cache für heruntergeladene Listen | + +#### Externe Blocklist einrichten + +1. Erstelle eine Textdatei auf einem Webserver mit einer IP pro Zeile: + +```text +# Kommentare werden ignoriert +192.168.100.50 +10.0.0.99 +2001:db8::dead:beef +``` + +2. Aktiviere die Blocklist in der Konfiguration: + +```bash +EXTERNAL_BLOCKLIST_ENABLED=true +EXTERNAL_BLOCKLIST_URLS="https://example.com/blocklist.txt" +EXTERNAL_BLOCKLIST_INTERVAL=300 +``` + +3. Mehrere Listen können kommagetrennt angegeben werden: + +```bash +EXTERNAL_BLOCKLIST_URLS="https://example.com/list1.txt,https://other.com/list2.txt" +``` + +4. Service neustarten: + +```bash +sudo systemctl restart adguard-ratelimit +``` +## Gesperrte Ports im Detail + +Bei einem Rate-Limit-Verstoß werden **alle** DNS-Protokoll-Ports für den Client gesperrt: + +| Port | Protokoll | Beschreibung | +|------|-----------|-------------| +| 53 | UDP/TCP | Standard DNS | +| 443 | TCP | DNS-over-HTTPS (DoH) | +| 853 | TCP | DNS-over-TLS (DoT) | +| 853 | UDP | DNS-over-QUIC (DoQ) | +| 784 | UDP | DNS-over-QUIC (alternativ) | +| 8853 | UDP | DNS-over-QUIC (alternativ) | + +## Whitelist richtig pflegen + +Die Whitelist sollte mindestens enthalten: + +- `127.0.0.1` und `::1` (Localhost) +- Die IP deines Routers / Gateways +- Deine eigenen Management-IPs +- Andere vertrauenswürdige DNS-Clients + +Beispiel: + +``` +WHITELIST="127.0.0.1,::1,192.168.1.1,192.168.1.10,fd00::1" +``` diff --git a/doc/tipps-und-troubleshooting.md b/doc/tipps-und-troubleshooting.md new file mode 100644 index 0000000..69d813b --- /dev/null +++ b/doc/tipps-und-troubleshooting.md @@ -0,0 +1,94 @@ +# Tipps & Troubleshooting + +## Best Practices + +- **Erst immer im Dry-Run testen**, bevor der scharfe Modus aktiviert wird + ```bash + sudo /opt/adguard-ratelimit/adguard-ratelimit.sh dry-run + ``` +- **Whitelist großzügig pflegen**: Eigene IPs, Router, wichtige Server nicht vergessen +- **Sperrdauer anpassen**: Für DDoS-artige Muster ggf. länger sperren +- **Logs regelmäßig prüfen**: Falsche Positive erkennen und Whitelist anpassen +- **Ban-History nutzen**: `history`-Befehl zeigt alle vergangenen Sperren — hilfreich um Muster zu erkennen +- **Log-Level auf DEBUG** setzen wenn etwas nicht funktioniert + +## Häufige Probleme + +### API-Verbindung schlägt fehl + +```bash +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh test +``` + +**Mögliche Ursachen:** +- Falsche URL in `ADGUARD_URL` (Port prüfen!) +- Falsche Zugangsdaten (`ADGUARD_USER` / `ADGUARD_PASS`) +- AdGuard Home läuft nicht +- Firewall blockiert lokale Verbindung + +**Lösung:** URL manuell testen: +```bash +curl -s -u admin:passwort http://127.0.0.1:3000/control/querylog?limit=1 +``` + +### iptables-Fehler: "Permission denied" + +Das Script muss als **root** laufen, da iptables Root-Rechte benötigt. + +```bash +sudo /opt/adguard-ratelimit/adguard-ratelimit.sh start +``` + +### Client wird fälschlich gesperrt + +1. Client sofort entsperren: + ```bash + sudo /opt/adguard-ratelimit/adguard-ratelimit.sh unban 192.168.1.100 + ``` +2. In der Ban-History prüfen, warum gesperrt wurde: + ```bash + sudo /opt/adguard-ratelimit/adguard-ratelimit.sh history | grep 192.168.1.100 + ``` +3. IP zur Whitelist hinzufügen in `adguard-ratelimit.conf` +3. Service neustarten: + ```bash + sudo systemctl restart adguard-ratelimit + ``` + +### Sperren überleben Reboot nicht + +Das ist normal — iptables-Regeln sind flüchtig. Der **Service** erstellt die Chain beim Start automatisch neu. Aktive Sperren aus dem State-Verzeichnis werden aber nicht automatisch wiederhergestellt. + +**Optionen:** +- `iptables-persistent` installieren (`apt install iptables-persistent`) +- Oder den State beim Boot wiederherstellen lassen (Feature-Idee) + +### Zu viele false positives + +- `RATE_LIMIT_MAX_REQUESTS` erhöhen (z.B. 50 oder 100) +- `RATE_LIMIT_WINDOW` vergrößern (z.B. 120 Sekunden) +- Windows-Clients fragen manche Domains von Natur aus sehr oft an — Whitelist nutzen + +### Monitor startet nicht (PID-File) + +```bash +# Altes PID-File entfernen +sudo rm -f /var/run/adguard-ratelimit.pid +sudo systemctl start adguard-ratelimit +``` + +## Deinstallation + +```bash +sudo bash install.sh uninstall +``` + +Oder manuell: +```bash +sudo systemctl stop adguard-ratelimit +sudo systemctl disable adguard-ratelimit +sudo /opt/adguard-ratelimit/iptables-helper.sh remove +sudo rm -rf /opt/adguard-ratelimit +sudo rm -f /etc/systemd/system/adguard-ratelimit.service +sudo systemctl daemon-reload +``` diff --git a/external-blocklist-worker.sh b/external-blocklist-worker.sh new file mode 100644 index 0000000..a2d212b --- /dev/null +++ b/external-blocklist-worker.sh @@ -0,0 +1,640 @@ +#!/bin/bash +############################################################################### +# AdGuard Shield - Externer Blocklist-Worker +# Lädt externe IP-Blocklisten herunter und sperrt/entsperrt IPs automatisch. +# Wird als Hintergrundprozess vom Hauptscript gestartet. +# +# Autor: Patrick Asmus +# E-Mail: support@techniverse.net +# Datum: 2026-03-03 +# Lizenz: MIT +############################################################################### + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_FILE="${SCRIPT_DIR}/adguard-ratelimit.conf" + +# ─── Konfiguration laden ─────────────────────────────────────────────────────── +if [[ ! -f "$CONFIG_FILE" ]]; then + echo "FEHLER: Konfigurationsdatei nicht gefunden: $CONFIG_FILE" >&2 + exit 1 +fi +# shellcheck source=adguard-ratelimit.conf +source "$CONFIG_FILE" + +# ─── Worker PID-File ────────────────────────────────────────────────────────── +WORKER_PID_FILE="/var/run/adguard-blocklist-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] [BLOCKLIST-WORKER] $message" + echo "$log_entry" | tee -a "$LOG_FILE" + fi +} + +# ─── Ban-History ───────────────────────────────────────────────────────────── +log_ban_history() { + local action="$1" + local client_ip="$2" + local reason="${3:-external-blocklist}" + local timestamp + timestamp="$(date '+%Y-%m-%d %H:%M:%S')" + + if [[ ! -f "$BAN_HISTORY_FILE" ]]; then + echo "# AdGuard Shield - Ban History" > "$BAN_HISTORY_FILE" + echo "# Format: ZEITSTEMPEL | AKTION | CLIENT-IP | DOMAIN | ANFRAGEN | SPERRDAUER | GRUND" >> "$BAN_HISTORY_FILE" + echo "#───────────────────────────────────────────────────────────────────────────────" >> "$BAN_HISTORY_FILE" + fi + + local duration="permanent" + [[ "$EXTERNAL_BLOCKLIST_BAN_DURATION" -gt 0 ]] && duration="${EXTERNAL_BLOCKLIST_BAN_DURATION}s" + + printf "%-19s | %-6s | %-39s | %-30s | %-8s | %-10s | %s\n" \ + "$timestamp" "$action" "$client_ip" "-" "-" "$duration" "$reason" \ + >> "$BAN_HISTORY_FILE" +} + +# ─── Verzeichnisse erstellen ────────────────────────────────────────────────── +init_directories() { + mkdir -p "$EXTERNAL_BLOCKLIST_CACHE_DIR" + mkdir -p "$STATE_DIR" + mkdir -p "$(dirname "$LOG_FILE")" +} + +# ─── 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 + return 1 +} + +# ─── iptables Chain Setup ──────────────────────────────────────────────────── +setup_iptables_chain() { + # IPv4 Chain erstellen falls nicht vorhanden + if ! iptables -n -L "$IPTABLES_CHAIN" &>/dev/null; then + log "INFO" "Erstelle iptables Chain: $IPTABLES_CHAIN (IPv4)" + 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 + + # IPv6 Chain erstellen falls nicht vorhanden + if ! ip6tables -n -L "$IPTABLES_CHAIN" &>/dev/null; then + log "INFO" "Erstelle ip6tables Chain: $IPTABLES_CHAIN (IPv6)" + 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 +} + +# ─── IP sperren ────────────────────────────────────────────────────────────── +ban_ip() { + local ip="$1" + local state_file="${STATE_DIR}/ext_${ip//[:\/]/_}.ban" + + # Bereits gesperrt? + if [[ -f "$state_file" ]]; then + log "DEBUG" "IP $ip bereits über externe Blocklist gesperrt" + return 0 + fi + + # Nicht auch vom Hauptscript gesperrt? (State-Datei ohne ext_ Prefix) + local main_state_file="${STATE_DIR}/${ip//[:\/]/_}.ban" + if [[ -f "$main_state_file" ]]; then + log "DEBUG" "IP $ip bereits vom Rate-Limiter gesperrt - überspringe" + return 0 + fi + + if [[ "$DRY_RUN" == "true" ]]; then + log "WARN" "[DRY-RUN] WÜRDE sperren (externe Blocklist): $ip" + log_ban_history "DRY" "$ip" "external-blocklist-dry-run" + return 0 + fi + + log "WARN" "SPERRE IP (externe Blocklist): $ip" + + # iptables-Regel setzen + if [[ "$ip" == *:* ]]; then + ip6tables -I "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null || true + else + iptables -I "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null || true + fi + + # State speichern + local ban_until_epoch="0" + local ban_until_display="permanent" + if [[ "$EXTERNAL_BLOCKLIST_BAN_DURATION" -gt 0 ]]; then + ban_until_epoch=$(date -d "+${EXTERNAL_BLOCKLIST_BAN_DURATION} seconds" '+%s' 2>/dev/null \ + || date -v "+${EXTERNAL_BLOCKLIST_BAN_DURATION}S" '+%s') + ban_until_display=$(date -d "@$ban_until_epoch" '+%Y-%m-%d %H:%M:%S' 2>/dev/null \ + || date -r "$ban_until_epoch" '+%Y-%m-%d %H:%M:%S') + fi + + cat > "$state_file" << EOF +CLIENT_IP=$ip +DOMAIN=- +COUNT=- +BAN_TIME=$(date '+%Y-%m-%d %H:%M:%S') +BAN_UNTIL_EPOCH=$ban_until_epoch +BAN_UNTIL=$ban_until_display +SOURCE=external-blocklist +EOF + + log_ban_history "BAN" "$ip" "external-blocklist" + + # Benachrichtigung senden + if [[ "$NOTIFY_ENABLED" == "true" ]]; then + send_notification "ban" "$ip" + fi +} + +# ─── IP entsperren ─────────────────────────────────────────────────────────── +unban_ip() { + local ip="$1" + local reason="${2:-external-blocklist-removed}" + local state_file="${STATE_DIR}/ext_${ip//[:\/]/_}.ban" + + [[ -f "$state_file" ]] || return 0 + + log "INFO" "ENTSPERRE IP (externe Blocklist entfernt): $ip" + + if [[ "$ip" == *:* ]]; then + ip6tables -D "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null || true + else + iptables -D "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null || true + fi + + rm -f "$state_file" + log_ban_history "UNBAN" "$ip" "$reason" + + if [[ "$NOTIFY_ENABLED" == "true" ]]; then + send_notification "unban" "$ip" + fi +} + +# ─── Benachrichtigung ──────────────────────────────────────────────────────── +send_notification() { + local action="$1" + local ip="$2" + + [[ -z "${NOTIFY_WEBHOOK_URL:-}" ]] && return + + local message + if [[ "$action" == "ban" ]]; then + message="🚫 Externe Blocklist: IP **$ip** gesperrt." + else + message="✅ Externe Blocklist: IP **$ip** entsperrt (aus Liste entfernt)." + fi + + case "${NOTIFY_TYPE:-generic}" in + discord) + curl -s -H "Content-Type: application/json" \ + -d "{\"content\": \"$message\"}" \ + "$NOTIFY_WEBHOOK_URL" &>/dev/null & + ;; + slack) + curl -s -H "Content-Type: application/json" \ + -d "{\"text\": \"$message\"}" \ + "$NOTIFY_WEBHOOK_URL" &>/dev/null & + ;; + gotify) + curl -s -X POST "$NOTIFY_WEBHOOK_URL" \ + -F "title=AdGuard Shield - Externe Blocklist" \ + -F "message=$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 - Externe Blocklist" + -H "Priority: ${NTFY_PRIORITY:-3}" + -H "Tags: rotating_light,blocklist" + -d "$(echo "$message" | sed 's/\*\*//g')" + ) + [[ -n "${NTFY_TOKEN:-}" ]] && curl_args+=(-H "Authorization: Bearer ${NTFY_TOKEN}") + curl "${curl_args[@]}" &>/dev/null & + ;; + generic) + curl -s -H "Content-Type: application/json" \ + -d "{\"message\": \"$message\", \"action\": \"$action\", \"client\": \"$ip\", \"source\": \"external-blocklist\"}" \ + "$NOTIFY_WEBHOOK_URL" &>/dev/null & + ;; + esac +} + +# ─── Externe Blocklist herunterladen ───────────────────────────────────────── +download_blocklist() { + local url="$1" + local index="$2" + local cache_file="${EXTERNAL_BLOCKLIST_CACHE_DIR}/blocklist_${index}.txt" + local etag_file="${EXTERNAL_BLOCKLIST_CACHE_DIR}/blocklist_${index}.etag" + local tmp_file="${EXTERNAL_BLOCKLIST_CACHE_DIR}/blocklist_${index}.tmp" + + log "DEBUG" "Prüfe externe Blocklist: $url" + + # HTTP-Header für bedingte Anfrage vorbereiten + local -a curl_args=( + -s + -L + --connect-timeout 10 + --max-time 30 + -o "$tmp_file" + -w "%{http_code}" + ) + + # ETag für If-None-Match Header nutzen falls vorhanden + if [[ -f "$etag_file" ]]; then + local stored_etag + stored_etag=$(cat "$etag_file") + curl_args+=(-H "If-None-Match: ${stored_etag}") + fi + + # Download-Header separat abfragen für ETag + local http_code + http_code=$(curl "${curl_args[@]}" -D "${tmp_file}.headers" "$url" 2>/dev/null) || { + log "WARN" "Fehler beim Download der Blocklist: $url" + rm -f "$tmp_file" "${tmp_file}.headers" + return 1 + } + + # 304 Not Modified - keine Änderung + if [[ "$http_code" == "304" ]]; then + log "DEBUG" "Blocklist nicht geändert (HTTP 304): $url" + rm -f "$tmp_file" "${tmp_file}.headers" + return 1 + fi + + # Fehlerhafte HTTP-Codes + if [[ "$http_code" != "200" ]]; then + log "WARN" "Blocklist Download fehlgeschlagen (HTTP $http_code): $url" + rm -f "$tmp_file" "${tmp_file}.headers" + return 1 + fi + + # Neuen ETag speichern falls vorhanden + 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" + + # Prüfen ob sich der Inhalt tatsächlich geändert hat (Fallback für Server ohne ETag) + if [[ -f "$cache_file" ]]; then + if diff -q "$tmp_file" "$cache_file" &>/dev/null; then + log "DEBUG" "Blocklist Inhalt unverändert: $url" + rm -f "$tmp_file" + return 1 + fi + fi + + # Neue Datei übernehmen + mv "$tmp_file" "$cache_file" + log "INFO" "Blocklist aktualisiert: $url" + return 0 +} + +# ─── IPs aus Blocklist-Datei parsen ────────────────────────────────────────── +parse_blocklist_ips() { + local cache_file="$1" + + [[ -f "$cache_file" ]] || return + + # Zeilen lesen, Leerzeilen und Kommentare ignorieren, IPs extrahieren + while IFS= read -r line; do + # Leerzeilen überspringen + [[ -z "$line" ]] && continue + # Kommentare überspringen (# am Anfang) + [[ "$line" =~ ^[[:space:]]*# ]] && continue + # Whitespace trimmen + line=$(echo "$line" | xargs) + # Leere Zeilen nach Trim überspringen + [[ -z "$line" ]] && continue + # CIDR-Notation oder reine IP ausgeben + echo "$line" + done < "$cache_file" +} + +# ─── Aktuelle externe Sperren ermitteln ────────────────────────────────────── +get_currently_banned_external_ips() { + for state_file in "${STATE_DIR}"/ext_*.ban; do + [[ -f "$state_file" ]] || continue + grep '^CLIENT_IP=' "$state_file" | cut -d= -f2 + done +} + +# ─── Abgelaufene externe Sperren prüfen ───────────────────────────────────── +check_expired_external_bans() { + [[ "$EXTERNAL_BLOCKLIST_BAN_DURATION" -gt 0 ]] || return + + local now + now=$(date '+%s') + + for state_file in "${STATE_DIR}"/ext_*.ban; do + [[ -f "$state_file" ]] || continue + + local ban_until_epoch + ban_until_epoch=$(grep '^BAN_UNTIL_EPOCH=' "$state_file" | cut -d= -f2) + local client_ip + client_ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2) + + if [[ -n "$ban_until_epoch" && "$ban_until_epoch" -gt 0 && "$now" -ge "$ban_until_epoch" ]]; then + unban_ip "$client_ip" "external-blocklist-expired" + fi + done +} + +# ─── Blocklisten synchronisieren ───────────────────────────────────────────── +sync_blocklists() { + local any_updated=false + + # Alle URLs holen + IFS=',' read -ra urls <<< "$EXTERNAL_BLOCKLIST_URLS" + local index=0 + + for url in "${urls[@]}"; do + url=$(echo "$url" | xargs) # trim + [[ -z "$url" ]] && continue + + if download_blocklist "$url" "$index"; then + any_updated=true + fi + index=$((index + 1)) + done + + # Alle gewünschten IPs zusammenführen (aus allen Cache-Dateien) + local all_desired_ips_file="${EXTERNAL_BLOCKLIST_CACHE_DIR}/.all_ips.tmp" + > "$all_desired_ips_file" + + for cache_file in "${EXTERNAL_BLOCKLIST_CACHE_DIR}"/blocklist_*.txt; do + [[ -f "$cache_file" ]] || continue + parse_blocklist_ips "$cache_file" >> "$all_desired_ips_file" + done + + # Duplikate entfernen und sortieren + local unique_ips_file="${EXTERNAL_BLOCKLIST_CACHE_DIR}/.all_ips_unique.tmp" + sort -u "$all_desired_ips_file" > "$unique_ips_file" + + local desired_count + desired_count=$(wc -l < "$unique_ips_file" | xargs) + log "DEBUG" "Externe Blockliste enthält $desired_count eindeutige IPs" + + # ─── Neue IPs sperren ──────────────────────────────────────────────────── + local new_bans=0 + while IFS= read -r ip; do + [[ -z "$ip" ]] && continue + + # Whitelist prüfen + if is_whitelisted "$ip"; then + log "DEBUG" "IP $ip ist auf der Whitelist - überspringe (externe Blocklist)" + continue + fi + + ban_ip "$ip" + new_bans=$((new_bans + 1)) + done < "$unique_ips_file" + + # ─── Entfernte IPs entsperren ──────────────────────────────────────────── + if [[ "$EXTERNAL_BLOCKLIST_AUTO_UNBAN" == "true" ]]; then + local removed_count=0 + while IFS= read -r banned_ip; do + [[ -z "$banned_ip" ]] && continue + # Prüfen ob die IP noch in der gewünschten Liste ist + if ! grep -qxF "$banned_ip" "$unique_ips_file" 2>/dev/null; then + unban_ip "$banned_ip" "external-blocklist-removed" + removed_count=$((removed_count + 1)) + fi + done < <(get_currently_banned_external_ips) + + if [[ $removed_count -gt 0 ]]; then + log "INFO" "$removed_count IPs aus externer Blocklist entfernt und entsperrt" + fi + fi + + # Abgelaufene Sperren prüfen (nur bei zeitlich begrenzten Sperren) + check_expired_external_bans + + # Aufräumen + rm -f "$all_desired_ips_file" "$unique_ips_file" + + if [[ "$new_bans" -gt 0 ]]; then + log "INFO" "$new_bans neue IPs aus externer Blocklist gesperrt" + fi +} + +# ─── PID-Management ────────────────────────────────────────────────────────── +write_pid() { + echo $$ > "$WORKER_PID_FILE" +} + +cleanup() { + log "INFO" "Externer Blocklist-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" "Blocklist-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 Blocklist-Worker - Status" + echo "═══════════════════════════════════════════════════════════════" + echo "" + + if [[ "$EXTERNAL_BLOCKLIST_ENABLED" != "true" ]]; then + echo " ⚠️ Externer Blocklist-Worker ist deaktiviert" + echo " Aktivieren: EXTERNAL_BLOCKLIST_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 Blocklisten:" + IFS=',' read -ra urls <<< "$EXTERNAL_BLOCKLIST_URLS" + local index=0 + for url in "${urls[@]}"; do + url=$(echo "$url" | xargs) + [[ -z "$url" ]] && continue + local cache_file="${EXTERNAL_BLOCKLIST_CACHE_DIR}/blocklist_${index}.txt" + local ip_count=0 + if [[ -f "$cache_file" ]]; then + ip_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 " IPs: $ip_count | Zuletzt aktualisiert: $last_modified" + else + echo " [$index] $url (noch nicht heruntergeladen)" + fi + index=$((index + 1)) + done + + echo "" + + # Aktive externe Sperren + local ext_ban_count=0 + for state_file in "${STATE_DIR}"/ext_*.ban; do + [[ -f "$state_file" ]] || continue + ext_ban_count=$((ext_ban_count + 1)) + done + echo " Aktive Sperren (externe Blocklist): $ext_ban_count" + + echo "" + echo " Prüfintervall: ${EXTERNAL_BLOCKLIST_INTERVAL}s" + echo " Auto-Unban: ${EXTERNAL_BLOCKLIST_AUTO_UNBAN}" + if [[ "$EXTERNAL_BLOCKLIST_BAN_DURATION" -gt 0 ]]; then + echo " Sperrdauer: ${EXTERNAL_BLOCKLIST_BAN_DURATION}s" + else + echo " Sperrdauer: permanent (bis aus Liste entfernt)" + fi + echo "" + echo "═══════════════════════════════════════════════════════════════" +} + +# ─── Einmalig synchronisieren ──────────────────────────────────────────────── +run_once() { + init_directories + setup_iptables_chain + + if [[ -z "${EXTERNAL_BLOCKLIST_URLS:-}" ]]; then + log "ERROR" "Keine externen Blocklist-URLs konfiguriert (EXTERNAL_BLOCKLIST_URLS)" + exit 1 + fi + + log "INFO" "Einmalige Blocklist-Synchronisation..." + sync_blocklists + log "INFO" "Synchronisation abgeschlossen" +} + +# ─── Hauptschleife ────────────────────────────────────────────────────────── +main_loop() { + init_directories + setup_iptables_chain + + if [[ -z "${EXTERNAL_BLOCKLIST_URLS:-}" ]]; then + log "ERROR" "Keine externen Blocklist-URLs konfiguriert (EXTERNAL_BLOCKLIST_URLS)" + exit 1 + fi + + log "INFO" "═══════════════════════════════════════════════════════════" + log "INFO" "Externer Blocklist-Worker gestartet" + log "INFO" " URLs: ${EXTERNAL_BLOCKLIST_URLS}" + log "INFO" " Prüfintervall: ${EXTERNAL_BLOCKLIST_INTERVAL}s" + log "INFO" " Auto-Unban: ${EXTERNAL_BLOCKLIST_AUTO_UNBAN}" + log "INFO" "═══════════════════════════════════════════════════════════" + + while true; do + sync_blocklists + sleep "$EXTERNAL_BLOCKLIST_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 "Blocklist-Worker gestoppt" + else + echo "Blocklist-Worker läuft nicht" + fi + ;; + sync) + run_once + ;; + status) + init_directories + show_status + ;; + flush) + init_directories + echo "Entferne alle externen Blocklist-Sperren..." + for state_file in "${STATE_DIR}"/ext_*.ban; do + [[ -f "$state_file" ]] || continue + _ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2) + unban_ip "$_ip" "manual-flush" + done + echo "Alle externen Blocklist-Sperren aufgehoben" + ;; + *) + cat << USAGE +AdGuard Shield - Externer Blocklist-Worker + +Nutzung: $0 {start|stop|sync|status|flush} + +Befehle: + start Startet den Worker (Dauerbetrieb) + stop Stoppt den Worker + sync Einmalige Synchronisation + status Zeigt Status und konfigurierte Listen + flush Entfernt alle externen Blocklist-Sperren + +Konfiguration: $CONFIG_FILE + +USAGE + exit 0 + ;; +esac diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..275b8a0 --- /dev/null +++ b/install.sh @@ -0,0 +1,300 @@ +#!/bin/bash +############################################################################### +# AdGuard Shield - Installer +# Autor: Patrick Asmus +# E-Mail: support@techniverse.net +# Lizenz: MIT +############################################################################### + +VERSION="1.0.0" + +set -euo pipefail + +INSTALL_DIR="/opt/adguard-ratelimit" +SERVICE_FILE="/etc/systemd/system/adguard-ratelimit.service" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Farben +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +print_header() { + echo "" + echo -e "${BLUE}" + echo " ▄▄▄ ▓█████▄ ▄████ █ ██ ▄▄▄ ██▀███ ▓█████▄ ██████ ██░ ██ ██▓▓█████ ██▓ ▓█████▄ " + echo "▒████▄ ▒██▀ ██▌ ██▒ ▀█▒ ██ ▓██▒▒████▄ ▓██ ▒ ██▒▒██▀ ██▌ ▒██ ▒ ▓██░ ██▒▓██▒▓█ ▀ ▓██▒ ▒██▀ ██▌" + echo "▒██ ▀█▄ ░██ █▌▒██░▄▄▄░▓██ ▒██░▒██ ▀█▄ ▓██ ░▄█ ▒░██ █▌ ░ ▓██▄ ▒██▀▀██░▒██▒▒███ ▒██░ ░██ █▌" + echo "░██▄▄▄▄██ ░▓█▄ ▌░▓█ ██▓▓▓█ ░██░░██▄▄▄▄██ ▒██▀▀█▄ ░▓█▄ ▌ ▒ ██▒░▓█ ░██ ░██░▒▓█ ▄ ▒██░ ░▓█▄ ▌" + echo " ▓█ ▓██▒░▒████▓ ░▒▓███▀▒▒▒█████▓ ▓█ ▓██▒░██▓ ▒██▒░▒████▓ ▒██████▒▒░▓█▒░██▓░██░░▒████▒░██████▒░▒████▓ " + echo " ▒▒ ▓▒█░ ▒▒▓ ▒ ░▒ ▒ ░▒▓▒ ▒ ▒ ▒▒ ▓▒█░░ ▒▓ ░▒▓░ ▒▒▓ ▒ ▒ ▒▓▒ ▒ ░ ▒ ░░▒░▒░▓ ░░ ▒░ ░░ ▒░▓ ░ ▒▒▓ ▒ " + echo " ▒ ▒▒ ░ ░ ▒ ▒ ░ ░ ░░▒░ ░ ░ ▒ ▒▒ ░ ░▒ ░ ▒░ ░ ▒ ▒ ░ ░▒ ░ ░ ▒ ░▒░ ░ ▒ ░ ░ ░ ░░ ░ ▒ ░ ░ ▒ ▒ " + echo " ░ ▒ ░ ░ ░ ░ ░ ░ ░░░ ░ ░ ░ ▒ ░░ ░ ░ ░ ░ ░ ░ ░ ░ ░░ ░ ▒ ░ ░ ░ ░ ░ ░ ░ " + echo " ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ " + echo " ░ ░ ░ " + echo -e "${NC}" + echo -e "${GREEN} Version: ${VERSION}${NC}" + echo -e "${BLUE} Autor: Patrick Asmus${NC}" + echo -e "${BLUE} E-Mail: support@techniverse.net${NC}" + echo "" + echo -e "${BLUE}═══════════════════════════════════════════════════════════════════════════════════════════════════════════════${NC}" + echo "" +} + +check_root() { + if [[ $EUID -ne 0 ]]; then + echo -e "${RED}Dieses Script muss als root ausgeführt werden!${NC}" >&2 + echo "Bitte mit 'sudo $0' ausführen." + exit 1 + fi +} + +check_dependencies() { + echo -e "${YELLOW}Prüfe Abhängigkeiten...${NC}" + local missing=() + + for cmd in curl jq iptables ip6tables; do + if command -v "$cmd" &>/dev/null; then + echo -e " ✅ $cmd" + else + echo -e " ❌ $cmd" + missing+=("$cmd") + fi + done + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "" + echo -e "${YELLOW}Installiere fehlende Pakete...${NC}" + + if command -v apt &>/dev/null; then + apt update -qq + apt install -y -qq curl jq iptables + elif command -v dnf &>/dev/null; then + dnf install -y curl jq iptables + elif command -v yum &>/dev/null; then + yum install -y curl jq iptables + elif command -v pacman &>/dev/null; then + pacman -S --noconfirm curl jq iptables + else + echo -e "${RED}Konnte Paketmanager nicht erkennen. Bitte installiere manuell: ${missing[*]}${NC}" + exit 1 + fi + fi + echo "" +} + +install_files() { + echo -e "${YELLOW}Installiere Dateien nach $INSTALL_DIR ...${NC}" + + mkdir -p "$INSTALL_DIR" + mkdir -p /var/lib/adguard-ratelimit + mkdir -p /var/log + + # Dateien kopieren + cp "$SCRIPT_DIR/adguard-ratelimit.sh" "$INSTALL_DIR/" + 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/" + + # Konfigurationsdatei nur kopieren wenn nicht vorhanden (Update-Sicher) + if [[ ! -f "$INSTALL_DIR/adguard-ratelimit.conf" ]]; then + cp "$SCRIPT_DIR/adguard-ratelimit.conf" "$INSTALL_DIR/" + echo -e " ✅ Konfiguration kopiert (NEU)" + else + cp "$SCRIPT_DIR/adguard-ratelimit.conf" "$INSTALL_DIR/adguard-ratelimit.conf.new" + echo -e " ℹ️ Konfiguration existiert bereits - neue Version als .conf.new gespeichert" + fi + + # Ausführbar machen + chmod +x "$INSTALL_DIR/adguard-ratelimit.sh" + chmod +x "$INSTALL_DIR/iptables-helper.sh" + chmod +x "$INSTALL_DIR/unban-expired.sh" + chmod +x "$INSTALL_DIR/external-blocklist-worker.sh" + chmod 600 "$INSTALL_DIR/adguard-ratelimit.conf" + + echo -e " ✅ Dateien installiert" + echo "" +} + +install_service() { + echo -e "${YELLOW}Installiere systemd Service...${NC}" + + cp "$SCRIPT_DIR/adguard-ratelimit.service" "$SERVICE_FILE" + systemctl daemon-reload + systemctl enable adguard-ratelimit.service + + echo -e " ✅ Service installiert und aktiviert" + echo "" +} + +configure() { + echo -e "${YELLOW}Konfiguration:${NC}" + echo "" + + local conf="$INSTALL_DIR/adguard-ratelimit.conf" + + # AdGuard URL + read -rp " AdGuard Home URL [http://127.0.0.1:3000]: " adguard_url + adguard_url="${adguard_url:-http://127.0.0.1:3000}" + sed -i "s|^ADGUARD_URL=.*|ADGUARD_URL=\"$adguard_url\"|" "$conf" + + # Benutzername + read -rp " AdGuard Home Benutzername [admin]: " adguard_user + adguard_user="${adguard_user:-admin}" + sed -i "s|^ADGUARD_USER=.*|ADGUARD_USER=\"$adguard_user\"|" "$conf" + + # Passwort + read -rsp " AdGuard Home Passwort: " adguard_pass + echo "" + if [[ -n "$adguard_pass" ]]; then + # Einfache Quotes damit $-Zeichen im Passwort nicht expandiert werden + sed -i "s|^ADGUARD_PASS=.*|ADGUARD_PASS='$adguard_pass'|" "$conf" + fi + + # Rate Limit + read -rp " Max. Anfragen pro Domain/Client pro Minute [30]: " rate_limit + rate_limit="${rate_limit:-30}" + sed -i "s|^RATE_LIMIT_MAX_REQUESTS=.*|RATE_LIMIT_MAX_REQUESTS=$rate_limit|" "$conf" + + # Sperrdauer + read -rp " Sperrdauer in Sekunden [3600]: " ban_duration + ban_duration="${ban_duration:-3600}" + sed -i "s|^BAN_DURATION=.*|BAN_DURATION=$ban_duration|" "$conf" + + # Whitelist + read -rp " Whitelist IPs (kommagetrennt) [127.0.0.1,::1]: " whitelist + whitelist="${whitelist:-127.0.0.1,::1}" + sed -i "s|^WHITELIST=.*|WHITELIST=\"$whitelist\"|" "$conf" + + echo "" + echo -e " ✅ Konfiguration gespeichert" + echo "" +} + +test_connection() { + echo -e "${YELLOW}Teste Verbindung zur AdGuard Home API...${NC}" + + source "$INSTALL_DIR/adguard-ratelimit.conf" + + local response + response=$(curl -s -o /dev/null -w "%{http_code}" \ + -u "${ADGUARD_USER}:${ADGUARD_PASS}" \ + --connect-timeout 5 \ + "${ADGUARD_URL}/control/querylog?limit=1" 2>/dev/null) + + if [[ "$response" == "200" ]]; then + echo -e " ✅ Verbindung erfolgreich! (HTTP $response)" + else + echo -e " ❌ Verbindung fehlgeschlagen (HTTP $response)" + echo -e " ${YELLOW}Bitte prüfe URL und Zugangsdaten in: $INSTALL_DIR/adguard-ratelimit.conf${NC}" + fi + echo "" +} + +print_summary() { + echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${GREEN} AdGuard Shield - Installation abgeschlossen!${NC}" + echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}" + echo "" + echo " Installationspfad: $INSTALL_DIR" + echo " Konfiguration: $INSTALL_DIR/adguard-ratelimit.conf" + echo " Service: adguard-ratelimit.service" + echo " Log-Datei: /var/log/adguard-ratelimit.log" + echo "" + echo " Nächste Schritte:" + echo " ─────────────────" + echo " 1. Konfiguration prüfen:" + echo " sudo nano $INSTALL_DIR/adguard-ratelimit.conf" + echo "" + echo " 2. Erst im Dry-Run testen:" + echo " sudo $INSTALL_DIR/adguard-ratelimit.sh dry-run" + echo "" + echo " 3. Service starten:" + echo " sudo systemctl start adguard-ratelimit" + echo "" + echo " 4. Status prüfen:" + echo " sudo systemctl status adguard-ratelimit" + echo " sudo $INSTALL_DIR/adguard-ratelimit.sh status" + echo "" + echo " 5. Logs verfolgen:" + echo " sudo journalctl -u adguard-ratelimit -f" + echo " sudo tail -f /var/log/adguard-ratelimit.log" + echo "" + echo " Weitere Befehle:" + echo " sudo $INSTALL_DIR/iptables-helper.sh status" + echo " sudo $INSTALL_DIR/adguard-ratelimit.sh flush" + echo " sudo $INSTALL_DIR/adguard-ratelimit.sh unban " + echo "" +} + +# ─── Deinstallation ───────────────────────────────────────────────────────── +uninstall() { + echo -e "${YELLOW}Deinstalliere AdGuard Shield...${NC}" + echo "" + + # Service stoppen und deaktivieren + if systemctl is-active adguard-ratelimit &>/dev/null; then + systemctl stop adguard-ratelimit + echo " ✅ Service gestoppt" + fi + if systemctl is-enabled adguard-ratelimit &>/dev/null; then + systemctl disable adguard-ratelimit + echo " ✅ Service deaktiviert" + fi + rm -f "$SERVICE_FILE" + systemctl daemon-reload + echo " ✅ Service-Datei entfernt" + + # iptables Chain aufräumen + if [[ -f "$INSTALL_DIR/iptables-helper.sh" ]]; then + bash "$INSTALL_DIR/iptables-helper.sh" remove || true + fi + + # Dateien entfernen + read -rp " Konfiguration und Logs behalten? [j/N]: " keep + if [[ "${keep,,}" == "j" ]]; then + rm -f "$INSTALL_DIR/adguard-ratelimit.sh" + rm -f "$INSTALL_DIR/iptables-helper.sh" + echo " ✅ Scripts entfernt (Konfiguration behalten)" + else + rm -rf "$INSTALL_DIR" + rm -rf /var/lib/adguard-ratelimit + rm -f /var/log/adguard-ratelimit.log* + echo " ✅ Alles entfernt" + fi + + echo "" + echo -e "${GREEN}Deinstallation abgeschlossen.${NC}" +} + +# ─── Hauptprogramm ────────────────────────────────────────────────────────── +case "${1:-install}" in + install) + print_header + check_root + check_dependencies + install_files + configure + install_service + test_connection + print_summary + ;; + uninstall) + print_header + check_root + uninstall + ;; + update) + print_header + check_root + install_files + systemctl daemon-reload + echo -e "${GREEN}AdGuard Shield Update abgeschlossen. Service neustarten mit: sudo systemctl restart adguard-ratelimit${NC}" + ;; + *) + echo "Nutzung: $0 {install|uninstall|update}" + exit 1 + ;; +esac diff --git a/iptables-helper.sh b/iptables-helper.sh new file mode 100644 index 0000000..160afb8 --- /dev/null +++ b/iptables-helper.sh @@ -0,0 +1,234 @@ +#!/bin/bash +############################################################################### +# AdGuard Shield - iptables Helper +# Verwaltet die Firewall-Regeln für AdGuard Shield +# Kann auch standalone genutzt werden zur Verwaltung der Sperren +############################################################################### + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_FILE="${SCRIPT_DIR}/adguard-ratelimit.conf" + +if [[ ! -f "$CONFIG_FILE" ]]; then + echo "FEHLER: Konfigurationsdatei nicht gefunden: $CONFIG_FILE" >&2 + exit 1 +fi +source "$CONFIG_FILE" + +# ─── Chain erstellen ───────────────────────────────────────────────────────── +create_chain() { + echo "Erstelle iptables Chain: $IPTABLES_CHAIN" + + # IPv4 + 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 + echo " ✅ IPv4 Chain erstellt" + else + echo " ℹ️ IPv4 Chain existiert bereits" + fi + + # IPv6 + 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 + echo " ✅ IPv6 Chain erstellt" + else + echo " ℹ️ IPv6 Chain existiert bereits" + fi +} + +# ─── Chain entfernen ───────────────────────────────────────────────────────── +remove_chain() { + echo "Entferne iptables Chain: $IPTABLES_CHAIN" + + # IPv4 - Referenzen entfernen, dann Chain löschen + if iptables -n -L "$IPTABLES_CHAIN" &>/dev/null; then + for port in $BLOCKED_PORTS; do + iptables -D INPUT -p tcp --dport "$port" -j "$IPTABLES_CHAIN" 2>/dev/null || true + iptables -D INPUT -p udp --dport "$port" -j "$IPTABLES_CHAIN" 2>/dev/null || true + done + iptables -F "$IPTABLES_CHAIN" 2>/dev/null || true + iptables -X "$IPTABLES_CHAIN" 2>/dev/null || true + echo " ✅ IPv4 Chain entfernt" + else + echo " ℹ️ IPv4 Chain existiert nicht" + fi + + # IPv6 + if ip6tables -n -L "$IPTABLES_CHAIN" &>/dev/null; then + for port in $BLOCKED_PORTS; do + ip6tables -D INPUT -p tcp --dport "$port" -j "$IPTABLES_CHAIN" 2>/dev/null || true + ip6tables -D INPUT -p udp --dport "$port" -j "$IPTABLES_CHAIN" 2>/dev/null || true + done + ip6tables -F "$IPTABLES_CHAIN" 2>/dev/null || true + ip6tables -X "$IPTABLES_CHAIN" 2>/dev/null || true + echo " ✅ IPv6 Chain entfernt" + else + echo " ℹ️ IPv6 Chain existiert nicht" + fi +} + +# ─── Chain leeren ──────────────────────────────────────────────────────────── +flush_chain() { + echo "Leere iptables Chain: $IPTABLES_CHAIN" + iptables -F "$IPTABLES_CHAIN" 2>/dev/null && echo " ✅ IPv4 geleert" || echo " ⚠️ IPv4 Chain nicht gefunden" + ip6tables -F "$IPTABLES_CHAIN" 2>/dev/null && echo " ✅ IPv6 geleert" || echo " ⚠️ IPv6 Chain nicht gefunden" + + # State-Dateien auch aufräumen + rm -f "${STATE_DIR}"/*.ban 2>/dev/null || true + echo " ✅ State-Dateien bereinigt" +} + +# ─── IP manuell sperren ───────────────────────────────────────────────────── +ban_ip() { + local ip="$1" + echo "Sperre IP: $ip" + + if [[ "$ip" == *:* ]]; then + ip6tables -I "$IPTABLES_CHAIN" -s "$ip" -j DROP + echo " ✅ IPv6 Adresse gesperrt" + else + iptables -I "$IPTABLES_CHAIN" -s "$ip" -j DROP + echo " ✅ IPv4 Adresse gesperrt" + fi +} + +# ─── IP entsperren ────────────────────────────────────────────────────────── +unban_ip() { + local ip="$1" + echo "Entsperre IP: $ip" + + if [[ "$ip" == *:* ]]; then + ip6tables -D "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null \ + && echo " ✅ IPv6 Adresse entsperrt" \ + || echo " ⚠️ IPv6 Regel nicht gefunden" + else + iptables -D "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null \ + && echo " ✅ IPv4 Adresse entsperrt" \ + || echo " ⚠️ IPv4 Regel nicht gefunden" + fi + + # State-Datei entfernen + rm -f "${STATE_DIR}/${ip//[:\/]/_}.ban" 2>/dev/null || true +} + +# ─── Status anzeigen ───────────────────────────────────────────────────────── +show_rules() { + echo "" + echo "══════════════════════════════════════════════════════════════════" + echo " iptables Regeln für Chain: $IPTABLES_CHAIN" + echo "══════════════════════════════════════════════════════════════════" + echo "" + + echo " --- IPv4 ---" + if iptables -n -L "$IPTABLES_CHAIN" --line-numbers &>/dev/null; then + iptables -n -L "$IPTABLES_CHAIN" --line-numbers -v 2>/dev/null | sed 's/^/ /' + else + echo " Chain existiert nicht" + fi + + echo "" + echo " --- IPv6 ---" + if ip6tables -n -L "$IPTABLES_CHAIN" --line-numbers &>/dev/null; then + ip6tables -n -L "$IPTABLES_CHAIN" --line-numbers -v 2>/dev/null | sed 's/^/ /' + else + echo " Chain existiert nicht" + fi + + echo "" + echo " --- Aktive Sperren (State) ---" + local count=0 + if [[ -d "$STATE_DIR" ]]; then + for f in "${STATE_DIR}"/*.ban; do + [[ -f "$f" ]] || continue + count=$((count + 1)) + local ip domain ban_time ban_until + ip=$(grep '^CLIENT_IP=' "$f" | cut -d= -f2) + domain=$(grep '^DOMAIN=' "$f" | cut -d= -f2) + ban_time=$(grep '^BAN_TIME=' "$f" | cut -d= -f2) + ban_until=$(grep '^BAN_UNTIL=' "$f" | cut -d= -f2) + printf " %-20s %-30s seit %-20s bis %s\n" "$ip" "$domain" "$ban_time" "$ban_until" + done + fi + + if [[ $count -eq 0 ]]; then + echo " Keine aktiven Sperren" + fi + echo "" +} + +# ─── Persistenz (iptables-save/restore kompatibel) ────────────────────────── +save_rules() { + local save_file="${STATE_DIR}/iptables-rules.v4" + local save_file6="${STATE_DIR}/iptables-rules.v6" + + iptables-save > "$save_file" 2>/dev/null && echo " ✅ IPv4 Regeln gespeichert: $save_file" + ip6tables-save > "$save_file6" 2>/dev/null && echo " ✅ IPv6 Regeln gespeichert: $save_file6" +} + +restore_rules() { + local save_file="${STATE_DIR}/iptables-rules.v4" + local save_file6="${STATE_DIR}/iptables-rules.v6" + + [[ -f "$save_file" ]] && iptables-restore < "$save_file" && echo " ✅ IPv4 Regeln wiederhergestellt" + [[ -f "$save_file6" ]] && ip6tables-restore < "$save_file6" && echo " ✅ IPv6 Regeln wiederhergestellt" +} + +# ─── Hauptprogramm ────────────────────────────────────────────────────────── +case "${1:-help}" in + create) + create_chain + ;; + remove) + remove_chain + ;; + flush) + flush_chain + ;; + ban) + [[ -z "${2:-}" ]] && { echo "Nutzung: $0 ban " >&2; exit 1; } + ban_ip "$2" + ;; + unban) + [[ -z "${2:-}" ]] && { echo "Nutzung: $0 unban " >&2; exit 1; } + unban_ip "$2" + ;; + status|show) + show_rules + ;; + save) + save_rules + ;; + restore) + restore_rules + ;; + *) + cat << USAGE +iptables Helper für AdGuard Rate-Limit + +Nutzung: $0 {create|remove|flush|ban|unban|status|save|restore} + +Befehle: + create Erstellt die iptables Chain + remove Entfernt die Chain und alle Regeln + flush Leert alle Regeln in der Chain + ban Sperrt eine IP-Adresse manuell + unban Entsperrt eine IP-Adresse + status Zeigt alle aktuellen Regeln + save Speichert die aktuellen Regeln + restore Stellt gespeicherte Regeln wieder her + +Chain-Name: $IPTABLES_CHAIN +Gesperrte Ports: $BLOCKED_PORTS + +USAGE + ;; +esac diff --git a/unban-expired.sh b/unban-expired.sh new file mode 100644 index 0000000..4ff4cfc --- /dev/null +++ b/unban-expired.sh @@ -0,0 +1,75 @@ +#!/bin/bash +############################################################################### +# AdGuard Shield - Cron-basierter Unban-Timer +# Kann als Alternative zum Haupt-Script für das Entsperren genutzt werden. +# Wird z.B. alle 5 Minuten per Cron aufgerufen um abgelaufene Sperren zu prüfen. +# +# Crontab-Eintrag: +# */5 * * * * /opt/adguard-ratelimit/unban-expired.sh +############################################################################### + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_FILE="${SCRIPT_DIR}/adguard-ratelimit.conf" + +if [[ ! -f "$CONFIG_FILE" ]]; then + exit 1 +fi +source "$CONFIG_FILE" + +BAN_HISTORY_FILE="${BAN_HISTORY_FILE:-/var/log/adguard-ratelimit-bans.log}" +LOG_PREFIX="[$(date '+%Y-%m-%d %H:%M:%S')] [UNBAN-TIMER]" +NOW=$(date '+%s') + +# History-Eintrag schreiben +log_ban_history() { + local action="$1" + local client_ip="$2" + local domain="${3:-}" + local count="${4:-}" + local reason="${5:-}" + local 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 | GRUND" >> "$BAN_HISTORY_FILE" + echo "#─────────────────────────────────────────────────────────────────────────────────" >> "$BAN_HISTORY_FILE" + fi + + printf "%-19s | %-6s | %-39s | %-30s | %-8s | %-10s | %s\n" \ + "$timestamp" "$action" "$client_ip" "${domain:--}" "${count:--}" "-" "${reason:-expired}" \ + >> "$BAN_HISTORY_FILE" +} + +unban_count=0 + +for state_file in "${STATE_DIR}"/*.ban; do + [[ -f "$state_file" ]] || continue + + ban_until_epoch=$(grep '^BAN_UNTIL_EPOCH=' "$state_file" | cut -d= -f2) + client_ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2) + domain=$(grep '^DOMAIN=' "$state_file" | cut -d= -f2) + + if [[ -n "$ban_until_epoch" && "$NOW" -ge "$ban_until_epoch" ]]; then + echo "$LOG_PREFIX Entsperre abgelaufene Sperre: $client_ip" >> "$LOG_FILE" + + # 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 + + # Ban-History Eintrag + log_ban_history "UNBAN" "$client_ip" "$domain" "-" "expired-cron" + + rm -f "$state_file" + unban_count=$((unban_count + 1)) + fi +done + +if [[ $unban_count -gt 0 ]]; then + echo "$LOG_PREFIX $unban_count Sperren aufgehoben" >> "$LOG_FILE" +fi