Länder IP Ban mit Fail2Ban, KeyHelp und Static Files

Für Modifikationen in und um KeyHelp.
R@iner
Posts: 2
Joined: Thu 24. Apr 2025, 13:17

Re: Länder IP Ban mit Fail2Ban, KeyHelp und Static Files

Post by R@iner »

Hallo Gemeinde,

zunächst vielen Dank für das Importskript. Ich hatte eine kleine Idee, wie man dessen Ausführungszeit signifikant reduzieren kann.
Man kann viel Zeit sparen, indem man vor dem Import der einzelnen IPs nachschaut, welche Adressen bereits in der f2b-Datenbank stehen, um so die heruntergeladene Datei auf die bisher noch unbekannten IPs zu beschränken. Das Ergebnis des folgenden Progrämmchens ist eine stark reduzierte Blocklisten-Datei, die, wie oben vorgeschlagen, Zeile für Zeile eingelesen werden kann.
Sicher geht das mit php auch, ich hatte es aber schneller mit Python zusammengekloppt, was die Erstellung einer virtuellen Umgebung erfordert, damit man nicht Python Packages in die Systemumgebung installiert.
Alles weitere steht im Skript.

shrink_blocklist.py:

Code: Select all

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
/root/banip/py/shrink_blocklist.py
----------------------------------
Kürzere Sperrliste für f2b erstellen.
Ref: https://community.keyhelp.de/viewtopic.php?p=54945

Holt die Datei aus der url und vergleicht die ip-Adressen mit denen, die in der f2b-Datenbank gespeichert sind.
Erstellt aus dem Vergleich der Adressen eine Datei, die nur die neu hinzugekommenen IPs beinhaltet.

Bitte vor der Installation der requirements ein virtuelles environment erstellen.
(Pandas gehört normalerweise nicht zur Systemumgebung.)

Getestet mit Keyhelp 15.1 (Python 3.11.*) / Debian 12.11

Vorbereitung:
-------------
Einloggen als root
$ apt install sqlite3 pip python3.11-venv

$ mkdir /root/banip/py
$ cd /root/banip/py

In das Verzeichnis muss diese Datei kopiert werden.

Wichtig: Virtual environment 'mypy' (oder anderer Name) im Verzeichnis erstellen:
------------------------------------------------------
$ python -m venv /root/banip/py/mypy

Wichtig: Virtual environment aktivieren:
----------------------------------------
§ source mypy/bin/activate

Jetzt sollte die Kommandozeile ein dem Prompt vorangestelltes (mypy) aufweisen.

(mypy)$ ls -al

drwxr-xr-x 3 root root 4096  3. Jul 00:01 .
drwxr-xr-x 3 root root 4096  3. Jul 00:01 ..
drwxr-xr-x 5 root root 4096  3. Jul 00:01 mypy
-rw-r--r-- 1 root root 7787  3. Jul 00:01 shrink_blocklist.py

Python Packages installieren:
------------------------------
(mypy)$ pip install pandas
(mypy)$ pip install requests
(mypy)$ pip install sqlite3

Kontrolle:
----------
Z.B.:
(mypy)$ pip list
Package            Version
------------------ -----------
certifi            2025.6.15
charset-normalizer 3.4.2
idna               3.10
numpy              2.3.1
pandas             2.3.0
pip                23.0.1
python-dateutil    2.9.0.post0
pytz               2025.2
requests           2.32.4
setuptools         66.1.1
six                1.17.0
tzdata             2025.2
urllib3            2.5.0

as Environment wird mit
$ deactivate
wieder auf die Basis des Systems zurückgesetzt.

Programm starten:
-----------------
$ python3 shrink_blocklist.py

Wenn alles installiert ist, dann sollte die Ausgabe etwa so aussehen:

Datei holen von https://raw.githubusercontent.com/borestad/blocklist-abuseipdb/refs/heads/main/abuseipdb-s100-3d.ipv4
Download beendet.
Die Dateigröße von blocklist.txt beträgt 7508167 Bytes.
Die Datei beinhaltet 129695 Zeilen.
Suche die erste Zeile in 'blocklist.txt', die mit einer gültigen ip-Adresse beginnt.
Beginn der ip-Adressen in Zeile: 14
Datenbank /var/lib/fail2ban/fail2ban.sqlite3 öffnen.
Die Tabelle 'bips' enthält 236318 Zeilen für den jail 'iplistblock'.
IP-Adressen der Datei laden.
Anzahl: 129681
IP-Adressen der f2b-Datenbank einlesen.
Anzahl: 236318
Neue IP-Adressen ermitteln
Anzahl: 11483
Datei mit neuen IP-Adressen erstellen
Alles OK. Die Dateigröße von shrinked_blocklist.txt beträgt 166190 Bytes.

Nun sollte das aktuelle Verzeichnis zusätzlich die Dateien 'blocklist.txt' und 'shrinked_blocklist.txt' aufweisen.

Das Environment wird mit
$ deactivate
wieder auf die Basis des Systems zurückgesetzt, falls noch andere Arbeiten anstehen, die Python-Skripte verwenden.
"""

__author__ = 'whocares'
__created__ = '03.07.2025'

import os
import shutil
import requests as req
import re
import sqlite3
import pandas as pd

url = 'https://raw.githubusercontent.com/borestad/blocklist-abuseipdb/refs/heads/main/abuseipdb-s100-3d.ipv4'
# größere 30-Tage-Liste alternativ, wie beschrieben unter https://github.com/borestad/blocklist-abuseipdb
# url = 'https://raw.githubusercontent.com/borestad/blocklist-abuseipdb/refs/heads/main/abuseipdb-s100-30d.ipv4
blfname = 'blocklist.txt'             # Heruntergeladene Blocklist
newblfname = 'shrinked_blocklist.txt' # Blocklist mit ausschließlich neuen Adressen, die f2b noch nicht kennt
jail = 'iplistblock'
minlines   = 100                      # Willkürlicher Wert für die Mindestanzahl an Zeilen in der Input-Datei
maxipstart =  25                      # Willkürlicher Wert für die erste Zeile mit einer ip-Adresse in der Input-Blocklist
                                      # Idee: Ist der Header zu lang, wird abgebrochen. Vlt. stehen in der Datei ganz andere Dinge?

db_filename = "/var/lib/fail2ban/fail2ban.sqlite3"

def getnumoflines(fname):
    with open(fname) as f:
        return len(f.readlines())


def errorexit(msg):
    if len(msg) > 0:
        for elem in msg:
            print(elem)

    print('Das Programm wird nach Fehler beendet.')
    exit(1)


def successexit(fname):
    fstat = os.stat(fname)
    print('Alles OK. Die Dateigröße von ' + fname + ' beträgt ' + str(fstat.st_size) + ' Bytes.')
    exit(0)


def main():
    """
    main()
    """
    print('Datei holen von ' + url)
    r = req.get(url, timeout=10)

    if r.ok:
        with open(blfname, mode="wb") as f:
            f.write(r.content)  # überschreibt die Datei, falls vorhanden
    else:
        errorexit(['Fehler beim Download', 'Fehlercode: ' + str(r.status_code), 'Stimmt die url?'])

    print('Download beendet.')

    fstat = os.stat(blfname)
    if fstat.st_size == 0:
        errorexit(['Die Dateigröße ist 0 Bytes', 'Stimmt die url?'])
    else:
        print('Die Dateigröße von ' + blfname + ' beträgt ' + str(fstat.st_size) + ' Bytes.')

    numoflines = getnumoflines(blfname)

    if numoflines < minlines:
        errorexit(['Die heruntegeladene Datei hat ' + str(numoflines) + ' Zeilen.', 'Sie sollte jedoch mehr als ' + str(minlines) + ' beinhalten.'])
    else:
        print('Die Datei beinhaltet ' + str(numoflines) + ' Zeilen.')

    print("Suche die erste Zeile in '" + blfname + "', die mit einer gültigen ip-Adresse beginnt.")

    # Regex: Nur ip-Adresse holen, die zu Beginn einer Zeile steht
    pat = re.compile(r"(?:\b|^)((?:(?:(?:\d)|(?:\d{2})|(?:1\d{2})|(?:2[0-4]\d)|(?:25[0-5]))\.){3}(?:(?:(?:\d)|(?:\d{2})|(?:1\d{2})|(?:2[0-4]\d)|(?:25[0-5]))))(?:\b|$)")

    with open(blfname) as f:
        i = 1
        for x in f:
            checkip4 = pat.findall(x)
            # print(checkip4)
            if len(checkip4) == 0:
                i += 1
                if i > maxipstart:
                    errorexit(['In den ersten ' + str(maxipstart) + ' Zeilen der Datei wurde keine IP-Adresse identifiziert.', \
                               'Header länger als erwartet? Enthält die Datei gültige ip-Adressen, die am Zeilenanfang stehen?'])
            else:
                break

    print('Beginn der ip-Adressen in Zeile: ' + str(i))
    print('Datenbank ' + db_filename + ' öffnen.')

    # ToDo: Fehlerbehandlung
    with sqlite3.connect(db_filename) as conn:
        cur = conn.cursor()

        sql = "SELECT COUNT(*) FROM bips WHERE jail='" + jail + "' or jail IS NULL;"
        cur.execute(sql)
        numofrows = cur.fetchone()[0]

        print("Die Tabelle 'bips' enthält " + str(numofrows) + " Zeilen für den jail '" + jail + "'." )

    if (numofrows == 0):
        print("In der Datenbak sind bisher keine Einträge für den jail '" + jail + "' vorhanden.")
        print('Die heruntergeladene Datei muss komplett eingelesen werden.')
        print('Kopiere ' + blfname + ' nach ' + newblfname)
        shutil.copy2(blfname, newblfname)
    else:
        print('IP-Adressen der Datei laden.')
        df_file = pd.read_csv(blfname, sep='#', skiprows=i, names=['ip', 'comment'])
        df_file['ip'] = df_file['ip'].str.strip() # Whitespace am Anfang und Ende der ip-Adressen für späteren Vergleich entfernen
        # print(df_file)
        print('Anzahl: ' + str(len(df_file.index)))

        print('IP-Adressen der f2b-Datenbank einlesen.')
        with sqlite3.connect(db_filename) as conn:
            df_db = pd.read_sql("SELECT ip FROM bips WHERE jail='" + jail + "'", conn)
        # print(df_db)
        print('Anzahl: ' + str(len(df_db.index)))

        print('Neue IP-Adressen ermitteln')
        df_new = df_file[~df_file['ip'].isin(df_db['ip'])]
        # print(df_new)
        print('Anzahl: ' + str(len(df_new.index)))
        print('Datei mit neuen IP-Adressen erstellen')
        df_new.to_csv(newblfname, columns=['ip'], header=False, index=False)
    
    successexit(newblfname)

if __name__ == "__main__":
    main() 
Das bash-Skript für cron muss entsprechend angepasst werden und sieht bei mir so aus:

cron.sh:

Code: Select all

#! /usr/bin/bash
echo `date "+%Y-%m-%d %H:%M:%S"`
cd /root/banip/py
source mypy/bin/activate
python3 shrink_blocklist.py
exit_status=$?
if [ "${exit_status}" -ne 0 ];
then
    echo "Python-Skript mit Fehler beendet. Shellskript wird abgebrochen(1)"
    echo `date "+%Y-%m-%d %H:%M:%S"`
    exit 1
fi
echo `date "+%Y-%m-%d %H:%M:%S"`
INFILE=/root/banip/py/shrinked_blocklist.txt
IFS=$'\n'
for IP in $(cat "$INFILE")
do
    fail2ban-client set iplistblock banip $IP >/dev/null 2>&1
done
echo `date "+%Y-%m-%d %H:%M:%S"`
echo "IP Blocklist verarbeitet"
rm /root/banip/py/blocklist.txt
rm /root/banip/py/shrinked_blocklist.txt
echo `date "+%Y-%m-%d %H:%M:%S"`
echo "fertig"
Lustig ist, dass man bei über 200000 Einträgen in der f2b-Verwaltung im Keyhelp-Dashboard einfach nur noch einen Timeout erhält.
Vielleicht wäre das eine Anregung, ein funktionierendes Paging zu programmieren?
Eine Alternative an dieser Stelle könnte dort eine nach Jails gruppierte Darstellung sein, sodass man auf den ersten Blick nur die Gesamtanzahl der Einträge per Gruppe erhielte. Denn wer will sich schon durch hundertausende Einträge blättern? Eine Suchfunktion für einzelne IPs wäre dann ausreichend.

Have phun!
User avatar
Fezzi
Posts: 262
Joined: Wed 12. Dec 2018, 04:04

Re: Länder IP Ban mit Fail2Ban, KeyHelp und Static Files

Post by Fezzi »

Echt? :o Du betreibst noch so eine alte Version?
Getestet mit Keyhelp 15.1
TImeouts kann ich bisher nicht erkennen, aber ja, es dauert ein wenig (bis zu einer Minute) dass sich die Fail2Ban Seite zeigt... Die Anregung...
Eine Alternative an dieser Stelle könnte dort eine nach Jails gruppierte Darstellung sein, sodass man auf den ersten Blick nur die Gesamtanzahl der Einträge per Gruppe erhielte. Denn wer will sich schon durch hundertausende Einträge blättern? Eine Suchfunktion für einzelne IPs wäre dann ausreichend.
...finde ich gut... und die Suchfunktion fuer einzelne IPs gibt es ja schon...

Noch zu der anderen IP Set Loesung... die ist ja gut und schoen.. aber in meinem Fall (viele Kunden in TH) ist die F2B Loesung besser, da ich dann schnell mal die ein oder andere TH IP entsperren kann die sich in der Ban Liste befindet... TOT, TTT etc. das sind hier die gaengigen ISPs... aber ja, die CPU Last ist auf jeden Fall hoeher mit F2B
Gruss

Fezzi

Everyone can do something, no one can do everything.
R@iner
Posts: 2
Joined: Thu 24. Apr 2025, 13:17

Re: Länder IP Ban mit Fail2Ban, KeyHelp und Static Files

Post by R@iner »

p.s.: Noch ein bisschen Eigenwerbung. Ich habe die Skripte jetzt seit zwei Tagen laufen. Resümee:

Code: Select all

$ sudo sqlite3 /var/lib/fail2ban/fail2ban.sqlite3 "select count(*) from bips where jail = 'iplistblock';"
liefert 293208 Datensätze als Ergebnis.
Seitdem die große Masse an Adressen von abuseipdb-s100-30d.ipv4 erst einmal eingelesen war, lasse ich den Cronjob stündlich ausführen. Da höchstens 500 neue Adressen hinzukommen, dauert das Einlesen nur ein paar Minuten.
Der Prozessor dümpelt ansonsten vor sich hin und es herrscht weitgehend Ruhe im Karton.

Danke nochmals für den Tip mit den AbuseIPDB blocklists!

Grüsse
R@iner
Post Reply