diff --git a/doc/konfiguration.md b/doc/konfiguration.md index ba6d7e8..684decb 100644 --- a/doc/konfiguration.md +++ b/doc/konfiguration.md @@ -151,12 +151,12 @@ Regelmäßige Statistik-Reports per E-Mail. Voraussetzung ist ein funktionierend | `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. +Ermöglicht das Einbinden externer Blocklisten, die IPv4-Adressen, IPv6-Adressen und Hostnamen enthalten können. Der Worker läuft als Hintergrundprozess, prüft periodisch auf Änderungen und löst Hostnamen automatisch über den lokalen DNS-Resolver auf. | 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_URLS` | *(leer)* | URL(s) zu Blocklist-Textdateien (kommagetrennt). Unterstützt IPv4, IPv6, CIDR und Hostnamen | | `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 | @@ -201,15 +201,32 @@ Der Report an AbuseIPDB enthält (auf Englisch): Die Kategorie `4` (DDoS Attack) wird standardmäßig verwendet. Weitere Kategorien können kommagetrennt angegeben werden (z.B. `"4,15"`). #### Externe Blocklist einrichten -1. Erstelle eine Textdatei auf einem Webserver mit einer IP pro Zeile: +1. Erstelle eine Textdatei auf einem Webserver. Pro Zeile ein Eintrag — IPv4, IPv6, CIDR oder Hostname: ```text # Kommentare werden ignoriert +# Inline-Kommentare ebenfalls: 1.2.3.4 # dieser Kommentar wird entfernt + +# IPv4 192.168.100.50 -10.0.0.99 +10.0.0.0/8 + +# IPv6 2001:db8::dead:beef +2001:db8::/32 + +# Hostnamen (werden über den lokalen DNS-Resolver aufgelöst) +# Liefert ein Hostname mehrere IPs, werden alle gesperrt +bad-actor.example.com +malware.example.net + +# Hosts-Datei-Format wird ebenfalls erkannt (Routing-IP wird ignoriert, Hostname aufgelöst) +0.0.0.0 bad-actor.example.com +127.0.0.1 malware.example.net ``` +> **Hinweis zur Hostname-Auflösung:** Da AdGuard Shield idealerweise auf demselben Host wie der DNS-Resolver läuft, verwendet der Worker automatisch den lokalen Resolver. Hostnamen die bereits von AdGuard geblockt werden (Antwort `0.0.0.0`) werden übersprungen und nicht importiert. + 2. Aktiviere die Blocklist in der Konfiguration: ```bash @@ -229,6 +246,27 @@ EXTERNAL_BLOCKLIST_URLS="https://example.com/list1.txt,https://other.com/list2.t ```bash sudo systemctl restart adguard-shield ``` + +#### Unterstützte Eintragsformate + +| Format | Beispiel | Verhalten | +|--------|----------|----------| +| IPv4 | `1.2.3.4` | direkt gesperrt | +| IPv4-CIDR | `10.0.0.0/8` | direkt gesperrt | +| IPv6 | `2001:db8::1` | direkt gesperrt | +| IPv6-CIDR | `2001:db8::/32` | direkt gesperrt | +| Hostname | `bad.example.com` | per lokalem DNS aufgelöst, alle IPs (IPv4 + IPv6) gesperrt | +| Hosts-Format | `0.0.0.0 bad.example.com` | Hostname extrahiert und aufgelöst | +| Kommentar | `# Text` | übersprungen | +| Inline-Kommentar | `1.2.3.4 # Text` | Kommentar entfernt, IP gesperrt | + +Folgende Einträge werden mit einer Warnung im Log übersprungen: + +- URLs (`https://...`, `http://...`) +- IP:Port-Kombinationen (`1.2.3.4:8080`) +- Hostnamen mit ungültigen Zeichen oder ohne Punkt +- Einträge mit nicht auflösbarem Hostnamen +- `0.0.0.0` und `::` (AdGuard-Blocking-Antwort) ## Gesperrte Ports im Detail Bei einem Rate-Limit-Verstoß werden **alle** DNS-Protokoll-Ports für den Client gesperrt (IPv4 via `iptables` und IPv6 via `ip6tables`): diff --git a/external-blocklist-worker.sh b/external-blocklist-worker.sh index 91e61d6..4e41300 100644 --- a/external-blocklist-worker.sh +++ b/external-blocklist-worker.sh @@ -318,24 +318,147 @@ download_blocklist() { return 0 } +# ─── Eintrag-Validierung ───────────────────────────────────────────────────── + +# Prüft IPv4-Adresse mit optionalem CIDR (z.B. 1.2.3.4 oder 1.2.3.0/24) +_is_valid_ipv4() { + local ip="$1" addr="$1" prefix="" + if [[ "$ip" == */* ]]; then + addr="${ip%/*}" + prefix="${ip#*/}" + { [[ "$prefix" =~ ^[0-9]+$ ]] && [[ "$prefix" -le 32 ]]; } || return 1 + fi + local IFS='.' + read -ra _octets <<< "$addr" + [[ ${#_octets[@]} -eq 4 ]] || return 1 + local o + for o in "${_octets[@]}"; do + [[ "$o" =~ ^[0-9]+$ ]] || return 1 + [[ "$o" -le 255 ]] || return 1 + done + return 0 +} + +# Prüft IPv6-Adresse mit optionalem CIDR (z.B. ::1 oder 2001:db8::/32) +# Fängt auch IPv4:Port-Kombinationen ab (z.B. 1.2.3.4:8080) +_is_valid_ipv6() { + local ip="$1" addr="$1" + if [[ "$ip" == */* ]]; then + addr="${ip%/*}" + local prefix="${ip#*/}" + { [[ "$prefix" =~ ^[0-9]+$ ]] && [[ "$prefix" -le 128 ]]; } || return 1 + fi + # IPv4:Port abfangen — enthält Punkt(e) vor dem ersten Doppelpunkt + [[ "$addr" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9] ]] && return 1 + # Muss mindestens einen Doppelpunkt haben und nur gültige Zeichen (Hex, Doppelpunkt, Punkt für IPv4-mapped) + [[ "$addr" == *:* ]] || return 1 + [[ "$addr" =~ ^[0-9a-fA-F:\.]+$ ]] || return 1 + return 0 +} + +# Prüft ob ein Hostname syntaktisch plausibel ist +# Akzeptiert: example.com, sub.example.com, example.com. (trailing dot) +# Lehnt ab: einzelne Wörter ohne Punkt, Sonderzeichen, überlange Einträge +_is_valid_hostname() { + local host="$1" + host="${host%.}" # trailing dot (FQDN) entfernen + [[ -z "$host" ]] && return 1 + [[ ${#host} -gt 253 ]] && return 1 + [[ "$host" =~ ^[a-zA-Z0-9._-]+$ ]] || return 1 + [[ "$host" =~ ^[.\-] ]] && return 1 # darf nicht mit . oder - beginnen + [[ "$host" == *.* ]] || return 1 # muss mindestens einen Punkt enthalten + return 0 +} + # ─── IPs aus Blocklist-Datei parsen ────────────────────────────────────────── +# Unterstützt IPv4, IPv6, CIDR-Notation und Hostnamen (werden aufgelöst). +# Unterstützt außerdem das Hosts-Datei-Format: "0.0.0.0 hostname" oder "127.0.0.1 hostname". +# Ungültige Einträge (URLs, IP:Port, fehlerhafte IPs, einzelne Wörter usw.) werden +# mit WARN geloggt und übersprungen. +# 0.0.0.0 / :: wird nie importiert (AdGuard-typische Blocking-Antwort). 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 + # Leerzeilen und Kommentarzeilen überspringen + [[ -z "$line" ]] && continue + [[ "$line" =~ ^[[:space:]]*# ]] && continue + + # Whitespace trimmen, dann Inline-Kommentare entfernen (# oder ;) line=$(echo "$line" | xargs) - # Leere Zeilen nach Trim überspringen + line=$(echo "$line" | sed 's/[[:space:]]*[#;].*$//' | xargs) [[ -z "$line" ]] && continue - # CIDR-Notation oder reine IP ausgeben - echo "$line" + + # ── URLs ablehnen (http://, https://, ftp:// …) ────────────────────── + if [[ "$line" =~ ^[a-zA-Z][a-zA-Z0-9+.-]*:// ]]; then + log "WARN" "Eintrag übersprungen (URL nicht erlaubt): $line" + continue + fi + + # ── Hosts-Datei-Format erkennen: " " ─────────────── + # z.B. "0.0.0.0 bad.com" oder "127.0.0.1 malware.net" + if [[ "$line" =~ ^[^[:space:]]+[[:space:]]+[^[:space:]] ]]; then + local _first="${line%% *}" + local _rest="${line#* }" + local _second="${_rest%% *}" + if [[ "$_first" == "0.0.0.0" || "$_first" =~ ^127\. || + "$_first" == "::1" || "$_first" == "::0" || + "$_first" == "::" ]]; then + log "DEBUG" "Hosts-Format erkannt, extrahiere Ziel: $_second" + line="$_second" + else + log "WARN" "Eintrag übersprungen (Leerzeichen im Eintrag, unbekanntes Format): $line" + continue + fi + fi + + # ── Klassifizieren und validieren ───────────────────────────────────── + if [[ "$line" == *:* ]]; then + # ── IPv6 ────────────────────────────────────────────────────────── + if _is_valid_ipv6 "$line"; then + echo "$line" + else + log "WARN" "Eintrag übersprungen (ungültige IPv6-Adresse oder IP:Port): $line" + fi + + elif [[ "$line" =~ ^[0-9] ]]; then + # ── IPv4 ────────────────────────────────────────────────────────── + [[ "$line" == "0.0.0.0"* ]] && continue + if _is_valid_ipv4 "$line"; then + echo "$line" + else + log "WARN" "Eintrag übersprungen (ungültige IPv4-Adresse oder ungültiges CIDR): $line" + fi + + else + # ── Hostname → DNS-Auflösung ────────────────────────────────────── + if ! _is_valid_hostname "$line"; then + log "WARN" "Eintrag übersprungen (kein gültiger Hostname): $line" + continue + fi + local resolved + resolved=$(getent ahosts "$line" 2>/dev/null | awk '{print $1}' | sort -u) + if [[ -z "$resolved" ]]; then + log "WARN" "Hostname konnte nicht aufgelöst werden: $line" + continue + fi + local resolved_count=0 + while IFS= read -r resolved_ip; do + [[ -z "$resolved_ip" ]] && continue + [[ "$resolved_ip" == "0.0.0.0" ]] && continue # AdGuard-Blocking-Antwort + [[ "$resolved_ip" == "::" ]] && continue # IPv6 unspecified + [[ "$resolved_ip" == "::0" ]] && continue + echo "$resolved_ip" + resolved_count=$((resolved_count + 1)) + done <<< "$resolved" + if [[ $resolved_count -gt 0 ]]; then + log "DEBUG" "Hostname aufgelöst: $line → $resolved_count IP(s)" + else + log "WARN" "Hostname lieferte nur ungültige Adressen (z.B. 0.0.0.0): $line – wird übersprungen" + fi + fi done < "$cache_file" }