846 lines
33 KiB
Bash
846 lines
33 KiB
Bash
#!/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-shield.conf"
|
||
|
||
# ─── Konfiguration laden ───────────────────────────────────────────────────────
|
||
if [[ ! -f "$CONFIG_FILE" ]]; then
|
||
echo "FEHLER: Konfigurationsdatei nicht gefunden: $CONFIG_FILE" >&2
|
||
exit 1
|
||
fi
|
||
# shellcheck source=adguard-shield.conf
|
||
source "$CONFIG_FILE"
|
||
|
||
# ─── Worker PID-File ──────────────────────────────────────────────────────────
|
||
WORKER_PID_FILE="/var/run/adguard-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" >&2
|
||
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 | PROTOKOLL | GRUND" >> "$BAN_HISTORY_FILE"
|
||
echo "#──────────────────────────────────────────────────────────────────────────────────────────────────" >> "$BAN_HISTORY_FILE"
|
||
fi
|
||
|
||
local duration="permanent"
|
||
[[ "$EXTERNAL_BLOCKLIST_BAN_DURATION" -gt 0 ]] && duration="${EXTERNAL_BLOCKLIST_BAN_DURATION}s"
|
||
|
||
printf "%-19s | %-6s | %-39s | %-30s | %-8s | %-10s | %-10s | %s\n" \
|
||
"$timestamp" "$action" "$client_ip" "-" "-" "$duration" "-" "$reason" \
|
||
>> "$BAN_HISTORY_FILE"
|
||
}
|
||
|
||
# ─── 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
|
||
|
||
# Externe Whitelist prüfen (aufgelöste IPs aus dem Whitelist-Worker)
|
||
local ext_wl_file="${EXTERNAL_WHITELIST_CACHE_DIR:-/var/lib/adguard-shield/external-whitelist}/resolved_ips.txt"
|
||
if [[ -f "$ext_wl_file" ]] && grep -qxF "$ip" "$ext_wl_file" 2>/dev/null; then
|
||
return 0
|
||
fi
|
||
|
||
return 1
|
||
}
|
||
|
||
# ─── 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
|
||
# iptables-Regel prüfen und ggf. nachziehen (z.B. nach Neustart verloren gegangen)
|
||
if [[ "$ip" == *:* ]]; then
|
||
if ! ip6tables -C "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null; then
|
||
ip6tables -I "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null || true
|
||
fi
|
||
else
|
||
if ! iptables -C "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null; then
|
||
iptables -I "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null || true
|
||
fi
|
||
fi
|
||
log "DEBUG" "IP $ip bereits über externe Blocklist gesperrt"
|
||
return 0
|
||
fi
|
||
|
||
# Nicht auch vom Hauptscript gesperrt? (State-Datei ohne ext_ Prefix)
|
||
local main_state_file="${STATE_DIR}/${ip//[:\/]/_}.ban"
|
||
if [[ -f "$main_state_file" ]]; then
|
||
log "DEBUG" "IP $ip bereits vom Rate-Limiter gesperrt - überspringe"
|
||
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 (nur wenn EXTERNAL_BLOCKLIST_NOTIFY=true)
|
||
if [[ "$NOTIFY_ENABLED" == "true" && "${EXTERNAL_BLOCKLIST_NOTIFY:-false}" == "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" && "${EXTERNAL_BLOCKLIST_NOTIFY:-false}" == "true" ]]; then
|
||
send_notification "unban" "$ip"
|
||
fi
|
||
}
|
||
|
||
# ─── Hostname-Auflösung ──────────────────────────────────────────────────────
|
||
# Versucht den Hostnamen einer IP per Reverse-DNS aufzulösen
|
||
resolve_hostname() {
|
||
local ip="$1"
|
||
local hostname=""
|
||
|
||
if command -v dig &>/dev/null; then
|
||
hostname=$(dig +short -x "$ip" 2>/dev/null | head -1 | sed 's/\.$//')
|
||
fi
|
||
|
||
if [[ -z "$hostname" ]] && command -v host &>/dev/null; then
|
||
hostname=$(host "$ip" 2>/dev/null | awk '/domain name pointer/ {print $NF}' | sed 's/\.$//' | head -1)
|
||
fi
|
||
|
||
if [[ -z "$hostname" ]] && command -v getent &>/dev/null; then
|
||
hostname=$(getent hosts "$ip" 2>/dev/null | awk '{print $2}' | head -1)
|
||
fi
|
||
|
||
echo "${hostname:-(unbekannt)}"
|
||
}
|
||
|
||
# ─── Benachrichtigung ────────────────────────────────────────────────────────
|
||
send_notification() {
|
||
local action="$1"
|
||
local ip="$2"
|
||
|
||
# ntfy benötigt keine NOTIFY_WEBHOOK_URL, alle anderen schon
|
||
if [[ "${NOTIFY_TYPE:-generic}" != "ntfy" && -z "${NOTIFY_WEBHOOK_URL:-}" ]]; then
|
||
return
|
||
fi
|
||
|
||
local title
|
||
local message
|
||
local my_hostname
|
||
my_hostname=$(hostname)
|
||
local client_hostname
|
||
client_hostname=$(resolve_hostname "$ip")
|
||
|
||
if [[ "$action" == "ban" ]]; then
|
||
title="🚨 🛡️ AdGuard Shield"
|
||
message="🚫 AdGuard Shield Ban auf ${my_hostname} (Externe Blocklist)
|
||
---
|
||
IP: ${ip}
|
||
Hostname: ${client_hostname}
|
||
|
||
Whois: https://www.whois.com/whois/${ip}
|
||
AbuseIPDB: https://www.abuseipdb.com/check/${ip}"
|
||
else
|
||
title="✅ AdGuard Shield"
|
||
message="✅ AdGuard Shield Freigabe auf ${my_hostname} (Externe Blocklist)
|
||
---
|
||
IP: ${ip}
|
||
Hostname: ${client_hostname}
|
||
|
||
AbuseIPDB: https://www.abuseipdb.com/check/${ip}"
|
||
fi
|
||
|
||
case "${NOTIFY_TYPE:-generic}" in
|
||
discord)
|
||
local json_payload
|
||
json_payload=$(jq -nc --arg msg "$message" '{content: $msg}')
|
||
curl -s -H "Content-Type: application/json" \
|
||
-d "$json_payload" \
|
||
"$NOTIFY_WEBHOOK_URL" &>/dev/null &
|
||
;;
|
||
slack)
|
||
local json_payload
|
||
json_payload=$(jq -nc --arg msg "$message" '{text: $msg}')
|
||
curl -s -H "Content-Type: application/json" \
|
||
-d "$json_payload" \
|
||
"$NOTIFY_WEBHOOK_URL" &>/dev/null &
|
||
;;
|
||
gotify)
|
||
curl -s -X POST "$NOTIFY_WEBHOOK_URL" \
|
||
-F "title=${title}" \
|
||
-F "message=${message}" \
|
||
-F "priority=5" &>/dev/null &
|
||
;;
|
||
ntfy)
|
||
local ntfy_url="${NTFY_SERVER_URL:-https://ntfy.sh}"
|
||
local tags="rotating_light,blocklist"
|
||
[[ "$action" != "ban" ]] && tags="white_check_mark,blocklist"
|
||
# Ntfy fügt Emojis über Tags hinzu → Titel ohne führende Emojis setzen
|
||
local ntfy_title
|
||
case "$action" in
|
||
ban) ntfy_title="🛡️ AdGuard Shield" ;;
|
||
*) ntfy_title="AdGuard Shield" ;;
|
||
esac
|
||
local -a curl_args=(
|
||
-s -X POST "${ntfy_url}/${NTFY_TOPIC}"
|
||
-H "Title: ${ntfy_title}"
|
||
-H "Priority: ${NTFY_PRIORITY:-3}"
|
||
-H "Tags: ${tags}"
|
||
-d "${message}"
|
||
)
|
||
[[ -n "${NTFY_TOKEN:-}" ]] && curl_args+=(-H "Authorization: Bearer ${NTFY_TOKEN}")
|
||
curl "${curl_args[@]}" &>/dev/null &
|
||
;;
|
||
generic)
|
||
local json_payload
|
||
json_payload=$(jq -nc --arg msg "$message" --arg act "$action" --arg cl "$ip" \
|
||
'{message: $msg, action: $act, client: $cl, source: "external-blocklist"}')
|
||
curl -s -H "Content-Type: application/json" \
|
||
-d "$json_payload" \
|
||
"$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
|
||
}
|
||
|
||
# ─── 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
|
||
|
||
while IFS= read -r line; do
|
||
line="${line%$'\r'}" # Windows-Zeilenenden (CRLF) entfernen
|
||
line="${line#$'\xef\xbb\xbf'}" # UTF-8 BOM entfernen (erste Zeile)
|
||
|
||
# Leerzeilen und Kommentarzeilen überspringen
|
||
[[ -z "$line" ]] && continue
|
||
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||
|
||
# Whitespace trimmen, dann Inline-Kommentare entfernen (# oder ;)
|
||
line=$(echo "$line" | xargs)
|
||
line=$(echo "$line" | sed 's/[[:space:]]*[#;].*$//' | xargs)
|
||
[[ -z "$line" ]] && continue
|
||
|
||
# ── 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: "<routing-IP> <ziel>" ───────────────
|
||
# 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) || resolved=""
|
||
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"
|
||
}
|
||
|
||
# ─── 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
|
||
|
||
local _state_file_before="${STATE_DIR}/ext_${ip//[:/]/_}.ban"
|
||
local _was_new=false
|
||
[[ ! -f "$_state_file_before" ]] && _was_new=true
|
||
|
||
ban_ip "$ip"
|
||
[[ "$_was_new" == "true" ]] && 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
|