Neue Funktion: Subomain Flooding wird nun auch erkannt.
This commit is contained in:
@@ -21,6 +21,22 @@ RATE_LIMIT_WINDOW=60
|
|||||||
# Wie oft das Script die Logs prüft (in Sekunden)
|
# Wie oft das Script die Logs prüft (in Sekunden)
|
||||||
CHECK_INTERVAL=10
|
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 ---
|
# --- Sperr-Einstellungen ---
|
||||||
# Wie lange ein Client gesperrt wird (in Sekunden, 3600 = 1 Stunde)
|
# Wie lange ein Client gesperrt wird (in Sekunden, 3600 = 1 Stunde)
|
||||||
BAN_DURATION=3600
|
BAN_DURATION=3600
|
||||||
|
|||||||
+176
-33
@@ -5,11 +5,11 @@
|
|||||||
#
|
#
|
||||||
# Autor: Patrick Asmus
|
# Autor: Patrick Asmus
|
||||||
# E-Mail: support@techniverse.net
|
# E-Mail: support@techniverse.net
|
||||||
# Datum: 2026-03-03
|
# Datum: 2026-03-04
|
||||||
# Lizenz: MIT
|
# Lizenz: MIT
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
VERSION="0.3.1"
|
VERSION="0.4.0"
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -111,8 +111,8 @@ get_offense_level() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local level last_offense now reset_after
|
local level last_offense now reset_after
|
||||||
level=$(grep '^OFFENSE_LEVEL=' "$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)
|
last_offense=$(grep '^LAST_OFFENSE_EPOCH=' "$offense_file" | cut -d= -f2 || true)
|
||||||
now=$(date '+%s')
|
now=$(date '+%s')
|
||||||
reset_after="${PROGRESSIVE_BAN_RESET_AFTER:-86400}"
|
reset_after="${PROGRESSIVE_BAN_RESET_AFTER:-86400}"
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ increment_offense_level() {
|
|||||||
|
|
||||||
# Erstes Vergehen merken (bevor Datei überschrieben wird)
|
# Erstes Vergehen merken (bevor Datei überschrieben wird)
|
||||||
local first_offense
|
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"
|
[[ -z "$first_offense" ]] && first_offense="$now_readable"
|
||||||
|
|
||||||
cat > "$offense_file" << EOF
|
cat > "$offense_file" << EOF
|
||||||
@@ -296,6 +296,8 @@ ban_client() {
|
|||||||
local client_ip="$1"
|
local client_ip="$1"
|
||||||
local domain="$2"
|
local domain="$2"
|
||||||
local count="$3"
|
local count="$3"
|
||||||
|
local reason="${4:-rate-limit}"
|
||||||
|
local window="${5:-$RATE_LIMIT_WINDOW}"
|
||||||
|
|
||||||
# Prüfen ob bereits gesperrt
|
# Prüfen ob bereits gesperrt
|
||||||
local state_file="${STATE_DIR}/${client_ip//[:\/]/_}.ban"
|
local state_file="${STATE_DIR}/${client_ip//[:\/]/_}.ban"
|
||||||
@@ -333,18 +335,18 @@ ban_client() {
|
|||||||
|
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "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) für ${duration_display} [Stufe $offense_level] [${reason}]"
|
||||||
else
|
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) [${reason}]"
|
||||||
fi
|
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}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" ]]; then
|
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) für ${duration_display} [Stufe ${offense_level}/${PROGRESSIVE_BAN_MAX_LEVEL:-0}] [${reason}]"
|
||||||
else
|
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) für ${duration_display} [${reason}]"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# IPv4 oder IPv6 erkennen
|
# IPv4 oder IPv6 erkennen
|
||||||
@@ -367,16 +369,17 @@ BAN_UNTIL=$ban_until_display
|
|||||||
BAN_DURATION=${effective_duration}
|
BAN_DURATION=${effective_duration}
|
||||||
OFFENSE_LEVEL=$offense_level
|
OFFENSE_LEVEL=$offense_level
|
||||||
IS_PERMANENT=$is_permanent
|
IS_PERMANENT=$is_permanent
|
||||||
|
REASON=$reason
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Ban-History Eintrag
|
# Ban-History Eintrag
|
||||||
local history_duration="${duration_display}"
|
local history_duration="${duration_display}"
|
||||||
[[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" ]] && history_duration="${duration_display} (Stufe ${offense_level})"
|
[[ "${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"
|
||||||
|
|
||||||
# Benachrichtigung senden
|
# Benachrichtigung senden
|
||||||
if [[ "$NOTIFY_ENABLED" == "true" ]]; then
|
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"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,7 +392,7 @@ unban_client() {
|
|||||||
# Domain aus State lesen bevor wir löschen
|
# Domain aus State lesen bevor wir löschen
|
||||||
local domain="-"
|
local domain="-"
|
||||||
if [[ -f "$state_file" ]]; then
|
if [[ -f "$state_file" ]]; then
|
||||||
domain=$(grep '^DOMAIN=' "$state_file" | cut -d= -f2)
|
domain=$(grep '^DOMAIN=' "$state_file" | cut -d= -f2 || true)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "INFO" "ENTSPERRE Client: $client_ip ($reason)"
|
log "INFO" "ENTSPERRE Client: $client_ip ($reason)"
|
||||||
@@ -419,11 +422,11 @@ check_expired_bans() {
|
|||||||
[[ -f "$state_file" ]] || continue
|
[[ -f "$state_file" ]] || continue
|
||||||
|
|
||||||
local ban_until_epoch
|
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
|
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
|
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
|
# Permanente Sperren nicht automatisch aufheben
|
||||||
if [[ "$is_permanent" == "true" || "$ban_until_epoch" == "0" ]]; then
|
if [[ "$is_permanent" == "true" || "$ban_until_epoch" == "0" ]]; then
|
||||||
@@ -445,20 +448,25 @@ send_notification() {
|
|||||||
local count="$4"
|
local count="$4"
|
||||||
local offense_level="${5:-}"
|
local offense_level="${5:-}"
|
||||||
local duration_display="${6:-}"
|
local duration_display="${6:-}"
|
||||||
|
local reason="${7:-rate-limit}"
|
||||||
|
local window="${8:-$RATE_LIMIT_WINDOW}"
|
||||||
|
|
||||||
# Ntfy benötigt keine Webhook-URL (nutzt NTFY_SERVER_URL + NTFY_TOPIC)
|
# Ntfy benötigt keine Webhook-URL (nutzt NTFY_SERVER_URL + NTFY_TOPIC)
|
||||||
if [[ "$NOTIFY_TYPE" != "ntfy" && -z "$NOTIFY_WEBHOOK_URL" ]]; then
|
if [[ "$NOTIFY_TYPE" != "ntfy" && -z "$NOTIFY_WEBHOOK_URL" ]]; then
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
local reason_label="Rate-Limit"
|
||||||
|
[[ "$reason" == "subdomain-flood" ]] && reason_label="Subdomain-Flood"
|
||||||
|
|
||||||
local message
|
local message
|
||||||
if [[ "$action" == "ban" ]]; then
|
if [[ "$action" == "ban" ]]; then
|
||||||
if [[ "${PROGRESSIVE_BAN_ENABLED:-false}" == "true" && -n "$offense_level" ]]; 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, ${reason_label}). Sperre für **${duration_display}** [Stufe ${offense_level}/${PROGRESSIVE_BAN_MAX_LEVEL:-0}]."
|
||||||
else
|
else
|
||||||
local simple_dur
|
local simple_dur
|
||||||
simple_dur=$(format_duration "${BAN_DURATION}")
|
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, ${reason_label}). Sperre für ${simple_dur}."
|
||||||
fi
|
fi
|
||||||
elif [[ "$action" == "service_start" ]]; then
|
elif [[ "$action" == "service_start" ]]; then
|
||||||
message="🟢 AdGuard Shield v${VERSION} wurde gestartet."
|
message="🟢 AdGuard Shield v${VERSION} wurde gestartet."
|
||||||
@@ -651,6 +659,112 @@ analyze_queries() {
|
|||||||
done <<< "$violations"
|
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
|
||||||
|
}
|
||||||
|
] |
|
||||||
|
# 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(", "))
|
||||||
|
}) |
|
||||||
|
.[] |
|
||||||
|
select(.unique_subdomains > $max_unique) |
|
||||||
|
"\(.client)|\(.base_domain)|\(.unique_subdomains)|\(.total_queries)|\(.example_domains)"
|
||||||
|
') || {
|
||||||
|
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; do
|
||||||
|
[[ -z "$client" || -z "$base_domain" || -z "$unique_count" ]] && continue
|
||||||
|
|
||||||
|
log "WARN" "Subdomain-Flood erkannt: $client → ${unique_count} eindeutige Subdomains von $base_domain (${total_count} Anfragen, 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)"
|
||||||
|
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"
|
||||||
|
done <<< "$violations"
|
||||||
|
}
|
||||||
|
|
||||||
# ─── Status anzeigen ─────────────────────────────────────────────────────────
|
# ─── Status anzeigen ─────────────────────────────────────────────────────────
|
||||||
show_status() {
|
show_status() {
|
||||||
echo "═══════════════════════════════════════════════════════════════"
|
echo "═══════════════════════════════════════════════════════════════"
|
||||||
@@ -667,26 +781,49 @@ show_status() {
|
|||||||
echo ""
|
echo ""
|
||||||
fi
|
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
|
# Aktive Sperren
|
||||||
local ban_count=0
|
local ban_count=0
|
||||||
if [[ -d "$STATE_DIR" ]]; then
|
if [[ -d "$STATE_DIR" ]]; then
|
||||||
for state_file in "${STATE_DIR}"/*.ban; do
|
for state_file in "${STATE_DIR}"/*.ban; do
|
||||||
[[ -f "$state_file" ]] || continue
|
[[ -f "$state_file" ]] || continue
|
||||||
ban_count=$((ban_count + 1))
|
ban_count=$((ban_count + 1))
|
||||||
local s_ip s_domain s_level s_perm s_dur s_until
|
local s_ip s_domain s_level s_perm s_dur s_until s_reason s_count
|
||||||
s_ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2)
|
s_ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2 || true)
|
||||||
s_domain=$(grep '^DOMAIN=' "$state_file" | cut -d= -f2)
|
s_domain=$(grep '^DOMAIN=' "$state_file" | cut -d= -f2 || true)
|
||||||
s_level=$(grep '^OFFENSE_LEVEL=' "$state_file" | cut -d= -f2)
|
s_level=$(grep '^OFFENSE_LEVEL=' "$state_file" | cut -d= -f2 || true)
|
||||||
s_perm=$(grep '^IS_PERMANENT=' "$state_file" | cut -d= -f2)
|
s_perm=$(grep '^IS_PERMANENT=' "$state_file" | cut -d= -f2 || true)
|
||||||
s_dur=$(grep '^BAN_DURATION=' "$state_file" | cut -d= -f2)
|
s_dur=$(grep '^BAN_DURATION=' "$state_file" | cut -d= -f2 || true)
|
||||||
s_until=$(grep '^BAN_UNTIL=' "$state_file" | cut -d= -f2)
|
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_reason="${s_reason:-rate-limit}"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
if [[ "$s_perm" == "true" ]]; then
|
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}]${reason_tag}"
|
||||||
elif [[ -n "$s_level" && "$s_level" -gt 0 ]]; then
|
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}]${reason_tag}"
|
||||||
else
|
else
|
||||||
echo " 🚫 Gesperrt: $s_ip → $s_domain [bis $s_until]"
|
echo " 🚫 Gesperrt: $s_ip → $s_domain [bis $s_until${count_info}]${reason_tag}"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
@@ -705,9 +842,9 @@ show_status() {
|
|||||||
for offense_file in "${STATE_DIR}"/*.offenses; do
|
for offense_file in "${STATE_DIR}"/*.offenses; do
|
||||||
[[ -f "$offense_file" ]] || continue
|
[[ -f "$offense_file" ]] || continue
|
||||||
local o_ip o_level o_last
|
local o_ip o_level o_last
|
||||||
o_ip=$(grep '^CLIENT_IP=' "$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)
|
o_level=$(grep '^OFFENSE_LEVEL=' "$offense_file" | cut -d= -f2 || true)
|
||||||
o_last=$(grep '^LAST_OFFENSE=' "$offense_file" | cut -d= -f2)
|
o_last=$(grep '^LAST_OFFENSE=' "$offense_file" | cut -d= -f2 || true)
|
||||||
offense_count=$((offense_count + 1))
|
offense_count=$((offense_count + 1))
|
||||||
local next_dur
|
local next_dur
|
||||||
next_dur=$(calculate_ban_duration "$((o_level + 1))")
|
next_dur=$(calculate_ban_duration "$((o_level + 1))")
|
||||||
@@ -781,7 +918,7 @@ flush_all_bans() {
|
|||||||
for state_file in "${STATE_DIR}"/*.ban; do
|
for state_file in "${STATE_DIR}"/*.ban; do
|
||||||
[[ -f "$state_file" ]] || continue
|
[[ -f "$state_file" ]] || continue
|
||||||
local client_ip
|
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"
|
unban_client "$client_ip" "manual-flush"
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -798,7 +935,7 @@ flush_all_offenses() {
|
|||||||
for offense_file in "${STATE_DIR}"/*.offenses; do
|
for offense_file in "${STATE_DIR}"/*.offenses; do
|
||||||
[[ -f "$offense_file" ]] || continue
|
[[ -f "$offense_file" ]] || continue
|
||||||
local o_ip
|
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"
|
log "INFO" "Offense-Zähler zurückgesetzt: $o_ip"
|
||||||
rm -f "$offense_file"
|
rm -f "$offense_file"
|
||||||
count=$((count + 1))
|
count=$((count + 1))
|
||||||
@@ -855,6 +992,11 @@ main_loop() {
|
|||||||
else
|
else
|
||||||
log "INFO" " Progressive Sperren: deaktiviert"
|
log "INFO" " Progressive Sperren: deaktiviert"
|
||||||
fi
|
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
|
||||||
log "INFO" "═══════════════════════════════════════════════════════════"
|
log "INFO" "═══════════════════════════════════════════════════════════"
|
||||||
|
|
||||||
# Service-Start-Benachrichtigung senden
|
# Service-Start-Benachrichtigung senden
|
||||||
@@ -873,6 +1015,7 @@ main_loop() {
|
|||||||
local api_response
|
local api_response
|
||||||
if api_response=$(query_adguard_log); then
|
if api_response=$(query_adguard_log); then
|
||||||
analyze_queries "$api_response"
|
analyze_queries "$api_response"
|
||||||
|
analyze_subdomain_flood "$api_response"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sleep "$CHECK_INTERVAL"
|
sleep "$CHECK_INTERVAL"
|
||||||
|
|||||||
@@ -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) |
|
| `CHECK_INTERVAL` | `10` | Wie oft die Logs geprüft werden (Sekunden) |
|
||||||
| `API_QUERY_LIMIT` | `500` | Anzahl API-Einträge pro Abfrage (max 5000) |
|
| `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
|
### Sperr-Einstellungen
|
||||||
|
|
||||||
| Parameter | Standard | Beschreibung |
|
| Parameter | Standard | Beschreibung |
|
||||||
|
|||||||
+2
-2
@@ -6,7 +6,7 @@
|
|||||||
# Lizenz: MIT
|
# Lizenz: MIT
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
VERSION="0.3.1"
|
VERSION="0.4.0"
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -306,7 +306,7 @@ migrate_config() {
|
|||||||
echo -n "$current_comment_block" >> "$existing_conf"
|
echo -n "$current_comment_block" >> "$existing_conf"
|
||||||
echo "$line" >> "$existing_conf"
|
echo "$line" >> "$existing_conf"
|
||||||
echo -e " ➕ Neuer Parameter hinzugefügt: ${GREEN}$key${NC}"
|
echo -e " ➕ Neuer Parameter hinzugefügt: ${GREEN}$key${NC}"
|
||||||
((new_keys_added++))
|
new_keys_added=$((new_keys_added + 1))
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user