Merge pull request 'v0.3.1' (#2) from v0.3.1 into main

Reviewed-on: #2
This commit is contained in:
2026-03-03 21:30:54 +00:00
12 changed files with 507 additions and 56 deletions

View File

@@ -1,5 +1,7 @@
{
"files.eol": "\n",
"chat.tools.terminal.autoApprove": {
"Rename-Item": true
"Rename-Item": true,
"ForEach-Object": true
}
}

View File

@@ -22,6 +22,7 @@ Wenn ein Client eine bestimmte Domain zu oft anfragt (z.B. >30x pro Minute), wir
## Features
- Automatische Erkennung und Sperre bei Rate-Limit-Verstößen
- **Progressive Sperren (Recidive)** — Wiederholungstäter werden stufenweise länger gesperrt (wie bei fail2ban)
- 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
@@ -79,6 +80,7 @@ sudo /opt/adguard-shield/adguard-shield.sh status # Aktive Sperren a
sudo /opt/adguard-shield/adguard-shield.sh history # Ban-History anzeigen
sudo /opt/adguard-shield/adguard-shield.sh unban IP # Einzelne IP entsperren
sudo /opt/adguard-shield/adguard-shield.sh flush # Alle Sperren aufheben
sudo /opt/adguard-shield/adguard-shield.sh reset-offenses # Offense-Zähler zurücksetzen
sudo /opt/adguard-shield/adguard-shield.sh test # API-Verbindung testen
sudo /opt/adguard-shield/adguard-shield.sh blocklist-status # Externe Blocklisten Status
sudo /opt/adguard-shield/adguard-shield.sh blocklist-sync # Blocklisten manuell synchronisieren

View File

@@ -5,10 +5,10 @@
# --- AdGuard Home API Einstellungen ---
# URL der AdGuard Home Web-Oberfläche (ohne trailing slash)
ADGUARD_URL="https://dns1.techniverse.net"
ADGUARD_URL="https://dns1.domain.com"
# AdGuard Home Zugangsdaten (Web-UI Login)
ADGUARD_USER="adg-admin"
ADGUARD_USER="admin"
ADGUARD_PASS='changeme'
# --- Rate-Limit Einstellungen ---
@@ -28,13 +28,11 @@ BAN_DURATION=3600
# iptables Chain-Name für die Sperren
IPTABLES_CHAIN="ADGUARD_SHIELD"
# Welche Ports gesperrt werden sollen (DNS, DoT, DoH, DNSv5/QUIC)
# Welche Ports gesperrt werden sollen (IPv4 + IPv6)
# 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"
# Port 853 = DNS-over-TLS (tls://...:853) / DNS-over-QUIC (quic://...:853)
BLOCKED_PORTS="53 443 853"
# --- Whitelist ---
# IP-Adressen die NIEMALS gesperrt werden (kommagetrennt)
@@ -55,17 +53,17 @@ LOG_MAX_SIZE_MB=50
BAN_HISTORY_FILE="/var/log/adguard-shield-bans.log"
# --- Benachrichtigungen (optional) ---
# Aktiviert Benachrichtigungen bei Sperren
# Aktiviert Benachrichtigungen bei Sperren/Entsperrungen
NOTIFY_ENABLED=false
# Webhook-URL für Benachrichtigungen (z.B. Discord, Slack, Gotify)
# Benachrichtigungs-Typ: "ntfy", "discord", "slack", "gotify", "generic"
NOTIFY_TYPE="ntfy"
# Webhook-URL (nur für discord, slack, gotify, generic bei ntfy nicht nötig)
# 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"
@@ -77,7 +75,7 @@ NTFY_TOPIC=""
NTFY_TOKEN=""
# Priorität der Ntfy-Nachrichten (1=min, 3=default, 5=max)
NTFY_PRIORITY="1"
NTFY_PRIORITY="4"
# --- Externe Blocklist (optional) ---
# Aktiviert den externen Blocklist-Worker
@@ -100,6 +98,26 @@ EXTERNAL_BLOCKLIST_AUTO_UNBAN=true
# Lokaler Cache-Pfad für die heruntergeladene Blocklist
EXTERNAL_BLOCKLIST_CACHE_DIR="/var/lib/adguard-shield/external-blocklist"
# --- Progressive Sperren (Recidive) ---
# Wiederholungstäter werden stufenweise länger gesperrt (wie bei fail2ban)
# Aktiviert das progressive Sperrsystem
PROGRESSIVE_BAN_ENABLED=true
# Multiplikator pro Wiederholung (2 = Verdopplung der Sperrdauer)
# Stufe 1: BAN_DURATION × 1 (Standard-Sperrdauer)
# Stufe 2: BAN_DURATION × 2
# Stufe 3: BAN_DURATION × 4
# Stufe 4: BAN_DURATION × 8 ... usw.
PROGRESSIVE_BAN_MULTIPLIER=2
# Ab dieser Stufe wird die IP permanent gesperrt (0 = nie permanent sperren)
# Beispiel: 5 = nach dem 5. Vergehen wird die IP dauerhaft gesperrt
PROGRESSIVE_BAN_MAX_LEVEL=5
# Nach wie vielen Sekunden ohne erneutes Vergehen wird der Zähler zurückgesetzt
# (86400 = 24 Stunden, 604800 = 7 Tage)
PROGRESSIVE_BAN_RESET_AFTER=86400
# --- Erweiterte Einstellungen ---
# Pfad zur State-Datei (speichert aktive Sperren)
STATE_DIR="/var/lib/adguard-shield"

View File

@@ -9,7 +9,7 @@
# Lizenz: MIT
###############################################################################
VERSION="0.3.0"
VERSION="0.3.1"
set -euo pipefail
@@ -78,6 +78,7 @@ log_ban_history() {
local domain="${3:-}"
local count="${4:-}"
local reason="${5:-}"
local duration="${6:-}"
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
@@ -88,14 +89,130 @@ log_ban_history() {
echo "#───────────────────────────────────────────────────────────────────────────────" >> "$BAN_HISTORY_FILE"
fi
local duration="-"
[[ "$action" == "BAN" ]] && duration="${BAN_DURATION}s"
if [[ -z "$duration" && "$action" == "BAN" ]]; then
duration="${BAN_DURATION}s"
fi
[[ -z "$duration" ]] && duration="-"
printf "%-19s | %-6s | %-39s | %-30s | %-8s | %-10s | %s\n" \
"$timestamp" "$action" "$client_ip" "${domain:--}" "${count:--}" "$duration" "${reason:-rate-limit}" \
>> "$BAN_HISTORY_FILE"
}
# ─── Progressive Ban (Recidive) ─────────────────────────────────────────────
# Liest die aktuelle Offense-Stufe einer IP aus der Offense-Datei
get_offense_level() {
local client_ip="$1"
local offense_file="${STATE_DIR}/${client_ip//[:\/]/_}.offenses"
if [[ ! -f "$offense_file" ]]; then
echo "0"
return
fi
local level last_offense now reset_after
level=$(grep '^OFFENSE_LEVEL=' "$offense_file" | cut -d= -f2)
last_offense=$(grep '^LAST_OFFENSE_EPOCH=' "$offense_file" | cut -d= -f2)
now=$(date '+%s')
reset_after="${PROGRESSIVE_BAN_RESET_AFTER:-86400}"
# Prüfen ob der Zähler abgelaufen ist (Reset nach Zeitraum ohne Vergehen)
if [[ -n "$last_offense" && $((now - last_offense)) -gt "$reset_after" ]]; then
log "INFO" "Progressive Ban: Offense-Zähler für $client_ip zurückgesetzt (>${reset_after}s ohne Vergehen)"
rm -f "$offense_file"
echo "0"
return
fi
echo "${level:-0}"
}
# Erhöht die Offense-Stufe einer IP und gibt die neue Stufe zurück
increment_offense_level() {
local client_ip="$1"
local offense_file="${STATE_DIR}/${client_ip//[:\/]/_}.offenses"
local current_level
current_level=$(get_offense_level "$client_ip")
local new_level=$((current_level + 1))
local now
now=$(date '+%s')
local now_readable
now_readable=$(date '+%Y-%m-%d %H:%M:%S')
# Erstes Vergehen merken (bevor Datei überschrieben wird)
local first_offense
first_offense=$(grep '^FIRST_OFFENSE=' "$offense_file" 2>/dev/null | cut -d= -f2)
[[ -z "$first_offense" ]] && first_offense="$now_readable"
cat > "$offense_file" << EOF
CLIENT_IP=$client_ip
OFFENSE_LEVEL=$new_level
LAST_OFFENSE_EPOCH=$now
LAST_OFFENSE=$now_readable
FIRST_OFFENSE=$first_offense
EOF
echo "$new_level"
}
# Berechnet die Sperrdauer basierend auf der Offense-Stufe
calculate_ban_duration() {
local offense_level="$1"
local base_duration="${BAN_DURATION:-3600}"
local multiplier="${PROGRESSIVE_BAN_MULTIPLIER:-2}"
local max_level="${PROGRESSIVE_BAN_MAX_LEVEL:-5}"
# Progressive Bans deaktiviert? → Standard-Dauer
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" != "true" ]]; then
echo "$base_duration"
return
fi
# Permanente Sperre ab max_level (0 = nie permanent)
if [[ "$max_level" -gt 0 && "$offense_level" -ge "$max_level" ]]; then
echo "0" # 0 = permanent
return
fi
# Exponentielle Steigerung: base × multiplier^(level-1)
# Stufe 1: base × 1, Stufe 2: base × mult, Stufe 3: base × mult², ...
if [[ "$offense_level" -le 1 ]]; then
echo "$base_duration"
else
local power=$((offense_level - 1))
local factor=1
for ((i=0; i<power; i++)); do
factor=$((factor * multiplier))
done
echo $((base_duration * factor))
fi
}
# Formatiert Sekunden in eine lesbare Dauer-Angabe
format_duration() {
local seconds="$1"
if [[ "$seconds" -eq 0 ]]; then
echo "PERMANENT"
return
fi
if [[ "$seconds" -ge 86400 ]]; then
echo "$((seconds / 86400))d $((seconds % 86400 / 3600))h"
elif [[ "$seconds" -ge 3600 ]]; then
echo "$((seconds / 3600))h $((seconds % 3600 / 60))m"
elif [[ "$seconds" -ge 60 ]]; then
echo "$((seconds / 60))m $((seconds % 60))s"
else
echo "${seconds}s"
fi
}
# Setzt den Offense-Zähler einer IP zurück
reset_offense_level() {
local client_ip="$1"
local offense_file="${STATE_DIR}/${client_ip//[:\/]/_}.offenses"
rm -f "$offense_file"
}
# ─── Verzeichnisse erstellen ──────────────────────────────────────────────────
init_directories() {
mkdir -p "$STATE_DIR"
@@ -111,6 +228,12 @@ write_pid() {
cleanup() {
log "INFO" "AdGuard Shield wird beendet..."
# Service-Stop-Benachrichtigung senden
if [[ "${NOTIFY_ENABLED:-false}" == "true" ]]; then
send_notification "service_stop" "" "" ""
# Kurz warten damit die Benachrichtigung gesendet wird (curl läuft im Hintergrund)
sleep 1
fi
stop_blocklist_worker
rm -f "$PID_FILE"
exit 0
@@ -173,8 +296,6 @@ 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"
@@ -183,13 +304,48 @@ ban_client() {
return 0
fi
# Progressive Ban: Offense-Level ermitteln und Sperrdauer berechnen
local offense_level=0
local effective_duration="$BAN_DURATION"
local is_permanent=false
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" ]]; then
offense_level=$(increment_offense_level "$client_ip")
effective_duration=$(calculate_ban_duration "$offense_level")
if [[ "$effective_duration" -eq 0 ]]; then
is_permanent=true
fi
fi
local ban_until
local ban_until_display
if [[ "$is_permanent" == "true" ]]; then
ban_until=0 # 0 = permanent
ban_until_display="PERMANENT"
else
ban_until=$(date -d "+${effective_duration} seconds" '+%s' 2>/dev/null || date -v "+${effective_duration}S" '+%s')
ban_until_display=$(date -d "@$ban_until" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || date -r "$ban_until" '+%Y-%m-%d %H:%M:%S')
fi
local duration_display
duration_display=$(format_duration "$effective_duration")
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"
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" ]]; then
log "WARN" "[DRY-RUN] WÜRDE sperren: $client_ip (${count}x $domain in ${RATE_LIMIT_WINDOW}s) für ${duration_display} [Stufe $offense_level]"
else
log "WARN" "[DRY-RUN] WÜRDE sperren: $client_ip (${count}x $domain in ${RATE_LIMIT_WINDOW}s)"
fi
log_ban_history "DRY" "$client_ip" "$domain" "$count" "dry-run" "${duration_display}"
return 0
fi
log "WARN" "SPERRE Client: $client_ip (${count}x $domain in ${RATE_LIMIT_WINDOW}s) für ${BAN_DURATION}s"
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" ]]; then
log "WARN" "SPERRE Client: $client_ip (${count}x $domain in ${RATE_LIMIT_WINDOW}s) für ${duration_display} [Stufe ${offense_level}/${PROGRESSIVE_BAN_MAX_LEVEL:-0}]"
else
log "WARN" "SPERRE Client: $client_ip (${count}x $domain in ${RATE_LIMIT_WINDOW}s) für ${duration_display}"
fi
# IPv4 oder IPv6 erkennen
if [[ "$client_ip" == *:* ]]; then
@@ -207,15 +363,20 @@ 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')
BAN_UNTIL=$ban_until_display
BAN_DURATION=${effective_duration}
OFFENSE_LEVEL=$offense_level
IS_PERMANENT=$is_permanent
EOF
# Ban-History Eintrag
log_ban_history "BAN" "$client_ip" "$domain" "$count" "rate-limit"
local history_duration="${duration_display}"
[[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" ]] && history_duration="${duration_display} (Stufe ${offense_level})"
log_ban_history "BAN" "$client_ip" "$domain" "$count" "rate-limit" "$history_duration"
# Benachrichtigung senden
if [[ "$NOTIFY_ENABLED" == "true" ]]; then
send_notification "ban" "$client_ip" "$domain" "$count"
send_notification "ban" "$client_ip" "$domain" "$count" "$offense_level" "$duration_display"
fi
}
@@ -261,6 +422,14 @@ check_expired_bans() {
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)
local is_permanent
is_permanent=$(grep '^IS_PERMANENT=' "$state_file" | cut -d= -f2)
# Permanente Sperren nicht automatisch aufheben
if [[ "$is_permanent" == "true" || "$ban_until_epoch" == "0" ]]; then
log "DEBUG" "Client $client_ip ist permanent gesperrt überspringe"
continue
fi
if [[ -n "$ban_until_epoch" && "$now" -ge "$ban_until_epoch" ]]; then
unban_client "$client_ip" "expired"
@@ -274,12 +443,27 @@ send_notification() {
local client_ip="$2"
local domain="$3"
local count="$4"
local offense_level="${5:-}"
local duration_display="${6:-}"
[[ -z "$NOTIFY_WEBHOOK_URL" ]] && return
# Ntfy benötigt keine Webhook-URL (nutzt NTFY_SERVER_URL + NTFY_TOPIC)
if [[ "$NOTIFY_TYPE" != "ntfy" && -z "$NOTIFY_WEBHOOK_URL" ]]; then
return
fi
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."
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" && -n "$offense_level" ]]; then
message="🚫 AdGuard Shield: Client **$client_ip** gesperrt (${count}x $domain in ${RATE_LIMIT_WINDOW}s). Sperre für **${duration_display}** [Stufe ${offense_level}/${PROGRESSIVE_BAN_MAX_LEVEL:-0}]."
else
local simple_dur
simple_dur=$(format_duration "${BAN_DURATION}")
message="🚫 AdGuard Shield: Client **$client_ip** gesperrt (${count}x $domain in ${RATE_LIMIT_WINDOW}s). Sperre für ${simple_dur}."
fi
elif [[ "$action" == "service_start" ]]; then
message="🟢 AdGuard Shield v${VERSION} wurde gestartet."
elif [[ "$action" == "service_stop" ]]; then
message="🔴 AdGuard Shield v${VERSION} wurde gestoppt."
else
message="✅ AdGuard Shield: Client **$client_ip** wurde entsperrt."
fi
@@ -306,7 +490,7 @@ send_notification() {
;;
generic)
curl -s -H "Content-Type: application/json" \
-d "{\"message\": \"$message\", \"action\": \"$action\", \"client\": \"$client_ip\", \"domain\": \"$domain\"}" \
-d "{\"message\": \"$message\", \"action\": \"$action\", \"client\": \"${client_ip:-}\", \"domain\": \"${domain:-}\"}" \
"$NOTIFY_WEBHOOK_URL" &>/dev/null &
;;
esac
@@ -329,6 +513,10 @@ send_ntfy_notification() {
if [[ "$action" == "ban" ]]; then
tags="rotating_light,ban"
elif [[ "$action" == "service_start" ]]; then
tags="green_circle,start"
elif [[ "$action" == "service_stop" ]]; then
tags="red_circle,stop"
else
tags="white_check_mark,unban"
fi
@@ -470,26 +658,73 @@ show_status() {
echo "═══════════════════════════════════════════════════════════════"
echo ""
# Progressive Ban Info
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" ]]; then
echo " 📈 Progressive Sperren: AKTIV"
echo " Multiplikator: ×${PROGRESSIVE_BAN_MULTIPLIER:-2}"
echo " Max-Stufe: ${PROGRESSIVE_BAN_MAX_LEVEL:-0} (0=kein Limit)"
echo " Zähler-Reset: $(format_duration "${PROGRESSIVE_BAN_RESET_AFTER:-86400}") ohne Vergehen"
echo ""
fi
# 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 ""
local s_ip s_domain s_level s_perm s_dur s_until
s_ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2)
s_domain=$(grep '^DOMAIN=' "$state_file" | cut -d= -f2)
s_level=$(grep '^OFFENSE_LEVEL=' "$state_file" | cut -d= -f2)
s_perm=$(grep '^IS_PERMANENT=' "$state_file" | cut -d= -f2)
s_dur=$(grep '^BAN_DURATION=' "$state_file" | cut -d= -f2)
s_until=$(grep '^BAN_UNTIL=' "$state_file" | cut -d= -f2)
if [[ "$s_perm" == "true" ]]; then
echo " 🚫 Gesperrt: $s_ip$s_domain [PERMANENT, Stufe ${s_level:-?}]"
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]"
else
echo " 🚫 Gesperrt: $s_ip$s_domain [bis $s_until]"
fi
done
fi
echo ""
if [[ $ban_count -eq 0 ]]; then
echo " ✅ Keine aktiven Sperren"
else
echo " Gesamt: $ban_count aktive Sperren"
fi
# Offense-Informationen anzeigen (Wiederholungstäter)
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" && -d "$STATE_DIR" ]]; then
local offense_count=0
local offense_output=""
for offense_file in "${STATE_DIR}"/*.offenses; do
[[ -f "$offense_file" ]] || continue
local o_ip o_level o_last
o_ip=$(grep '^CLIENT_IP=' "$offense_file" | cut -d= -f2)
o_level=$(grep '^OFFENSE_LEVEL=' "$offense_file" | cut -d= -f2)
o_last=$(grep '^LAST_OFFENSE=' "$offense_file" | cut -d= -f2)
offense_count=$((offense_count + 1))
local next_dur
next_dur=$(calculate_ban_duration "$((o_level + 1))")
if [[ "$next_dur" -eq 0 ]]; then
offense_output+="$o_ip: Stufe $o_level (letztes Vergehen: $o_last) → nächste Sperre: PERMANENT\n"
else
offense_output+="$o_ip: Stufe $o_level (letztes Vergehen: $o_last) → nächste Sperre: $(format_duration "$next_dur")\n"
fi
done
if [[ $offense_count -gt 0 ]]; then
echo ""
echo " 📋 Wiederholungstäter ($offense_count IPs mit Vorgeschichte):"
echo -e "$offense_output"
fi
fi
echo ""
# iptables Regeln anzeigen
@@ -557,6 +792,21 @@ flush_all_bans() {
log "INFO" "Alle Sperren aufgehoben"
}
# ─── Alle Offense-Zähler zurücksetzen ────────────────────────────────────────
flush_all_offenses() {
local count=0
for offense_file in "${STATE_DIR}"/*.offenses; do
[[ -f "$offense_file" ]] || continue
local o_ip
o_ip=$(grep '^CLIENT_IP=' "$offense_file" | cut -d= -f2)
log "INFO" "Offense-Zähler zurückgesetzt: $o_ip"
rm -f "$offense_file"
count=$((count + 1))
done
log "INFO" "$count Offense-Zähler zurückgesetzt"
echo "$count Offense-Zähler zurückgesetzt"
}
# ─── Externer Blocklist-Worker starten ───────────────────────────────────────
start_blocklist_worker() {
if [[ "${EXTERNAL_BLOCKLIST_ENABLED:-false}" != "true" ]]; then
@@ -595,13 +845,23 @@ 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" " Sperrdauer: $(format_duration "${BAN_DURATION}")"
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}"
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" ]]; then
log "INFO" " Progressive Sperren: AKTIV (×${PROGRESSIVE_BAN_MULTIPLIER:-2}, Max-Stufe: ${PROGRESSIVE_BAN_MAX_LEVEL:-0}, Reset: $(format_duration "${PROGRESSIVE_BAN_RESET_AFTER:-86400}"))"
else
log "INFO" " Progressive Sperren: deaktiviert"
fi
log "INFO" "═══════════════════════════════════════════════════════════"
# Service-Start-Benachrichtigung senden
if [[ "${NOTIFY_ENABLED:-false}" == "true" ]]; then
send_notification "service_start" "" "" ""
fi
# Blocklist-Worker als Hintergrundprozess starten
start_blocklist_worker
@@ -680,6 +940,15 @@ case "${1:-start}" in
flush_all_bans
echo "Alle Sperren aufgehoben"
;;
reset-offenses)
init_directories
if [[ -n "${2:-}" ]]; then
reset_offense_level "$2"
echo "Offense-Zähler für $2 zurückgesetzt"
else
flush_all_offenses
fi
;;
unban)
if [[ -z "${2:-}" ]]; then
echo "Nutzung: $0 unban <IP-Adresse>" >&2
@@ -718,15 +987,16 @@ case "${1:-start}" in
cat << USAGE
AdGuard Shield v${VERSION}
Nutzung: $0 {start|stop|status|history|flush|unban|test|dry-run|blocklist-status|blocklist-sync|blocklist-flush}
Nutzung: $0 {start|stop|status|history|flush|unban|reset-offenses|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
status Zeigt aktive Sperren, Regeln und Wiederholungstäter
history [N] Zeigt die letzten N Ban-Einträge (Standard: 50)
flush Hebt alle Sperren auf
unban IP Entsperrt eine bestimmte IP-Adresse
reset-offenses [IP] Setzt Offense-Zähler zurück (alle oder eine bestimmte IP)
test Testet die Verbindung zur AdGuard Home API
dry-run Startet im Testmodus (keine echten Sperren)
blocklist-status Zeigt Status der externen Blocklisten

View File

@@ -34,11 +34,13 @@
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-shield/192.168.1.50.ban`
8. Ban-History Eintrag wird in `/var/log/adguard-shield-bans.log` geschrieben
9. Log-Eintrag + optionale Webhook-Benachrichtigung
10. Nach 3600 Sekunden (1 Stunde): automatische Entsperrung + History-Eintrag
6. **Progressive Sperren:** Offense-Level wird geprüft/erhöht, Sperrdauer berechnet
7. iptables-Regel wird erstellt: `DROP` für `192.168.1.50` auf allen DNS-Ports
8. State-Datei wird angelegt: `/var/lib/adguard-shield/192.168.1.50.ban`
9. Offense-Datei wird aktualisiert: `/var/lib/adguard-shield/192.168.1.50.offenses`
10. Ban-History Eintrag wird in `/var/log/adguard-shield-bans.log` geschrieben
11. Log-Eintrag + optionale Webhook-Benachrichtigung
12. Nach Ablauf der (progressiven) Sperrdauer: automatische Entsperrung + History-Eintrag
## iptables Strategie
@@ -82,12 +84,31 @@ COUNT=45
BAN_TIME=2026-03-03 14:30:00
BAN_UNTIL_EPOCH=1741012200
BAN_UNTIL=2026-03-03 15:30:00
BAN_DURATION=3600
OFFENSE_LEVEL=1
IS_PERMANENT=false
```
Zusätzlich wird für jede IP ein Offense-Tracker gespeichert:
```
/var/lib/adguard-shield/192.168.1.50.offenses
```
Inhalt:
```
CLIENT_IP=192.168.1.50
OFFENSE_LEVEL=2
LAST_OFFENSE_EPOCH=1741008600
LAST_OFFENSE=2026-03-03 14:30:00
FIRST_OFFENSE=2026-03-03 12:15:00
```
Das ermöglicht:
- Persistenz über Script-Neustarts hinweg
- Statusabfragen jederzeit möglich
- Automatisches Aufräumen per Cron-Job
- Progressive Sperrzeiten über mehrere Ban-Zyklen hinweg
## Dateistruktur nach Installation
@@ -105,6 +126,7 @@ Das ermöglicht:
/var/lib/adguard-shield/
├── *.ban # State-Dateien aktiver Sperren
├── *.offenses # Offense-Zähler (Progressive Sperren)
└── external-blocklist/ # Cache für externe Blocklisten
/var/log/

View File

@@ -73,6 +73,12 @@ sudo /opt/adguard-shield/adguard-shield.sh test
# Dry-Run (nur loggen, nichts sperren)
sudo /opt/adguard-shield/adguard-shield.sh dry-run
# Offense-Zähler für alle IPs zurücksetzen (Progressive Sperren)
sudo /opt/adguard-shield/adguard-shield.sh reset-offenses
# Offense-Zähler für eine bestimmte IP zurücksetzen
sudo /opt/adguard-shield/adguard-shield.sh reset-offenses 192.168.1.100
# Externe Blocklist - Status anzeigen
sudo /opt/adguard-shield/adguard-shield.sh blocklist-status

View File

@@ -1,6 +1,6 @@
# Webhook-Benachrichtigungen
Das Tool kann bei Sperren und Entsperrungen Benachrichtigungen an verschiedene Dienste senden.
Das Tool kann beim Starten und Stoppen des Services sowie bei Sperren und Entsperrungen Benachrichtigungen an verschiedene Dienste senden.
## Aktivierung
@@ -103,6 +103,12 @@ Sendet einen POST mit JSON-Body:
## Beispiel-Nachrichten
**Service gestartet:**
> 🟢 AdGuard Shield v0.4.0 wurde gestartet.
**Service gestoppt:**
> 🔴 AdGuard Shield v0.4.0 wurde gestoppt.
**Sperre:**
> 🚫 AdGuard Shield: Client **192.168.1.50** gesperrt (45x microsoft.com in 60s). Sperre für 3600s.

View File

@@ -41,11 +41,34 @@ Dadurch muss der Benutzer bei Updates die Konfiguration nicht manuell austausche
| Parameter | Standard | Beschreibung |
|-----------|----------|--------------|
| `BAN_DURATION` | `3600` | Sperrdauer in Sekunden (3600 = 1 Stunde) |
| `BAN_DURATION` | `3600` | Basis-Sperrdauer in Sekunden (3600 = 1 Stunde) |
| `IPTABLES_CHAIN` | `ADGUARD_SHIELD` | Name der iptables Chain |
| `BLOCKED_PORTS` | `53 443 853 784 8853` | Ports die gesperrt werden |
| `BLOCKED_PORTS` | `53 443 853` | Ports die gesperrt werden (IPv4 + IPv6) |
| `WHITELIST` | `127.0.0.1,::1` | IPs die nie gesperrt werden (kommagetrennt) |
### Progressive Sperren (Recidive)
Wiederholungstäter werden wie bei fail2ban stufenweise länger gesperrt. Wird eine IP nach dem Ablauf ihrer Sperre erneut auffällig, steigt die Sperrdauer exponentiell.
| Parameter | Standard | Beschreibung |
|-----------|----------|--------------|
| `PROGRESSIVE_BAN_ENABLED` | `true` | Progressive Sperren aktivieren |
| `PROGRESSIVE_BAN_MULTIPLIER` | `2` | Multiplikator pro Stufe (2 = Verdopplung) |
| `PROGRESSIVE_BAN_MAX_LEVEL` | `5` | Ab dieser Stufe wird permanent gesperrt (0 = nie) |
| `PROGRESSIVE_BAN_RESET_AFTER` | `86400` | Zähler-Reset nach X Sekunden ohne Vergehen (86400 = 24h) |
#### Beispiel bei Standardwerten
| Vergehen | Stufe | Sperrdauer | Berechnung |
|----------|-------|------------|------------|
| 1. Mal | 1 | 1 Stunde | 3600 × 1 |
| 2. Mal | 2 | 2 Stunden | 3600 × 2 |
| 3. Mal | 3 | 4 Stunden | 3600 × 4 |
| 4. Mal | 4 | 8 Stunden | 3600 × 8 |
| 5. Mal | 5 | **PERMANENT** | Max-Stufe erreicht |
> **Hinweis:** Der Offense-Zähler wird automatisch zurückgesetzt, wenn eine IP für den konfigurierten Zeitraum (`PROGRESSIVE_BAN_RESET_AFTER`) kein erneutes Vergehen begeht. Permanente Sperren werden **nicht** automatisch aufgehoben sie müssen manuell mit `unban` oder `flush` entfernt werden.
### Logging
| Parameter | Standard | Beschreibung |
@@ -59,9 +82,13 @@ Dadurch muss der Benutzer bei Updates die Konfiguration nicht manuell austausche
| Parameter | Standard | Beschreibung |
|-----------|----------|--------------|
| `NOTIFY_ENABLED` | `false` | Webhook-Benachrichtigungen aktivieren |
| `NOTIFY_WEBHOOK_URL` | *(leer)* | Webhook-URL |
| `NOTIFY_TYPE` | `generic` | Typ: `discord`, `slack`, `gotify`, `generic` |
| `NOTIFY_ENABLED` | `false` | Benachrichtigungen aktivieren |
| `NOTIFY_TYPE` | `ntfy` | Typ: `ntfy`, `discord`, `slack`, `gotify`, `generic` |
| `NOTIFY_WEBHOOK_URL` | *(leer)* | Webhook-URL (nur für discord, slack, gotify, generic) |
| `NTFY_SERVER_URL` | `https://ntfy.sh` | Ntfy Server-URL |
| `NTFY_TOPIC` | *(leer)* | Ntfy Topic-Name |
| `NTFY_TOKEN` | *(leer)* | Optionaler Ntfy Access-Token |
| `NTFY_PRIORITY` | `4` | Ntfy Priorität (15) |
### Erweitert
@@ -75,7 +102,7 @@ Dadurch muss der Benutzer bei Updates die Konfiguration nicht manuell austausche
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.) |
@@ -115,16 +142,14 @@ sudo systemctl restart adguard-shield
```
## Gesperrte Ports im Detail
Bei einem Rate-Limit-Verstoß werden **alle** DNS-Protokoll-Ports für den Client gesperrt:
Bei einem Rate-Limit-Verstoß werden **alle** DNS-Protokoll-Ports für den Client gesperrt (IPv4 via `iptables` und IPv6 via `ip6tables`):
| 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) |
| 853 | TCP | DNS-over-TLS (`tls://dns1.techniverse.net:853`) |
| 853 | UDP | DNS-over-QUIC (`quic://dns1.techniverse.net:853`) |
## Whitelist richtig pflegen

View File

@@ -49,12 +49,30 @@ sudo /opt/adguard-shield/adguard-shield.sh start
```bash
sudo /opt/adguard-shield/adguard-shield.sh history | grep 192.168.1.100
```
3. IP zur Whitelist hinzufügen in `adguard-shield.conf`
3. Service neustarten:
3. Offense-Zähler für die IP zurücksetzen (damit die progressive Sperre wieder bei Stufe 1 beginnt):
```bash
sudo /opt/adguard-shield/adguard-shield.sh reset-offenses 192.168.1.100
```
4. IP zur Whitelist hinzufügen in `adguard-shield.conf`
5. Service neustarten:
```bash
sudo systemctl restart adguard-shield
```
### Client wurde permanent gesperrt (Progressive Sperren)
Wenn eine IP die maximale Stufe der progressiven Sperren erreicht hat, wird sie permanent gesperrt und nicht automatisch aufgehoben.
1. IP entsperren:
```bash
sudo /opt/adguard-shield/adguard-shield.sh unban 192.168.1.100
```
2. Offense-Zähler zurücksetzen:
```bash
sudo /opt/adguard-shield/adguard-shield.sh reset-offenses 192.168.1.100
```
3. Prüfen ob die IP auf die Whitelist gehört, oder die Progressive-Ban-Einstellungen anpassen (`PROGRESSIVE_BAN_MAX_LEVEL` erhöhen oder auf `0` setzen für keine permanenten Sperren)
### 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.

76
doc/update.md Normal file
View File

@@ -0,0 +1,76 @@
# Update-Anleitung
## Voraussetzungen
- AdGuard Shield ist bereits installiert (`/opt/adguard-shield/`)
- Git ist installiert (`sudo apt install git`)
- Zugriff auf den Server per SSH mit Root-Rechten
## Update durchführen
### 1. Git-Repository aktualisieren
Wechsle in das Verzeichnis, in dem du das Repository geklont hast, und hole die neueste Version:
```bash
cd /pfad/zum/adguard-shield
git pull
```
> **Hinweis:** Falls du das Repository z.B. nach `/opt/adguard-shield-repo` geklont hast:
> ```bash
> cd /opt/adguard-shield-repo
> git pull
> ```
### 2. Update-Script ausführen
```bash
sudo bash install.sh update
```
Das Update-Script macht automatisch folgendes:
1. **Abhängigkeiten prüfen** — Fehlende Pakete werden nachinstalliert
2. **Scripts aktualisieren** — Alle `.sh`-Dateien werden nach `/opt/adguard-shield/` kopiert
3. **Konfigurations-Migration** — Neue Parameter werden automatisch zur bestehenden Konfiguration hinzugefügt, bestehende Einstellungen bleiben **unverändert**
4. **Backup erstellen** — Die alte Konfiguration wird als `adguard-shield.conf.old` gesichert
5. **Service aktualisieren** — Die systemd Service-Datei wird aktualisiert und `daemon-reload` ausgeführt
6. **Service neustarten** — Der Service wird automatisch neu gestartet (falls er vorher lief)
### 3. Neue Parameter prüfen (optional)
Nach dem Update empfiehlt es sich, eventuell neu hinzugefügte Konfigurationsparameter zu prüfen:
```bash
sudo nano /opt/adguard-shield/adguard-shield.conf
```
Falls etwas nicht stimmt, kann das Backup wiederhergestellt werden:
```bash
sudo cp /opt/adguard-shield/adguard-shield.conf.old /opt/adguard-shield/adguard-shield.conf
sudo systemctl restart adguard-shield
```
## Kurzfassung (Copy & Paste)
```bash
cd /pfad/zum/adguard-shield
git pull
sudo bash install.sh update
```
## Versionsprüfung
Installierte Version anzeigen:
```bash
sudo /opt/adguard-shield/adguard-shield.sh status
```
Oder über den Installer:
```bash
sudo bash install.sh status
```

View File

@@ -6,7 +6,7 @@
# Lizenz: MIT
###############################################################################
VERSION="0.3.0"
VERSION="0.3.1"
set -euo pipefail

View File

@@ -51,6 +51,12 @@ for state_file in "${STATE_DIR}"/*.ban; do
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)
is_permanent=$(grep '^IS_PERMANENT=' "$state_file" | cut -d= -f2)
# Permanente Sperren nicht automatisch aufheben
if [[ "$is_permanent" == "true" || "$ban_until_epoch" == "0" ]]; then
continue
fi
if [[ -n "$ban_until_epoch" && "$NOW" -ge "$ban_until_epoch" ]]; then
echo "$LOG_PREFIX Entsperre abgelaufene Sperre: $client_ip" >> "$LOG_FILE"