@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
@@ -22,12 +22,14 @@ 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
|
||||
- **Subdomain-Flood-Erkennung** — erkennt Random-Subdomain-Attacken (z.B. `abc123.microsoft.com`, `xyz456.microsoft.com`, ...)
|
||||
- **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
|
||||
- Automatisches Entsperren nach konfigurierbarer Dauer
|
||||
- **Externe Blocklisten** — IP-Adressen von externen Textdateien (URLs) laden und automatisch sperren
|
||||
- **AbuseIPDB Reporting** — permanent gesperrte IPs automatisch an AbuseIPDB melden
|
||||
- **Ban-History** — lückenlose Protokollierung aller Sperren/Entsperrungen mit Zeitstempel
|
||||
- Whitelist für vertrauenswürdige IPs
|
||||
- Dry-Run Modus zum gefahrlosen Testen
|
||||
|
||||
@@ -21,6 +21,22 @@ RATE_LIMIT_WINDOW=60
|
||||
# Wie oft das Script die Logs prüft (in Sekunden)
|
||||
CHECK_INTERVAL=10
|
||||
|
||||
# --- Subdomain-Flood-Erkennung (Random Subdomain Attack) ---
|
||||
# Erkennt Bots/Clients die massenhaft zufällige Subdomains einer Domain abfragen
|
||||
# Beispiel: abc123.microsoft.com, xyz456.microsoft.com, ...
|
||||
# Dabei wird pro Client gezählt, wie viele EINDEUTIGE Subdomains einer
|
||||
# Basisdomain (z.B. microsoft.com) im Zeitfenster aufgerufen werden.
|
||||
|
||||
# Subdomain-Flood-Erkennung aktivieren
|
||||
SUBDOMAIN_FLOOD_ENABLED=true
|
||||
|
||||
# Maximale Anzahl eindeutiger Subdomains pro Basisdomain pro Client im Zeitfenster
|
||||
# Beispiel: 50 = ein Client darf max. 50 verschiedene Subdomains von microsoft.com abfragen
|
||||
SUBDOMAIN_FLOOD_MAX_UNIQUE=50
|
||||
|
||||
# Zeitfenster in Sekunden für die Subdomain-Flood-Erkennung (60 = 1 Minute)
|
||||
SUBDOMAIN_FLOOD_WINDOW=60
|
||||
|
||||
# --- Sperr-Einstellungen ---
|
||||
# Wie lange ein Client gesperrt wird (in Sekunden, 3600 = 1 Stunde)
|
||||
BAN_DURATION=3600
|
||||
@@ -32,6 +48,8 @@ IPTABLES_CHAIN="ADGUARD_SHIELD"
|
||||
# Port 53 = DNS (UDP + TCP)
|
||||
# Port 443 = DNS-over-HTTPS (DoH)
|
||||
# Port 853 = DNS-over-TLS (tls://...:853) / DNS-over-QUIC (quic://...:853)
|
||||
# Hinweis: Das verwendete Protokoll (DNS/DoH/DoT/DoQ) wird automatisch
|
||||
# aus der AdGuard Home API erkannt und in Logs/History angezeigt.
|
||||
BLOCKED_PORTS="53 443 853"
|
||||
|
||||
# --- Whitelist ---
|
||||
@@ -118,6 +136,19 @@ PROGRESSIVE_BAN_MAX_LEVEL=5
|
||||
# (86400 = 24 Stunden, 604800 = 7 Tage)
|
||||
PROGRESSIVE_BAN_RESET_AFTER=86400
|
||||
|
||||
# --- AbuseIPDB Reporting (optional) ---
|
||||
# Meldet permanent gesperrte IPs automatisch an AbuseIPDB
|
||||
# Nur bei PERMANENTEN Sperren wird ein Report gesendet.
|
||||
ABUSEIPDB_ENABLED=false
|
||||
|
||||
# AbuseIPDB API-Key (https://www.abuseipdb.com/account/api)
|
||||
ABUSEIPDB_API_KEY=""
|
||||
|
||||
# Kategorien für den Report (kommagetrennt)
|
||||
# 4 = DDoS Attack
|
||||
# Siehe: https://www.abuseipdb.com/categories
|
||||
ABUSEIPDB_CATEGORIES="4"
|
||||
|
||||
# --- Erweiterte Einstellungen ---
|
||||
# Pfad zur State-Datei (speichert aktive Sperren)
|
||||
STATE_DIR="/var/lib/adguard-shield"
|
||||
|
||||
+324
-52
@@ -5,11 +5,11 @@
|
||||
#
|
||||
# Autor: Patrick Asmus
|
||||
# E-Mail: support@techniverse.net
|
||||
# Datum: 2026-03-03
|
||||
# Datum: 2026-03-04
|
||||
# Lizenz: MIT
|
||||
###############################################################################
|
||||
|
||||
VERSION="0.3.1"
|
||||
VERSION="0.4.0"
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -79,23 +79,25 @@ log_ban_history() {
|
||||
local count="${4:-}"
|
||||
local reason="${5:-}"
|
||||
local duration="${6:-}"
|
||||
local protocol="${7:-}"
|
||||
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"
|
||||
echo "# Format: ZEITSTEMPEL | AKTION | CLIENT-IP | DOMAIN | ANFRAGEN | SPERRDAUER | PROTOKOLL | GRUND" >> "$BAN_HISTORY_FILE"
|
||||
echo "#──────────────────────────────────────────────────────────────────────────────────────────────────" >> "$BAN_HISTORY_FILE"
|
||||
fi
|
||||
|
||||
if [[ -z "$duration" && "$action" == "BAN" ]]; then
|
||||
duration="${BAN_DURATION}s"
|
||||
fi
|
||||
[[ -z "$duration" ]] && duration="-"
|
||||
[[ -z "$protocol" ]] && protocol="-"
|
||||
|
||||
printf "%-19s | %-6s | %-39s | %-30s | %-8s | %-10s | %s\n" \
|
||||
"$timestamp" "$action" "$client_ip" "${domain:--}" "${count:--}" "$duration" "${reason:-rate-limit}" \
|
||||
printf "%-19s | %-6s | %-39s | %-30s | %-8s | %-10s | %-10s | %s\n" \
|
||||
"$timestamp" "$action" "$client_ip" "${domain:--}" "${count:--}" "$duration" "$protocol" "${reason:-rate-limit}" \
|
||||
>> "$BAN_HISTORY_FILE"
|
||||
}
|
||||
|
||||
@@ -111,8 +113,8 @@ get_offense_level() {
|
||||
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)
|
||||
level=$(grep '^OFFENSE_LEVEL=' "$offense_file" | cut -d= -f2 || true)
|
||||
last_offense=$(grep '^LAST_OFFENSE_EPOCH=' "$offense_file" | cut -d= -f2 || true)
|
||||
now=$(date '+%s')
|
||||
reset_after="${PROGRESSIVE_BAN_RESET_AFTER:-86400}"
|
||||
|
||||
@@ -141,7 +143,7 @@ increment_offense_level() {
|
||||
|
||||
# Erstes Vergehen merken (bevor Datei überschrieben wird)
|
||||
local first_offense
|
||||
first_offense=$(grep '^FIRST_OFFENSE=' "$offense_file" 2>/dev/null | cut -d= -f2)
|
||||
first_offense=$(grep '^FIRST_OFFENSE=' "$offense_file" 2>/dev/null | cut -d= -f2 || true)
|
||||
[[ -z "$first_offense" ]] && first_offense="$now_readable"
|
||||
|
||||
cat > "$offense_file" << EOF
|
||||
@@ -213,6 +215,74 @@ reset_offense_level() {
|
||||
rm -f "$offense_file"
|
||||
}
|
||||
|
||||
# ─── Protokoll-Erkennung ─────────────────────────────────────────────────────
|
||||
# Wandelt AdGuard Home client_proto Werte in lesbare Protokoll-Namen um
|
||||
# API-Werte: "" = Plain DNS, "doh" = DNS-over-HTTPS, "dot" = DNS-over-TLS,
|
||||
# "doq" = DNS-over-QUIC, "dnscrypt" = DNSCrypt
|
||||
format_protocol() {
|
||||
local proto="$1"
|
||||
case "${proto,,}" in
|
||||
doh) echo "DoH" ;;
|
||||
dot) echo "DoT" ;;
|
||||
doq) echo "DoQ" ;;
|
||||
dnscrypt) echo "DNSCrypt" ;;
|
||||
""|dns) echo "DNS" ;;
|
||||
*) echo "${proto:-DNS}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ─── AbuseIPDB Reporting ─────────────────────────────────────────────────────
|
||||
# Meldet eine IP an AbuseIPDB (nur bei permanenten Sperren)
|
||||
report_to_abuseipdb() {
|
||||
local client_ip="$1"
|
||||
local domain="$2"
|
||||
local count="$3"
|
||||
local reason="${4:-rate-limit}"
|
||||
|
||||
if [[ "${ABUSEIPDB_ENABLED:-false}" != "true" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -z "${ABUSEIPDB_API_KEY:-}" ]]; then
|
||||
log "WARN" "AbuseIPDB: API-Key nicht konfiguriert (ABUSEIPDB_API_KEY ist leer)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Kommentar für AbuseIPDB erstellen (englisch)
|
||||
local comment
|
||||
if [[ "$reason" == "subdomain-flood" ]]; then
|
||||
comment="DNS flooding on our DNS server: ${count}x ${domain} (random subdomain attack). Permanently banned by AdGuard Shield."
|
||||
else
|
||||
comment="DNS flooding on our DNS server: ${count}x ${domain}. Permanently banned by AdGuard Shield."
|
||||
fi
|
||||
|
||||
local categories="${ABUSEIPDB_CATEGORIES:-4}"
|
||||
|
||||
log "INFO" "AbuseIPDB: Melde IP $client_ip (${comment})"
|
||||
|
||||
local http_code
|
||||
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
--connect-timeout 10 \
|
||||
--max-time 15 \
|
||||
-X POST "https://api.abuseipdb.com/api/v2/report" \
|
||||
-H "Key: ${ABUSEIPDB_API_KEY}" \
|
||||
-H "Accept: application/json" \
|
||||
--data-urlencode "ip=${client_ip}" \
|
||||
--data-urlencode "categories=${categories}" \
|
||||
--data-urlencode "comment=${comment}" \
|
||||
2>/dev/null) || true
|
||||
|
||||
if [[ "$http_code" == "200" || "$http_code" == "429" ]]; then
|
||||
if [[ "$http_code" == "429" ]]; then
|
||||
log "WARN" "AbuseIPDB: Rate-Limit erreicht für $client_ip (HTTP 429) – Report wird später erneut versucht"
|
||||
else
|
||||
log "INFO" "AbuseIPDB: IP $client_ip erfolgreich gemeldet (HTTP $http_code)"
|
||||
fi
|
||||
else
|
||||
log "ERROR" "AbuseIPDB: Meldung fehlgeschlagen für $client_ip (HTTP ${http_code:-timeout})"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Verzeichnisse erstellen ──────────────────────────────────────────────────
|
||||
init_directories() {
|
||||
mkdir -p "$STATE_DIR"
|
||||
@@ -296,6 +366,9 @@ ban_client() {
|
||||
local client_ip="$1"
|
||||
local domain="$2"
|
||||
local count="$3"
|
||||
local reason="${4:-rate-limit}"
|
||||
local window="${5:-$RATE_LIMIT_WINDOW}"
|
||||
local protocol="${6:-DNS}"
|
||||
|
||||
# Prüfen ob bereits gesperrt
|
||||
local state_file="${STATE_DIR}/${client_ip//[:\/]/_}.ban"
|
||||
@@ -333,18 +406,18 @@ ban_client() {
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
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]"
|
||||
log "WARN" "[DRY-RUN] WÜRDE sperren: $client_ip (${count}x $domain in ${window}s via $protocol) für ${duration_display} [Stufe $offense_level] [${reason}]"
|
||||
else
|
||||
log "WARN" "[DRY-RUN] WÜRDE sperren: $client_ip (${count}x $domain in ${RATE_LIMIT_WINDOW}s)"
|
||||
log "WARN" "[DRY-RUN] WÜRDE sperren: $client_ip (${count}x $domain in ${window}s via $protocol) [${reason}]"
|
||||
fi
|
||||
log_ban_history "DRY" "$client_ip" "$domain" "$count" "dry-run" "${duration_display}"
|
||||
log_ban_history "DRY" "$client_ip" "$domain" "$count" "dry-run (${reason})" "${duration_display}" "$protocol"
|
||||
return 0
|
||||
fi
|
||||
|
||||
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}]"
|
||||
log "WARN" "SPERRE Client: $client_ip (${count}x $domain in ${window}s via $protocol) für ${duration_display} [Stufe ${offense_level}/${PROGRESSIVE_BAN_MAX_LEVEL:-0}] [${reason}]"
|
||||
else
|
||||
log "WARN" "SPERRE Client: $client_ip (${count}x $domain in ${RATE_LIMIT_WINDOW}s) für ${duration_display}"
|
||||
log "WARN" "SPERRE Client: $client_ip (${count}x $domain in ${window}s via $protocol) für ${duration_display} [${reason}]"
|
||||
fi
|
||||
|
||||
# IPv4 oder IPv6 erkennen
|
||||
@@ -367,16 +440,23 @@ BAN_UNTIL=$ban_until_display
|
||||
BAN_DURATION=${effective_duration}
|
||||
OFFENSE_LEVEL=$offense_level
|
||||
IS_PERMANENT=$is_permanent
|
||||
REASON=$reason
|
||||
PROTOCOL=$protocol
|
||||
EOF
|
||||
|
||||
# Ban-History Eintrag
|
||||
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"
|
||||
log_ban_history "BAN" "$client_ip" "$domain" "$count" "$reason" "$history_duration" "$protocol"
|
||||
|
||||
# Benachrichtigung senden
|
||||
if [[ "$NOTIFY_ENABLED" == "true" ]]; then
|
||||
send_notification "ban" "$client_ip" "$domain" "$count" "$offense_level" "$duration_display"
|
||||
send_notification "ban" "$client_ip" "$domain" "$count" "$offense_level" "$duration_display" "$reason" "$window" "$protocol"
|
||||
fi
|
||||
|
||||
# AbuseIPDB Report (nur bei permanenter Sperre)
|
||||
if [[ "$is_permanent" == "true" ]]; then
|
||||
report_to_abuseipdb "$client_ip" "$domain" "$count" "$reason" &
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -386,11 +466,14 @@ unban_client() {
|
||||
local reason="${2:-expired}"
|
||||
local state_file="${STATE_DIR}/${client_ip//[:\/]/_}.ban"
|
||||
|
||||
# Domain aus State lesen bevor wir löschen
|
||||
# Domain und Protokoll aus State lesen bevor wir löschen
|
||||
local domain="-"
|
||||
local protocol="-"
|
||||
if [[ -f "$state_file" ]]; then
|
||||
domain=$(grep '^DOMAIN=' "$state_file" | cut -d= -f2)
|
||||
domain=$(grep '^DOMAIN=' "$state_file" | cut -d= -f2 || true)
|
||||
protocol=$(grep '^PROTOCOL=' "$state_file" | cut -d= -f2 || true)
|
||||
fi
|
||||
[[ -z "$protocol" ]] && protocol="-"
|
||||
|
||||
log "INFO" "ENTSPERRE Client: $client_ip ($reason)"
|
||||
|
||||
@@ -403,7 +486,7 @@ unban_client() {
|
||||
rm -f "$state_file"
|
||||
|
||||
# Ban-History Eintrag
|
||||
log_ban_history "UNBAN" "$client_ip" "$domain" "-" "$reason"
|
||||
log_ban_history "UNBAN" "$client_ip" "$domain" "-" "$reason" "-" "$protocol"
|
||||
|
||||
if [[ "$NOTIFY_ENABLED" == "true" ]]; then
|
||||
send_notification "unban" "$client_ip" "" ""
|
||||
@@ -419,11 +502,11 @@ check_expired_bans() {
|
||||
[[ -f "$state_file" ]] || continue
|
||||
|
||||
local ban_until_epoch
|
||||
ban_until_epoch=$(grep '^BAN_UNTIL_EPOCH=' "$state_file" | cut -d= -f2)
|
||||
ban_until_epoch=$(grep '^BAN_UNTIL_EPOCH=' "$state_file" | cut -d= -f2 || true)
|
||||
local client_ip
|
||||
client_ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2)
|
||||
client_ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2 || true)
|
||||
local is_permanent
|
||||
is_permanent=$(grep '^IS_PERMANENT=' "$state_file" | cut -d= -f2)
|
||||
is_permanent=$(grep '^IS_PERMANENT=' "$state_file" | cut -d= -f2 || true)
|
||||
|
||||
# Permanente Sperren nicht automatisch aufheben
|
||||
if [[ "$is_permanent" == "true" || "$ban_until_epoch" == "0" ]]; then
|
||||
@@ -445,20 +528,26 @@ send_notification() {
|
||||
local count="$4"
|
||||
local offense_level="${5:-}"
|
||||
local duration_display="${6:-}"
|
||||
local reason="${7:-rate-limit}"
|
||||
local window="${8:-$RATE_LIMIT_WINDOW}"
|
||||
local protocol="${9:-DNS}"
|
||||
|
||||
# Ntfy benötigt keine Webhook-URL (nutzt NTFY_SERVER_URL + NTFY_TOPIC)
|
||||
if [[ "$NOTIFY_TYPE" != "ntfy" && -z "$NOTIFY_WEBHOOK_URL" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local reason_label="Rate-Limit"
|
||||
[[ "$reason" == "subdomain-flood" ]] && reason_label="Subdomain-Flood"
|
||||
|
||||
local message
|
||||
if [[ "$action" == "ban" ]]; then
|
||||
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}]."
|
||||
message="🚫 AdGuard Shield: Client **$client_ip** gesperrt (${count}x $domain in ${window}s via **$protocol**, ${reason_label}). 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}."
|
||||
message="🚫 AdGuard Shield: Client **$client_ip** gesperrt (${count}x $domain in ${window}s via **$protocol**, ${reason_label}). Sperre für ${simple_dur}."
|
||||
fi
|
||||
elif [[ "$action" == "service_start" ]]; then
|
||||
message="🟢 AdGuard Shield v${VERSION} wurde gestartet."
|
||||
@@ -580,10 +669,11 @@ analyze_queries() {
|
||||
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
|
||||
# Extrahiere Client-IP + Domain + Protokoll 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")
|
||||
# Protokoll: client_proto aus der API → ""/dns = Plain DNS, doh, dot, doq, dnscrypt
|
||||
local violations=""
|
||||
violations=$(echo "$api_response" | jq -r --argjson window_start "$window_start" '
|
||||
# ISO 8601 Timestamp zu Unix-Epoch konvertieren
|
||||
@@ -612,18 +702,20 @@ analyze_queries() {
|
||||
select((.time | to_epoch) >= $window_start) |
|
||||
{
|
||||
client: (.client // .client_info.ip // "unknown"),
|
||||
domain: ((.question.name // .question.host // "unknown") | rtrimstr("."))
|
||||
domain: ((.question.name // .question.host // "unknown") | rtrimstr(".")),
|
||||
proto: (.client_proto // "")
|
||||
}
|
||||
] |
|
||||
group_by(.client + "|" + .domain) |
|
||||
map({
|
||||
client: .[0].client,
|
||||
domain: .[0].domain,
|
||||
count: length
|
||||
count: length,
|
||||
protocols: ([.[].proto | if . == "" then "dns" else . end] | unique | join(","))
|
||||
}) |
|
||||
.[] |
|
||||
select(.count > 0) |
|
||||
"\(.client)|\(.domain)|\(.count)"
|
||||
"\(.client)|\(.domain)|\(.count)|\(.protocols)"
|
||||
') || {
|
||||
log "ERROR" "jq Analyse fehlgeschlagen - API-Antwort-Format prüfen (ist AdGuard Home erreichbar?)"
|
||||
return
|
||||
@@ -635,22 +727,156 @@ analyze_queries() {
|
||||
fi
|
||||
|
||||
# Prüfe jede Kombination gegen das Limit
|
||||
while IFS='|' read -r client domain count; do
|
||||
while IFS='|' read -r client domain count protocols; do
|
||||
[[ -z "$client" || -z "$domain" || -z "$count" ]] && continue
|
||||
|
||||
log "INFO" "Client: $client, Domain: $domain, Anfragen: $count/$RATE_LIMIT_MAX_REQUESTS"
|
||||
# Protokoll-Namen formatieren für die Anzeige
|
||||
local proto_display=""
|
||||
if [[ -n "$protocols" ]]; then
|
||||
local -a proto_parts=()
|
||||
IFS=',' read -ra raw_protos <<< "$protocols"
|
||||
for p in "${raw_protos[@]}"; do
|
||||
proto_parts+=("$(format_protocol "$p")")
|
||||
done
|
||||
proto_display=$(IFS=','; echo "${proto_parts[*]}")
|
||||
else
|
||||
proto_display="DNS"
|
||||
fi
|
||||
|
||||
log "INFO" "Client: $client, Domain: $domain, Anfragen: $count/$RATE_LIMIT_MAX_REQUESTS, Protokoll: $proto_display"
|
||||
|
||||
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)"
|
||||
log "INFO" "Client $client ist auf der Whitelist - keine Sperre (${count}x $domain via $proto_display)"
|
||||
continue
|
||||
fi
|
||||
|
||||
ban_client "$client" "$domain" "$count"
|
||||
ban_client "$client" "$domain" "$count" "rate-limit" "$RATE_LIMIT_WINDOW" "$proto_display"
|
||||
fi
|
||||
done <<< "$violations"
|
||||
}
|
||||
|
||||
# ─── Subdomain-Flood-Erkennung ──────────────────────────────────────────────
|
||||
# Erkennt Random-Subdomain-Attacken: Bots die massenhaft zufällige Subdomains
|
||||
# einer Domain abfragen (z.B. abc123.microsoft.com, xyz456.microsoft.com, ...)
|
||||
# Zählt eindeutige Subdomains pro Basisdomain und Client im Zeitfenster
|
||||
analyze_subdomain_flood() {
|
||||
local api_response="$1"
|
||||
|
||||
if [[ "${SUBDOMAIN_FLOOD_ENABLED:-false}" != "true" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local now_epoch
|
||||
now_epoch=$(date '+%s')
|
||||
local window="${SUBDOMAIN_FLOOD_WINDOW:-60}"
|
||||
local window_start=$((now_epoch - window))
|
||||
local max_unique="${SUBDOMAIN_FLOOD_MAX_UNIQUE:-50}"
|
||||
|
||||
log "DEBUG" "Subdomain-Flood-Prüfung: max ${max_unique} eindeutige Subdomains pro Basisdomain in ${window}s"
|
||||
|
||||
# jq-Analyse: Gruppiere nach Client + Basisdomain, zähle eindeutige Subdomains
|
||||
local violations=""
|
||||
violations=$(echo "$api_response" | jq -r --argjson window_start "$window_start" --argjson max_unique "$max_unique" '
|
||||
# Basisdomain extrahieren (eTLD+1)
|
||||
# Behandelt gängige Multi-Part-TLDs wie .co.uk, .com.au, .co.jp etc.
|
||||
def base_domain:
|
||||
split(".") |
|
||||
if length <= 2 then join(".")
|
||||
elif ((.[-2:] | join(".")) | test("^(co|com|net|org|gov|edu|ac|gv|ne|or|go)\\.[a-z]{2,3}$")) then
|
||||
if length >= 3 then .[-3:] | join(".") else join(".") end
|
||||
else
|
||||
.[-2:] | join(".")
|
||||
end;
|
||||
|
||||
# ISO 8601 Timestamp zu Unix-Epoch konvertieren
|
||||
def to_epoch:
|
||||
sub("\\.[0-9]+(?=[+-Z])"; "") |
|
||||
if endswith("Z") then
|
||||
fromdateiso8601
|
||||
elif test("[+-][0-9]{2}:[0-9]{2}$") then
|
||||
(.[:-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) |
|
||||
((.question.name // .question.host // "unknown") | rtrimstr(".")) as $domain |
|
||||
($domain | base_domain) as $base |
|
||||
{
|
||||
client: (.client // .client_info.ip // "unknown"),
|
||||
domain: $domain,
|
||||
base_domain: $base,
|
||||
proto: (.client_proto // "")
|
||||
}
|
||||
] |
|
||||
# Nur Einträge mit echten Subdomains (domain != base_domain)
|
||||
[.[] | select(.domain != .base_domain)] |
|
||||
group_by(.client + "|" + .base_domain) |
|
||||
map({
|
||||
client: .[0].client,
|
||||
base_domain: .[0].base_domain,
|
||||
unique_subdomains: ([.[].domain] | unique | length),
|
||||
total_queries: length,
|
||||
example_domains: ([.[].domain] | unique | .[0:3] | join(", ")),
|
||||
protocols: ([.[].proto | if . == "" then "dns" else . end] | unique | join(","))
|
||||
}) |
|
||||
.[] |
|
||||
select(.unique_subdomains > $max_unique) |
|
||||
"\(.client)|\(.base_domain)|\(.unique_subdomains)|\(.total_queries)|\(.example_domains)|\(.protocols)"
|
||||
') || {
|
||||
log "ERROR" "jq Subdomain-Flood-Analyse fehlgeschlagen"
|
||||
return
|
||||
}
|
||||
|
||||
if [[ -z "$violations" ]]; then
|
||||
log "DEBUG" "Keine Subdomain-Flood-Verstöße erkannt"
|
||||
return
|
||||
fi
|
||||
|
||||
# Gefundene Verstöße verarbeiten
|
||||
while IFS='|' read -r client base_domain unique_count total_count examples protocols; do
|
||||
[[ -z "$client" || -z "$base_domain" || -z "$unique_count" ]] && continue
|
||||
|
||||
# Protokoll-Namen formatieren
|
||||
local proto_display=""
|
||||
if [[ -n "$protocols" ]]; then
|
||||
local -a proto_parts=()
|
||||
IFS=',' read -ra raw_protos <<< "$protocols"
|
||||
for p in "${raw_protos[@]}"; do
|
||||
proto_parts+=("$(format_protocol "$p")")
|
||||
done
|
||||
proto_display=$(IFS=','; echo "${proto_parts[*]}")
|
||||
else
|
||||
proto_display="DNS"
|
||||
fi
|
||||
|
||||
log "WARN" "Subdomain-Flood erkannt: $client → ${unique_count} eindeutige Subdomains von $base_domain (${total_count} Anfragen via $proto_display, z.B. $examples)"
|
||||
|
||||
if is_whitelisted "$client"; then
|
||||
log "INFO" "Client $client ist auf der Whitelist - keine Sperre (Subdomain-Flood: ${unique_count}x $base_domain via $proto_display)"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Prüfen ob bereits gesperrt
|
||||
local state_file="${STATE_DIR}/${client//[:\/]/_}.ban"
|
||||
if [[ -f "$state_file" ]]; then
|
||||
log "DEBUG" "Client $client ist bereits gesperrt (Subdomain-Flood übersprungen)"
|
||||
continue
|
||||
fi
|
||||
|
||||
ban_client "$client" "*.${base_domain}" "$unique_count" "subdomain-flood" "$window" "$proto_display"
|
||||
done <<< "$violations"
|
||||
}
|
||||
|
||||
# ─── Status anzeigen ─────────────────────────────────────────────────────────
|
||||
show_status() {
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
@@ -667,26 +893,53 @@ show_status() {
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Subdomain-Flood-Schutz Info
|
||||
if [[ "${SUBDOMAIN_FLOOD_ENABLED:-false}" == "true" ]]; then
|
||||
echo " 🌐 Subdomain-Flood-Schutz: AKTIV"
|
||||
echo " Max eindeutige Subdomains: ${SUBDOMAIN_FLOOD_MAX_UNIQUE:-50} pro Basisdomain"
|
||||
echo " Zeitfenster: ${SUBDOMAIN_FLOOD_WINDOW:-60}s"
|
||||
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))
|
||||
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)
|
||||
local s_ip s_domain s_level s_perm s_dur s_until s_reason s_count s_proto
|
||||
s_ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2 || true)
|
||||
s_domain=$(grep '^DOMAIN=' "$state_file" | cut -d= -f2 || true)
|
||||
s_level=$(grep '^OFFENSE_LEVEL=' "$state_file" | cut -d= -f2 || true)
|
||||
s_perm=$(grep '^IS_PERMANENT=' "$state_file" | cut -d= -f2 || true)
|
||||
s_dur=$(grep '^BAN_DURATION=' "$state_file" | cut -d= -f2 || true)
|
||||
s_until=$(grep '^BAN_UNTIL=' "$state_file" | cut -d= -f2 || true)
|
||||
s_reason=$(grep '^REASON=' "$state_file" | cut -d= -f2 || true)
|
||||
s_count=$(grep '^COUNT=' "$state_file" | cut -d= -f2 || true)
|
||||
s_proto=$(grep '^PROTOCOL=' "$state_file" | cut -d= -f2 || true)
|
||||
s_reason="${s_reason:-rate-limit}"
|
||||
s_proto="${s_proto:-?}"
|
||||
|
||||
local reason_tag=""
|
||||
[[ "$s_reason" == "subdomain-flood" ]] && reason_tag=" (Subdomain-Flood)"
|
||||
|
||||
local count_info=""
|
||||
if [[ -n "$s_count" && "$s_count" != "-" ]]; then
|
||||
if [[ "$s_reason" == "subdomain-flood" ]]; then
|
||||
count_info=", ${s_count} Subdomains"
|
||||
else
|
||||
count_info=", ${s_count} Anfragen"
|
||||
fi
|
||||
fi
|
||||
|
||||
local proto_tag=" via ${s_proto}"
|
||||
|
||||
if [[ "$s_perm" == "true" ]]; then
|
||||
echo " 🚫 Gesperrt: $s_ip → $s_domain [PERMANENT, Stufe ${s_level:-?}]"
|
||||
echo " 🚫 Gesperrt: $s_ip → $s_domain [PERMANENT, Stufe ${s_level:-?}${count_info}${proto_tag}]${reason_tag}"
|
||||
elif [[ -n "$s_level" && "$s_level" -gt 0 ]]; then
|
||||
echo " 🚫 Gesperrt: $s_ip → $s_domain [Stufe ${s_level}, $(format_duration "${s_dur:-$BAN_DURATION}"), bis $s_until]"
|
||||
echo " 🚫 Gesperrt: $s_ip → $s_domain [Stufe ${s_level}, $(format_duration "${s_dur:-$BAN_DURATION}"), bis $s_until${count_info}${proto_tag}]${reason_tag}"
|
||||
else
|
||||
echo " 🚫 Gesperrt: $s_ip → $s_domain [bis $s_until]"
|
||||
echo " 🚫 Gesperrt: $s_ip → $s_domain [bis $s_until${count_info}${proto_tag}]${reason_tag}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
@@ -705,9 +958,9 @@ show_status() {
|
||||
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)
|
||||
o_ip=$(grep '^CLIENT_IP=' "$offense_file" | cut -d= -f2 || true)
|
||||
o_level=$(grep '^OFFENSE_LEVEL=' "$offense_file" | cut -d= -f2 || true)
|
||||
o_last=$(grep '^LAST_OFFENSE=' "$offense_file" | cut -d= -f2 || true)
|
||||
offense_count=$((offense_count + 1))
|
||||
local next_dur
|
||||
next_dur=$(calculate_ban_duration "$((o_level + 1))")
|
||||
@@ -781,7 +1034,7 @@ flush_all_bans() {
|
||||
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)
|
||||
client_ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2 || true)
|
||||
unban_client "$client_ip" "manual-flush"
|
||||
done
|
||||
|
||||
@@ -798,7 +1051,7 @@ flush_all_offenses() {
|
||||
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)
|
||||
o_ip=$(grep '^CLIENT_IP=' "$offense_file" | cut -d= -f2 || true)
|
||||
log "INFO" "Offense-Zähler zurückgesetzt: $o_ip"
|
||||
rm -f "$offense_file"
|
||||
count=$((count + 1))
|
||||
@@ -855,6 +1108,16 @@ main_loop() {
|
||||
else
|
||||
log "INFO" " Progressive Sperren: deaktiviert"
|
||||
fi
|
||||
if [[ "${SUBDOMAIN_FLOOD_ENABLED:-false}" == "true" ]]; then
|
||||
log "INFO" " Subdomain-Flood-Schutz: AKTIV (max ${SUBDOMAIN_FLOOD_MAX_UNIQUE:-50} Subdomains/${SUBDOMAIN_FLOOD_WINDOW:-60}s)"
|
||||
else
|
||||
log "INFO" " Subdomain-Flood-Schutz: deaktiviert"
|
||||
fi
|
||||
if [[ "${ABUSEIPDB_ENABLED:-false}" == "true" ]]; then
|
||||
log "INFO" " AbuseIPDB Reporting: AKTIV (Kategorien: ${ABUSEIPDB_CATEGORIES:-4})"
|
||||
else
|
||||
log "INFO" " AbuseIPDB Reporting: deaktiviert"
|
||||
fi
|
||||
log "INFO" "═══════════════════════════════════════════════════════════"
|
||||
|
||||
# Service-Start-Benachrichtigung senden
|
||||
@@ -873,6 +1136,7 @@ main_loop() {
|
||||
local api_response
|
||||
if api_response=$(query_adguard_log); then
|
||||
analyze_queries "$api_response"
|
||||
analyze_subdomain_flood "$api_response"
|
||||
fi
|
||||
|
||||
sleep "$CHECK_INTERVAL"
|
||||
@@ -987,22 +1251,30 @@ case "${1:-start}" in
|
||||
cat << USAGE
|
||||
AdGuard Shield v${VERSION}
|
||||
|
||||
Nutzung: $0 {start|stop|status|history|flush|unban|reset-offenses|test|dry-run|blocklist-status|blocklist-sync|blocklist-flush}
|
||||
Service-Steuerung (empfohlen):
|
||||
sudo systemctl start adguard-shield
|
||||
sudo systemctl stop adguard-shield
|
||||
sudo systemctl restart adguard-shield
|
||||
sudo systemctl status adguard-shield
|
||||
|
||||
Befehle:
|
||||
start Startet den Monitor (inkl. Blocklist-Worker)
|
||||
stop Stoppt den Monitor
|
||||
Nutzung: $0 {status|history|flush|unban|reset-offenses|test|dry-run|blocklist-status|blocklist-sync|blocklist-flush}
|
||||
|
||||
Verwaltungsbefehle:
|
||||
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)
|
||||
dry-run Startet im Testmodus (keine echten Sperren, Vordergrund!)
|
||||
blocklist-status Zeigt Status der externen Blocklisten
|
||||
blocklist-sync Einmalige Synchronisation der externen Blocklisten
|
||||
blocklist-flush Entfernt alle Sperren der externen Blocklisten
|
||||
|
||||
Interne Befehle (nicht direkt verwenden — nur über systemd):
|
||||
start Startet den Monitor im Vordergrund
|
||||
stop Stoppt den Monitor
|
||||
|
||||
Konfiguration: $CONFIG_FILE
|
||||
Log-Datei: $LOG_FILE
|
||||
Ban-History: $BAN_HISTORY_FILE
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
|
||||
## Ablauf einer Sperre
|
||||
|
||||
### Rate-Limit-Sperre
|
||||
|
||||
1. Client `192.168.1.50` fragt `microsoft.com` 45x in 60 Sekunden an
|
||||
2. Monitor fragt die AdGuard Home API alle 10 Sekunden ab (`/control/querylog`)
|
||||
3. Die Anfragen werden pro Client+Domain-Kombination gezählt
|
||||
@@ -42,6 +44,18 @@
|
||||
11. Log-Eintrag + optionale Webhook-Benachrichtigung
|
||||
12. Nach Ablauf der (progressiven) Sperrdauer: automatische Entsperrung + History-Eintrag
|
||||
|
||||
### Subdomain-Flood-Sperre (Random Subdomain Attack)
|
||||
|
||||
1. Client `10.0.0.99` fragt `abc123.microsoft.com`, `xyz456.microsoft.com`, ... ab
|
||||
2. Monitor extrahiert die **Basisdomain** (`microsoft.com`) aus jeder Anfrage
|
||||
3. Pro Client wird gezählt, wie viele **eindeutige Subdomains** einer Basisdomain im Zeitfenster abgefragt wurden
|
||||
4. Monitor erkennt: 63 eindeutige Subdomains > 50 (Schwellwert überschritten)
|
||||
5. Prüfung: Ist der Client auf der Whitelist? → Nein
|
||||
6. Sperre wird ausgeführt mit Domain `*.microsoft.com` und Grund `subdomain-flood`
|
||||
7. Progressive Sperren greifen auch hier — Wiederholungstäter werden stufenweise länger gesperrt
|
||||
|
||||
> **Hinweis:** Die Subdomain-Flood-Erkennung hat ein eigenes Zeitfenster (`SUBDOMAIN_FLOOD_WINDOW`) und einen eigenen Schwellwert (`SUBDOMAIN_FLOOD_MAX_UNIQUE`), unabhängig von den Rate-Limit-Einstellungen.
|
||||
|
||||
## iptables Strategie
|
||||
|
||||
Das Tool erstellt eine eigene Chain `ADGUARD_SHIELD`:
|
||||
@@ -87,6 +101,7 @@ BAN_UNTIL=2026-03-03 15:30:00
|
||||
BAN_DURATION=3600
|
||||
OFFENSE_LEVEL=1
|
||||
IS_PERMANENT=false
|
||||
REASON=rate-limit
|
||||
```
|
||||
|
||||
Zusätzlich wird für jede IP ein Offense-Tracker gespeichert:
|
||||
@@ -185,7 +200,9 @@ ZEITSTEMPEL | AKTION | CLIENT-IP | DOMAIN
|
||||
| Grund | Bedeutung |
|
||||
|-------|----------|
|
||||
| `rate-limit` | Automatische Sperre wegen Limit-Überschreitung |
|
||||
| `subdomain-flood` | Sperre wegen zu vieler eindeutiger Subdomains einer Basisdomain |
|
||||
| `dry-run` | Im Dry-Run erkannt (nicht wirklich gesperrt) |
|
||||
| `dry-run (subdomain-flood)` | Subdomain-Flood im Dry-Run erkannt |
|
||||
| `expired` | Automatisch entsperrt nach Ablauf der Sperrdauer |
|
||||
| `expired-cron` | Entsperrt durch den Cron-Job (`unban-expired.sh`) |
|
||||
| `manual` | Manuell entsperrt per `unban`-Befehl |
|
||||
|
||||
+165
-26
@@ -34,6 +34,18 @@ Beim Update passiert automatisch:
|
||||
5. Der systemd Service wird per `daemon-reload` neu geladen
|
||||
6. Der Service wird automatisch neu gestartet (falls er lief)
|
||||
|
||||
### API-Verbindungstest nach Installation
|
||||
|
||||
Nach der Installation wird automatisch ein **zweistufiger Verbindungstest** durchgeführt:
|
||||
|
||||
1. **Base-URL Erreichbarkeit** — Prüft ob die konfigurierte `ADGUARD_URL` erreichbar ist (DNS, TCP, HTTP). Bei Fehlern werden spezifische Hinweise angezeigt (z.B. DNS-Fehler, Timeout, SSL-Problem).
|
||||
2. **API-Authentifizierung** — Testet ob die hinterlegten Zugangsdaten (`ADGUARD_USER` / `ADGUARD_PASS`) korrekt sind, indem der API-Endpunkt `/control/querylog` abgefragt wird.
|
||||
|
||||
> **Hinweis:** Dieser Test kann auch jederzeit manuell ausgeführt werden:
|
||||
> ```bash
|
||||
> sudo /opt/adguard-shield/adguard-shield.sh test
|
||||
> ```
|
||||
|
||||
### Voraussetzungen
|
||||
|
||||
Folgende Pakete werden bei der Installation automatisch installiert (via `apt`):
|
||||
@@ -43,15 +55,31 @@ Folgende Pakete werden bei der Installation automatisch installiert (via `apt`):
|
||||
- `gawk` — Textverarbeitung
|
||||
- `systemd` — Service-Management
|
||||
|
||||
## Monitor (Hauptscript)
|
||||
## systemd Service
|
||||
|
||||
AdGuard Shield wird als systemd Service betrieben. **Zum Starten, Stoppen und Neustarten immer `systemctl` verwenden:**
|
||||
|
||||
```bash
|
||||
# Starten
|
||||
sudo /opt/adguard-shield/adguard-shield.sh start
|
||||
# Start / Stop / Restart
|
||||
sudo systemctl start adguard-shield
|
||||
sudo systemctl stop adguard-shield
|
||||
sudo systemctl restart adguard-shield
|
||||
|
||||
# Stoppen
|
||||
sudo /opt/adguard-shield/adguard-shield.sh stop
|
||||
# Status
|
||||
sudo systemctl status adguard-shield
|
||||
|
||||
# Autostart aktivieren / deaktivieren
|
||||
sudo systemctl enable adguard-shield
|
||||
sudo systemctl disable adguard-shield
|
||||
```
|
||||
|
||||
> **Hinweis:** Der Service wird bei der Installation automatisch für den Autostart beim Booten aktiviert. Nach einem Update wird der Service automatisch neu gestartet — ein manueller Neustart ist nicht nötig.
|
||||
|
||||
## Monitor — Verwaltungsbefehle
|
||||
|
||||
Die folgenden Befehle dienen der **Verwaltung und Diagnose** und können jederzeit ausgeführt werden, auch während der Service läuft:
|
||||
|
||||
```bash
|
||||
# Status + aktive Sperren anzeigen
|
||||
sudo /opt/adguard-shield/adguard-shield.sh status
|
||||
|
||||
@@ -70,7 +98,7 @@ sudo /opt/adguard-shield/adguard-shield.sh unban 192.168.1.100
|
||||
# API-Verbindung testen
|
||||
sudo /opt/adguard-shield/adguard-shield.sh test
|
||||
|
||||
# Dry-Run (nur loggen, nichts sperren)
|
||||
# Dry-Run (nur loggen, nichts sperren — läuft im Vordergrund!)
|
||||
sudo /opt/adguard-shield/adguard-shield.sh dry-run
|
||||
|
||||
# Offense-Zähler für alle IPs zurücksetzen (Progressive Sperren)
|
||||
@@ -89,6 +117,8 @@ sudo /opt/adguard-shield/adguard-shield.sh blocklist-sync
|
||||
sudo /opt/adguard-shield/adguard-shield.sh blocklist-flush
|
||||
```
|
||||
|
||||
> **⚠ Wichtig:** Zum Starten und Stoppen des Monitors **nicht** `adguard-shield.sh start` bzw. `stop` verwenden! Diese Befehle starten den Prozess im **Vordergrund** — die Ausgabe wird live angezeigt und `Strg+C` beendet den gesamten Prozess. Stattdessen immer `sudo systemctl start/stop/restart adguard-shield` nutzen.
|
||||
|
||||
## iptables Helper
|
||||
|
||||
Für die manuelle Verwaltung der Firewall-Regeln:
|
||||
@@ -138,26 +168,6 @@ sudo /opt/adguard-shield/external-blocklist-worker.sh status
|
||||
sudo /opt/adguard-shield/external-blocklist-worker.sh flush
|
||||
```
|
||||
|
||||
## systemd Service
|
||||
|
||||
Der Service wird bei der Installation automatisch für den **Autostart beim Booten** aktiviert.
|
||||
|
||||
```bash
|
||||
# Start / Stop / Restart
|
||||
sudo systemctl start adguard-shield
|
||||
sudo systemctl stop adguard-shield
|
||||
sudo systemctl restart adguard-shield
|
||||
|
||||
# Status
|
||||
sudo systemctl status adguard-shield
|
||||
|
||||
# Autostart aktivieren / deaktivieren
|
||||
sudo systemctl enable adguard-shield
|
||||
sudo systemctl disable adguard-shield
|
||||
```
|
||||
|
||||
> **Hinweis:** Nach einem Update wird der Service automatisch neu gestartet. Ein manueller Neustart ist nicht nötig.
|
||||
|
||||
## Logs
|
||||
|
||||
```bash
|
||||
@@ -186,6 +196,135 @@ sudo crontab -e
|
||||
*/5 * * * * /opt/adguard-shield/unban-expired.sh
|
||||
```
|
||||
|
||||
## DNS-Abfragen zum Testen (von einem Linux-Client)
|
||||
|
||||
> **⚠ WARNUNG — Bitte unbedingt lesen:**
|
||||
>
|
||||
> Die folgenden Befehle dienen **ausschließlich zu Testzwecken**, um die eigene AdGuard-Shield-Installation zu überprüfen. Sie simulieren erhöhtes DNS-Aufkommen und können dazu genutzt werden, die Erkennungs- und Sperrmechanismen zu validieren.
|
||||
>
|
||||
> **DNS-Flooding ist illegal!** Das massenhafte Senden von DNS-Anfragen an fremde Server oder Infrastruktur ohne ausdrückliche Genehmigung kann als **Denial-of-Service-Angriff (DoS)** gewertet werden und ist in den meisten Ländern **strafbar**. Die Konsequenzen reichen von Abmahnungen über Strafanzeigen bis hin zu empfindlichen Geld- und Freiheitsstrafen.
|
||||
>
|
||||
> **Diese Befehle dürfen nur gegen den eigenen DNS-Server in einer kontrollierten Testumgebung eingesetzt werden.** Die Nutzung gegen fremde Server ist ausdrücklich untersagt. Jede Verantwortung liegt beim Anwender.
|
||||
|
||||
### Voraussetzungen
|
||||
|
||||
Die folgenden Tools müssen auf dem **Linux-Client** installiert sein (nicht auf dem Server):
|
||||
|
||||
```bash
|
||||
# Für DNS-Abfragen (dig)
|
||||
sudo apt install dnsutils
|
||||
|
||||
# Für DoH-Abfragen (curl)
|
||||
sudo apt install curl
|
||||
|
||||
# Für DoT-Abfragen (knotc)
|
||||
sudo apt install knot-dnsutils
|
||||
|
||||
# Für DoQ-Abfragen
|
||||
# https://github.com/natesales/q — Releases herunterladen oder via Go installieren:
|
||||
go install github.com/natesales/q@latest
|
||||
```
|
||||
|
||||
> **Hinweis:** In den folgenden Befehlen muss die IP-Adresse `203.0.113.50` durch die **eigene DNS-Server-IP** und `microsoft.com` durch die gewünschte **Ziel-Domain** ersetzt werden.
|
||||
|
||||
---
|
||||
|
||||
### Klassisches DNS (Port 53/UDP)
|
||||
|
||||
#### Direkte Abfragen (gleiche Domain, viele Anfragen)
|
||||
|
||||
200 parallele DNS-Anfragen für dieselbe Domain — jede mit einem zufälligen DNS-Cookie, um Caching zu umgehen:
|
||||
|
||||
```bash
|
||||
for i in {1..200}; do \
|
||||
dig @203.0.113.50 microsoft.com +short +cookie=$(openssl rand -hex 8) > /dev/null & \
|
||||
done; wait
|
||||
```
|
||||
|
||||
#### Zufällige Subdomain-Abfragen (NXDOMAIN-Flood)
|
||||
|
||||
200 parallele Anfragen mit zufällig generierten Subdomains — simuliert typisches Verhalten von DNS-basierten Angriffen:
|
||||
|
||||
```bash
|
||||
for i in {1..200}; do \
|
||||
dig @203.0.113.50 $(openssl rand -hex 6).microsoft.com +short > /dev/null & \
|
||||
done; wait
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### DNS over HTTPS (DoH)
|
||||
|
||||
DoH-Anfragen werden über HTTPS (Port 443) gesendet. Die meisten AdGuard-Home-Instanzen bieten DoH unter `/dns-query` an:
|
||||
|
||||
#### Direkte Abfragen via DoH
|
||||
|
||||
```bash
|
||||
for i in {1..200}; do \
|
||||
curl -s -H "accept: application/dns-json" \
|
||||
"https://203.0.113.50/dns-query?name=microsoft.com&type=A" > /dev/null & \
|
||||
done; wait
|
||||
```
|
||||
|
||||
#### Zufällige Subdomain-Abfragen via DoH
|
||||
|
||||
```bash
|
||||
for i in {1..200}; do \
|
||||
curl -s -H "accept: application/dns-json" \
|
||||
"https://203.0.113.50/dns-query?name=$(openssl rand -hex 6).microsoft.com&type=A" > /dev/null & \
|
||||
done; wait
|
||||
```
|
||||
|
||||
> **Hinweis:** Falls der Server ein selbstsigniertes Zertifikat verwendet, muss `-k` (unsicherer Modus) an `curl` angehängt werden.
|
||||
|
||||
---
|
||||
|
||||
### DNS over TLS (DoT)
|
||||
|
||||
DoT verwendet TLS über Port 853. Mit `kdig` (aus dem Paket `knot-dnsutils`):
|
||||
|
||||
#### Direkte Abfragen via DoT
|
||||
|
||||
```bash
|
||||
for i in {1..200}; do \
|
||||
kdig @203.0.113.50 microsoft.com +tls +short > /dev/null & \
|
||||
done; wait
|
||||
```
|
||||
|
||||
#### Zufällige Subdomain-Abfragen via DoT
|
||||
|
||||
```bash
|
||||
for i in {1..200}; do \
|
||||
kdig @203.0.113.50 $(openssl rand -hex 6).microsoft.com +tls +short > /dev/null & \
|
||||
done; wait
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### DNS over QUIC (DoQ)
|
||||
|
||||
DoQ verwendet das QUIC-Protokoll über Port 853/UDP. Mit dem Tool [`q`](https://github.com/natesales/q):
|
||||
|
||||
#### Direkte Abfragen via DoQ
|
||||
|
||||
```bash
|
||||
for i in {1..200}; do \
|
||||
q microsoft.com A @quic://203.0.113.50 --short > /dev/null & \
|
||||
done; wait
|
||||
```
|
||||
|
||||
#### Zufällige Subdomain-Abfragen via DoQ
|
||||
|
||||
```bash
|
||||
for i in {1..200}; do \
|
||||
q $(openssl rand -hex 6).microsoft.com A @quic://203.0.113.50 --short > /dev/null & \
|
||||
done; wait
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **⚠ Abschließender Hinweis:** Alle oben genannten Befehle sind **ausschließlich für das Testen der eigenen Infrastruktur** gedacht. Wer diese Befehle gegen fremde DNS-Server oder Dienste einsetzt, macht sich unter Umständen **strafbar**. Sei verantwortungsvoll — teste nur, was dir gehört.
|
||||
|
||||
## Hilfe
|
||||
|
||||
Alle verfügbaren Befehle und Optionen des Installers anzeigen:
|
||||
|
||||
@@ -37,6 +37,39 @@ Dadurch muss der Benutzer bei Updates die Konfiguration nicht manuell austausche
|
||||
| `CHECK_INTERVAL` | `10` | Wie oft die Logs geprüft werden (Sekunden) |
|
||||
| `API_QUERY_LIMIT` | `500` | Anzahl API-Einträge pro Abfrage (max 5000) |
|
||||
|
||||
### Subdomain-Flood-Erkennung (Random Subdomain Attack)
|
||||
|
||||
Erkennt Bot-Angriffe, bei denen massenhaft zufällige Subdomains einer Domain abgefragt werden (z.B. `abc123.microsoft.com`, `xyz456.microsoft.com`, ...). Dabei wird pro Client gezählt, wie viele **eindeutige** Subdomains einer Basisdomain (z.B. `microsoft.com`) im Zeitfenster aufgerufen werden.
|
||||
|
||||
| Parameter | Standard | Beschreibung |
|
||||
|-----------|----------|--------------|
|
||||
| `SUBDOMAIN_FLOOD_ENABLED` | `true` | Subdomain-Flood-Erkennung aktivieren |
|
||||
| `SUBDOMAIN_FLOOD_MAX_UNIQUE` | `50` | Max. eindeutige Subdomains pro Basisdomain/Client im Zeitfenster |
|
||||
| `SUBDOMAIN_FLOOD_WINDOW` | `60` | Zeitfenster in Sekunden |
|
||||
|
||||
#### Wie funktioniert die Erkennung?
|
||||
|
||||
1. Aus jeder DNS-Anfrage wird die **Basisdomain** extrahiert (z.B. `microsoft.com` aus `abc.microsoft.com`)
|
||||
2. Pro Client wird gezählt, wie viele **verschiedene** Subdomains einer Basisdomain im Zeitfenster abgefragt wurden
|
||||
3. Überschreitet die Anzahl eindeutiger Subdomains den Schwellwert, wird der Client gesperrt
|
||||
|
||||
#### Beispiel
|
||||
|
||||
Ein Bot fragt innerhalb von 60 Sekunden folgende Domains ab:
|
||||
|
||||
```
|
||||
hbidcw.microsoft.com
|
||||
ftdzewf.microsoft.com
|
||||
xk9z3a.microsoft.com
|
||||
... (50+ verschiedene Subdomains)
|
||||
```
|
||||
|
||||
→ Alle Anfragen haben die gleiche Basisdomain `microsoft.com`. Sobald mehr als 50 eindeutige Subdomains erkannt werden, wird der Client gesperrt.
|
||||
|
||||
> **Hinweis:** Nur echte Subdomains werden gezählt. Anfragen direkt an `microsoft.com` (ohne Subdomain) lösen diese Erkennung nicht aus. Multi-Part-TLDs wie `.co.uk`, `.com.au` etc. werden korrekt behandelt.
|
||||
|
||||
> **Tipp:** Der Schwellwert `SUBDOMAIN_FLOOD_MAX_UNIQUE` sollte hoch genug sein, um legitime Clients nicht zu stören (z.B. CDNs nutzen oft viele Subdomains). Ein Wert von 50–100 ist in den meisten Fällen sinnvoll.
|
||||
|
||||
### Sperr-Einstellungen
|
||||
|
||||
| Parameter | Standard | Beschreibung |
|
||||
@@ -109,7 +142,44 @@ Ermöglicht das Einbinden externer IP-Blocklisten (z.B. gehostete Textdateien mi
|
||||
| `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 |
|
||||
| `EXTERNAL_BLOCKLIST_CACHE_DIR` | `/var/lib/adguard-shield/external-blocklist` | Lokaler Cache für heruntergeladene Listen |
|
||||
### AbuseIPDB Reporting
|
||||
|
||||
Meldet permanent gesperrte IPs automatisch an [AbuseIPDB](https://www.abuseipdb.com/). Damit wird die IP in einer öffentlichen Datenbank als missbräuchlich markiert und andere Administratoren können davon profitieren.
|
||||
|
||||
> **Wichtig:** Es werden **nur permanent gesperrte IPs** gemeldet — also erst wenn die maximale Progressive-Ban-Stufe erreicht ist. Einzelne temporäre Sperren lösen keinen AbuseIPDB-Report aus.
|
||||
|
||||
| Parameter | Standard | Beschreibung |
|
||||
|-----------|----------|---------------|
|
||||
| `ABUSEIPDB_ENABLED` | `false` | AbuseIPDB-Reporting aktivieren |
|
||||
| `ABUSEIPDB_API_KEY` | *(leer)* | API-Key von [abuseipdb.com/account/api](https://www.abuseipdb.com/account/api) |
|
||||
| `ABUSEIPDB_CATEGORIES` | `4` | Report-Kategorien (4 = DDoS Attack). Siehe [Kategorien](https://www.abuseipdb.com/categories) |
|
||||
|
||||
#### AbuseIPDB einrichten
|
||||
|
||||
1. Erstelle einen kostenlosen Account auf [abuseipdb.com](https://www.abuseipdb.com/)
|
||||
2. Erstelle einen API-Key unter [Account → API](https://www.abuseipdb.com/account/api)
|
||||
3. Aktiviere das Reporting in der Konfiguration:
|
||||
|
||||
```bash
|
||||
ABUSEIPDB_ENABLED=true
|
||||
ABUSEIPDB_API_KEY="dein-api-key-hier"
|
||||
ABUSEIPDB_CATEGORIES="4"
|
||||
```
|
||||
|
||||
4. Service neustarten:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart adguard-shield
|
||||
```
|
||||
|
||||
#### Was wird gemeldet?
|
||||
|
||||
Der Report an AbuseIPDB enthält (auf Englisch):
|
||||
|
||||
- **Bei Rate-Limit:** `DNS flooding on our DNS server: 100x microsoft.com. Permanently banned by AdGuard Shield.`
|
||||
- **Bei Subdomain-Flood:** `DNS flooding on our DNS server: 85x *.microsoft.com (random subdomain attack). Permanently banned by AdGuard Shield.`
|
||||
|
||||
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:
|
||||
@@ -151,6 +221,29 @@ Bei einem Rate-Limit-Verstoß werden **alle** DNS-Protokoll-Ports für den Clien
|
||||
| 853 | TCP | DNS-over-TLS (`tls://dns1.techniverse.net:853`) |
|
||||
| 853 | UDP | DNS-over-QUIC (`quic://dns1.techniverse.net:853`) |
|
||||
|
||||
## Protokoll-Erkennung
|
||||
|
||||
AdGuard Shield erkennt **automatisch**, welches DNS-Protokoll ein Client verwendet. Diese Information wird aus dem Feld `client_proto` der AdGuard Home Query Log API extrahiert und an folgenden Stellen angezeigt:
|
||||
|
||||
- **Log-Datei**: Jede Anfrage wird mit dem verwendeten Protokoll geloggt
|
||||
- **Ban-History**: Die Protokoll-Spalte zeigt, über welches Protokoll die Anfragen kamen
|
||||
- **Status-Anzeige**: Aktive Sperren zeigen das verwendete Protokoll an
|
||||
- **Benachrichtigungen**: Push-Nachrichten enthalten das Protokoll
|
||||
|
||||
### Unterstützte Protokolle
|
||||
|
||||
| API-Wert | Anzeige | Beschreibung |
|
||||
|----------|---------|-------------|
|
||||
| *(leer)* | `DNS` | Klassisches DNS über UDP/TCP (Port 53) |
|
||||
| `doh` | `DoH` | DNS-over-HTTPS (Port 443) |
|
||||
| `dot` | `DoT` | DNS-over-TLS (Port 853) |
|
||||
| `doq` | `DoQ` | DNS-over-QUIC (Port 853/UDP) |
|
||||
| `dnscrypt` | `DNSCrypt` | DNSCrypt-Protokoll |
|
||||
|
||||
Verwendet ein Client mehrere Protokolle gleichzeitig (z.B. DoH und DNS), werden alle erkannten Protokolle kommagetrennt angezeigt (z.B. `DNS,DoH`).
|
||||
|
||||
> **Wichtig:** Alle Protokolle werden gleichermaßen überwacht und gegen das Rate-Limit geprüft. Ein DoH-Flood wird genauso erkannt und gesperrt wie ein klassischer DNS-Flood – die Erkennung basiert auf den AdGuard Home Logdaten, nicht auf Netzwerk-Traffic.
|
||||
|
||||
## Whitelist richtig pflegen
|
||||
|
||||
Die Whitelist sollte mindestens enthalten:
|
||||
|
||||
@@ -25,10 +25,84 @@ sudo /opt/adguard-shield/adguard-shield.sh test
|
||||
- Falsche Zugangsdaten (`ADGUARD_USER` / `ADGUARD_PASS`)
|
||||
- AdGuard Home läuft nicht
|
||||
- Firewall blockiert lokale Verbindung
|
||||
- DNS-Auflösung des Hostnames fehlgeschlagen
|
||||
- SSL/TLS-Zertifikatfehler (bei HTTPS)
|
||||
|
||||
**Lösung:** URL manuell testen:
|
||||
#### Schritt-für-Schritt Diagnose
|
||||
|
||||
**1. Base-URL Erreichbarkeit prüfen (ohne Auth):**
|
||||
```bash
|
||||
curl -s -u admin:passwort http://127.0.0.1:3000/control/querylog?limit=1
|
||||
# Vollständige Diagnose mit HTTP-Headern und Verbindungsdetails
|
||||
curl -ikv https://dns1.domain.com 2>&1
|
||||
|
||||
# Nur HTTP-Statuscode prüfen (schnell)
|
||||
curl -s -o /dev/null -w "%{http_code}\n" -k https://dns1.domain.com
|
||||
```
|
||||
|
||||
> `-i` zeigt HTTP-Response-Header, `-k` ignoriert SSL-Fehler, `-v` zeigt Verbindungsdetails (DNS, TLS-Handshake, etc.)
|
||||
|
||||
**2. DNS-Auflösung testen:**
|
||||
```bash
|
||||
# Hostname auflösen
|
||||
dig +short dns1.domain.com
|
||||
|
||||
# Oder mit nslookup
|
||||
nslookup dns1.domain.com
|
||||
```
|
||||
|
||||
**3. Port-Erreichbarkeit testen:**
|
||||
```bash
|
||||
# TCP-Verbindung zum Port prüfen (z.B. Port 3000)
|
||||
nc -zv 127.0.0.1 3000
|
||||
|
||||
# Oder mit curl
|
||||
curl -v telnet://127.0.0.1:3000
|
||||
```
|
||||
|
||||
**4. API-Endpunkt mit Authentifizierung testen:**
|
||||
```bash
|
||||
# Query-Log abfragen (mit Auth + Response-Header)
|
||||
curl -i -u admin:passwort https://dns1.domain.com/control/querylog?limit=1
|
||||
|
||||
# Nur HTTP-Status zurückgeben
|
||||
curl -s -o /dev/null -w "%{http_code}\n" -u admin:passwort https://dns1.domain.com/control/querylog?limit=1
|
||||
```
|
||||
|
||||
**5. AdGuard Home Status-API prüfen:**
|
||||
```bash
|
||||
# Allgemeinen Status abfragen (benötigt keine Auth)
|
||||
curl -ik https://dns1.domain.com/control/status
|
||||
```
|
||||
|
||||
#### Typische Fehlercodes
|
||||
|
||||
| HTTP-Code | Bedeutung | Lösung |
|
||||
|-----------|-----------|--------|
|
||||
| `000` | Keine Verbindung | Host nicht erreichbar, DNS-Fehler oder Firewall |
|
||||
| `200` | Erfolg | Alles in Ordnung ✅ |
|
||||
| `301/302` | Weiterleitung | URL prüfen — evtl. fehlt `https://` oder Port |
|
||||
| `401` | Nicht autorisiert | `ADGUARD_USER` / `ADGUARD_PASS` prüfen |
|
||||
| `403` | Zugriff verweigert | Zugangsdaten oder IP-Beschränkung in AdGuard Home |
|
||||
| `404` | Nicht gefunden | URL falsch oder AdGuard Home Version zu alt |
|
||||
| `502/503` | Service nicht verfügbar | AdGuard Home läuft nicht oder wird gerade neu gestartet |
|
||||
|
||||
#### curl Exit-Codes
|
||||
|
||||
| Exit-Code | Bedeutung |
|
||||
|-----------|-----------|
|
||||
| `6` | DNS-Auflösung fehlgeschlagen — Hostname prüfen |
|
||||
| `7` | Verbindung abgelehnt — Läuft AdGuard Home? Port korrekt? |
|
||||
| `28` | Timeout — Host nicht erreichbar oder Firewall blockiert |
|
||||
| `35` | SSL/TLS-Handshake fehlgeschlagen |
|
||||
| `51` | SSL-Zertifikat: Hostname stimmt nicht überein |
|
||||
| `60` | SSL-Zertifikat: nicht vertrauenswürdig (selbstsigniert?) |
|
||||
|
||||
> **Tipp:** Bei selbstsignierten Zertifikaten `-k` an curl anhängen, um SSL-Fehler zu ignorieren. AdGuard Shield verwendet intern automatisch `-k` bei der API-Kommunikation.
|
||||
|
||||
**Lösung:** URL und Zugangsdaten in der Konfiguration anpassen:
|
||||
```bash
|
||||
sudo nano /opt/adguard-shield/adguard-shield.conf
|
||||
sudo systemctl restart adguard-shield
|
||||
```
|
||||
|
||||
### iptables-Fehler: "Permission denied"
|
||||
@@ -87,6 +161,30 @@ Das ist normal — iptables-Regeln sind flüchtig. Der **Service** erstellt die
|
||||
- `RATE_LIMIT_WINDOW` vergrößern (z.B. 120 Sekunden)
|
||||
- Windows-Clients fragen manche Domains von Natur aus sehr oft an — Whitelist nutzen
|
||||
|
||||
### Subdomain-Flood-Erkennung sperrt legitime Clients
|
||||
|
||||
Manche Dienste (z.B. CDNs, Cloud-Dienste, Microsoft 365) nutzen von Natur aus viele verschiedene Subdomains. Falls ein legitimer Client fälschlicherweise durch die Subdomain-Flood-Erkennung gesperrt wird:
|
||||
|
||||
1. Client sofort entsperren:
|
||||
```bash
|
||||
sudo /opt/adguard-shield/adguard-shield.sh unban <IP>
|
||||
```
|
||||
2. Schwellwert erhöhen — z.B. von 50 auf 100 oder 150:
|
||||
```bash
|
||||
SUBDOMAIN_FLOOD_MAX_UNIQUE=100
|
||||
```
|
||||
3. Zeitfenster vergrößern — z.B. auf 120 Sekunden:
|
||||
```bash
|
||||
SUBDOMAIN_FLOOD_WINDOW=120
|
||||
```
|
||||
4. Oder die IP zur Whitelist hinzufügen
|
||||
5. Im Zweifelsfall die Erkennung temporär deaktivieren:
|
||||
```bash
|
||||
SUBDOMAIN_FLOOD_ENABLED=false
|
||||
```
|
||||
|
||||
> **Tipp:** Im Dry-Run-Modus (`sudo /opt/adguard-shield/adguard-shield.sh dry-run`) kann man beobachten, welche Clients die Subdomain-Flood-Erkennung auslösen würden, ohne sie wirklich zu sperren.
|
||||
|
||||
### Monitor startet nicht (PID-File)
|
||||
|
||||
```bash
|
||||
|
||||
@@ -53,15 +53,15 @@ log_ban_history() {
|
||||
|
||||
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"
|
||||
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 | %s\n" \
|
||||
"$timestamp" "$action" "$client_ip" "-" "-" "$duration" "$reason" \
|
||||
printf "%-19s | %-6s | %-39s | %-30s | %-8s | %-10s | %-10s | %s\n" \
|
||||
"$timestamp" "$action" "$client_ip" "-" "-" "$duration" "-" "$reason" \
|
||||
>> "$BAN_HISTORY_FILE"
|
||||
}
|
||||
|
||||
|
||||
+67
-19
@@ -6,7 +6,7 @@
|
||||
# Lizenz: MIT
|
||||
###############################################################################
|
||||
|
||||
VERSION="0.3.1"
|
||||
VERSION="0.4.0"
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -40,6 +40,11 @@ print_header() {
|
||||
echo -e "${GREEN} Version: ${VERSION}${NC}"
|
||||
echo -e "${BLUE} Autor: Patrick Asmus${NC}"
|
||||
echo -e "${BLUE} E-Mail: support@techniverse.net${NC}"
|
||||
echo -e "${BLUE}───────────────────────────────────────────────────────────────────────────────────────────────────────────────${NC}"
|
||||
echo -e "${BLUE} Web: https://www.patrick-asmus.de${NC}"
|
||||
echo -e "${BLUE} Blog: https://www.cleveradmin.de${NC}"
|
||||
echo -e "${BLUE}───────────────────────────────────────────────────────────────────────────────────────────────────────────────${NC}"
|
||||
echo -e "${BLUE} Repo: https://git.techniverse.net/scriptos/adguard-shield${NC}"
|
||||
echo ""
|
||||
echo -e "${BLUE}═══════════════════════════════════════════════════════════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
@@ -77,8 +82,15 @@ print_help() {
|
||||
echo -e " ${CYAN}sudo bash install.sh uninstall${NC} # Deinstallation"
|
||||
echo -e " ${CYAN}sudo bash install.sh status${NC} # Status prüfen"
|
||||
echo ""
|
||||
echo -e "${BOLD}Service-Befehle:${NC}"
|
||||
echo -e " ${CYAN}sudo systemctl start adguard-shield${NC} # Service starten"
|
||||
echo -e " ${CYAN}sudo systemctl stop adguard-shield${NC} # Service stoppen"
|
||||
echo -e " ${CYAN}sudo systemctl restart adguard-shield${NC} # Service neustarten"
|
||||
echo -e " ${CYAN}sudo systemctl status adguard-shield${NC} # Service-Status"
|
||||
echo -e " ${CYAN}sudo journalctl -u adguard-shield -f${NC} # Logs live verfolgen"
|
||||
echo ""
|
||||
echo -e "${BOLD}Monitor-Befehle (nach Installation):${NC}"
|
||||
echo -e " ${CYAN}sudo /opt/adguard-shield/adguard-shield.sh start${NC} # Monitor starten"
|
||||
echo -e " ${CYAN}sudo /opt/adguard-shield/adguard-shield.sh start${NC} # Monitor im Vordergrund starten"
|
||||
echo -e " ${CYAN}sudo /opt/adguard-shield/adguard-shield.sh stop${NC} # Monitor stoppen"
|
||||
echo -e " ${CYAN}sudo /opt/adguard-shield/adguard-shield.sh status${NC} # Status & aktive Sperren"
|
||||
echo -e " ${CYAN}sudo /opt/adguard-shield/adguard-shield.sh history${NC} # Ban-History anzeigen"
|
||||
@@ -97,13 +109,6 @@ print_help() {
|
||||
echo -e " ${CYAN}sudo /opt/adguard-shield/iptables-helper.sh save${NC} # Regeln speichern"
|
||||
echo -e " ${CYAN}sudo /opt/adguard-shield/iptables-helper.sh restore${NC} # Regeln wiederherstellen"
|
||||
echo ""
|
||||
echo -e "${BOLD}Service-Befehle:${NC}"
|
||||
echo -e " ${CYAN}sudo systemctl start adguard-shield${NC} # Service starten"
|
||||
echo -e " ${CYAN}sudo systemctl stop adguard-shield${NC} # Service stoppen"
|
||||
echo -e " ${CYAN}sudo systemctl restart adguard-shield${NC} # Service neustarten"
|
||||
echo -e " ${CYAN}sudo systemctl status adguard-shield${NC} # Service-Status"
|
||||
echo -e " ${CYAN}sudo journalctl -u adguard-shield -f${NC} # Logs live verfolgen"
|
||||
echo ""
|
||||
echo -e "${BOLD}Voraussetzungen:${NC}"
|
||||
echo " - Linux Server (Debian/Ubuntu empfohlen)"
|
||||
echo " - Root-Zugriff (sudo)"
|
||||
@@ -301,7 +306,7 @@ migrate_config() {
|
||||
echo -n "$current_comment_block" >> "$existing_conf"
|
||||
echo "$line" >> "$existing_conf"
|
||||
echo -e " ➕ Neuer Parameter hinzugefügt: ${GREEN}$key${NC}"
|
||||
((new_keys_added++))
|
||||
new_keys_added=$((new_keys_added + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -391,17 +396,60 @@ test_connection() {
|
||||
|
||||
source "$INSTALL_DIR/adguard-shield.conf"
|
||||
|
||||
local response
|
||||
response=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-u "${ADGUARD_USER}:${ADGUARD_PASS}" \
|
||||
--connect-timeout 5 \
|
||||
"${ADGUARD_URL}/control/querylog?limit=1" 2>/dev/null)
|
||||
# ── Schritt 1: Base-URL Erreichbarkeit prüfen ────────────────────────
|
||||
echo -e " ${CYAN}1)${NC} Prüfe Erreichbarkeit von ${BOLD}${ADGUARD_URL}${NC} ..."
|
||||
|
||||
if [[ "$response" == "200" ]]; then
|
||||
echo -e " ✅ Verbindung erfolgreich! (HTTP $response)"
|
||||
local base_http_code
|
||||
local base_curl_exit
|
||||
base_http_code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
--connect-timeout 5 --max-time 10 \
|
||||
-k "${ADGUARD_URL}" 2>/dev/null) || base_curl_exit=$?
|
||||
base_curl_exit=${base_curl_exit:-0}
|
||||
|
||||
if [[ "$base_curl_exit" -ne 0 ]]; then
|
||||
# curl konnte keine Verbindung aufbauen
|
||||
echo -e " ❌ Base-URL nicht erreichbar! (curl Exit-Code: $base_curl_exit)"
|
||||
case "$base_curl_exit" in
|
||||
6) echo -e " ${YELLOW}→ DNS-Auflösung fehlgeschlagen. Hostname prüfen!${NC}" ;;
|
||||
7) echo -e " ${YELLOW}→ Verbindung abgelehnt. Läuft AdGuard Home? Port korrekt?${NC}" ;;
|
||||
28) echo -e " ${YELLOW}→ Timeout. Host nicht erreichbar oder Firewall blockiert.${NC}" ;;
|
||||
35|51|60) echo -e " ${YELLOW}→ SSL/TLS-Fehler. Zertifikat oder HTTPS-Konfiguration prüfen.${NC}" ;;
|
||||
*) echo -e " ${YELLOW}→ Unbekannter Fehler. Manuell testen: curl -v ${ADGUARD_URL}${NC}" ;;
|
||||
esac
|
||||
echo ""
|
||||
echo -e " ${YELLOW}Troubleshooting:${NC}"
|
||||
echo -e " curl -ikv ${ADGUARD_URL}"
|
||||
echo ""
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$base_http_code" == "000" ]]; then
|
||||
echo -e " ❌ Base-URL nicht erreichbar (keine HTTP-Antwort)"
|
||||
echo -e " ${YELLOW}→ Manuell testen: curl -ikv ${ADGUARD_URL}${NC}"
|
||||
echo ""
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e " ✅ Base-URL erreichbar (HTTP $base_http_code)"
|
||||
|
||||
# ── Schritt 2: API-Endpunkt mit Authentifizierung testen ─────────────
|
||||
echo -e " ${CYAN}2)${NC} Teste API-Authentifizierung ..."
|
||||
|
||||
local api_response
|
||||
api_response=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-u "${ADGUARD_USER}:${ADGUARD_PASS}" \
|
||||
--connect-timeout 5 --max-time 10 \
|
||||
-k "${ADGUARD_URL}/control/querylog?limit=1" 2>/dev/null)
|
||||
|
||||
if [[ "$api_response" == "200" ]]; then
|
||||
echo -e " ✅ API-Authentifizierung erfolgreich! (HTTP $api_response)"
|
||||
elif [[ "$api_response" == "401" || "$api_response" == "403" ]]; then
|
||||
echo -e " ❌ Authentifizierung fehlgeschlagen (HTTP $api_response)"
|
||||
echo -e " ${YELLOW}→ Benutzername oder Passwort falsch!${NC}"
|
||||
echo -e " ${YELLOW}→ Prüfe ADGUARD_USER und ADGUARD_PASS in: $INSTALL_DIR/adguard-shield.conf${NC}"
|
||||
else
|
||||
echo -e " ❌ Verbindung fehlgeschlagen (HTTP $response)"
|
||||
echo -e " ${YELLOW}Bitte prüfe URL und Zugangsdaten in: $INSTALL_DIR/adguard-shield.conf${NC}"
|
||||
echo -e " ❌ API-Verbindung fehlgeschlagen (HTTP $api_response)"
|
||||
echo -e " ${YELLOW}→ Bitte prüfe URL und Zugangsdaten in: $INSTALL_DIR/adguard-shield.conf${NC}"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
+9
-5
@@ -29,17 +29,20 @@ log_ban_history() {
|
||||
local domain="${3:-}"
|
||||
local count="${4:-}"
|
||||
local reason="${5:-}"
|
||||
local protocol="${6:-}"
|
||||
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 | GRUND" >> "$BAN_HISTORY_FILE"
|
||||
echo "#─────────────────────────────────────────────────────────────────────────────────" >> "$BAN_HISTORY_FILE"
|
||||
echo "# Format: ZEITSTEMPEL | AKTION | CLIENT-IP | DOMAIN | ANFRAGEN | SPERRDAUER | PROTOKOLL | GRUND" >> "$BAN_HISTORY_FILE"
|
||||
echo "#────────────────────────────────────────────────────────────────────────────────────────────────" >> "$BAN_HISTORY_FILE"
|
||||
fi
|
||||
|
||||
printf "%-19s | %-6s | %-39s | %-30s | %-8s | %-10s | %s\n" \
|
||||
"$timestamp" "$action" "$client_ip" "${domain:--}" "${count:--}" "-" "${reason:-expired}" \
|
||||
[[ -z "$protocol" ]] && protocol="-"
|
||||
|
||||
printf "%-19s | %-6s | %-39s | %-30s | %-8s | %-10s | %-10s | %s\n" \
|
||||
"$timestamp" "$action" "$client_ip" "${domain:--}" "${count:--}" "-" "$protocol" "${reason:-expired}" \
|
||||
>> "$BAN_HISTORY_FILE"
|
||||
}
|
||||
|
||||
@@ -52,6 +55,7 @@ for state_file in "${STATE_DIR}"/*.ban; do
|
||||
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)
|
||||
protocol=$(grep '^PROTOCOL=' "$state_file" | cut -d= -f2)
|
||||
|
||||
# Permanente Sperren nicht automatisch aufheben
|
||||
if [[ "$is_permanent" == "true" || "$ban_until_epoch" == "0" ]]; then
|
||||
@@ -69,7 +73,7 @@ for state_file in "${STATE_DIR}"/*.ban; do
|
||||
fi
|
||||
|
||||
# Ban-History Eintrag
|
||||
log_ban_history "UNBAN" "$client_ip" "$domain" "-" "expired-cron"
|
||||
log_ban_history "UNBAN" "$client_ip" "$domain" "-" "expired-cron" "${protocol:-}"
|
||||
|
||||
rm -f "$state_file"
|
||||
unban_count=$((unban_count + 1))
|
||||
|
||||
Reference in New Issue
Block a user