Geoblocking: Script, systemd Timer, checkmk Plugin, Installer
This commit is contained in:
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -0,0 +1,35 @@
|
||||
; Beispiel-Konfiguration fuer aria_geoblock.py
|
||||
;
|
||||
; countries: Komma-getrennte ISO-3166-1-alpha-2-Laendercodes (klein- oder
|
||||
; grossgeschrieben, egal), die geblockt werden sollen.
|
||||
; Liste der Codes: https://www.ipdeny.com/ipblocks/
|
||||
;
|
||||
; chain: iptables-Chain, in die die DROP-Regel eingehaengt wird.
|
||||
; INPUT = Verbindungen ZU diesem Host (Standard-Anwendungsfall).
|
||||
;
|
||||
; interface: optional, z.B. eth0 -- nur Traffic auf diesem Interface pruefen.
|
||||
; Leer lassen = alle Interfaces.
|
||||
;
|
||||
; log: true/false -- wenn true, wird vor dem DROP ein iptables LOG-
|
||||
; Eintrag geschrieben (dmesg / /var/log/kern.log), praefixed
|
||||
; mit log_prefix. Das ist NICHT dasselbe wie log_file (siehe unten)!
|
||||
;
|
||||
; ipset_name: Name des ipset-Sets, das alle IP-Bereiche der Laender enthaelt.
|
||||
;
|
||||
; log_file: optional Pfad zu einer eigenen Log-Datei des Scripts selbst
|
||||
; (zusaetzlich zu stderr/systemd-Journal). Bei jedem Lauf wird
|
||||
; eine Zeile angehaengt, u.a. eine maschinenlesbare
|
||||
; "RESULT status=OK/ERROR ..."-Zeile. Genau diese wertet das
|
||||
; mitgelieferte checkmk-Plugin (aria_geoblock_checkmk) aus.
|
||||
; Leer lassen = keine Datei-Logs.
|
||||
; Verzeichnis muss existieren und fuer root beschreibbar sein
|
||||
; (das Installer-Script legt es automatisch an).
|
||||
|
||||
[geoblock]
|
||||
countries = RU, CN, KP, IR
|
||||
chain = INPUT
|
||||
interface =
|
||||
log = true
|
||||
log_prefix = GEOBLOCK-DROP:
|
||||
ipset_name = geoblock
|
||||
log_file = /var/log/aria_geoblock/aria_geoblock.log
|
||||
Executable
+324
@@ -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()
|
||||
Executable
+116
@@ -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<ts>\S+) status=(?P<status>\S+) action=(?P<action>\S+)'
|
||||
r'(?: countries=(?P<countries>\d+))?(?: entries=(?P<entries>\d+))? '
|
||||
r'msg="(?P<msg>.*)"\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()
|
||||
Executable
+78
@@ -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)"
|
||||
Reference in New Issue
Block a user