From b93689bbafe3c3cf52b6b50e8ff605ac9d3ecf4d Mon Sep 17 00:00:00 2001 From: Patrick Asmus Date: Tue, 28 Apr 2026 23:31:58 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20DNS-Flood-Watchlist=20=E2=80=93=20s?= =?UTF-8?q?ofortiger=20permanenter=20Ban=20f=C3=BCr=20definierte=20Domains?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 + adguard-shield.conf | 4 ++ adguard-shield.sh | 82 +++++++++++++++++++++++++++++++++----- docs/architektur.md | 13 ++++++ docs/benachrichtigungen.md | 13 ++++++ docs/konfiguration.md | 30 +++++++++++++- 6 files changed, 133 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 395cf50..1a7cb9f 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Das schützt klassische DNS-Anfragen genauso wie DoH, DoT und DoQ, ohne deine be - Automatische Sperren bei Rate-Limit-Verstößen - Erkennung von Random-Subdomain-Floods, z.B. `abc123.example.com` +- DNS-Flood-Watchlist: sofortiger permanenter Ban + AbuseIPDB-Meldung für definierte Domains - Progressive Sperren für Wiederholungstäter, ähnlich wie bei fail2ban - Unterstützung für DNS, DoH, DoT, DoQ und DNSCrypt - IPv4 und IPv6 @@ -127,6 +128,7 @@ Wichtige Startpunkte: - `RATE_LIMIT_MAX_REQUESTS`, `RATE_LIMIT_WINDOW` und `CHECK_INTERVAL` für die Erkennung - `BAN_DURATION` und `PROGRESSIVE_BAN_*` für temporäre und progressive Sperren - `WHITELIST` für vertrauenswürdige Clients wie Router, Management-IPs oder lokale Resolver +- `DNS_FLOOD_WATCHLIST_*` für sofortigen Permanent-Ban bei bekannten Flood-Domains - `NOTIFY_*`, `REPORT_*`, `GEOIP_*`, `EXTERNAL_BLOCKLIST_*` und `EXTERNAL_WHITELIST_*` für optionale Funktionen Bei Updates migriert der Installer die bestehende Konfiguration automatisch: vorhandene Werte bleiben erhalten, neue Parameter werden ergänzt und die alte Datei wird als `adguard-shield.conf.old` gesichert. diff --git a/adguard-shield.conf b/adguard-shield.conf index aae7ca0..ffc2032 100644 --- a/adguard-shield.conf +++ b/adguard-shield.conf @@ -18,6 +18,10 @@ SUBDOMAIN_FLOOD_ENABLED=true SUBDOMAIN_FLOOD_MAX_UNIQUE=50 # Max. eindeutige Subdomains pro Basisdomain/Client SUBDOMAIN_FLOOD_WINDOW=60 # Zeitfenster in Sekunden +# --- DNS-Flood-Watchlist --- +DNS_FLOOD_WATCHLIST_ENABLED=false +DNS_FLOOD_WATCHLIST="" # Kommagetrennt, z.B. "example.com,evil.org" + # --- Sperr-Einstellungen --- BAN_DURATION=3600 # Basis-Sperrdauer in Sekunden IPTABLES_CHAIN="ADGUARD_SHIELD" diff --git a/adguard-shield.sh b/adguard-shield.sh index 5a4a2c5..7c032fa 100644 --- a/adguard-shield.sh +++ b/adguard-shield.sh @@ -369,6 +369,32 @@ is_whitelisted() { return 1 } +# ─── DNS-Flood-Watchlist Prüfung ──────────────────────────────────────────── +is_dns_flood_watchlist_match() { + local domain="$1" + + if [[ "${DNS_FLOOD_WATCHLIST_ENABLED:-false}" != "true" ]]; then + return 1 + fi + + if [[ -z "${DNS_FLOOD_WATCHLIST:-}" ]]; then + return 1 + fi + + local entry + IFS=',' read -ra watchlist_entries <<< "$DNS_FLOOD_WATCHLIST" + for entry in "${watchlist_entries[@]}"; do + entry=$(echo "$entry" | xargs) + [[ -z "$entry" ]] && continue + + if [[ "$domain" == "$entry" || "$domain" == *".$entry" ]]; then + return 0 + fi + done + + return 1 +} + # ─── iptables Chain Setup ──────────────────────────────────────────────────── setup_iptables_chain() { # IPv4 Chain erstellen falls nicht vorhanden @@ -425,6 +451,13 @@ ban_client() { fi fi + # DNS-Flood-Watchlist: Sofort permanent sperren + if [[ "$reason" == "dns-flood-watchlist" ]]; then + is_permanent=true + effective_duration=0 + log "WARN" "DNS-Flood-Watchlist: Erzwinge permanente Sperre für $client_ip ($domain)" + fi + local ban_until local ban_until_display if [[ "$is_permanent" == "true" ]]; then @@ -439,16 +472,16 @@ ban_client() { duration_display=$(format_duration "$effective_duration") if [[ "$DRY_RUN" == "true" ]]; then - if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" ]]; then + if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" && "$reason" != "dns-flood-watchlist" ]]; then log "WARN" "[DRY-RUN] WÜRDE sperren: $client_ip (${count}x $domain in ${window}s via $protocol) für ${duration_display} [Stufe $offense_level] [${reason}]" else - log "WARN" "[DRY-RUN] WÜRDE sperren: $client_ip (${count}x $domain in ${window}s via $protocol) [${reason}]" + log "WARN" "[DRY-RUN] WÜRDE sperren: $client_ip (${count}x $domain in ${window}s via $protocol) für ${duration_display} [${reason}]" fi log_ban_history "DRY" "$client_ip" "$domain" "$count" "dry-run (${reason})" "${duration_display}" "$protocol" return 0 fi - if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" ]]; then + if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" && "$reason" != "dns-flood-watchlist" ]]; then log "WARN" "SPERRE Client: $client_ip (${count}x $domain in ${window}s via $protocol) für ${duration_display} [Stufe ${offense_level}/${PROGRESSIVE_BAN_MAX_LEVEL:-0}] [${reason}]" else log "WARN" "SPERRE Client: $client_ip (${count}x $domain in ${window}s via $protocol) für ${duration_display} [${reason}]" @@ -480,7 +513,7 @@ EOF # Ban-History Eintrag local history_duration="${duration_display}" - [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" ]] && history_duration="${duration_display} (Stufe ${offense_level})" + [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" && "$reason" != "dns-flood-watchlist" ]] && history_duration="${duration_display} (Stufe ${offense_level})" log_ban_history "BAN" "$client_ip" "$domain" "$count" "$reason" "$history_duration" "$protocol" # Benachrichtigung senden @@ -574,6 +607,7 @@ send_notification() { local reason_label="Rate-Limit" [[ "$reason" == "subdomain-flood" ]] && reason_label="Subdomain-Flood" + [[ "$reason" == "dns-flood-watchlist" ]] && reason_label="DNS-Flood-Watchlist" local title local message @@ -591,12 +625,12 @@ send_notification() { abuseipdb_hint=$'\n⚠️ IP wurde an AbuseIPDB gemeldet' fi - # Dauer-Anzeige mit Stufe + # Dauer-Anzeige mit Stufe (nicht bei Watchlist – dort ist es immer permanent) local dur_line - if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" && -n "$offense_level" ]]; then + if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" && -n "$offense_level" && "$reason" != "dns-flood-watchlist" ]]; then dur_line="**${duration_display}** [Stufe ${offense_level}/${PROGRESSIVE_BAN_MAX_LEVEL:-0}]" else - dur_line=$(format_duration "${BAN_DURATION}") + dur_line="**${duration_display}**" fi message="🚫 AdGuard Shield Ban auf ${my_hostname}${abuseipdb_hint} @@ -839,7 +873,13 @@ analyze_queries() { continue fi - ban_client "$client" "$domain" "$count" "rate-limit" "$RATE_LIMIT_WINDOW" "$proto_display" + local ban_reason="rate-limit" + if is_dns_flood_watchlist_match "$domain"; then + ban_reason="dns-flood-watchlist" + log "WARN" "DNS-Flood-Watchlist Treffer: $client → $domain (${count}x in ${RATE_LIMIT_WINDOW}s) → permanenter Ban + AbuseIPDB" + fi + + ban_client "$client" "$domain" "$count" "$ban_reason" "$RATE_LIMIT_WINDOW" "$proto_display" fi done <<< "$violations" } @@ -961,7 +1001,13 @@ analyze_subdomain_flood() { continue fi - ban_client "$client" "*.${base_domain}" "$unique_count" "subdomain-flood" "$window" "$proto_display" + local ban_reason="subdomain-flood" + if is_dns_flood_watchlist_match "$base_domain"; then + ban_reason="dns-flood-watchlist" + log "WARN" "DNS-Flood-Watchlist Treffer (Subdomain-Flood): $client → *.${base_domain} → permanenter Ban + AbuseIPDB" + fi + + ban_client "$client" "*.${base_domain}" "$unique_count" "$ban_reason" "$window" "$proto_display" done <<< "$violations" } @@ -989,6 +1035,14 @@ show_status() { echo "" fi + # DNS-Flood-Watchlist Info + if [[ "${DNS_FLOOD_WATCHLIST_ENABLED:-false}" == "true" ]]; then + echo " 🎯 DNS-Flood-Watchlist: AKTIV" + echo " Domains: ${DNS_FLOOD_WATCHLIST:-}" + echo " Aktion: Sofort permanenter Ban + AbuseIPDB-Meldung" + echo "" + fi + # GeoIP-Filter Info if [[ "${GEOIP_ENABLED:-false}" == "true" ]]; then local geoip_mode_label @@ -1021,6 +1075,7 @@ show_status() { local reason_tag="" [[ "$s_reason" == "subdomain-flood" ]] && reason_tag=" (Subdomain-Flood)" + [[ "$s_reason" == "dns-flood-watchlist" ]] && reason_tag=" (DNS-Flood-Watchlist)" local count_info="" if [[ -n "$s_count" && "$s_count" != "-" ]]; then @@ -1033,7 +1088,9 @@ show_status() { local proto_tag=" via ${s_proto}" - if [[ "$s_perm" == "true" ]]; then + if [[ "$s_perm" == "true" && "$s_reason" == "dns-flood-watchlist" ]]; then + echo " 🚫 Gesperrt: $s_ip → $s_domain [PERMANENT${count_info}${proto_tag}]${reason_tag}" + elif [[ "$s_perm" == "true" ]]; then echo " 🚫 Gesperrt: $s_ip → $s_domain [PERMANENT, Stufe ${s_level:-?}${count_info}${proto_tag}]${reason_tag}" elif [[ -n "$s_level" && "$s_level" -gt 0 ]]; then echo " 🚫 Gesperrt: $s_ip → $s_domain [Stufe ${s_level}, $(format_duration "${s_dur:-$BAN_DURATION}"), bis $s_until${count_info}${proto_tag}]${reason_tag}" @@ -1312,6 +1369,11 @@ main_loop() { else log "INFO" " Subdomain-Flood-Schutz: deaktiviert" fi + if [[ "${DNS_FLOOD_WATCHLIST_ENABLED:-false}" == "true" ]]; then + log "INFO" " DNS-Flood-Watchlist: AKTIV (Domains: ${DNS_FLOOD_WATCHLIST:-})" + else + log "INFO" " DNS-Flood-Watchlist: deaktiviert" + fi if [[ "${ABUSEIPDB_ENABLED:-false}" == "true" ]]; then log "INFO" " AbuseIPDB Reporting: AKTIV (Kategorien: ${ABUSEIPDB_CATEGORIES:-4})" else diff --git a/docs/architektur.md b/docs/architektur.md index 34637a6..ee68854 100644 --- a/docs/architektur.md +++ b/docs/architektur.md @@ -56,6 +56,17 @@ > **Hinweis:** Die Subdomain-Flood-Erkennung hat ein eigenes Zeitfenster (`SUBDOMAIN_FLOOD_WINDOW`) und einen eigenen Schwellwert (`SUBDOMAIN_FLOOD_MAX_UNIQUE`), unabhängig von den Rate-Limit-Einstellungen. +### DNS-Flood-Watchlist-Sperre + +1. Client `10.0.0.42` fragt `microsoft.com` 35x in 60 Sekunden an +2. Monitor erkennt: 35 > 30 (Limit überschritten) +3. Domain `microsoft.com` steht auf der DNS-Flood-Watchlist → **sofortige permanente Sperre** +4. Progressive-Ban-Stufe wird ignoriert — kein stufenweises Hochstufen +5. IP wird an AbuseIPDB gemeldet (falls aktiviert) +6. Permanente Sperre bleibt bis zur manuellen Freigabe aktiv + +> **Hinweis:** Die Watchlist greift sowohl bei normalen Rate-Limit-Verstößen als auch bei Subdomain-Flood-Erkennungen. Subdomains werden automatisch erkannt: `foo.microsoft.com` matcht den Watchlist-Eintrag `microsoft.com`. + ## iptables Strategie Das Tool erstellt eine eigene Chain `ADGUARD_SHIELD`: @@ -210,8 +221,10 @@ ZEITSTEMPEL | AKTION | CLIENT-IP | DOMAIN |-------|----------| | `rate-limit` | Automatische Sperre wegen Limit-Überschreitung | | `subdomain-flood` | Sperre wegen zu vieler eindeutiger Subdomains einer Basisdomain | +| `dns-flood-watchlist` | Sofortige permanente Sperre + AbuseIPDB-Meldung (Domain auf der Watchlist) | | `dry-run` | Im Dry-Run erkannt (nicht wirklich gesperrt) | | `dry-run (subdomain-flood)` | Subdomain-Flood im Dry-Run erkannt | +| `dry-run (dns-flood-watchlist)` | Watchlist-Treffer im Dry-Run erkannt | | `expired` | Automatisch entsperrt nach Ablauf der Sperrdauer | | `expired-cron` | Entsperrt durch den Cron-Job (`unban-expired.sh`) | | `manual` | Manuell entsperrt per `unban`-Befehl | diff --git a/docs/benachrichtigungen.md b/docs/benachrichtigungen.md index 5911d99..a67eda7 100644 --- a/docs/benachrichtigungen.md +++ b/docs/benachrichtigungen.md @@ -167,6 +167,19 @@ Bei permanenter Sperre mit aktiviertem AbuseIPDB-Reporting erscheint zusätzlich > Whois: https://www.whois.com/whois/95.71.42.116 > AbuseIPDB: https://www.abuseipdb.com/check/95.71.42.116 +Bei DNS-Flood-Watchlist-Treffer (sofort permanent, ohne Stufe): + +> 🚫 AdGuard Shield Ban auf dns1 +> ⚠️ IP wurde an AbuseIPDB gemeldet +> --- +> IP: 95.71.42.116 +> Hostname: example-host.provider.net +> Grund: 45x microsoft.com in 60s via DNS, DNS-Flood-Watchlist +> Dauer: PERMANENT +> +> Whois: https://www.whois.com/whois/95.71.42.116 +> AbuseIPDB: https://www.abuseipdb.com/check/95.71.42.116 + ### Entsperrung (Unban) **Überschrift:** ✅ AdGuard Shield diff --git a/docs/konfiguration.md b/docs/konfiguration.md index 4bd4889..e28e4cc 100644 --- a/docs/konfiguration.md +++ b/docs/konfiguration.md @@ -70,6 +70,34 @@ xk9z3a.microsoft.com > **Tipp:** Der Schwellwert `SUBDOMAIN_FLOOD_MAX_UNIQUE` sollte hoch genug sein, um legitime Clients nicht zu stören (z.B. CDNs nutzen oft viele Subdomains). Ein Wert von 50–100 ist in den meisten Fällen sinnvoll. +### DNS-Flood-Watchlist + +Domains bei denen eine Rate-Limit-Überschreitung **sofort** zu einer **permanenten Sperre** und einer **AbuseIPDB-Meldung** führt — ohne progressive Eskalation. Ideal für bekannte Angriffsziele, die regelmäßig geflutet werden (z.B. `microsoft.com`, `google.com`). + +| Parameter | Standard | Beschreibung | +|-----------|----------|--------------| +| `DNS_FLOOD_WATCHLIST_ENABLED` | `false` | DNS-Flood-Watchlist aktivieren | +| `DNS_FLOOD_WATCHLIST` | *(leer)* | Überwachte Domains, kommagetrennt (z.B. `"microsoft.com,google.com"`) | + +#### Wie funktioniert die Watchlist? + +1. Die reguläre Rate-Limit-Prüfung erkennt, dass ein Client mehr als `RATE_LIMIT_MAX_REQUESTS` Anfragen für eine Domain gestellt hat +2. Zusätzlich wird geprüft, ob die angefragte Domain in der Watchlist steht (inkl. Subdomains: `foo.microsoft.com` matcht `microsoft.com`) +3. Trifft beides zu → **sofortige permanente Sperre** + **AbuseIPDB-Meldung** (falls aktiviert) + +Die Watchlist greift sowohl bei normalen Rate-Limit-Verstößen als auch bei Subdomain-Flood-Erkennungen. + +#### Beispiel + +```bash +DNS_FLOOD_WATCHLIST_ENABLED=true +DNS_FLOOD_WATCHLIST="microsoft.com,google.com,apple.com" +``` + +→ Ein Client der `35x foo.microsoft.com` in 60s abfragt (bei `RATE_LIMIT_MAX_REQUESTS=30`) wird **sofort permanent** gesperrt und an AbuseIPDB gemeldet. + +> **Hinweis:** Damit die AbuseIPDB-Meldung funktioniert, muss `ABUSEIPDB_ENABLED=true` und ein gültiger `ABUSEIPDB_API_KEY` konfiguriert sein. Ohne AbuseIPDB-Konfiguration wird nur permanent gesperrt. + ### Sperr-Einstellungen | Parameter | Standard | Beschreibung | @@ -251,7 +279,7 @@ sudo systemctl restart adguard-shield Der Report an AbuseIPDB enthält (auf Englisch): -- **Bei Rate-Limit:** `DNS flooding on our DNS server: 100x microsoft.com in 60s. Banned by Adguard Shield 🔗 https://tnvs.de/as` +- **Bei Rate-Limit / DNS-Flood-Watchlist:** `DNS flooding on our DNS server: 100x microsoft.com in 60s. Banned by Adguard Shield 🔗 https://tnvs.de/as` - **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"`). From 16b2c5950eb65a1fcfb6cf891848884e2a454ab5 Mon Sep 17 00:00:00 2001 From: Patrick Asmus Date: Tue, 28 Apr 2026 23:34:29 +0200 Subject: [PATCH 2/2] Release: Version v0.9.0 Co-Authored-By: Claude Opus 4.6 --- adguard-shield.sh | 2 +- docs/benachrichtigungen.md | 4 ++-- install.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/adguard-shield.sh b/adguard-shield.sh index 7c032fa..72cbc6b 100644 --- a/adguard-shield.sh +++ b/adguard-shield.sh @@ -8,7 +8,7 @@ # Lizenz: MIT ############################################################################### -VERSION="v0.8.2" +VERSION="v0.9.0" set -euo pipefail diff --git a/docs/benachrichtigungen.md b/docs/benachrichtigungen.md index a67eda7..a89148e 100644 --- a/docs/benachrichtigungen.md +++ b/docs/benachrichtigungen.md @@ -116,12 +116,12 @@ Bei Sperren aus der **externen Blocklist** werden Benachrichtigungen separat üb ### Service gestartet **Überschrift:** ✅ AdGuard Shield -> 🟢 AdGuard Shield v0.8.2 wurde auf dns1 gestartet. +> 🟢 AdGuard Shield v0.9.0 wurde auf dns1 gestartet. ### Service gestoppt **Überschrift:** 🚨 🛡️ AdGuard Shield -> 🔴 AdGuard Shield v0.8.2 wurde auf dns1 gestoppt. +> 🔴 AdGuard Shield v0.9.0 wurde auf dns1 gestoppt. ### Watchdog — Service wiederhergestellt **Überschrift:** 🔄 AdGuard Shield Watchdog diff --git a/install.sh b/install.sh index 2b247c1..89049fb 100644 --- a/install.sh +++ b/install.sh @@ -6,7 +6,7 @@ # Lizenz: MIT ############################################################################### -VERSION="v0.8.2" +VERSION="v0.9.0" set -euo pipefail