745 lines
27 KiB
Bash
745 lines
27 KiB
Bash
#!/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="0.3.0"
|
|
|
|
set -euo pipefail
|
|
|
|
# Fehler-Trap: Bei unerwartetem Abbruch Fehlerdetails ausgeben
|
|
trap 'echo "[$(date "+%Y-%m-%d %H:%M:%S")] [ERROR] Unerwarteter Fehler in Zeile $LINENO (Exit-Code: $?)" >&2' ERR
|
|
|
|
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"
|
|
|
|
# ─── 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="🚫 AdGuard Shield: Client **$client_ip** gesperrt (${count}x $domain in ${RATE_LIMIT_WINDOW}s). Sperre für ${BAN_DURATION}s."
|
|
else
|
|
message="✅ AdGuard Shield: 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 Shield" \
|
|
-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 Shield"
|
|
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() {
|
|
# Hinweis: Zeitfilterung erfolgt client-seitig in analyze_queries(),
|
|
# da die AdGuard API keinen "newer_than" Parameter unterstützt.
|
|
|
|
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))
|
|
|
|
# Anzahl der API-Einträge loggen
|
|
local entry_count
|
|
entry_count=$(echo "$api_response" | jq '.data // [] | length' 2>/dev/null || echo "0")
|
|
log "INFO" "API-Abfrage: ${entry_count} Einträge erhalten, prüfe Zeitfenster ${RATE_LIMIT_WINDOW}s..."
|
|
|
|
# Extrahiere Client-IP + Domain Paare aus dem Zeitfenster
|
|
# und zähle die Häufigkeit pro (client, domain) Kombination
|
|
# Unterstützt .question.name (alte API) und .question.host (neue API)
|
|
# Unterstützt Timestamps mit UTC ("Z") und Zeitzonen-Offset ("+01:00")
|
|
local violations=""
|
|
violations=$(echo "$api_response" | jq -r --argjson window_start "$window_start" '
|
|
# ISO 8601 Timestamp zu Unix-Epoch konvertieren
|
|
# Unterstützt: "2026-03-03T20:01:48Z", "2026-03-03T20:01:48.123Z",
|
|
# "2026-03-03T20:01:48+01:00", "2026-03-03T20:01:48.123+01:00"
|
|
def to_epoch:
|
|
sub("\\.[0-9]+(?=[+-Z])"; "") |
|
|
if endswith("Z") then
|
|
fromdateiso8601
|
|
elif test("[+-][0-9]{2}:[0-9]{2}$") then
|
|
# Zeitzonen-Offset per String-Slicing extrahieren (zuverlässiger als Regex)
|
|
# Letzten 6 Zeichen = "+01:00" bzw. "-05:00"
|
|
(.[:-6]) as $base |
|
|
(.[-6:-5]) as $sign |
|
|
(.[-5:-3] | tonumber) as $h |
|
|
(.[-2:] | tonumber) as $m |
|
|
($base + "Z" | fromdateiso8601) +
|
|
(if $sign == "+" then -1 else 1 end * ($h * 3600 + $m * 60))
|
|
else
|
|
fromdateiso8601
|
|
end;
|
|
|
|
.data // [] |
|
|
[.[] |
|
|
select(.time != null) |
|
|
select((.time | to_epoch) >= $window_start) |
|
|
{
|
|
client: (.client // .client_info.ip // "unknown"),
|
|
domain: ((.question.name // .question.host // "unknown") | rtrimstr("."))
|
|
}
|
|
] |
|
|
group_by(.client + "|" + .domain) |
|
|
map({
|
|
client: .[0].client,
|
|
domain: .[0].domain,
|
|
count: length
|
|
}) |
|
|
.[] |
|
|
select(.count > 0) |
|
|
"\(.client)|\(.domain)|\(.count)"
|
|
') || {
|
|
log "ERROR" "jq Analyse fehlgeschlagen - API-Antwort-Format prüfen (ist AdGuard Home erreichbar?)"
|
|
return
|
|
}
|
|
|
|
if [[ -z "$violations" ]]; then
|
|
log "INFO" "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 "INFO" "Client: $client, Domain: $domain, Anfragen: $count/$RATE_LIMIT_MAX_REQUESTS"
|
|
|
|
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 Shield - 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 Shield - 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)
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] AdGuard Shield v${VERSION} wird gestartet..."
|
|
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 <IP-Adresse>" >&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
|