This commit is contained in:
2026-03-03 19:27:37 +01:00
parent cc5ac0154d
commit a5fb3bcf25
14 changed files with 2840 additions and 5 deletions

View File

@@ -29,5 +29,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

126
README.md
View File

@@ -1,11 +1,131 @@
# template_repository
```
▄▄▄ ▓█████▄ ▄████ █ ██ ▄▄▄ ██▀███ ▓█████▄ ██████ ██░ ██ ██▓▓█████ ██▓ ▓█████▄
▒████▄ ▒██▀ ██▌ ██▒ ▀█▒ ██ ▓██▒▒████▄ ▓██ ▒ ██▒▒██▀ ██▌ ▒██ ▒ ▓██░ ██▒▓██▒▓█ ▀ ▓██▒ ▒██▀ ██▌
▒██ ▀█▄ ░██ █▌▒██░▄▄▄░▓██ ▒██░▒██ ▀█▄ ▓██ ░▄█ ▒░██ █▌ ░ ▓██▄ ▒██▀▀██░▒██▒▒███ ▒██░ ░██ █▌
░██▄▄▄▄██ ░▓█▄ ▌░▓█ ██▓▓▓█ ░██░░██▄▄▄▄██ ▒██▀▀█▄ ░▓█▄ ▌ ▒ ██▒░▓█ ░██ ░██░▒▓█ ▄ ▒██░ ░▓█▄ ▌
▓█ ▓██▒░▒████▓ ░▒▓███▀▒▒▒█████▓ ▓█ ▓██▒░██▓ ▒██▒░▒████▓ ▒██████▒▒░▓█▒░██▓░██░░▒████▒░██████▒░▒████▓
▒▒ ▓▒█░ ▒▒▓ ▒ ░▒ ▒ ░▒▓▒ ▒ ▒ ▒▒ ▓▒█░░ ▒▓ ░▒▓░ ▒▒▓ ▒ ▒ ▒▓▒ ▒ ░ ▒ ░░▒░▒░▓ ░░ ▒░ ░░ ▒░▓ ░ ▒▒▓ ▒
▒ ▒▒ ░ ░ ▒ ▒ ░ ░ ░░▒░ ░ ░ ▒ ▒▒ ░ ░▒ ░ ▒░ ░ ▒ ▒ ░ ░▒ ░ ░ ▒ ░▒░ ░ ▒ ░ ░ ░ ░░ ░ ▒ ░ ░ ▒ ▒
░ ▒ ░ ░ ░ ░ ░ ░ ░░░ ░ ░ ░ ▒ ░░ ░ ░ ░ ░ ░ ░ ░ ░ ░░ ░ ▒ ░ ░ ░ ░ ░ ░ ░
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
░ ░ ░
```
# AdGuard Shield
> **Autor:** Patrick Asmus | **E-Mail:** support@techniverse.net | **Version:** 1.0.0
Automatischer Schutz für deinen AdGuard Home DNS-Server gegen übermäßige Anfragen einzelner Clients. Überwacht die AdGuard Home API, erkennt Rate-Limit-Verstöße und sperrt missbrauchende Clients per iptables — für alle DNS-Protokolle (DNS, DoH, DoT, DoQ).
Wichtig: Link für Lizenz anpassen.
## Was macht das Tool?
Wenn ein Client eine bestimmte Domain zu oft anfragt (z.B. >30x pro Minute), wird er automatisch auf Firewall-Ebene für alle DNS-Ports gesperrt. Nach einer konfigurierbaren Zeitspanne wird die Sperre automatisch aufgehoben.
## Features
- Automatische Erkennung und Sperre bei Rate-Limit-Verstößen
- 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
- **Ban-History** — lückenlose Protokollierung aller Sperren/Entsperrungen mit Zeitstempel
- Whitelist für vertrauenswürdige IPs
- Dry-Run Modus zum gefahrlosen Testen
- Benachrichtigungen (Discord, Slack, Gotify, Ntfy)
- systemd Service für dauerhaften Betrieb
## Voraussetzungen
- Linux Server mit AdGuard Home (bare metal)
- `curl`, `jq`, `iptables` / `ip6tables`
- Root-Zugriff
- AdGuard Home Web-API erreichbar (Standard: Port 3000)
## Schnellstart
```bash
# 1. Repository klonen
git clone <repo-url> /tmp/adguard-security
cd /tmp/adguard-security
# 2. Installer ausführen (fragt interaktiv nach Zugangsdaten & Einstellungen)
sudo bash install.sh install
# 3. Erst im Dry-Run testen (loggt nur, sperrt nichts)
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh dry-run
# 4. Wenn alles passt — Service starten
sudo systemctl start adguard-ratelimit
sudo systemctl status adguard-ratelimit
```
## Wichtigste Befehle
```bash
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh status # Aktive Sperren anzeigen
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh history # Ban-History anzeigen
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh unban IP # Einzelne IP entsperren
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh flush # Alle Sperren aufheben
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh test # API-Verbindung testen
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh blocklist-status # Externe Blocklisten Status
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh blocklist-sync # Blocklisten manuell synchronisieren
sudo journalctl -u adguard-ratelimit -f # Logs live verfolgen
```
## Projektstruktur
```
├── adguard-ratelimit.sh # Haupt-Monitor-Script
├── adguard-ratelimit.conf # Konfiguration
├── adguard-ratelimit.service # systemd Unit
├── external-blocklist-worker.sh # Externer Blocklist-Worker
├── iptables-helper.sh # Manuelle iptables-Verwaltung
├── unban-expired.sh # Cron-basiertes Entsperren
├── install.sh # Installer / Uninstaller
├── README.md
└── doc/
├── architektur.md # Architektur & Funktionsweise
├── konfiguration.md # Alle Parameter erklärt
├── befehle.md # Vollständige Befehlsreferenz
├── benachrichtigungen.md # Webhook-Setup (Discord, Slack, Gotify)
└── tipps-und-troubleshooting.md
```
## Dokumentation
| Dokument | Inhalt |
|----------|--------|
| [Architektur](doc/architektur.md) | Wie das Tool funktioniert, iptables-Strategie, Ablauf einer Sperre |
| [Konfiguration](doc/konfiguration.md) | Alle Parameter, Ports, Whitelist-Pflege, externe Blocklisten |
| [Befehle](doc/befehle.md) | Vollständige Befehlsreferenz für Monitor, iptables-Helper und systemd |
| [Benachrichtigungen](doc/benachrichtigungen.md) | Setup für Discord, Slack, Gotify, Ntfy |
| [Tipps & Troubleshooting](doc/tipps-und-troubleshooting.md) | Best Practices, häufige Probleme, Deinstallation |
## Lizenz
[MIT](LICENSE)
---
## 👥 Techniverse Community
Lust auf Austausch rund um Matrix, Selfhosting und andere smarte IT-Lösungen?
In der **Techniverse Community** triffst du Gleichgesinnte, kannst Fragen stellen oder einfach nerdigen Talk genießen. 🚀
👉 **[Jetzt der Gruppe auf Matrix beitreten](https://matrix.to/#/#community:techniverse.net)**
~ Direkte Raumadresse: `#community:techniverse.net`
👉 **[Für lockere Gespräche abseits der Kernthemen komm in den Talkraum](https://matrix.to/#/#talk:techniverse.net)**
~ Direkte Raumadresse: `#talk:techniverse.net`
Wir freuen uns, wenn du dabei bist!
---
📝 **Blog:** [www.cleveradmin.de](https://www.cleveradmin.de)
🌐 **Webseite:** [www.patrick-asmus.de](https://www.patrick-asmus.de)
📧 **E-Mail:** [support@techniverse.net](mailto:support@techniverse.net)
<p align="center">
<img src="https://assets.techniverse.net/f1/git/graphics/gray0-catonline.svg" alt="">
@@ -13,4 +133,4 @@ Wichtig: Link für Lizenz anpassen.
<p align="center">
<img src="https://assets.techniverse.net/f1/logos/small/license.png" alt="License" width="15" height="15"> <a href="./LICENSE">License</a> | <img src="https://assets.techniverse.net/f1/logos/small/matrix2.svg" alt="Matrix" width="15" height="15"> <a href="https://matrix.to/#/#community:techniverse.net">Matrix</a> | <img src="https://assets.techniverse.net/f1/logos/small/mastodon2.svg" alt="Mastodon" width="15" height="15"> <a href="https://social.techniverse.net/@donnerwolke">Mastodon</a>
</p>
</p>

114
adguard-ratelimit.conf Normal file
View File

@@ -0,0 +1,114 @@
###############################################################################
# AdGuard Shield - Konfigurationsdatei
# Schutz vor übermäßigen DNS-Anfragen einzelner Clients
###############################################################################
# --- AdGuard Home API Einstellungen ---
# URL der AdGuard Home Web-Oberfläche (ohne trailing slash)
ADGUARD_URL="http://127.0.0.1:3000"
# AdGuard Home Zugangsdaten (Web-UI Login)
ADGUARD_USER="admin"
ADGUARD_PASS='changeme'
# --- Rate-Limit Einstellungen ---
# Maximale Anfragen pro Domain pro Client innerhalb des Zeitfensters
RATE_LIMIT_MAX_REQUESTS=30
# Zeitfenster in Sekunden (60 = 1 Minute)
RATE_LIMIT_WINDOW=60
# Wie oft das Script die Logs prüft (in Sekunden)
CHECK_INTERVAL=10
# --- Sperr-Einstellungen ---
# Wie lange ein Client gesperrt wird (in Sekunden, 3600 = 1 Stunde)
BAN_DURATION=3600
# iptables Chain-Name für die Sperren
IPTABLES_CHAIN="ADGUARD_RATELIMIT"
# Welche Ports gesperrt werden sollen (DNS, DoT, DoH, DNSv5/QUIC)
# 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"
# --- Whitelist ---
# IP-Adressen die NIEMALS gesperrt werden (kommagetrennt)
# Lokale Netze und wichtige Server hier eintragen
WHITELIST="127.0.0.1,::1"
# --- Logging ---
# Log-Datei Pfad
LOG_FILE="/var/log/adguard-ratelimit.log"
# Log-Level: DEBUG, INFO, WARN, ERROR
LOG_LEVEL="INFO"
# Maximale Größe der Log-Datei in MB (danach wird rotiert)
LOG_MAX_SIZE_MB=50
# Ban-History Datei (protokolliert alle Sperren & Entsperrungen dauerhaft)
BAN_HISTORY_FILE="/var/log/adguard-ratelimit-bans.log"
# --- Benachrichtigungen (optional) ---
# Aktiviert Benachrichtigungen bei Sperren
NOTIFY_ENABLED=false
# Webhook-URL für Benachrichtigungen (z.B. Discord, Slack, Gotify)
# 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"
# Topic-Name für die Benachrichtigungen
NTFY_TOPIC=""
# Optionaler Access-Token (leer lassen wenn nicht benötigt)
NTFY_TOKEN=""
# Priorität der Ntfy-Nachrichten (1=min, 3=default, 5=max)
NTFY_PRIORITY="1"
# --- Externe Blocklist (optional) ---
# Aktiviert den externen Blocklist-Worker
EXTERNAL_BLOCKLIST_ENABLED=false
# URL(s) zu externen Textdateien mit IP-Adressen (eine IP pro Zeile)
# Mehrere URLs kommagetrennt angeben
# Beispiel: "https://example.com/blocklist.txt,https://other.com/bad-ips.txt"
EXTERNAL_BLOCKLIST_URLS=""
# Wie oft die externe Blocklist geprüft wird (in Sekunden, 300 = 5 Minuten)
EXTERNAL_BLOCKLIST_INTERVAL=300
# Sperrdauer für externe Blocklist-IPs in Sekunden (0 = permanent bis IP aus Liste entfernt)
EXTERNAL_BLOCKLIST_BAN_DURATION=0
# Automatisch IPs entsperren die aus der externen Liste entfernt wurden?
EXTERNAL_BLOCKLIST_AUTO_UNBAN=true
# Lokaler Cache-Pfad für die heruntergeladene Blocklist
EXTERNAL_BLOCKLIST_CACHE_DIR="/var/lib/adguard-ratelimit/external-blocklist"
# --- Erweiterte Einstellungen ---
# Pfad zur State-Datei (speichert aktive Sperren)
STATE_DIR="/var/lib/adguard-ratelimit"
# Pfad zum PID-File
PID_FILE="/var/run/adguard-ratelimit.pid"
# Anzahl der API-Einträge die pro Abfrage geholt werden (max 5000)
API_QUERY_LIMIT=500
# Dry-Run Modus: true = nur loggen, nicht sperren (zum Testen)
DRY_RUN=false

36
adguard-ratelimit.service Normal file
View File

@@ -0,0 +1,36 @@
[Unit]
Description=AdGuard Shield - DNS Rate-Limit Monitor
Documentation=https://github.com/your-repo/adguard-security
After=network.target AdGuardHome.service
Wants=AdGuardHome.service
StartLimitBurst=5
StartLimitIntervalSec=60
[Service]
Type=simple
ExecStart=/opt/adguard-ratelimit/adguard-ratelimit.sh start
ExecStop=/opt/adguard-ratelimit/adguard-ratelimit.sh stop
ExecReload=/bin/kill -HUP $MAINPID
# Neustart-Verhalten
Restart=on-failure
RestartSec=10
# Sicherheits-Hardening
ProtectSystem=full
ReadWritePaths=/var/log /var/lib/adguard-ratelimit /var/lib/adguard-ratelimit/external-blocklist /var/run
ProtectHome=true
NoNewPrivileges=false
PrivateTmp=true
# iptables benötigt CAP_NET_ADMIN
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=adguard-ratelimit
[Install]
WantedBy=multi-user.target

711
adguard-ratelimit.sh Normal file
View File

@@ -0,0 +1,711 @@
#!/bin/bash
###############################################################################
# AdGuard Shield
# Überwacht DNS-Anfragen und sperrt Clients bei Überschreitung des Limits
#
# Autor: Patrick Asmus
# E-Mail: support@techniverse.net
# Datum: 2026-03-03
# Lizenz: MIT
###############################################################################
VERSION="1.0.0"
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/adguard-ratelimit.conf"
# ─── Konfiguration laden ───────────────────────────────────────────────────────
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "FEHLER: Konfigurationsdatei nicht gefunden: $CONFIG_FILE" >&2
exit 1
fi
# shellcheck source=adguard-ratelimit.conf
source "$CONFIG_FILE"
# ─── Abhängigkeiten prüfen ────────────────────────────────────────────────────
check_dependencies() {
local missing=()
for cmd in curl jq iptables ip6tables date; do
if ! command -v "$cmd" &>/dev/null; then
missing+=("$cmd")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
log "ERROR" "Fehlende Abhängigkeiten: ${missing[*]}"
echo "Bitte installieren: sudo apt install ${missing[*]}" >&2
exit 1
fi
}
# ─── Logging ──────────────────────────────────────────────────────────────────
declare -A LOG_LEVELS=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3)
log() {
local level="$1"
shift
local message="$*"
local configured_level="${LOG_LEVEL:-INFO}"
if [[ ${LOG_LEVELS[$level]:-1} -ge ${LOG_LEVELS[$configured_level]:-1} ]]; then
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
local log_entry="[$timestamp] [$level] $message"
echo "$log_entry" | tee -a "$LOG_FILE"
# Log-Rotation prüfen
if [[ -f "$LOG_FILE" ]]; then
local size_kb
size_kb=$(du -k "$LOG_FILE" 2>/dev/null | cut -f1)
local max_kb=$((LOG_MAX_SIZE_MB * 1024))
if [[ ${size_kb:-0} -gt $max_kb ]]; then
mv "$LOG_FILE" "${LOG_FILE}.old"
log "INFO" "Log-Datei rotiert"
fi
fi
fi
}
# ─── Ban-History ─────────────────────────────────────────────────────────────
log_ban_history() {
local action="$1"
local client_ip="$2"
local domain="${3:-}"
local count="${4:-}"
local reason="${5:-}"
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
# Header schreiben falls Datei neu ist
if [[ ! -f "$BAN_HISTORY_FILE" ]]; then
echo "# AdGuard Shield - Ban History" > "$BAN_HISTORY_FILE"
echo "# Format: ZEITSTEMPEL | AKTION | CLIENT-IP | DOMAIN | ANFRAGEN | SPERRDAUER | GRUND" >> "$BAN_HISTORY_FILE"
echo "#───────────────────────────────────────────────────────────────────────────────" >> "$BAN_HISTORY_FILE"
fi
local duration="-"
[[ "$action" == "BAN" ]] && duration="${BAN_DURATION}s"
printf "%-19s | %-6s | %-39s | %-30s | %-8s | %-10s | %s\n" \
"$timestamp" "$action" "$client_ip" "${domain:--}" "${count:--}" "$duration" "${reason:-rate-limit}" \
>> "$BAN_HISTORY_FILE"
}
# ─── Verzeichnisse erstellen ──────────────────────────────────────────────────
init_directories() {
mkdir -p "$STATE_DIR"
mkdir -p "$(dirname "$LOG_FILE")"
mkdir -p "$(dirname "$PID_FILE")"
mkdir -p "$(dirname "$BAN_HISTORY_FILE")"
}
# ─── PID-Management ──────────────────────────────────────────────────────────
write_pid() {
echo $$ > "$PID_FILE"
}
cleanup() {
log "INFO" "AdGuard Shield wird beendet..."
stop_blocklist_worker
rm -f "$PID_FILE"
exit 0
}
check_already_running() {
if [[ -f "$PID_FILE" ]]; then
local old_pid
old_pid=$(cat "$PID_FILE")
if kill -0 "$old_pid" 2>/dev/null; then
echo "Monitor läuft bereits (PID: $old_pid). Beende." >&2
exit 1
else
rm -f "$PID_FILE"
fi
fi
}
# ─── Whitelist Prüfung ───────────────────────────────────────────────────────
is_whitelisted() {
local ip="$1"
IFS=',' read -ra wl_entries <<< "$WHITELIST"
for entry in "${wl_entries[@]}"; do
entry=$(echo "$entry" | xargs) # trim
if [[ "$ip" == "$entry" ]]; then
return 0
fi
done
return 1
}
# ─── iptables Chain Setup ────────────────────────────────────────────────────
setup_iptables_chain() {
# IPv4 Chain erstellen falls nicht vorhanden
if ! iptables -n -L "$IPTABLES_CHAIN" &>/dev/null; then
log "INFO" "Erstelle iptables Chain: $IPTABLES_CHAIN (IPv4)"
iptables -N "$IPTABLES_CHAIN"
# Chain in INPUT einhängen für alle relevanten Ports
for port in $BLOCKED_PORTS; do
iptables -I INPUT -p tcp --dport "$port" -j "$IPTABLES_CHAIN"
iptables -I INPUT -p udp --dport "$port" -j "$IPTABLES_CHAIN"
done
fi
# IPv6 Chain erstellen falls nicht vorhanden
if ! ip6tables -n -L "$IPTABLES_CHAIN" &>/dev/null; then
log "INFO" "Erstelle ip6tables Chain: $IPTABLES_CHAIN (IPv6)"
ip6tables -N "$IPTABLES_CHAIN"
for port in $BLOCKED_PORTS; do
ip6tables -I INPUT -p tcp --dport "$port" -j "$IPTABLES_CHAIN"
ip6tables -I INPUT -p udp --dport "$port" -j "$IPTABLES_CHAIN"
done
fi
}
# ─── Client sperren ─────────────────────────────────────────────────────────
ban_client() {
local client_ip="$1"
local domain="$2"
local count="$3"
local ban_until
ban_until=$(date -d "+${BAN_DURATION} seconds" '+%s' 2>/dev/null || date -v "+${BAN_DURATION}S" '+%s')
# Prüfen ob bereits gesperrt
local state_file="${STATE_DIR}/${client_ip//[:\/]/_}.ban"
if [[ -f "$state_file" ]]; then
log "DEBUG" "Client $client_ip ist bereits gesperrt"
return 0
fi
if [[ "$DRY_RUN" == "true" ]]; then
log "WARN" "[DRY-RUN] WÜRDE sperren: $client_ip (${count}x $domain in ${RATE_LIMIT_WINDOW}s)"
log_ban_history "DRY" "$client_ip" "$domain" "$count" "dry-run"
return 0
fi
log "WARN" "SPERRE Client: $client_ip (${count}x $domain in ${RATE_LIMIT_WINDOW}s) für ${BAN_DURATION}s"
# IPv4 oder IPv6 erkennen
if [[ "$client_ip" == *:* ]]; then
# IPv6
ip6tables -I "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true
else
# IPv4
iptables -I "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true
fi
# State speichern
cat > "$state_file" << EOF
CLIENT_IP=$client_ip
DOMAIN=$domain
COUNT=$count
BAN_TIME=$(date '+%Y-%m-%d %H:%M:%S')
BAN_UNTIL_EPOCH=$ban_until
BAN_UNTIL=$(date -d "@$ban_until" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || date -r "$ban_until" '+%Y-%m-%d %H:%M:%S')
EOF
# Ban-History Eintrag
log_ban_history "BAN" "$client_ip" "$domain" "$count" "rate-limit"
# Benachrichtigung senden
if [[ "$NOTIFY_ENABLED" == "true" ]]; then
send_notification "ban" "$client_ip" "$domain" "$count"
fi
}
# ─── Client entsperren ──────────────────────────────────────────────────────
unban_client() {
local client_ip="$1"
local reason="${2:-expired}"
local state_file="${STATE_DIR}/${client_ip//[:\/]/_}.ban"
# Domain aus State lesen bevor wir löschen
local domain="-"
if [[ -f "$state_file" ]]; then
domain=$(grep '^DOMAIN=' "$state_file" | cut -d= -f2)
fi
log "INFO" "ENTSPERRE Client: $client_ip ($reason)"
if [[ "$client_ip" == *:* ]]; then
ip6tables -D "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true
else
iptables -D "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true
fi
rm -f "$state_file"
# Ban-History Eintrag
log_ban_history "UNBAN" "$client_ip" "$domain" "-" "$reason"
if [[ "$NOTIFY_ENABLED" == "true" ]]; then
send_notification "unban" "$client_ip" "" ""
fi
}
# ─── Abgelaufene Sperren aufheben ───────────────────────────────────────────
check_expired_bans() {
local now
now=$(date '+%s')
for state_file in "${STATE_DIR}"/*.ban; do
[[ -f "$state_file" ]] || continue
local ban_until_epoch
ban_until_epoch=$(grep '^BAN_UNTIL_EPOCH=' "$state_file" | cut -d= -f2)
local client_ip
client_ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2)
if [[ -n "$ban_until_epoch" && "$now" -ge "$ban_until_epoch" ]]; then
unban_client "$client_ip" "expired"
fi
done
}
# ─── Benachrichtigungen ─────────────────────────────────────────────────────
send_notification() {
local action="$1"
local client_ip="$2"
local domain="$3"
local count="$4"
[[ -z "$NOTIFY_WEBHOOK_URL" ]] && return
local message
if [[ "$action" == "ban" ]]; then
message="🚫 DNS Rate-Limit: Client **$client_ip** gesperrt (${count}x $domain in ${RATE_LIMIT_WINDOW}s). Sperre für ${BAN_DURATION}s."
else
message="✅ DNS Rate-Limit: Client **$client_ip** wurde entsperrt."
fi
case "$NOTIFY_TYPE" in
discord)
curl -s -H "Content-Type: application/json" \
-d "{\"content\": \"$message\"}" \
"$NOTIFY_WEBHOOK_URL" &>/dev/null &
;;
slack)
curl -s -H "Content-Type: application/json" \
-d "{\"text\": \"$message\"}" \
"$NOTIFY_WEBHOOK_URL" &>/dev/null &
;;
gotify)
curl -s -X POST "$NOTIFY_WEBHOOK_URL" \
-F "title=AdGuard Rate-Limit" \
-F "message=$message" \
-F "priority=5" &>/dev/null &
;;
ntfy)
send_ntfy_notification "$action" "$message"
;;
generic)
curl -s -H "Content-Type: application/json" \
-d "{\"message\": \"$message\", \"action\": \"$action\", \"client\": \"$client_ip\", \"domain\": \"$domain\"}" \
"$NOTIFY_WEBHOOK_URL" &>/dev/null &
;;
esac
}
# ─── Ntfy Benachrichtigung ───────────────────────────────────────────────────
send_ntfy_notification() {
local action="$1"
local message="$2"
if [[ -z "${NTFY_TOPIC:-}" ]]; then
log "WARN" "Ntfy: Kein Topic konfiguriert (NTFY_TOPIC ist leer)"
return 1
fi
local ntfy_url="${NTFY_SERVER_URL:-https://ntfy.sh}"
local priority="${NTFY_PRIORITY:-4}"
local title="AdGuard Rate-Limit"
local tags
if [[ "$action" == "ban" ]]; then
tags="rotating_light,ban"
else
tags="white_check_mark,unban"
fi
# Markdown-Formatierung entfernen für Ntfy
local clean_message
clean_message=$(echo "$message" | sed 's/\*\*//g')
local -a curl_args=(
-s
-X POST
"${ntfy_url}/${NTFY_TOPIC}"
-H "Title: ${title}"
-H "Priority: ${priority}"
-H "Tags: ${tags}"
-d "${clean_message}"
)
# Token hinzufügen falls konfiguriert
if [[ -n "${NTFY_TOKEN:-}" ]]; then
curl_args+=(-H "Authorization: Bearer ${NTFY_TOKEN}")
fi
curl "${curl_args[@]}" &>/dev/null &
}
# ─── AdGuard Home API abfragen ──────────────────────────────────────────────
query_adguard_log() {
local time_from
time_from=$(date -u -d "-${RATE_LIMIT_WINDOW} seconds" '+%Y-%m-%dT%H:%M:%S.000Z' 2>/dev/null \
|| date -u -v "-${RATE_LIMIT_WINDOW}S" '+%Y-%m-%dT%H:%M:%S.000Z')
local response
response=$(curl -s -u "${ADGUARD_USER}:${ADGUARD_PASS}" \
--connect-timeout 5 \
--max-time 10 \
"${ADGUARD_URL}/control/querylog?limit=${API_QUERY_LIMIT}&response_status=all" 2>/dev/null)
if [[ -z "$response" || "$response" == "null" ]]; then
log "ERROR" "Keine Antwort von AdGuard Home API"
return 1
fi
# Prüfen ob die Antwort gültiges JSON ist
if ! echo "$response" | jq . &>/dev/null; then
log "ERROR" "Ungültige API-Antwort (kein JSON)"
return 1
fi
echo "$response"
}
# ─── Anfragen analysieren ───────────────────────────────────────────────────
analyze_queries() {
local api_response="$1"
local now_epoch
now_epoch=$(date '+%s')
local window_start=$((now_epoch - RATE_LIMIT_WINDOW))
# Extrahiere Client-IP + Domain Paare aus dem Zeitfenster
# und zähle die Häufigkeit pro (client, domain) Kombination
local violations
violations=$(echo "$api_response" | jq -r --argjson window_start "$window_start" '
.data // [] |
[.[] |
select(.time != null) |
{
client: (.client // .client_info.ip // "unknown"),
domain: (.question.name // "unknown" | rtrimstr(".")),
time_epoch: (.time | split(".")[0] | sub("T"; " ") | sub("Z$"; "") )
}
] |
group_by(.client + "|" + .domain) |
map({
client: .[0].client,
domain: .[0].domain,
count: length
}) |
.[] |
select(.count > 0) |
"\(.client)|\(.domain)|\(.count)"
' 2>/dev/null)
if [[ -z "$violations" ]]; then
log "DEBUG" "Keine Anfragen im Zeitfenster gefunden"
return
fi
# Prüfe jede Kombination gegen das Limit
while IFS='|' read -r client domain count; do
[[ -z "$client" || -z "$domain" || -z "$count" ]] && continue
log "DEBUG" "Client: $client, Domain: $domain, Anfragen: $count"
if [[ "$count" -gt "$RATE_LIMIT_MAX_REQUESTS" ]]; then
if is_whitelisted "$client"; then
log "INFO" "Client $client ist auf der Whitelist - keine Sperre (${count}x $domain)"
continue
fi
ban_client "$client" "$domain" "$count"
fi
done <<< "$violations"
}
# ─── Status anzeigen ─────────────────────────────────────────────────────────
show_status() {
echo "═══════════════════════════════════════════════════════════════"
echo " AdGuard Home Rate-Limit Monitor - Status"
echo "═══════════════════════════════════════════════════════════════"
echo ""
# Aktive Sperren
local ban_count=0
if [[ -d "$STATE_DIR" ]]; then
for state_file in "${STATE_DIR}"/*.ban; do
[[ -f "$state_file" ]] || continue
ban_count=$((ban_count + 1))
echo " 🚫 Gesperrt:"
while IFS='=' read -r key value; do
printf " %-20s %s\n" "$key:" "$value"
done < "$state_file"
echo ""
done
fi
if [[ $ban_count -eq 0 ]]; then
echo " ✅ Keine aktiven Sperren"
else
echo " Gesamt: $ban_count aktive Sperren"
fi
echo ""
# iptables Regeln anzeigen
echo " iptables Regeln ($IPTABLES_CHAIN):"
if iptables -n -L "$IPTABLES_CHAIN" &>/dev/null; then
iptables -n -L "$IPTABLES_CHAIN" --line-numbers 2>/dev/null | sed 's/^/ /'
else
echo " Chain existiert noch nicht"
fi
echo ""
echo "═══════════════════════════════════════════════════════════════"
}
# ─── Ban-History anzeigen ────────────────────────────────────────────────────
show_history() {
local lines="${1:-50}"
echo "═══════════════════════════════════════════════════════════════"
echo " AdGuard Home Rate-Limit - Ban History (letzte $lines Einträge)"
echo "═══════════════════════════════════════════════════════════════"
echo ""
if [[ ! -f "$BAN_HISTORY_FILE" ]]; then
echo " Noch keine History vorhanden."
echo " Datei: $BAN_HISTORY_FILE"
echo ""
return
fi
# Header zeigen
head -3 "$BAN_HISTORY_FILE" | sed 's/^/ /'
echo ""
# Letzte N Einträge (ohne Header-Zeilen)
grep -v '^#' "$BAN_HISTORY_FILE" | tail -n "$lines" | sed 's/^/ /'
echo ""
local total
total=$(grep -vc '^#' "$BAN_HISTORY_FILE" 2>/dev/null || echo "0")
local bans
bans=$(grep -c '| BAN ' "$BAN_HISTORY_FILE" 2>/dev/null || echo "0")
local unbans
unbans=$(grep -c '| UNBAN ' "$BAN_HISTORY_FILE" 2>/dev/null || echo "0")
echo " Gesamt: $total Einträge ($bans Sperren, $unbans Entsperrungen)"
echo " Datei: $BAN_HISTORY_FILE"
echo ""
echo "═══════════════════════════════════════════════════════════════"
}
# ─── Alle Sperren aufheben ──────────────────────────────────────────────────
flush_all_bans() {
log "INFO" "Alle Sperren werden aufgehoben..."
for state_file in "${STATE_DIR}"/*.ban; do
[[ -f "$state_file" ]] || continue
local client_ip
client_ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2)
unban_client "$client_ip" "manual-flush"
done
# Chain leeren
iptables -F "$IPTABLES_CHAIN" 2>/dev/null || true
ip6tables -F "$IPTABLES_CHAIN" 2>/dev/null || true
log "INFO" "Alle Sperren aufgehoben"
}
# ─── Externer Blocklist-Worker starten ───────────────────────────────────────
start_blocklist_worker() {
if [[ "${EXTERNAL_BLOCKLIST_ENABLED:-false}" != "true" ]]; then
log "DEBUG" "Externer Blocklist-Worker ist deaktiviert"
return
fi
local worker_script="${SCRIPT_DIR}/external-blocklist-worker.sh"
if [[ ! -f "$worker_script" ]]; then
log "WARN" "Blocklist-Worker Script nicht gefunden: $worker_script"
return
fi
log "INFO" "Starte externen Blocklist-Worker im Hintergrund..."
bash "$worker_script" start &
BLOCKLIST_WORKER_PID=$!
log "INFO" "Blocklist-Worker gestartet (PID: $BLOCKLIST_WORKER_PID)"
}
# ─── Externer Blocklist-Worker stoppen ───────────────────────────────────────
stop_blocklist_worker() {
local worker_pid_file="/var/run/adguard-blocklist-worker.pid"
if [[ -f "$worker_pid_file" ]]; then
local wpid
wpid=$(cat "$worker_pid_file")
if kill -0 "$wpid" 2>/dev/null; then
log "INFO" "Stoppe Blocklist-Worker (PID: $wpid)..."
kill "$wpid" 2>/dev/null || true
rm -f "$worker_pid_file"
fi
fi
}
# ─── Hauptschleife ──────────────────────────────────────────────────────────
main_loop() {
log "INFO" "═══════════════════════════════════════════════════════════"
log "INFO" "AdGuard Shield v${VERSION} gestartet"
log "INFO" " Limit: ${RATE_LIMIT_MAX_REQUESTS} Anfragen pro ${RATE_LIMIT_WINDOW}s"
log "INFO" " Sperrdauer: ${BAN_DURATION}s"
log "INFO" " Prüfintervall: ${CHECK_INTERVAL}s"
log "INFO" " Dry-Run: ${DRY_RUN}"
log "INFO" " Whitelist: ${WHITELIST}"
log "INFO" " Externe Blocklist: ${EXTERNAL_BLOCKLIST_ENABLED:-false}"
log "INFO" "═══════════════════════════════════════════════════════════"
# Blocklist-Worker als Hintergrundprozess starten
start_blocklist_worker
while true; do
# Abgelaufene Sperren prüfen
check_expired_bans
# API abfragen
local api_response
if api_response=$(query_adguard_log); then
analyze_queries "$api_response"
fi
sleep "$CHECK_INTERVAL"
done
}
# ─── Signal-Handler ──────────────────────────────────────────────────────────
trap cleanup SIGTERM SIGINT SIGHUP
# ─── Kommandozeilen-Argumente ────────────────────────────────────────────────
case "${1:-start}" in
start)
check_dependencies
check_already_running
init_directories
write_pid
setup_iptables_chain
main_loop
;;
stop)
if [[ -f "$PID_FILE" ]]; then
kill "$(cat "$PID_FILE")" 2>/dev/null || true
rm -f "$PID_FILE"
echo "Monitor gestoppt"
else
echo "Monitor läuft nicht"
fi
;;
blocklist-status)
init_directories
_worker_script="${SCRIPT_DIR}/external-blocklist-worker.sh"
if [[ -f "$_worker_script" ]]; then
bash "$_worker_script" status
else
echo "Blocklist-Worker nicht gefunden"
fi
;;
blocklist-sync)
init_directories
setup_iptables_chain
_worker_script="${SCRIPT_DIR}/external-blocklist-worker.sh"
if [[ -f "$_worker_script" ]]; then
bash "$_worker_script" sync
else
echo "Blocklist-Worker nicht gefunden"
fi
;;
blocklist-flush)
init_directories
_worker_script="${SCRIPT_DIR}/external-blocklist-worker.sh"
if [[ -f "$_worker_script" ]]; then
bash "$_worker_script" flush
else
echo "Blocklist-Worker nicht gefunden"
fi
;;
status)
init_directories
show_status
;;
flush)
init_directories
setup_iptables_chain
flush_all_bans
echo "Alle Sperren aufgehoben"
;;
unban)
if [[ -z "${2:-}" ]]; then
echo "Nutzung: $0 unban <IP-Adresse>" >&2
exit 1
fi
init_directories
unban_client "$2" "manual"
echo "Client $2 entsperrt"
;;
test)
echo "Teste Verbindung zur AdGuard Home API..."
check_dependencies
init_directories
if response=$(query_adguard_log); then
entry_count=$(echo "$response" | jq '.data | length' 2>/dev/null || echo "0")
echo "✅ Verbindung erfolgreich! $entry_count Log-Einträge gefunden."
else
echo "❌ Verbindung fehlgeschlagen! Prüfe URL und Zugangsdaten in $CONFIG_FILE"
exit 1
fi
;;
history)
init_directories
show_history "${2:-50}"
;;
dry-run)
DRY_RUN=true
check_dependencies
check_already_running
init_directories
write_pid
setup_iptables_chain
main_loop
;;
*)
cat << USAGE
AdGuard Shield v${VERSION}
Nutzung: $0 {start|stop|status|history|flush|unban|test|dry-run|blocklist-status|blocklist-sync|blocklist-flush}
Befehle:
start Startet den Monitor (inkl. Blocklist-Worker)
stop Stoppt den Monitor
status Zeigt aktive Sperren und Regeln
history [N] Zeigt die letzten N Ban-Einträge (Standard: 50)
flush Hebt alle Sperren auf
unban IP Entsperrt eine bestimmte IP-Adresse
test Testet die Verbindung zur AdGuard Home API
dry-run Startet im Testmodus (keine echten Sperren)
blocklist-status Zeigt Status der externen Blocklisten
blocklist-sync Einmalige Synchronisation der externen Blocklisten
blocklist-flush Entfernt alle Sperren der externen Blocklisten
Konfiguration: $CONFIG_FILE
Log-Datei: $LOG_FILE
Ban-History: $BAN_HISTORY_FILE
State: $STATE_DIR
USAGE
exit 0
;;
esac

138
doc/architektur.md Normal file
View File

@@ -0,0 +1,138 @@
# Architektur & Funktionsweise
## Überblick
```
┌─────────────────────┐
│ Client Anfragen │
│ (DNS/DoH/DoT/DoQ) │
└──────────┬──────────┘
┌─────────────────────┐ ┌──────────────────────┐
│ AdGuard Home │────▶│ Query Log (API) │
│ DNS Server │ └──────────┬───────────┘
└─────────────────────┘ │
┌──────────────────────┐
│ adguard-ratelimit.sh │
│ (Monitor Script) │
└──────────┬───────────┘
┌──────────────┼──────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ iptables │ │ Log │ │ Webhook │
│ DROP │ │ Datei │ │ Notify │
└──────────┘ └──────────┘ └──────────┘
```
## Ablauf einer 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
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-ratelimit/192.168.1.50.ban`
8. Ban-History Eintrag wird in `/var/log/adguard-ratelimit-bans.log` geschrieben
9. Log-Eintrag + optionale Webhook-Benachrichtigung
10. Nach 3600 Sekunden (1 Stunde): automatische Entsperrung + History-Eintrag
## iptables Strategie
Das Tool erstellt eine eigene Chain `ADGUARD_RATELIMIT`:
```
INPUT Chain
├── ... (bestehende Regeln bleiben unberührt)
├── -p tcp --dport 53 → ADGUARD_RATELIMIT
├── -p udp --dport 53 → ADGUARD_RATELIMIT
├── -p tcp --dport 443 → ADGUARD_RATELIMIT
├── -p udp --dport 443 → ADGUARD_RATELIMIT
├── -p tcp --dport 853 → ADGUARD_RATELIMIT
├── -p udp --dport 853 → ADGUARD_RATELIMIT
└── ...
ADGUARD_RATELIMIT Chain
├── -s 192.168.1.50 → DROP (gesperrter Client)
├── -s 10.0.0.25 → DROP (gesperrter Client)
└── RETURN (alle anderen passieren)
```
**Vorteile der eigenen Chain:**
- Greift nicht in bestehende Firewall-Regeln ein
- Kann komplett geflusht werden ohne andere Regeln zu beeinflussen
- Einfaches Debugging per `iptables -L ADGUARD_RATELIMIT`
## State-Management
Jede aktive Sperre wird als Datei gespeichert:
```
/var/lib/adguard-ratelimit/192.168.1.50.ban
```
Inhalt:
```
CLIENT_IP=192.168.1.50
DOMAIN=microsoft.com
COUNT=45
BAN_TIME=2026-03-03 14:30:00
BAN_UNTIL_EPOCH=1741012200
BAN_UNTIL=2026-03-03 15:30:00
```
Das ermöglicht:
- Persistenz über Script-Neustarts hinweg
- Statusabfragen jederzeit möglich
- Automatisches Aufräumen per Cron-Job
## Dateistruktur nach Installation
```
/opt/adguard-ratelimit/
├── adguard-ratelimit.sh # Haupt-Monitor-Script
├── adguard-ratelimit.conf # Konfiguration (chmod 600)
├── iptables-helper.sh # iptables Verwaltung
└── unban-expired.sh # Cron-basiertes Entsperren
/etc/systemd/system/
└── adguard-ratelimit.service
/var/lib/adguard-ratelimit/
└── *.ban # State-Dateien aktiver Sperren
/var/log/
├── adguard-ratelimit.log # Laufzeit-Log
└── adguard-ratelimit-bans.log # Ban-History (alle Sperren/Entsperrungen)
```
## Ban-History
Jede Sperre und Entsperrung wird dauerhaft in der Ban-History protokolliert (`/var/log/adguard-ratelimit-bans.log`). Das ermöglicht eine lückenlose Nachvollziehbarkeit, auch nachdem State-Dateien bereits gelöscht wurden.
**Format:**
```
ZEITSTEMPEL | AKTION | CLIENT-IP | DOMAIN | ANFRAGEN | SPERRDAUER | GRUND
2026-03-03 14:30:12 | BAN | 192.168.1.50 | microsoft.com | 45 | 3600s | rate-limit
2026-03-03 15:30:12 | UNBAN | 192.168.1.50 | microsoft.com | - | - | expired
2026-03-03 16:10:33 | UNBAN | 10.0.0.25 | telemetry.example.com | - | - | manual
```
**Mögliche Gründe (GRUND-Spalte):**
| Grund | Bedeutung |
|-------|----------|
| `rate-limit` | Automatische Sperre wegen Limit-Überschreitung |
| `dry-run` | Im Dry-Run erkannt (nicht wirklich gesperrt) |
| `expired` | Automatisch entsperrt nach Ablauf der Sperrdauer |
| `expired-cron` | Entsperrt durch den Cron-Job (`unban-expired.sh`) |
| `manual` | Manuell entsperrt per `unban`-Befehl |
| `manual-flush` | Entsperrt durch `flush`-Befehl (alle Sperren aufgehoben) |
**History anzeigen:**
```bash
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh history # letzte 50
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh history 200 # letzte 200
```

134
doc/befehle.md Normal file
View File

@@ -0,0 +1,134 @@
# Befehle & Nutzung
## Monitor (Hauptscript)
```bash
# Starten
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh start
# Stoppen
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh stop
# Status + aktive Sperren anzeigen
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh status
# Ban-History anzeigen (letzte 50 Einträge)
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh history
# Ban-History anzeigen (letzte 100 Einträge)
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh history 100
# Alle Sperren aufheben
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh flush
# Einzelne IP entsperren
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh unban 192.168.1.100
# API-Verbindung testen
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh test
# Dry-Run (nur loggen, nichts sperren)
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh dry-run
# Externe Blocklist - Status anzeigen
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh blocklist-status
# Externe Blocklist - Einmalige Synchronisation
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh blocklist-sync
# Externe Blocklist - Alle Sperren der externen Liste aufheben
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh blocklist-flush
```
## iptables Helper
Für die manuelle Verwaltung der Firewall-Regeln:
```bash
# Chain erstellen
sudo /opt/adguard-ratelimit/iptables-helper.sh create
# Alle Regeln anzeigen
sudo /opt/adguard-ratelimit/iptables-helper.sh status
# IP manuell sperren
sudo /opt/adguard-ratelimit/iptables-helper.sh ban 192.168.1.100
# IP entsperren
sudo /opt/adguard-ratelimit/iptables-helper.sh unban 192.168.1.100
# Alle Regeln leeren
sudo /opt/adguard-ratelimit/iptables-helper.sh flush
# Chain komplett entfernen
sudo /opt/adguard-ratelimit/iptables-helper.sh remove
# Regeln speichern / wiederherstellen
sudo /opt/adguard-ratelimit/iptables-helper.sh save
sudo /opt/adguard-ratelimit/iptables-helper.sh restore
```
## Externer Blocklist-Worker
Der Worker kann auch standalone gesteuert werden:
```bash
# Worker manuell starten (normalerweise automatisch per Hauptscript)
sudo /opt/adguard-ratelimit/external-blocklist-worker.sh start
# Worker stoppen
sudo /opt/adguard-ratelimit/external-blocklist-worker.sh stop
# Einmalige Synchronisation (z.B. nach Konfigurationsänderung)
sudo /opt/adguard-ratelimit/external-blocklist-worker.sh sync
# Status anzeigen
sudo /opt/adguard-ratelimit/external-blocklist-worker.sh status
# Alle externen Sperren aufheben
sudo /opt/adguard-ratelimit/external-blocklist-worker.sh flush
```
## systemd Service
```bash
# Start / Stop / Restart
sudo systemctl start adguard-ratelimit
sudo systemctl stop adguard-ratelimit
sudo systemctl restart adguard-ratelimit
# Status
sudo systemctl status adguard-ratelimit
# Autostart aktivieren / deaktivieren
sudo systemctl enable adguard-ratelimit
sudo systemctl disable adguard-ratelimit
```
## Logs
```bash
# systemd Journal
sudo journalctl -u adguard-ratelimit -f
# Log-Datei direkt
sudo tail -f /var/log/adguard-ratelimit.log
# Nur Sperr-Einträge
sudo grep "SPERRE" /var/log/adguard-ratelimit.log
# Nur Entsperr-Einträge
sudo grep "ENTSPERRE" /var/log/adguard-ratelimit.log
```
## Cron-basiertes Entsperren
Als Alternative oder Ergänzung zum Haupt-Monitor:
```bash
# Crontab bearbeiten
sudo crontab -e
# Alle 5 Minuten abgelaufene Sperren prüfen
*/5 * * * * /opt/adguard-ratelimit/unban-expired.sh
```

110
doc/benachrichtigungen.md Normal file
View File

@@ -0,0 +1,110 @@
# Webhook-Benachrichtigungen
Das Tool kann bei Sperren und Entsperrungen Benachrichtigungen an verschiedene Dienste senden.
## Aktivierung
In der Konfiguration (`adguard-ratelimit.conf`):
```bash
NOTIFY_ENABLED=true
NOTIFY_TYPE="<typ>"
NOTIFY_WEBHOOK_URL="<url>"
```
## Ntfy
```bash
NOTIFY_ENABLED=true
NOTIFY_TYPE="ntfy"
NTFY_SERVER_URL="https://ntfy.sh"
NTFY_TOPIC="adguard-ratelimit"
NTFY_TOKEN=""
NTFY_PRIORITY="4"
```
> **Hinweis:** Bei Ntfy wird `NOTIFY_WEBHOOK_URL` nicht benötigt Server-URL und Topic werden separat konfiguriert.
**Eigene Ntfy-Instanz:**
```bash
NTFY_SERVER_URL="https://ntfy.mein-server.de"
NTFY_TOPIC="dns-security"
NTFY_TOKEN="tk_mein_geheimer_token"
```
**Prioritäten:**
| Wert | Bedeutung |
|------|-----------|
| 1 | Minimum |
| 2 | Niedrig |
| 3 | Standard |
| 4 | Hoch |
| 5 | Maximum |
**Token erstellen (Self-hosted):**
1. Ntfy Web-UI → Benutzer/Tokens
2. Token kopieren und in `NTFY_TOKEN` eintragen
3. Bei ntfy.sh: Account erstellen → Access Token generieren
## Discord
```bash
NOTIFY_ENABLED=true
NOTIFY_TYPE="discord"
NOTIFY_WEBHOOK_URL="https://discord.com/api/webhooks/xxx/yyy"
```
**Webhook erstellen:**
1. Discord Server → Servereinstellungen → Integrationen → Webhooks
2. Neuer Webhook → URL kopieren
## Gotify
```bash
NOTIFY_ENABLED=true
NOTIFY_TYPE="gotify"
NOTIFY_WEBHOOK_URL="https://gotify.example.com/message?token=xxx"
```
**Token erstellen:**
1. Gotify Web-UI → Apps → App erstellen
2. Token kopieren und in die URL einfügen
## Slack
```bash
NOTIFY_ENABLED=true
NOTIFY_TYPE="slack"
NOTIFY_WEBHOOK_URL="https://hooks.slack.com/services/xxx/yyy/zzz"
```
**Webhook erstellen:**
1. Slack App → Incoming Webhooks aktivieren
2. Webhook-URL kopieren
## Generic (eigener Endpoint)
```bash
NOTIFY_ENABLED=true
NOTIFY_TYPE="generic"
NOTIFY_WEBHOOK_URL="https://your-server.com/webhook"
```
Sendet einen POST mit JSON-Body:
```json
{
"message": "🚫 DNS Rate-Limit: Client 192.168.1.50 gesperrt ...",
"action": "ban",
"client": "192.168.1.50",
"domain": "microsoft.com"
}
```
## Beispiel-Nachrichten
**Sperre:**
> 🚫 DNS Rate-Limit: Client **192.168.1.50** gesperrt (45x microsoft.com in 60s). Sperre für 3600s.
**Entsperrung:**
> ✅ DNS Rate-Limit: Client **192.168.1.50** wurde entsperrt.

130
doc/konfiguration.md Normal file
View File

@@ -0,0 +1,130 @@
# Konfiguration
Die Konfigurationsdatei liegt nach der Installation unter:
```
/opt/adguard-ratelimit/adguard-ratelimit.conf
```
## Alle Parameter
### AdGuard Home API
| Parameter | Standard | Beschreibung |
|-----------|----------|--------------|
| `ADGUARD_URL` | `http://127.0.0.1:3000` | AdGuard Home Web-UI URL |
| `ADGUARD_USER` | `admin` | API Benutzername |
| `ADGUARD_PASS` | `changeme` | API Passwort |
### Rate-Limit
| Parameter | Standard | Beschreibung |
|-----------|----------|--------------|
| `RATE_LIMIT_MAX_REQUESTS` | `30` | Max. Anfragen pro Domain/Client innerhalb des Zeitfensters |
| `RATE_LIMIT_WINDOW` | `60` | Zeitfenster in Sekunden |
| `CHECK_INTERVAL` | `10` | Wie oft die Logs geprüft werden (Sekunden) |
| `API_QUERY_LIMIT` | `500` | Anzahl API-Einträge pro Abfrage (max 5000) |
### Sperr-Einstellungen
| Parameter | Standard | Beschreibung |
|-----------|----------|--------------|
| `BAN_DURATION` | `3600` | Sperrdauer in Sekunden (3600 = 1 Stunde) |
| `IPTABLES_CHAIN` | `ADGUARD_RATELIMIT` | Name der iptables Chain |
| `BLOCKED_PORTS` | `53 443 853 784 8853` | Ports die gesperrt werden |
| `WHITELIST` | `127.0.0.1,::1` | IPs die nie gesperrt werden (kommagetrennt) |
### Logging
| Parameter | Standard | Beschreibung |
|-----------|----------|--------------|
| `LOG_FILE` | `/var/log/adguard-ratelimit.log` | Pfad zur Log-Datei |
| `LOG_LEVEL` | `INFO` | Log-Level: `DEBUG`, `INFO`, `WARN`, `ERROR` |
| `LOG_MAX_SIZE_MB` | `50` | Max. Log-Größe bevor rotiert wird |
| `BAN_HISTORY_FILE` | `/var/log/adguard-ratelimit-bans.log` | Datei für die Ban-History (alle Sperren/Entsperrungen) |
### Benachrichtigungen
| Parameter | Standard | Beschreibung |
|-----------|----------|--------------|
| `NOTIFY_ENABLED` | `false` | Webhook-Benachrichtigungen aktivieren |
| `NOTIFY_WEBHOOK_URL` | *(leer)* | Webhook-URL |
| `NOTIFY_TYPE` | `generic` | Typ: `discord`, `slack`, `gotify`, `generic` |
### Erweitert
| Parameter | Standard | Beschreibung |
|-----------|----------|--------------|
| `STATE_DIR` | `/var/lib/adguard-ratelimit` | Verzeichnis für State-Dateien |
| `PID_FILE` | `/var/run/adguard-ratelimit.pid` | PID-Datei |
| `DRY_RUN` | `false` | Testmodus — nur loggen, nicht sperren |
### Externe Blocklist
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.) |
| `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-ratelimit/external-blocklist` | Lokaler Cache für heruntergeladene Listen |
#### Externe Blocklist einrichten
1. Erstelle eine Textdatei auf einem Webserver mit einer IP pro Zeile:
```text
# Kommentare werden ignoriert
192.168.100.50
10.0.0.99
2001:db8::dead:beef
```
2. Aktiviere die Blocklist in der Konfiguration:
```bash
EXTERNAL_BLOCKLIST_ENABLED=true
EXTERNAL_BLOCKLIST_URLS="https://example.com/blocklist.txt"
EXTERNAL_BLOCKLIST_INTERVAL=300
```
3. Mehrere Listen können kommagetrennt angegeben werden:
```bash
EXTERNAL_BLOCKLIST_URLS="https://example.com/list1.txt,https://other.com/list2.txt"
```
4. Service neustarten:
```bash
sudo systemctl restart adguard-ratelimit
```
## Gesperrte Ports im Detail
Bei einem Rate-Limit-Verstoß werden **alle** DNS-Protokoll-Ports für den Client gesperrt:
| 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) |
## Whitelist richtig pflegen
Die Whitelist sollte mindestens enthalten:
- `127.0.0.1` und `::1` (Localhost)
- Die IP deines Routers / Gateways
- Deine eigenen Management-IPs
- Andere vertrauenswürdige DNS-Clients
Beispiel:
```
WHITELIST="127.0.0.1,::1,192.168.1.1,192.168.1.10,fd00::1"
```

View File

@@ -0,0 +1,94 @@
# Tipps & Troubleshooting
## Best Practices
- **Erst immer im Dry-Run testen**, bevor der scharfe Modus aktiviert wird
```bash
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh dry-run
```
- **Whitelist großzügig pflegen**: Eigene IPs, Router, wichtige Server nicht vergessen
- **Sperrdauer anpassen**: Für DDoS-artige Muster ggf. länger sperren
- **Logs regelmäßig prüfen**: Falsche Positive erkennen und Whitelist anpassen
- **Ban-History nutzen**: `history`-Befehl zeigt alle vergangenen Sperren — hilfreich um Muster zu erkennen
- **Log-Level auf DEBUG** setzen wenn etwas nicht funktioniert
## Häufige Probleme
### API-Verbindung schlägt fehl
```bash
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh test
```
**Mögliche Ursachen:**
- Falsche URL in `ADGUARD_URL` (Port prüfen!)
- Falsche Zugangsdaten (`ADGUARD_USER` / `ADGUARD_PASS`)
- AdGuard Home läuft nicht
- Firewall blockiert lokale Verbindung
**Lösung:** URL manuell testen:
```bash
curl -s -u admin:passwort http://127.0.0.1:3000/control/querylog?limit=1
```
### iptables-Fehler: "Permission denied"
Das Script muss als **root** laufen, da iptables Root-Rechte benötigt.
```bash
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh start
```
### Client wird fälschlich gesperrt
1. Client sofort entsperren:
```bash
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh unban 192.168.1.100
```
2. In der Ban-History prüfen, warum gesperrt wurde:
```bash
sudo /opt/adguard-ratelimit/adguard-ratelimit.sh history | grep 192.168.1.100
```
3. IP zur Whitelist hinzufügen in `adguard-ratelimit.conf`
3. Service neustarten:
```bash
sudo systemctl restart adguard-ratelimit
```
### 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.
**Optionen:**
- `iptables-persistent` installieren (`apt install iptables-persistent`)
- Oder den State beim Boot wiederherstellen lassen (Feature-Idee)
### Zu viele false positives
- `RATE_LIMIT_MAX_REQUESTS` erhöhen (z.B. 50 oder 100)
- `RATE_LIMIT_WINDOW` vergrößern (z.B. 120 Sekunden)
- Windows-Clients fragen manche Domains von Natur aus sehr oft an — Whitelist nutzen
### Monitor startet nicht (PID-File)
```bash
# Altes PID-File entfernen
sudo rm -f /var/run/adguard-ratelimit.pid
sudo systemctl start adguard-ratelimit
```
## Deinstallation
```bash
sudo bash install.sh uninstall
```
Oder manuell:
```bash
sudo systemctl stop adguard-ratelimit
sudo systemctl disable adguard-ratelimit
sudo /opt/adguard-ratelimit/iptables-helper.sh remove
sudo rm -rf /opt/adguard-ratelimit
sudo rm -f /etc/systemd/system/adguard-ratelimit.service
sudo systemctl daemon-reload
```

View File

@@ -0,0 +1,640 @@
#!/bin/bash
###############################################################################
# AdGuard Shield - Externer Blocklist-Worker
# Lädt externe IP-Blocklisten herunter und sperrt/entsperrt IPs automatisch.
# Wird als Hintergrundprozess vom Hauptscript gestartet.
#
# Autor: Patrick Asmus
# E-Mail: support@techniverse.net
# Datum: 2026-03-03
# Lizenz: MIT
###############################################################################
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/adguard-ratelimit.conf"
# ─── Konfiguration laden ───────────────────────────────────────────────────────
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "FEHLER: Konfigurationsdatei nicht gefunden: $CONFIG_FILE" >&2
exit 1
fi
# shellcheck source=adguard-ratelimit.conf
source "$CONFIG_FILE"
# ─── Worker PID-File ──────────────────────────────────────────────────────────
WORKER_PID_FILE="/var/run/adguard-blocklist-worker.pid"
# ─── Logging (eigene Funktion, nutzt gleiche Log-Datei) ───────────────────────
declare -A LOG_LEVELS=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3)
log() {
local level="$1"
shift
local message="$*"
local configured_level="${LOG_LEVEL:-INFO}"
if [[ ${LOG_LEVELS[$level]:-1} -ge ${LOG_LEVELS[$configured_level]:-1} ]]; then
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
local log_entry="[$timestamp] [$level] [BLOCKLIST-WORKER] $message"
echo "$log_entry" | tee -a "$LOG_FILE"
fi
}
# ─── Ban-History ─────────────────────────────────────────────────────────────
log_ban_history() {
local action="$1"
local client_ip="$2"
local reason="${3:-external-blocklist}"
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
if [[ ! -f "$BAN_HISTORY_FILE" ]]; then
echo "# AdGuard Shield - Ban History" > "$BAN_HISTORY_FILE"
echo "# Format: ZEITSTEMPEL | AKTION | CLIENT-IP | DOMAIN | ANFRAGEN | SPERRDAUER | 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" \
>> "$BAN_HISTORY_FILE"
}
# ─── Verzeichnisse erstellen ──────────────────────────────────────────────────
init_directories() {
mkdir -p "$EXTERNAL_BLOCKLIST_CACHE_DIR"
mkdir -p "$STATE_DIR"
mkdir -p "$(dirname "$LOG_FILE")"
}
# ─── Whitelist Prüfung ───────────────────────────────────────────────────────
is_whitelisted() {
local ip="$1"
IFS=',' read -ra wl_entries <<< "$WHITELIST"
for entry in "${wl_entries[@]}"; do
entry=$(echo "$entry" | xargs) # trim
if [[ "$ip" == "$entry" ]]; then
return 0
fi
done
return 1
}
# ─── iptables Chain Setup ────────────────────────────────────────────────────
setup_iptables_chain() {
# IPv4 Chain erstellen falls nicht vorhanden
if ! iptables -n -L "$IPTABLES_CHAIN" &>/dev/null; then
log "INFO" "Erstelle iptables Chain: $IPTABLES_CHAIN (IPv4)"
iptables -N "$IPTABLES_CHAIN"
for port in $BLOCKED_PORTS; do
iptables -I INPUT -p tcp --dport "$port" -j "$IPTABLES_CHAIN"
iptables -I INPUT -p udp --dport "$port" -j "$IPTABLES_CHAIN"
done
fi
# IPv6 Chain erstellen falls nicht vorhanden
if ! ip6tables -n -L "$IPTABLES_CHAIN" &>/dev/null; then
log "INFO" "Erstelle ip6tables Chain: $IPTABLES_CHAIN (IPv6)"
ip6tables -N "$IPTABLES_CHAIN"
for port in $BLOCKED_PORTS; do
ip6tables -I INPUT -p tcp --dport "$port" -j "$IPTABLES_CHAIN"
ip6tables -I INPUT -p udp --dport "$port" -j "$IPTABLES_CHAIN"
done
fi
}
# ─── IP sperren ──────────────────────────────────────────────────────────────
ban_ip() {
local ip="$1"
local state_file="${STATE_DIR}/ext_${ip//[:\/]/_}.ban"
# Bereits gesperrt?
if [[ -f "$state_file" ]]; then
log "DEBUG" "IP $ip bereits über externe Blocklist gesperrt"
return 0
fi
# Nicht auch vom Hauptscript gesperrt? (State-Datei ohne ext_ Prefix)
local main_state_file="${STATE_DIR}/${ip//[:\/]/_}.ban"
if [[ -f "$main_state_file" ]]; then
log "DEBUG" "IP $ip bereits vom Rate-Limiter gesperrt - überspringe"
return 0
fi
if [[ "$DRY_RUN" == "true" ]]; then
log "WARN" "[DRY-RUN] WÜRDE sperren (externe Blocklist): $ip"
log_ban_history "DRY" "$ip" "external-blocklist-dry-run"
return 0
fi
log "WARN" "SPERRE IP (externe Blocklist): $ip"
# iptables-Regel setzen
if [[ "$ip" == *:* ]]; then
ip6tables -I "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null || true
else
iptables -I "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null || true
fi
# State speichern
local ban_until_epoch="0"
local ban_until_display="permanent"
if [[ "$EXTERNAL_BLOCKLIST_BAN_DURATION" -gt 0 ]]; then
ban_until_epoch=$(date -d "+${EXTERNAL_BLOCKLIST_BAN_DURATION} seconds" '+%s' 2>/dev/null \
|| date -v "+${EXTERNAL_BLOCKLIST_BAN_DURATION}S" '+%s')
ban_until_display=$(date -d "@$ban_until_epoch" '+%Y-%m-%d %H:%M:%S' 2>/dev/null \
|| date -r "$ban_until_epoch" '+%Y-%m-%d %H:%M:%S')
fi
cat > "$state_file" << EOF
CLIENT_IP=$ip
DOMAIN=-
COUNT=-
BAN_TIME=$(date '+%Y-%m-%d %H:%M:%S')
BAN_UNTIL_EPOCH=$ban_until_epoch
BAN_UNTIL=$ban_until_display
SOURCE=external-blocklist
EOF
log_ban_history "BAN" "$ip" "external-blocklist"
# Benachrichtigung senden
if [[ "$NOTIFY_ENABLED" == "true" ]]; then
send_notification "ban" "$ip"
fi
}
# ─── IP entsperren ───────────────────────────────────────────────────────────
unban_ip() {
local ip="$1"
local reason="${2:-external-blocklist-removed}"
local state_file="${STATE_DIR}/ext_${ip//[:\/]/_}.ban"
[[ -f "$state_file" ]] || return 0
log "INFO" "ENTSPERRE IP (externe Blocklist entfernt): $ip"
if [[ "$ip" == *:* ]]; then
ip6tables -D "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null || true
else
iptables -D "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null || true
fi
rm -f "$state_file"
log_ban_history "UNBAN" "$ip" "$reason"
if [[ "$NOTIFY_ENABLED" == "true" ]]; then
send_notification "unban" "$ip"
fi
}
# ─── Benachrichtigung ────────────────────────────────────────────────────────
send_notification() {
local action="$1"
local ip="$2"
[[ -z "${NOTIFY_WEBHOOK_URL:-}" ]] && return
local message
if [[ "$action" == "ban" ]]; then
message="🚫 Externe Blocklist: IP **$ip** gesperrt."
else
message="✅ Externe Blocklist: IP **$ip** entsperrt (aus Liste entfernt)."
fi
case "${NOTIFY_TYPE:-generic}" in
discord)
curl -s -H "Content-Type: application/json" \
-d "{\"content\": \"$message\"}" \
"$NOTIFY_WEBHOOK_URL" &>/dev/null &
;;
slack)
curl -s -H "Content-Type: application/json" \
-d "{\"text\": \"$message\"}" \
"$NOTIFY_WEBHOOK_URL" &>/dev/null &
;;
gotify)
curl -s -X POST "$NOTIFY_WEBHOOK_URL" \
-F "title=AdGuard Shield - Externe Blocklist" \
-F "message=$message" \
-F "priority=5" &>/dev/null &
;;
ntfy)
local ntfy_url="${NTFY_SERVER_URL:-https://ntfy.sh}"
local -a curl_args=(
-s -X POST "${ntfy_url}/${NTFY_TOPIC}"
-H "Title: AdGuard Shield - Externe Blocklist"
-H "Priority: ${NTFY_PRIORITY:-3}"
-H "Tags: rotating_light,blocklist"
-d "$(echo "$message" | sed 's/\*\*//g')"
)
[[ -n "${NTFY_TOKEN:-}" ]] && curl_args+=(-H "Authorization: Bearer ${NTFY_TOKEN}")
curl "${curl_args[@]}" &>/dev/null &
;;
generic)
curl -s -H "Content-Type: application/json" \
-d "{\"message\": \"$message\", \"action\": \"$action\", \"client\": \"$ip\", \"source\": \"external-blocklist\"}" \
"$NOTIFY_WEBHOOK_URL" &>/dev/null &
;;
esac
}
# ─── Externe Blocklist herunterladen ─────────────────────────────────────────
download_blocklist() {
local url="$1"
local index="$2"
local cache_file="${EXTERNAL_BLOCKLIST_CACHE_DIR}/blocklist_${index}.txt"
local etag_file="${EXTERNAL_BLOCKLIST_CACHE_DIR}/blocklist_${index}.etag"
local tmp_file="${EXTERNAL_BLOCKLIST_CACHE_DIR}/blocklist_${index}.tmp"
log "DEBUG" "Prüfe externe Blocklist: $url"
# HTTP-Header für bedingte Anfrage vorbereiten
local -a curl_args=(
-s
-L
--connect-timeout 10
--max-time 30
-o "$tmp_file"
-w "%{http_code}"
)
# ETag für If-None-Match Header nutzen falls vorhanden
if [[ -f "$etag_file" ]]; then
local stored_etag
stored_etag=$(cat "$etag_file")
curl_args+=(-H "If-None-Match: ${stored_etag}")
fi
# Download-Header separat abfragen für ETag
local http_code
http_code=$(curl "${curl_args[@]}" -D "${tmp_file}.headers" "$url" 2>/dev/null) || {
log "WARN" "Fehler beim Download der Blocklist: $url"
rm -f "$tmp_file" "${tmp_file}.headers"
return 1
}
# 304 Not Modified - keine Änderung
if [[ "$http_code" == "304" ]]; then
log "DEBUG" "Blocklist nicht geändert (HTTP 304): $url"
rm -f "$tmp_file" "${tmp_file}.headers"
return 1
fi
# Fehlerhafte HTTP-Codes
if [[ "$http_code" != "200" ]]; then
log "WARN" "Blocklist Download fehlgeschlagen (HTTP $http_code): $url"
rm -f "$tmp_file" "${tmp_file}.headers"
return 1
fi
# Neuen ETag speichern falls vorhanden
if [[ -f "${tmp_file}.headers" ]]; then
local new_etag
new_etag=$(grep -i '^etag:' "${tmp_file}.headers" | head -1 | sed 's/^[^:]*: *//;s/\r$//')
if [[ -n "$new_etag" ]]; then
echo "$new_etag" > "$etag_file"
fi
fi
rm -f "${tmp_file}.headers"
# Prüfen ob sich der Inhalt tatsächlich geändert hat (Fallback für Server ohne ETag)
if [[ -f "$cache_file" ]]; then
if diff -q "$tmp_file" "$cache_file" &>/dev/null; then
log "DEBUG" "Blocklist Inhalt unverändert: $url"
rm -f "$tmp_file"
return 1
fi
fi
# Neue Datei übernehmen
mv "$tmp_file" "$cache_file"
log "INFO" "Blocklist aktualisiert: $url"
return 0
}
# ─── IPs aus Blocklist-Datei parsen ──────────────────────────────────────────
parse_blocklist_ips() {
local cache_file="$1"
[[ -f "$cache_file" ]] || return
# Zeilen lesen, Leerzeilen und Kommentare ignorieren, IPs extrahieren
while IFS= read -r line; do
# Leerzeilen überspringen
[[ -z "$line" ]] && continue
# Kommentare überspringen (# am Anfang)
[[ "$line" =~ ^[[:space:]]*# ]] && continue
# Whitespace trimmen
line=$(echo "$line" | xargs)
# Leere Zeilen nach Trim überspringen
[[ -z "$line" ]] && continue
# CIDR-Notation oder reine IP ausgeben
echo "$line"
done < "$cache_file"
}
# ─── Aktuelle externe Sperren ermitteln ──────────────────────────────────────
get_currently_banned_external_ips() {
for state_file in "${STATE_DIR}"/ext_*.ban; do
[[ -f "$state_file" ]] || continue
grep '^CLIENT_IP=' "$state_file" | cut -d= -f2
done
}
# ─── Abgelaufene externe Sperren prüfen ─────────────────────────────────────
check_expired_external_bans() {
[[ "$EXTERNAL_BLOCKLIST_BAN_DURATION" -gt 0 ]] || return
local now
now=$(date '+%s')
for state_file in "${STATE_DIR}"/ext_*.ban; do
[[ -f "$state_file" ]] || continue
local ban_until_epoch
ban_until_epoch=$(grep '^BAN_UNTIL_EPOCH=' "$state_file" | cut -d= -f2)
local client_ip
client_ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2)
if [[ -n "$ban_until_epoch" && "$ban_until_epoch" -gt 0 && "$now" -ge "$ban_until_epoch" ]]; then
unban_ip "$client_ip" "external-blocklist-expired"
fi
done
}
# ─── Blocklisten synchronisieren ─────────────────────────────────────────────
sync_blocklists() {
local any_updated=false
# Alle URLs holen
IFS=',' read -ra urls <<< "$EXTERNAL_BLOCKLIST_URLS"
local index=0
for url in "${urls[@]}"; do
url=$(echo "$url" | xargs) # trim
[[ -z "$url" ]] && continue
if download_blocklist "$url" "$index"; then
any_updated=true
fi
index=$((index + 1))
done
# Alle gewünschten IPs zusammenführen (aus allen Cache-Dateien)
local all_desired_ips_file="${EXTERNAL_BLOCKLIST_CACHE_DIR}/.all_ips.tmp"
> "$all_desired_ips_file"
for cache_file in "${EXTERNAL_BLOCKLIST_CACHE_DIR}"/blocklist_*.txt; do
[[ -f "$cache_file" ]] || continue
parse_blocklist_ips "$cache_file" >> "$all_desired_ips_file"
done
# Duplikate entfernen und sortieren
local unique_ips_file="${EXTERNAL_BLOCKLIST_CACHE_DIR}/.all_ips_unique.tmp"
sort -u "$all_desired_ips_file" > "$unique_ips_file"
local desired_count
desired_count=$(wc -l < "$unique_ips_file" | xargs)
log "DEBUG" "Externe Blockliste enthält $desired_count eindeutige IPs"
# ─── Neue IPs sperren ────────────────────────────────────────────────────
local new_bans=0
while IFS= read -r ip; do
[[ -z "$ip" ]] && continue
# Whitelist prüfen
if is_whitelisted "$ip"; then
log "DEBUG" "IP $ip ist auf der Whitelist - überspringe (externe Blocklist)"
continue
fi
ban_ip "$ip"
new_bans=$((new_bans + 1))
done < "$unique_ips_file"
# ─── Entfernte IPs entsperren ────────────────────────────────────────────
if [[ "$EXTERNAL_BLOCKLIST_AUTO_UNBAN" == "true" ]]; then
local removed_count=0
while IFS= read -r banned_ip; do
[[ -z "$banned_ip" ]] && continue
# Prüfen ob die IP noch in der gewünschten Liste ist
if ! grep -qxF "$banned_ip" "$unique_ips_file" 2>/dev/null; then
unban_ip "$banned_ip" "external-blocklist-removed"
removed_count=$((removed_count + 1))
fi
done < <(get_currently_banned_external_ips)
if [[ $removed_count -gt 0 ]]; then
log "INFO" "$removed_count IPs aus externer Blocklist entfernt und entsperrt"
fi
fi
# Abgelaufene Sperren prüfen (nur bei zeitlich begrenzten Sperren)
check_expired_external_bans
# Aufräumen
rm -f "$all_desired_ips_file" "$unique_ips_file"
if [[ "$new_bans" -gt 0 ]]; then
log "INFO" "$new_bans neue IPs aus externer Blocklist gesperrt"
fi
}
# ─── PID-Management ──────────────────────────────────────────────────────────
write_pid() {
echo $$ > "$WORKER_PID_FILE"
}
cleanup() {
log "INFO" "Externer Blocklist-Worker wird beendet..."
rm -f "$WORKER_PID_FILE"
exit 0
}
check_already_running() {
if [[ -f "$WORKER_PID_FILE" ]]; then
local old_pid
old_pid=$(cat "$WORKER_PID_FILE")
if kill -0 "$old_pid" 2>/dev/null; then
log "DEBUG" "Blocklist-Worker läuft bereits (PID: $old_pid)"
return 1
else
rm -f "$WORKER_PID_FILE"
fi
fi
return 0
}
# ─── Status anzeigen ─────────────────────────────────────────────────────────
show_status() {
echo "═══════════════════════════════════════════════════════════════"
echo " Externer Blocklist-Worker - Status"
echo "═══════════════════════════════════════════════════════════════"
echo ""
if [[ "$EXTERNAL_BLOCKLIST_ENABLED" != "true" ]]; then
echo " ⚠️ Externer Blocklist-Worker ist deaktiviert"
echo " Aktivieren: EXTERNAL_BLOCKLIST_ENABLED=true in $CONFIG_FILE"
echo ""
return
fi
# Worker-Prozess Status
if [[ -f "$WORKER_PID_FILE" ]]; then
local pid
pid=$(cat "$WORKER_PID_FILE")
if kill -0 "$pid" 2>/dev/null; then
echo " ✅ Worker läuft (PID: $pid)"
else
echo " ❌ Worker nicht aktiv (veraltete PID-Datei)"
fi
else
echo " ❌ Worker nicht aktiv"
fi
echo ""
# Konfigurierte URLs
echo " Konfigurierte Blocklisten:"
IFS=',' read -ra urls <<< "$EXTERNAL_BLOCKLIST_URLS"
local index=0
for url in "${urls[@]}"; do
url=$(echo "$url" | xargs)
[[ -z "$url" ]] && continue
local cache_file="${EXTERNAL_BLOCKLIST_CACHE_DIR}/blocklist_${index}.txt"
local ip_count=0
if [[ -f "$cache_file" ]]; then
ip_count=$(grep -cv '^\s*#\|^\s*$' "$cache_file" 2>/dev/null || echo "0")
local last_modified
last_modified=$(date -r "$cache_file" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "unbekannt")
echo " [$index] $url"
echo " IPs: $ip_count | Zuletzt aktualisiert: $last_modified"
else
echo " [$index] $url (noch nicht heruntergeladen)"
fi
index=$((index + 1))
done
echo ""
# Aktive externe Sperren
local ext_ban_count=0
for state_file in "${STATE_DIR}"/ext_*.ban; do
[[ -f "$state_file" ]] || continue
ext_ban_count=$((ext_ban_count + 1))
done
echo " Aktive Sperren (externe Blocklist): $ext_ban_count"
echo ""
echo " Prüfintervall: ${EXTERNAL_BLOCKLIST_INTERVAL}s"
echo " Auto-Unban: ${EXTERNAL_BLOCKLIST_AUTO_UNBAN}"
if [[ "$EXTERNAL_BLOCKLIST_BAN_DURATION" -gt 0 ]]; then
echo " Sperrdauer: ${EXTERNAL_BLOCKLIST_BAN_DURATION}s"
else
echo " Sperrdauer: permanent (bis aus Liste entfernt)"
fi
echo ""
echo "═══════════════════════════════════════════════════════════════"
}
# ─── Einmalig synchronisieren ────────────────────────────────────────────────
run_once() {
init_directories
setup_iptables_chain
if [[ -z "${EXTERNAL_BLOCKLIST_URLS:-}" ]]; then
log "ERROR" "Keine externen Blocklist-URLs konfiguriert (EXTERNAL_BLOCKLIST_URLS)"
exit 1
fi
log "INFO" "Einmalige Blocklist-Synchronisation..."
sync_blocklists
log "INFO" "Synchronisation abgeschlossen"
}
# ─── Hauptschleife ──────────────────────────────────────────────────────────
main_loop() {
init_directories
setup_iptables_chain
if [[ -z "${EXTERNAL_BLOCKLIST_URLS:-}" ]]; then
log "ERROR" "Keine externen Blocklist-URLs konfiguriert (EXTERNAL_BLOCKLIST_URLS)"
exit 1
fi
log "INFO" "═══════════════════════════════════════════════════════════"
log "INFO" "Externer Blocklist-Worker gestartet"
log "INFO" " URLs: ${EXTERNAL_BLOCKLIST_URLS}"
log "INFO" " Prüfintervall: ${EXTERNAL_BLOCKLIST_INTERVAL}s"
log "INFO" " Auto-Unban: ${EXTERNAL_BLOCKLIST_AUTO_UNBAN}"
log "INFO" "═══════════════════════════════════════════════════════════"
while true; do
sync_blocklists
sleep "$EXTERNAL_BLOCKLIST_INTERVAL"
done
}
# ─── Signal-Handler ──────────────────────────────────────────────────────────
trap cleanup SIGTERM SIGINT SIGHUP
# ─── Kommandozeilen-Argumente ────────────────────────────────────────────────
case "${1:-start}" in
start)
if ! check_already_running; then
exit 0
fi
write_pid
main_loop
;;
stop)
if [[ -f "$WORKER_PID_FILE" ]]; then
kill "$(cat "$WORKER_PID_FILE")" 2>/dev/null || true
rm -f "$WORKER_PID_FILE"
echo "Blocklist-Worker gestoppt"
else
echo "Blocklist-Worker läuft nicht"
fi
;;
sync)
run_once
;;
status)
init_directories
show_status
;;
flush)
init_directories
echo "Entferne alle externen Blocklist-Sperren..."
for state_file in "${STATE_DIR}"/ext_*.ban; do
[[ -f "$state_file" ]] || continue
_ip=$(grep '^CLIENT_IP=' "$state_file" | cut -d= -f2)
unban_ip "$_ip" "manual-flush"
done
echo "Alle externen Blocklist-Sperren aufgehoben"
;;
*)
cat << USAGE
AdGuard Shield - Externer Blocklist-Worker
Nutzung: $0 {start|stop|sync|status|flush}
Befehle:
start Startet den Worker (Dauerbetrieb)
stop Stoppt den Worker
sync Einmalige Synchronisation
status Zeigt Status und konfigurierte Listen
flush Entfernt alle externen Blocklist-Sperren
Konfiguration: $CONFIG_FILE
USAGE
exit 0
;;
esac

300
install.sh Normal file
View File

@@ -0,0 +1,300 @@
#!/bin/bash
###############################################################################
# AdGuard Shield - Installer
# Autor: Patrick Asmus
# E-Mail: support@techniverse.net
# Lizenz: MIT
###############################################################################
VERSION="1.0.0"
set -euo pipefail
INSTALL_DIR="/opt/adguard-ratelimit"
SERVICE_FILE="/etc/systemd/system/adguard-ratelimit.service"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Farben
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
print_header() {
echo ""
echo -e "${BLUE}"
echo " ▄▄▄ ▓█████▄ ▄████ █ ██ ▄▄▄ ██▀███ ▓█████▄ ██████ ██░ ██ ██▓▓█████ ██▓ ▓█████▄ "
echo "▒████▄ ▒██▀ ██▌ ██▒ ▀█▒ ██ ▓██▒▒████▄ ▓██ ▒ ██▒▒██▀ ██▌ ▒██ ▒ ▓██░ ██▒▓██▒▓█ ▀ ▓██▒ ▒██▀ ██▌"
echo "▒██ ▀█▄ ░██ █▌▒██░▄▄▄░▓██ ▒██░▒██ ▀█▄ ▓██ ░▄█ ▒░██ █▌ ░ ▓██▄ ▒██▀▀██░▒██▒▒███ ▒██░ ░██ █▌"
echo "░██▄▄▄▄██ ░▓█▄ ▌░▓█ ██▓▓▓█ ░██░░██▄▄▄▄██ ▒██▀▀█▄ ░▓█▄ ▌ ▒ ██▒░▓█ ░██ ░██░▒▓█ ▄ ▒██░ ░▓█▄ ▌"
echo " ▓█ ▓██▒░▒████▓ ░▒▓███▀▒▒▒█████▓ ▓█ ▓██▒░██▓ ▒██▒░▒████▓ ▒██████▒▒░▓█▒░██▓░██░░▒████▒░██████▒░▒████▓ "
echo " ▒▒ ▓▒█░ ▒▒▓ ▒ ░▒ ▒ ░▒▓▒ ▒ ▒ ▒▒ ▓▒█░░ ▒▓ ░▒▓░ ▒▒▓ ▒ ▒ ▒▓▒ ▒ ░ ▒ ░░▒░▒░▓ ░░ ▒░ ░░ ▒░▓ ░ ▒▒▓ ▒ "
echo " ▒ ▒▒ ░ ░ ▒ ▒ ░ ░ ░░▒░ ░ ░ ▒ ▒▒ ░ ░▒ ░ ▒░ ░ ▒ ▒ ░ ░▒ ░ ░ ▒ ░▒░ ░ ▒ ░ ░ ░ ░░ ░ ▒ ░ ░ ▒ ▒ "
echo " ░ ▒ ░ ░ ░ ░ ░ ░ ░░░ ░ ░ ░ ▒ ░░ ░ ░ ░ ░ ░ ░ ░ ░ ░░ ░ ▒ ░ ░ ░ ░ ░ ░ ░ "
echo " ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ "
echo " ░ ░ ░ "
echo -e "${NC}"
echo -e "${GREEN} Version: ${VERSION}${NC}"
echo -e "${BLUE} Autor: Patrick Asmus${NC}"
echo -e "${BLUE} E-Mail: support@techniverse.net${NC}"
echo ""
echo -e "${BLUE}═══════════════════════════════════════════════════════════════════════════════════════════════════════════════${NC}"
echo ""
}
check_root() {
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}Dieses Script muss als root ausgeführt werden!${NC}" >&2
echo "Bitte mit 'sudo $0' ausführen."
exit 1
fi
}
check_dependencies() {
echo -e "${YELLOW}Prüfe Abhängigkeiten...${NC}"
local missing=()
for cmd in curl jq iptables ip6tables; do
if command -v "$cmd" &>/dev/null; then
echo -e "$cmd"
else
echo -e "$cmd"
missing+=("$cmd")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
echo ""
echo -e "${YELLOW}Installiere fehlende Pakete...${NC}"
if command -v apt &>/dev/null; then
apt update -qq
apt install -y -qq curl jq iptables
elif command -v dnf &>/dev/null; then
dnf install -y curl jq iptables
elif command -v yum &>/dev/null; then
yum install -y curl jq iptables
elif command -v pacman &>/dev/null; then
pacman -S --noconfirm curl jq iptables
else
echo -e "${RED}Konnte Paketmanager nicht erkennen. Bitte installiere manuell: ${missing[*]}${NC}"
exit 1
fi
fi
echo ""
}
install_files() {
echo -e "${YELLOW}Installiere Dateien nach $INSTALL_DIR ...${NC}"
mkdir -p "$INSTALL_DIR"
mkdir -p /var/lib/adguard-ratelimit
mkdir -p /var/log
# Dateien kopieren
cp "$SCRIPT_DIR/adguard-ratelimit.sh" "$INSTALL_DIR/"
cp "$SCRIPT_DIR/iptables-helper.sh" "$INSTALL_DIR/"
cp "$SCRIPT_DIR/unban-expired.sh" "$INSTALL_DIR/"
cp "$SCRIPT_DIR/external-blocklist-worker.sh" "$INSTALL_DIR/"
# Konfigurationsdatei nur kopieren wenn nicht vorhanden (Update-Sicher)
if [[ ! -f "$INSTALL_DIR/adguard-ratelimit.conf" ]]; then
cp "$SCRIPT_DIR/adguard-ratelimit.conf" "$INSTALL_DIR/"
echo -e " ✅ Konfiguration kopiert (NEU)"
else
cp "$SCRIPT_DIR/adguard-ratelimit.conf" "$INSTALL_DIR/adguard-ratelimit.conf.new"
echo -e " Konfiguration existiert bereits - neue Version als .conf.new gespeichert"
fi
# Ausführbar machen
chmod +x "$INSTALL_DIR/adguard-ratelimit.sh"
chmod +x "$INSTALL_DIR/iptables-helper.sh"
chmod +x "$INSTALL_DIR/unban-expired.sh"
chmod +x "$INSTALL_DIR/external-blocklist-worker.sh"
chmod 600 "$INSTALL_DIR/adguard-ratelimit.conf"
echo -e " ✅ Dateien installiert"
echo ""
}
install_service() {
echo -e "${YELLOW}Installiere systemd Service...${NC}"
cp "$SCRIPT_DIR/adguard-ratelimit.service" "$SERVICE_FILE"
systemctl daemon-reload
systemctl enable adguard-ratelimit.service
echo -e " ✅ Service installiert und aktiviert"
echo ""
}
configure() {
echo -e "${YELLOW}Konfiguration:${NC}"
echo ""
local conf="$INSTALL_DIR/adguard-ratelimit.conf"
# AdGuard URL
read -rp " AdGuard Home URL [http://127.0.0.1:3000]: " adguard_url
adguard_url="${adguard_url:-http://127.0.0.1:3000}"
sed -i "s|^ADGUARD_URL=.*|ADGUARD_URL=\"$adguard_url\"|" "$conf"
# Benutzername
read -rp " AdGuard Home Benutzername [admin]: " adguard_user
adguard_user="${adguard_user:-admin}"
sed -i "s|^ADGUARD_USER=.*|ADGUARD_USER=\"$adguard_user\"|" "$conf"
# Passwort
read -rsp " AdGuard Home Passwort: " adguard_pass
echo ""
if [[ -n "$adguard_pass" ]]; then
# Einfache Quotes damit $-Zeichen im Passwort nicht expandiert werden
sed -i "s|^ADGUARD_PASS=.*|ADGUARD_PASS='$adguard_pass'|" "$conf"
fi
# Rate Limit
read -rp " Max. Anfragen pro Domain/Client pro Minute [30]: " rate_limit
rate_limit="${rate_limit:-30}"
sed -i "s|^RATE_LIMIT_MAX_REQUESTS=.*|RATE_LIMIT_MAX_REQUESTS=$rate_limit|" "$conf"
# Sperrdauer
read -rp " Sperrdauer in Sekunden [3600]: " ban_duration
ban_duration="${ban_duration:-3600}"
sed -i "s|^BAN_DURATION=.*|BAN_DURATION=$ban_duration|" "$conf"
# Whitelist
read -rp " Whitelist IPs (kommagetrennt) [127.0.0.1,::1]: " whitelist
whitelist="${whitelist:-127.0.0.1,::1}"
sed -i "s|^WHITELIST=.*|WHITELIST=\"$whitelist\"|" "$conf"
echo ""
echo -e " ✅ Konfiguration gespeichert"
echo ""
}
test_connection() {
echo -e "${YELLOW}Teste Verbindung zur AdGuard Home API...${NC}"
source "$INSTALL_DIR/adguard-ratelimit.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)
if [[ "$response" == "200" ]]; then
echo -e " ✅ Verbindung erfolgreich! (HTTP $response)"
else
echo -e " ❌ Verbindung fehlgeschlagen (HTTP $response)"
echo -e " ${YELLOW}Bitte prüfe URL und Zugangsdaten in: $INSTALL_DIR/adguard-ratelimit.conf${NC}"
fi
echo ""
}
print_summary() {
echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${GREEN} AdGuard Shield - Installation abgeschlossen!${NC}"
echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}"
echo ""
echo " Installationspfad: $INSTALL_DIR"
echo " Konfiguration: $INSTALL_DIR/adguard-ratelimit.conf"
echo " Service: adguard-ratelimit.service"
echo " Log-Datei: /var/log/adguard-ratelimit.log"
echo ""
echo " Nächste Schritte:"
echo " ─────────────────"
echo " 1. Konfiguration prüfen:"
echo " sudo nano $INSTALL_DIR/adguard-ratelimit.conf"
echo ""
echo " 2. Erst im Dry-Run testen:"
echo " sudo $INSTALL_DIR/adguard-ratelimit.sh dry-run"
echo ""
echo " 3. Service starten:"
echo " sudo systemctl start adguard-ratelimit"
echo ""
echo " 4. Status prüfen:"
echo " sudo systemctl status adguard-ratelimit"
echo " sudo $INSTALL_DIR/adguard-ratelimit.sh status"
echo ""
echo " 5. Logs verfolgen:"
echo " sudo journalctl -u adguard-ratelimit -f"
echo " sudo tail -f /var/log/adguard-ratelimit.log"
echo ""
echo " Weitere Befehle:"
echo " sudo $INSTALL_DIR/iptables-helper.sh status"
echo " sudo $INSTALL_DIR/adguard-ratelimit.sh flush"
echo " sudo $INSTALL_DIR/adguard-ratelimit.sh unban <IP>"
echo ""
}
# ─── Deinstallation ─────────────────────────────────────────────────────────
uninstall() {
echo -e "${YELLOW}Deinstalliere AdGuard Shield...${NC}"
echo ""
# Service stoppen und deaktivieren
if systemctl is-active adguard-ratelimit &>/dev/null; then
systemctl stop adguard-ratelimit
echo " ✅ Service gestoppt"
fi
if systemctl is-enabled adguard-ratelimit &>/dev/null; then
systemctl disable adguard-ratelimit
echo " ✅ Service deaktiviert"
fi
rm -f "$SERVICE_FILE"
systemctl daemon-reload
echo " ✅ Service-Datei entfernt"
# iptables Chain aufräumen
if [[ -f "$INSTALL_DIR/iptables-helper.sh" ]]; then
bash "$INSTALL_DIR/iptables-helper.sh" remove || true
fi
# Dateien entfernen
read -rp " Konfiguration und Logs behalten? [j/N]: " keep
if [[ "${keep,,}" == "j" ]]; then
rm -f "$INSTALL_DIR/adguard-ratelimit.sh"
rm -f "$INSTALL_DIR/iptables-helper.sh"
echo " ✅ Scripts entfernt (Konfiguration behalten)"
else
rm -rf "$INSTALL_DIR"
rm -rf /var/lib/adguard-ratelimit
rm -f /var/log/adguard-ratelimit.log*
echo " ✅ Alles entfernt"
fi
echo ""
echo -e "${GREEN}Deinstallation abgeschlossen.${NC}"
}
# ─── Hauptprogramm ──────────────────────────────────────────────────────────
case "${1:-install}" in
install)
print_header
check_root
check_dependencies
install_files
configure
install_service
test_connection
print_summary
;;
uninstall)
print_header
check_root
uninstall
;;
update)
print_header
check_root
install_files
systemctl daemon-reload
echo -e "${GREEN}AdGuard Shield Update abgeschlossen. Service neustarten mit: sudo systemctl restart adguard-ratelimit${NC}"
;;
*)
echo "Nutzung: $0 {install|uninstall|update}"
exit 1
;;
esac

234
iptables-helper.sh Normal file
View File

@@ -0,0 +1,234 @@
#!/bin/bash
###############################################################################
# AdGuard Shield - iptables Helper
# Verwaltet die Firewall-Regeln für AdGuard Shield
# Kann auch standalone genutzt werden zur Verwaltung der Sperren
###############################################################################
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/adguard-ratelimit.conf"
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "FEHLER: Konfigurationsdatei nicht gefunden: $CONFIG_FILE" >&2
exit 1
fi
source "$CONFIG_FILE"
# ─── Chain erstellen ─────────────────────────────────────────────────────────
create_chain() {
echo "Erstelle iptables Chain: $IPTABLES_CHAIN"
# IPv4
if ! iptables -n -L "$IPTABLES_CHAIN" &>/dev/null; then
iptables -N "$IPTABLES_CHAIN"
for port in $BLOCKED_PORTS; do
iptables -I INPUT -p tcp --dport "$port" -j "$IPTABLES_CHAIN"
iptables -I INPUT -p udp --dport "$port" -j "$IPTABLES_CHAIN"
done
echo " ✅ IPv4 Chain erstellt"
else
echo " IPv4 Chain existiert bereits"
fi
# IPv6
if ! ip6tables -n -L "$IPTABLES_CHAIN" &>/dev/null; then
ip6tables -N "$IPTABLES_CHAIN"
for port in $BLOCKED_PORTS; do
ip6tables -I INPUT -p tcp --dport "$port" -j "$IPTABLES_CHAIN"
ip6tables -I INPUT -p udp --dport "$port" -j "$IPTABLES_CHAIN"
done
echo " ✅ IPv6 Chain erstellt"
else
echo " IPv6 Chain existiert bereits"
fi
}
# ─── Chain entfernen ─────────────────────────────────────────────────────────
remove_chain() {
echo "Entferne iptables Chain: $IPTABLES_CHAIN"
# IPv4 - Referenzen entfernen, dann Chain löschen
if iptables -n -L "$IPTABLES_CHAIN" &>/dev/null; then
for port in $BLOCKED_PORTS; do
iptables -D INPUT -p tcp --dport "$port" -j "$IPTABLES_CHAIN" 2>/dev/null || true
iptables -D INPUT -p udp --dport "$port" -j "$IPTABLES_CHAIN" 2>/dev/null || true
done
iptables -F "$IPTABLES_CHAIN" 2>/dev/null || true
iptables -X "$IPTABLES_CHAIN" 2>/dev/null || true
echo " ✅ IPv4 Chain entfernt"
else
echo " IPv4 Chain existiert nicht"
fi
# IPv6
if ip6tables -n -L "$IPTABLES_CHAIN" &>/dev/null; then
for port in $BLOCKED_PORTS; do
ip6tables -D INPUT -p tcp --dport "$port" -j "$IPTABLES_CHAIN" 2>/dev/null || true
ip6tables -D INPUT -p udp --dport "$port" -j "$IPTABLES_CHAIN" 2>/dev/null || true
done
ip6tables -F "$IPTABLES_CHAIN" 2>/dev/null || true
ip6tables -X "$IPTABLES_CHAIN" 2>/dev/null || true
echo " ✅ IPv6 Chain entfernt"
else
echo " IPv6 Chain existiert nicht"
fi
}
# ─── Chain leeren ────────────────────────────────────────────────────────────
flush_chain() {
echo "Leere iptables Chain: $IPTABLES_CHAIN"
iptables -F "$IPTABLES_CHAIN" 2>/dev/null && echo " ✅ IPv4 geleert" || echo " ⚠️ IPv4 Chain nicht gefunden"
ip6tables -F "$IPTABLES_CHAIN" 2>/dev/null && echo " ✅ IPv6 geleert" || echo " ⚠️ IPv6 Chain nicht gefunden"
# State-Dateien auch aufräumen
rm -f "${STATE_DIR}"/*.ban 2>/dev/null || true
echo " ✅ State-Dateien bereinigt"
}
# ─── IP manuell sperren ─────────────────────────────────────────────────────
ban_ip() {
local ip="$1"
echo "Sperre IP: $ip"
if [[ "$ip" == *:* ]]; then
ip6tables -I "$IPTABLES_CHAIN" -s "$ip" -j DROP
echo " ✅ IPv6 Adresse gesperrt"
else
iptables -I "$IPTABLES_CHAIN" -s "$ip" -j DROP
echo " ✅ IPv4 Adresse gesperrt"
fi
}
# ─── IP entsperren ──────────────────────────────────────────────────────────
unban_ip() {
local ip="$1"
echo "Entsperre IP: $ip"
if [[ "$ip" == *:* ]]; then
ip6tables -D "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null \
&& echo " ✅ IPv6 Adresse entsperrt" \
|| echo " ⚠️ IPv6 Regel nicht gefunden"
else
iptables -D "$IPTABLES_CHAIN" -s "$ip" -j DROP 2>/dev/null \
&& echo " ✅ IPv4 Adresse entsperrt" \
|| echo " ⚠️ IPv4 Regel nicht gefunden"
fi
# State-Datei entfernen
rm -f "${STATE_DIR}/${ip//[:\/]/_}.ban" 2>/dev/null || true
}
# ─── Status anzeigen ─────────────────────────────────────────────────────────
show_rules() {
echo ""
echo "══════════════════════════════════════════════════════════════════"
echo " iptables Regeln für Chain: $IPTABLES_CHAIN"
echo "══════════════════════════════════════════════════════════════════"
echo ""
echo " --- IPv4 ---"
if iptables -n -L "$IPTABLES_CHAIN" --line-numbers &>/dev/null; then
iptables -n -L "$IPTABLES_CHAIN" --line-numbers -v 2>/dev/null | sed 's/^/ /'
else
echo " Chain existiert nicht"
fi
echo ""
echo " --- IPv6 ---"
if ip6tables -n -L "$IPTABLES_CHAIN" --line-numbers &>/dev/null; then
ip6tables -n -L "$IPTABLES_CHAIN" --line-numbers -v 2>/dev/null | sed 's/^/ /'
else
echo " Chain existiert nicht"
fi
echo ""
echo " --- Aktive Sperren (State) ---"
local count=0
if [[ -d "$STATE_DIR" ]]; then
for f in "${STATE_DIR}"/*.ban; do
[[ -f "$f" ]] || continue
count=$((count + 1))
local ip domain ban_time ban_until
ip=$(grep '^CLIENT_IP=' "$f" | cut -d= -f2)
domain=$(grep '^DOMAIN=' "$f" | cut -d= -f2)
ban_time=$(grep '^BAN_TIME=' "$f" | cut -d= -f2)
ban_until=$(grep '^BAN_UNTIL=' "$f" | cut -d= -f2)
printf " %-20s %-30s seit %-20s bis %s\n" "$ip" "$domain" "$ban_time" "$ban_until"
done
fi
if [[ $count -eq 0 ]]; then
echo " Keine aktiven Sperren"
fi
echo ""
}
# ─── Persistenz (iptables-save/restore kompatibel) ──────────────────────────
save_rules() {
local save_file="${STATE_DIR}/iptables-rules.v4"
local save_file6="${STATE_DIR}/iptables-rules.v6"
iptables-save > "$save_file" 2>/dev/null && echo " ✅ IPv4 Regeln gespeichert: $save_file"
ip6tables-save > "$save_file6" 2>/dev/null && echo " ✅ IPv6 Regeln gespeichert: $save_file6"
}
restore_rules() {
local save_file="${STATE_DIR}/iptables-rules.v4"
local save_file6="${STATE_DIR}/iptables-rules.v6"
[[ -f "$save_file" ]] && iptables-restore < "$save_file" && echo " ✅ IPv4 Regeln wiederhergestellt"
[[ -f "$save_file6" ]] && ip6tables-restore < "$save_file6" && echo " ✅ IPv6 Regeln wiederhergestellt"
}
# ─── Hauptprogramm ──────────────────────────────────────────────────────────
case "${1:-help}" in
create)
create_chain
;;
remove)
remove_chain
;;
flush)
flush_chain
;;
ban)
[[ -z "${2:-}" ]] && { echo "Nutzung: $0 ban <IP>" >&2; exit 1; }
ban_ip "$2"
;;
unban)
[[ -z "${2:-}" ]] && { echo "Nutzung: $0 unban <IP>" >&2; exit 1; }
unban_ip "$2"
;;
status|show)
show_rules
;;
save)
save_rules
;;
restore)
restore_rules
;;
*)
cat << USAGE
iptables Helper für AdGuard Rate-Limit
Nutzung: $0 {create|remove|flush|ban|unban|status|save|restore}
Befehle:
create Erstellt die iptables Chain
remove Entfernt die Chain und alle Regeln
flush Leert alle Regeln in der Chain
ban <IP> Sperrt eine IP-Adresse manuell
unban <IP> Entsperrt eine IP-Adresse
status Zeigt alle aktuellen Regeln
save Speichert die aktuellen Regeln
restore Stellt gespeicherte Regeln wieder her
Chain-Name: $IPTABLES_CHAIN
Gesperrte Ports: $BLOCKED_PORTS
USAGE
;;
esac

75
unban-expired.sh Normal file
View File

@@ -0,0 +1,75 @@
#!/bin/bash
###############################################################################
# AdGuard Shield - Cron-basierter Unban-Timer
# Kann als Alternative zum Haupt-Script für das Entsperren genutzt werden.
# Wird z.B. alle 5 Minuten per Cron aufgerufen um abgelaufene Sperren zu prüfen.
#
# Crontab-Eintrag:
# */5 * * * * /opt/adguard-ratelimit/unban-expired.sh
###############################################################################
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/adguard-ratelimit.conf"
if [[ ! -f "$CONFIG_FILE" ]]; then
exit 1
fi
source "$CONFIG_FILE"
BAN_HISTORY_FILE="${BAN_HISTORY_FILE:-/var/log/adguard-ratelimit-bans.log}"
LOG_PREFIX="[$(date '+%Y-%m-%d %H:%M:%S')] [UNBAN-TIMER]"
NOW=$(date '+%s')
# History-Eintrag schreiben
log_ban_history() {
local action="$1"
local client_ip="$2"
local domain="${3:-}"
local count="${4:-}"
local reason="${5:-}"
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
if [[ ! -f "$BAN_HISTORY_FILE" ]]; then
echo "# AdGuard Shield - Ban History" > "$BAN_HISTORY_FILE"
echo "# Format: ZEITSTEMPEL | AKTION | CLIENT-IP | DOMAIN | ANFRAGEN | SPERRDAUER | GRUND" >> "$BAN_HISTORY_FILE"
echo "#─────────────────────────────────────────────────────────────────────────────────" >> "$BAN_HISTORY_FILE"
fi
printf "%-19s | %-6s | %-39s | %-30s | %-8s | %-10s | %s\n" \
"$timestamp" "$action" "$client_ip" "${domain:--}" "${count:--}" "-" "${reason:-expired}" \
>> "$BAN_HISTORY_FILE"
}
unban_count=0
for state_file in "${STATE_DIR}"/*.ban; do
[[ -f "$state_file" ]] || continue
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)
if [[ -n "$ban_until_epoch" && "$NOW" -ge "$ban_until_epoch" ]]; then
echo "$LOG_PREFIX Entsperre abgelaufene Sperre: $client_ip" >> "$LOG_FILE"
# iptables Regel entfernen
if [[ "$client_ip" == *:* ]]; then
ip6tables -D "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true
else
iptables -D "$IPTABLES_CHAIN" -s "$client_ip" -j DROP 2>/dev/null || true
fi
# Ban-History Eintrag
log_ban_history "UNBAN" "$client_ip" "$domain" "-" "expired-cron"
rm -f "$state_file"
unban_count=$((unban_count + 1))
fi
done
if [[ $unban_count -gt 0 ]]; then
echo "$LOG_PREFIX $unban_count Sperren aufgehoben" >> "$LOG_FILE"
fi