commit de65890c4f3376c1c7e00986f429c08739ce0f5e Author: ARIA Date: Tue Jul 21 10:50:52 2026 +0200 Geoblocking: Script, systemd Timer, checkmk Plugin, Installer diff --git a/README.md b/README.md new file mode 100644 index 0000000..4eb76a4 --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# ARIA Geoblocking + +Blockt Verbindungen aus bestimmten Laendern auf Kernel-Ebene (iptables + +ipset). Die IP-Bereiche je Land kommen kostenlos (kein API-Key) von +[ipdeny.com](https://www.ipdeny.com/ipblocks/). + +Kein Live-GeoIP-Lookup pro Verbindung (langsam, meist kostenpflichtig) — +stattdessen der Standardansatz von fail2ban/CSF: die komplette IP-Range- +Liste jedes Landes liegt im Kernel (`ipset`), `iptables` matched dagegen +und droppt Pakete bereits auf Netzwerk-Ebene. + +## Dateien + +| Datei | Zweck | +|---|---| +| `aria_geoblock.py` | Hauptscript: laedt Laender-IPs, setzt/entfernt ipset+iptables-Regeln | +| `aria_geoblock.ini` | Konfiguration (Laenderliste, Chain, Logging, ...) | +| `aria-geoblock.service` | systemd-Unit, fuehrt `--apply` aus | +| `aria-geoblock.timer` | systemd-Timer: beim Booten + einmal taeglich | +| `install_aria_geoblock.sh` | Installer: kopiert alles nach `/opt/aria-geoblock`, richtet Timer ein | +| `aria_geoblock_checkmk` | checkmk local-check Plugin (manuell kopieren) | + +## Installation + +Auf dem zu schuetzenden Host (root, `iptables`+`ipset`+`python3` installiert): + +```bash +git clone https://git.hacker-net.de/Aria-Software/geoblocking-python-script.git +cd geoblocking-python-script +sudo ./install_aria_geoblock.sh +``` + +Das: +- installiert `aria_geoblock.py` + `aria_geoblock.ini` nach `/opt/aria-geoblock/` + (bestehende `.ini` wird NICHT ueberschrieben, neue landet als `.example`) +- legt `/var/log/aria_geoblock/` an +- installiert + aktiviert `aria-geoblock.timer` (`systemctl enable --now`) + +Danach `/opt/aria-geoblock/aria_geoblock.ini` anpassen (Laenderliste!) und +einmal manuell testen: + +```bash +sudo systemctl start aria-geoblock.service +journalctl -u aria-geoblock.service -f +``` + +Der Timer laeuft danach automatisch: **~2 Minuten nach jedem Systemstart** +und **einmal taeglich** (mit bis zu 15min Zufallsversatz, damit nicht alle +Hosts gleichzeitig ipdeny.com anfragen). Verpasste Laeufe (Host war aus) +werden dank `Persistent=true` beim naechsten Boot nachgeholt. + +## Manuelle Nutzung (ohne systemd) + +```bash +sudo python3 aria_geoblock.py --config aria_geoblock.ini --apply # aktivieren/aktualisieren +sudo python3 aria_geoblock.py --config aria_geoblock.ini --apply --dry-run # nur simulieren +sudo python3 aria_geoblock.py --config aria_geoblock.ini --status # Zustand pruefen +sudo python3 aria_geoblock.py --config aria_geoblock.ini --remove # rueckgaengig +``` + +`--apply` ist idempotent (kein doppeltes Anlegen von Regeln) und tauscht +das ipset atomar per `swap`, damit waehrend der Aktualisierung kein Loch +im Blocking entsteht. + +**Achtung:** iptables-Regeln sind nicht reboot-persistent. Wer das +zusaetzlich braucht: `iptables-persistent` / `netfilter-persistent save` +nutzen — der systemd-Timer sorgt aber ohnehin bei jedem Boot fuer einen +frischen `--apply`-Lauf, das reicht in der Praxis meist aus. + +## Logging + +In `aria_geoblock.ini` unter `log_file` einen Pfad eintragen (Default: +`/var/log/aria_geoblock/aria_geoblock.log`, wird vom Installer angelegt). +Bei jedem Lauf wird eine Zeile angehaengt, u.a. eine maschinenlesbare +`RESULT status=OK/ERROR ...`-Zeile — genau die wertet das checkmk-Plugin +aus. Leer lassen = keine Datei-Logs (nur stderr/systemd-Journal). + +## checkmk-Monitoring + +`aria_geoblock_checkmk` ist ein klassisches checkmk **local check**-Plugin +(kein extra Agent-Plugin-Verzeichnis noetig). Manuell einrichten: + +1. Im Script-Kopf `LOG_FILE` auf den Pfad aus der `.ini` (`log_file`) anpassen. +2. Ausfuehrbar machen und kopieren: + ```bash + chmod 755 aria_geoblock_checkmk + cp aria_geoblock_checkmk /usr/lib/check_mk_agent/local/ + ``` + (Pfad haengt vom Setup ab — bei OMD-Sites z.B. + `~/local/lib/check_mk_agent/local/`.) +3. Naechster Agent-Abruf zeigt den Service **ARIA_Geoblock**. + +Status-Logik: `ERROR` im letzten Lauf → CRIT. Letzter erfolgreicher Lauf +> 36h alt → WARN, > 72h alt → CRIT (Timer laeuft taeglich, das faengt +einen einzelnen verpassten Lauf ab, ohne sofort zu alarmieren). Sonst OK, +inkl. Perfdata (Alter in Sekunden, Anzahl geblockter IP-Bereiche). + +## Konfiguration (`aria_geoblock.ini`) + +Siehe Kommentare in der Datei selbst — kurz: + +- `countries`: Komma-Liste ISO-3166-1-alpha-2 Codes (z.B. `RU, CN, KP, IR`) +- `chain`: iptables-Chain (`INPUT` = eingehender Traffic zu diesem Host) +- `interface`: optional, nur ein Interface pruefen +- `log`: iptables-LOG-Eintrag vor dem DROP (dmesg/kern.log) an/aus +- `ipset_name`: Name des ipset-Sets +- `log_file`: Pfad zur script-eigenen Log-Datei (fuer checkmk) diff --git a/aria-geoblock.service b/aria-geoblock.service new file mode 100644 index 0000000..47042ac --- /dev/null +++ b/aria-geoblock.service @@ -0,0 +1,12 @@ +[Unit] +Description=ARIA Geoblocking - Laender-IP-Sperrliste aktualisieren (ipset+iptables) +Documentation=https://git.hacker-net.de/Aria-Software/geoblocking-python-script +After=network-online.target +Wants=network-online.target +ConditionPathExists=__INSTALL_DIR__/aria_geoblock.ini + +[Service] +Type=oneshot +User=root +ExecStart=/usr/bin/python3 __INSTALL_DIR__/aria_geoblock.py --config __INSTALL_DIR__/aria_geoblock.ini --apply +# iptables/ipset aendern den Kernel-Netfilter, braucht root (siehe User=root oben) diff --git a/aria-geoblock.timer b/aria-geoblock.timer new file mode 100644 index 0000000..9c79fd8 --- /dev/null +++ b/aria-geoblock.timer @@ -0,0 +1,17 @@ +[Unit] +Description=ARIA Geoblocking - beim Systemstart + einmal taeglich aktualisieren + +[Timer] +# Einmal kurz nach dem Booten (Netzwerk muss stehen -> aria-geoblock.service +# wartet selbst per After=network-online.target) +OnBootSec=2min +# Und danach einmal taeglich +OnCalendar=daily +# kleine Streuung, damit nicht alle Hosts exakt um 00:00 gleichzeitig ipdeny.com hämmern +RandomizedDelaySec=15min +# Verpasste Laeufe (Host war aus) beim naechsten Boot nachholen +Persistent=true +Unit=aria-geoblock.service + +[Install] +WantedBy=timers.target diff --git a/aria_geoblock.py b/aria_geoblock.py new file mode 100755 index 0000000..ceacca3 --- /dev/null +++ b/aria_geoblock.py @@ -0,0 +1,324 @@ +#!/usr/bin/env python3 +""" +aria_geoblock.py — Geoblocking via ipset + iptables + +Liest eine .ini-Datei mit einer Laenderliste, laedt die zugehoerigen +IP-Bereiche (CIDR) kostenlos von ipdeny.com (kein API-Key noetig) und +blockt sie am Linux-Kernel-Paketfilter (iptables + ipset). + +WARUM ipset+iptables statt "Verbindung live pruefen": + - Live pro Connection ein GeoIP-Lookup zu machen ist langsam, braucht + eine (meist kostenpflichtige/registrierungspflichtige) GeoIP-DB und + blockt den Traffic erst NACH dem TCP-Handshake. + - ipset haelt die komplette IP-Range-Liste jedes Landes im Kernel vor; + iptables matched dagegen in O(1)/sehr schnell und droppt Pakete + schon auf Netzwerk-Ebene, bevor sie den Server ueberhaupt erreichen. + - Das ist der Standard-Ansatz den auch fail2ban/CSF-Firewalls nutzen. + +VORAUSSETZUNGEN (auf dem Ziel-Host, NICHT im ARIA-Container): + - root-Rechte (iptables/ipset aendern den Kernel-Netfilter) + - Pakete installiert: iptables, ipset + Debian/Ubuntu: apt install iptables ipset + - Internetzugang zum Laden der Zonefiles von ipdeny.com + +BENUTZUNG: + sudo python3 aria_geoblock.py --config geoblock.ini --apply + -> laedt IP-Ranges, befuellt das ipset, setzt die iptables-Regel + + sudo python3 aria_geoblock.py --config geoblock.ini --apply --dry-run + -> zeigt nur an was gemacht wuerde, aendert nichts + + sudo python3 aria_geoblock.py --config geoblock.ini --remove + -> entfernt die iptables-Regel und loescht das ipset wieder + + sudo python3 aria_geoblock.py --config geoblock.ini --status + -> zeigt aktuellen Stand (Set vorhanden? wie viele Eintraege? Regel aktiv?) + +.ini-Format siehe mitgelieferte aria_geoblock.ini (Beispiel). + +LOGGING: + Jeder Lauf schreibt nach stderr (systemd-Journal). Zusaetzlich kann in + der .ini unter "log_file" ein Pfad angegeben werden — dort wird bei + jedem Lauf eine Zeile angehaengt, inkl. einer maschinenlesbaren + "RESULT ..."-Zeile (status=OK/ERROR, Anzahl Laender/IP-Bereiche). Genau + diese Zeile wertet das mitgelieferte checkmk-Plugin aus. + +AUTOMATISCH AKTUELL HALTEN (Systemstart + taeglich): + Siehe systemd/aria-geoblock.service + aria-geoblock.timer sowie + install_aria_geoblock.sh im gleichen Repo — das richtet Timer fuer + "beim Booten" + "einmal taeglich" automatisch ein. + --apply ist idempotent: es flusht das bestehende Set und befuellt es neu, + legt aber KEINE doppelte iptables-Regel an (wird vorher geprueft). +""" + +import argparse +import configparser +import datetime +import ipaddress +import subprocess +import sys +import time +import urllib.request +import urllib.error + +ZONE_URL_TMPL = "https://www.ipdeny.com/ipblocks/data/countries/{cc}.zone" + +# Wird in main() aus der .ini gesetzt (log_file). None = keine Datei-Logs. +LOG_FILE = None + + +def _timestamp(): + return datetime.datetime.now().astimezone().isoformat(timespec="seconds") + + +def _write_log_file(line): + if not LOG_FILE: + return + try: + with open(LOG_FILE, "a", encoding="utf-8") as f: + f.write(f"{_timestamp()} {line}\n") + except OSError as e: + print(f"[geoblock] WARNUNG: konnte Log-Datei nicht schreiben ({LOG_FILE}): {e}", + file=sys.stderr) + + +def log(msg): + line = f"[geoblock] {msg}" + print(line, file=sys.stderr) + _write_log_file(line) + + +def log_result(status, action, countries=None, entries=None, msg=""): + """Schreibt eine strukturierte, maschinenlesbare Ergebniszeile. + + Wird vom checkmk-Plugin (aria_geoblock_checkmk) ausgewertet, um den + Zustand des letzten Laufs zu beurteilen (OK/WARN/CRIT/UNKNOWN). + """ + parts = [f"RESULT ts={_timestamp()}", f"status={status}", f"action={action}"] + if countries is not None: + parts.append(f"countries={countries}") + if entries is not None: + parts.append(f"entries={entries}") + safe_msg = msg.replace('"', "'").replace("\n", " ") + parts.append(f'msg="{safe_msg}"') + line = "[geoblock] " + " ".join(parts) + print(line, file=sys.stderr) + _write_log_file(line) + + +def run(cmd, dry_run=False, check=True): + """Fuehrt einen Shell-Befehl (als Liste) aus. Bei dry_run nur anzeigen.""" + printable = " ".join(cmd) + if dry_run: + log(f"DRY-RUN: {printable}") + return None + result = subprocess.run(cmd, capture_output=True, text=True) + if check and result.returncode != 0: + log(f"FEHLER bei: {printable}") + log(f"stderr: {result.stderr.strip()}") + raise RuntimeError(f"Befehl fehlgeschlagen: {printable}") + return result + + +def load_config(path): + cfg = configparser.ConfigParser() + read_ok = cfg.read(path) + if not read_ok: + raise FileNotFoundError(f"Konnte .ini nicht lesen: {path}") + if "geoblock" not in cfg: + raise ValueError("Abschnitt [geoblock] fehlt in der .ini-Datei") + + sec = cfg["geoblock"] + countries_raw = sec.get("countries", "") + countries = [c.strip().lower() for c in countries_raw.split(",") if c.strip()] + if not countries: + raise ValueError("Keine Laender in 'countries' angegeben") + + return { + "countries": countries, + "chain": sec.get("chain", "INPUT").strip(), + "interface": sec.get("interface", "").strip(), + "log": sec.getboolean("log", fallback=False), + "log_prefix": sec.get("log_prefix", "GEOBLOCK-DROP:").strip(), + "ipset_name": sec.get("ipset_name", "geoblock").strip(), + "log_file": sec.get("log_file", "").strip(), + } + + +def fetch_country_cidrs(country_code): + """Laedt die CIDR-Liste eines Landes von ipdeny.com. Gibt Liste von Strings zurueck.""" + url = ZONE_URL_TMPL.format(cc=country_code) + log(f"Lade IP-Ranges fuer '{country_code}' von {url}") + try: + with urllib.request.urlopen(url, timeout=20) as resp: + text = resp.read().decode("utf-8", errors="ignore") + except urllib.error.HTTPError as e: + raise RuntimeError( + f"Laendercode '{country_code}' unbekannt oder ipdeny nicht erreichbar " + f"(HTTP {e.code}). Pruefe den ISO-3166-1-alpha-2 Code." + ) + except urllib.error.URLError as e: + raise RuntimeError(f"Konnte {url} nicht laden: {e}") + + cidrs = [] + for line in text.splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + try: + ipaddress.ip_network(line) # Validierung + except ValueError: + log(f" ignoriere ungueltige Zeile: {line}") + continue + cidrs.append(line) + log(f" -> {len(cidrs)} IP-Bereiche fuer '{country_code}'") + return cidrs + + +def ipset_exists(name): + result = subprocess.run(["ipset", "list", "-n"], capture_output=True, text=True) + return name in result.stdout.splitlines() + + +def iptables_rule_exists(chain, ipset_name, interface, dry_run=False): + check_cmd = ["iptables", "-C", chain] + if interface: + check_cmd += ["-i", interface] + check_cmd += ["-m", "set", "--match-set", ipset_name, "src", "-j", "DROP"] + if dry_run: + # Im Dry-Run wissen wir es nicht sicher, nehmen konservativ "existiert nicht" an + return False + result = subprocess.run(check_cmd, capture_output=True, text=True) + return result.returncode == 0 + + +def apply_geoblock(cfg, dry_run=False): + name = cfg["ipset_name"] + + # 1) ipset anlegen falls noetig + if dry_run or not ipset_exists(name): + run(["ipset", "create", name, "hash:net", "-exist"], dry_run=dry_run) + else: + log(f"ipset '{name}' existiert bereits, wird neu befuellt") + + # 2) alle CIDRs aller konfigurierten Laender sammeln + all_cidrs = [] + for cc in cfg["countries"]: + all_cidrs.extend(fetch_country_cidrs(cc)) + + if not all_cidrs and not dry_run: + raise RuntimeError("Keine IP-Bereiche geladen — Abbruch, um kein leeres Set zu aktivieren") + + # 3) Set atomar neu befuellen: temp-Set bauen, dann swap (kein Traffic-Loch) + tmp_name = f"{name}_tmp" + run(["ipset", "create", tmp_name, "hash:net", "-exist"], dry_run=dry_run) + run(["ipset", "flush", tmp_name], dry_run=dry_run) + for cidr in all_cidrs: + run(["ipset", "add", tmp_name, cidr, "-exist"], dry_run=dry_run, check=False) + run(["ipset", "create", name, "hash:net", "-exist"], dry_run=dry_run) + run(["ipset", "swap", tmp_name, name], dry_run=dry_run) + run(["ipset", "destroy", tmp_name], dry_run=dry_run, check=False) + + log(f"ipset '{name}' befuellt mit {len(all_cidrs)} Bereichen " + f"aus Laendern: {', '.join(c.upper() for c in cfg['countries'])}") + + # 4) optionale LOG-Regel + DROP-Regel in iptables, nur wenn noch nicht vorhanden + base_match = ["-m", "set", "--match-set", name, "src"] + iface_opt = ["-i", cfg["interface"]] if cfg["interface"] else [] + + if cfg["log"]: + log_check = ["iptables", "-C", cfg["chain"]] + iface_opt + base_match + [ + "-j", "LOG", "--log-prefix", cfg["log_prefix"] + " " + ] + exists = False if dry_run else subprocess.run(log_check, capture_output=True).returncode == 0 + if not exists: + run(["iptables", "-I", cfg["chain"]] + iface_opt + base_match + + ["-j", "LOG", "--log-prefix", cfg["log_prefix"] + " "], dry_run=dry_run) + else: + log("LOG-Regel existiert bereits, ueberspringe") + + if iptables_rule_exists(cfg["chain"], name, cfg["interface"], dry_run=dry_run): + log("DROP-Regel existiert bereits, ueberspringe (idempotent)") + else: + run(["iptables", "-A", cfg["chain"]] + iface_opt + base_match + ["-j", "DROP"], dry_run=dry_run) + log(f"DROP-Regel in Chain '{cfg['chain']}' aktiv fuer Set '{name}'") + + log("Fertig. Hinweis: Regeln sind NICHT reboot-persistent — " + "fuer Persistenz z.B. 'iptables-persistent' bzw. 'netfilter-persistent save' nutzen, " + "und das ipset per systemd-Unit / rc.local vor dem iptables-Restore neu befuellen.") + + return len(cfg["countries"]), len(all_cidrs) + + +def remove_geoblock(cfg, dry_run=False): + name = cfg["ipset_name"] + iface_opt = ["-i", cfg["interface"]] if cfg["interface"] else [] + base_match = ["-m", "set", "--match-set", name, "src"] + + # DROP-Regel entfernen (mehrfach versuchen falls doppelt vorhanden) + for _ in range(5): + result = run(["iptables", "-D", cfg["chain"]] + iface_opt + base_match + ["-j", "DROP"], + dry_run=dry_run, check=False) + if dry_run or result is None or result.returncode != 0: + break + + if cfg["log"]: + for _ in range(5): + result = run(["iptables", "-D", cfg["chain"]] + iface_opt + base_match + + ["-j", "LOG", "--log-prefix", cfg["log_prefix"] + " "], + dry_run=dry_run, check=False) + if dry_run or result is None or result.returncode != 0: + break + + run(["ipset", "destroy", name], dry_run=dry_run, check=False) + log(f"Geoblock-Regeln entfernt, ipset '{name}' geloescht.") + + +def show_status(cfg): + name = cfg["ipset_name"] + if ipset_exists(name): + result = subprocess.run(["ipset", "list", name, "-t"], capture_output=True, text=True) + print(result.stdout) + else: + print(f"ipset '{name}' existiert nicht (noch nicht aktiv).") + + active = iptables_rule_exists(cfg["chain"], name, cfg["interface"]) + print(f"iptables DROP-Regel in Chain '{cfg['chain']}' aktiv: {active}") + print(f"Konfigurierte Laender: {', '.join(c.upper() for c in cfg['countries'])}") + + +def main(): + global LOG_FILE + + parser = argparse.ArgumentParser(description="Geoblocking per Laenderliste (.ini) via ipset+iptables") + parser.add_argument("--config", required=True, help="Pfad zur .ini-Konfigurationsdatei") + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument("--apply", action="store_true", help="Laender laden und Block-Regeln setzen/aktualisieren") + group.add_argument("--remove", action="store_true", help="Block-Regeln und ipset wieder entfernen") + group.add_argument("--status", action="store_true", help="Aktuellen Zustand anzeigen") + parser.add_argument("--dry-run", action="store_true", help="Nur anzeigen was passieren wuerde, nichts aendern") + args = parser.parse_args() + + cfg = load_config(args.config) + LOG_FILE = cfg["log_file"] or None + + action = "apply" if args.apply else ("remove" if args.remove else "status") + + try: + if args.apply: + n_countries, n_entries = apply_geoblock(cfg, dry_run=args.dry_run) + log_result("OK", action, countries=n_countries, entries=n_entries, + msg=f"Geoblock aktualisiert ({n_countries} Laender, {n_entries} IP-Bereiche)") + elif args.remove: + remove_geoblock(cfg, dry_run=args.dry_run) + log_result("OK", action, msg="Geoblock-Regeln entfernt") + elif args.status: + show_status(cfg) + except Exception as e: + log_result("ERROR", action, msg=str(e)) + log(f"FEHLER: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/aria_geoblock_checkmk b/aria_geoblock_checkmk new file mode 100755 index 0000000..28b7888 --- /dev/null +++ b/aria_geoblock_checkmk @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +""" +checkmk local check: ARIA Geoblocking + +Wertet die Log-Datei von aria_geoblock.py aus und meldet an checkmk, ob +der letzte Lauf (systemd-Timer: beim Boot + taeglich) erfolgreich und +aktuell war. + +INSTALLATION: + 1. LOG_FILE unten auf den Pfad anpassen, der in der aria_geoblock.ini + unter [geoblock] log_file konfiguriert ist (Default passt zum + mitgelieferten install_aria_geoblock.sh). + 2. Datei ausfuehrbar machen und ins checkmk-Agent local-Verzeichnis legen: + cp aria_geoblock_checkmk /usr/lib/check_mk_agent/local/ + chmod 755 /usr/lib/check_mk_agent/local/aria_geoblock_checkmk + (Pfad haengt von Distro/Checkmk-Setup ab - ueblich sind + /usr/lib/check_mk_agent/local/ oder bei OMD-Sites + ~/local/lib/check_mk_agent/local/. Bei Bedarf: 'check_mk_agent' Pfad + des jeweiligen Hosts pruefen.) + 3. Naechster Agent-Abruf zeigt den Service "ARIA_Geoblock" in checkmk. + +AUSGABE-LOGIK: + - Keine Log-Datei / keine RESULT-Zeile gefunden -> UNKNOWN (3) + - Letzter Lauf hatte status=ERROR -> CRIT (2) + - Letzter erfolgreicher Lauf zu alt (> CRIT_AGE) -> CRIT (2) + - Letzter erfolgreicher Lauf etwas alt (> WARN) -> WARN (1) + - Sonst -> OK (0) +""" + +import datetime +import os +import re +import time + +# --- Anpassen: muss zum log_file-Pfad aus der aria_geoblock.ini passen --- +LOG_FILE = "/var/log/aria_geoblock/aria_geoblock.log" + +# Timer laeuft taeglich -> grosszuegig bemessen, damit ein einzelner +# verpasster Lauf (Host kurz aus) nicht sofort CRIT ausloest. +WARN_AGE_SEC = 36 * 3600 # 36h +CRIT_AGE_SEC = 72 * 3600 # 72h + +SERVICE_NAME = "ARIA_Geoblock" + +# Beispiel-Zeile in der Log-Datei (von aria_geoblock.py log_result() geschrieben): +# 2026-07-21T10:00:00+02:00 [geoblock] RESULT ts=2026-07-21T10:00:00+02:00 +# status=OK action=apply countries=4 entries=123456 msg="Geoblock aktualisiert (..)" +RESULT_RE = re.compile( + r'RESULT ts=(?P\S+) status=(?P\S+) action=(?P\S+)' + r'(?: countries=(?P\d+))?(?: entries=(?P\d+))? ' + r'msg="(?P.*)"\s*$' +) + + +def emit(status, perfdata, text): + perf = perfdata if perfdata else "-" + print(f"{status} {SERVICE_NAME} {perf} {text}") + + +def main(): + if not os.path.isfile(LOG_FILE): + emit(3, "-", f"Log-Datei nicht gefunden ({LOG_FILE}) - lief aria_geoblock.py schon einmal mit --apply?") + return + + last_match = None + try: + with open(LOG_FILE, "r", encoding="utf-8", errors="ignore") as f: + for line in f: + m = RESULT_RE.search(line.strip()) + if m: + last_match = m # letzte Übereinstimmung gewinnt (Datei waechst append-only) + except OSError as e: + emit(3, "-", f"Log-Datei nicht lesbar: {e}") + return + + if not last_match: + emit(3, "-", f"Keine RESULT-Zeile in {LOG_FILE} gefunden - lief aria_geoblock.py schon mit --apply?") + return + + ts_raw = last_match.group("ts") + action = last_match.group("action") + status_str = last_match.group("status") + countries = last_match.group("countries") + entries = last_match.group("entries") + msg = last_match.group("msg") + + try: + last_dt = datetime.datetime.fromisoformat(ts_raw) + age = time.time() - last_dt.timestamp() + except ValueError: + emit(3, "-", f"Konnte Zeitstempel nicht parsen: {ts_raw}") + return + + perf = f"age={int(age)}s;{WARN_AGE_SEC};{CRIT_AGE_SEC}" + if entries is not None: + perf += f"|entries={entries}" + + if status_str != "OK": + emit(2, perf, f"Letzter Lauf ({action}) fehlgeschlagen: {msg}") + return + + if age > CRIT_AGE_SEC: + emit(2, perf, f"Letzter erfolgreicher Lauf ist {int(age / 3600)}h alt " + f"(Schwelle {CRIT_AGE_SEC // 3600}h) - Timer laeuft nicht mehr? Letzte msg: {msg}") + return + if age > WARN_AGE_SEC: + emit(1, perf, f"Letzter erfolgreicher Lauf ist {int(age / 3600)}h alt " + f"(Schwelle {WARN_AGE_SEC // 3600}h) - Letzte msg: {msg}") + return + + extra = f", {countries} Laender" if countries else "" + emit(0, perf, f"OK - letzter Lauf vor {int(age / 60)}min{extra}: {msg}") + + +if __name__ == "__main__": + main() diff --git a/install_aria_geoblock.sh b/install_aria_geoblock.sh new file mode 100755 index 0000000..f368d96 --- /dev/null +++ b/install_aria_geoblock.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# ARIA Geoblocking - Installer +# +# Installiert aria_geoblock.py + .ini nach /opt/aria-geoblock (oder ein +# anderes Zielverzeichnis), legt das Log-Verzeichnis an und richtet einen +# systemd-Timer ein, der den Block beim Systemstart UND einmal taeglich +# aktualisiert. +# +# Nutzung (als root, im Verzeichnis mit allen Repo-Dateien): +# sudo ./install_aria_geoblock.sh [ZIELVERZEICHNIS] +# +# Benoetigt im selben Verzeichnis: +# aria_geoblock.py +# aria_geoblock.ini +# aria-geoblock.service +# aria-geoblock.timer +# +# Voraussetzungen auf dem Host: python3, iptables, ipset, systemd. + +set -euo pipefail + +if [[ $EUID -ne 0 ]]; then + echo "Bitte als root ausfuehren (sudo ./install_aria_geoblock.sh)." >&2 + exit 1 +fi + +SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +INSTALL_DIR="${1:-/opt/aria-geoblock}" +LOG_DIR="/var/log/aria_geoblock" + +for f in aria_geoblock.py aria_geoblock.ini aria-geoblock.service aria-geoblock.timer; do + if [[ ! -f "$SRC_DIR/$f" ]]; then + echo "FEHLER: $f fehlt in $SRC_DIR - bitte komplettes Repo verwenden." >&2 + exit 1 + fi +done + +for bin in python3 iptables ipset systemctl; do + if ! command -v "$bin" >/dev/null 2>&1; then + echo "FEHLER: '$bin' ist nicht installiert. Bitte nachinstallieren (z.B. apt install iptables ipset)." >&2 + exit 1 + fi +done + +echo "==> Installiere Script + Config nach $INSTALL_DIR" +mkdir -p "$INSTALL_DIR" +cp "$SRC_DIR/aria_geoblock.py" "$INSTALL_DIR/aria_geoblock.py" +chmod 755 "$INSTALL_DIR/aria_geoblock.py" + +if [[ -f "$INSTALL_DIR/aria_geoblock.ini" ]]; then + echo "==> Bestehende $INSTALL_DIR/aria_geoblock.ini gefunden - wird NICHT ueberschrieben." + cp "$SRC_DIR/aria_geoblock.ini" "$INSTALL_DIR/aria_geoblock.ini.example" + echo " Neue Beispiel-Config liegt unter $INSTALL_DIR/aria_geoblock.ini.example (zum Vergleich)." +else + cp "$SRC_DIR/aria_geoblock.ini" "$INSTALL_DIR/aria_geoblock.ini" + echo " -> bitte $INSTALL_DIR/aria_geoblock.ini pruefen/anpassen (Laenderliste!)." +fi + +echo "==> Lege Log-Verzeichnis an: $LOG_DIR" +mkdir -p "$LOG_DIR" +chmod 750 "$LOG_DIR" + +echo "==> Installiere systemd-Units" +sed "s#__INSTALL_DIR__#$INSTALL_DIR#g" "$SRC_DIR/aria-geoblock.service" \ + > /etc/systemd/system/aria-geoblock.service +cp "$SRC_DIR/aria-geoblock.timer" /etc/systemd/system/aria-geoblock.timer + +systemctl daemon-reload +systemctl enable --now aria-geoblock.timer + +echo "" +echo "==> Fertig." +echo " Config anpassen: $INSTALL_DIR/aria_geoblock.ini" +echo " Jetzt sofort testen: systemctl start aria-geoblock.service && journalctl -u aria-geoblock.service -f" +echo " Timer-Status: systemctl list-timers aria-geoblock.timer" +echo " Naechste/letzte Läufe: systemctl status aria-geoblock.timer" +echo " Log-Datei: $LOG_DIR/aria_geoblock.log" +echo " checkmk-Plugin: siehe aria_geoblock_checkmk im Repo (manuell kopieren, README beachten)"