浏览代码

Merge pull request #5127 from th-joerger/feature/bantime-increment

[Netfilter] Implemented exponentially incrementing bantime
Patrick Schult 2 年之前
父节点
当前提交
fd0205aafd

+ 38 - 24
data/Dockerfiles/netfilter/server.py

@@ -64,28 +64,40 @@ def refreshF2boptions():
   global f2boptions
   global quit_now
   global exit_code
+
+  f2boptions = {}
+
   if not r.get('F2B_OPTIONS'):
-    f2boptions = {}
-    f2boptions['ban_time'] = int
-    f2boptions['max_attempts'] = int
-    f2boptions['retry_window'] = int
-    f2boptions['netban_ipv4'] = int
-    f2boptions['netban_ipv6'] = int
-    f2boptions['ban_time'] = r.get('F2B_BAN_TIME') or 1800
-    f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS') or 10
-    f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW') or 600
-    f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4') or 32
-    f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6') or 128
-    r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
+    f2boptions['ban_time'] = r.get('F2B_BAN_TIME')
+    f2boptions['max_ban_time'] = r.get('F2B_MAX_BAN_TIME')
+    f2boptions['ban_time_increment'] = r.get('F2B_BAN_TIME_INCREMENT')
+    f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS')
+    f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW')
+    f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4')
+    f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6')
   else:
     try:
-      f2boptions = {}
       f2boptions = json.loads(r.get('F2B_OPTIONS'))
     except ValueError:
       print('Error loading F2B options: F2B_OPTIONS is not json')
       quit_now = True
       exit_code = 2
 
+  verifyF2boptions(f2boptions)
+  r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
+
+def verifyF2boptions(f2boptions):
+  verifyF2boption(f2boptions,'ban_time', 1800)
+  verifyF2boption(f2boptions,'max_ban_time', 10000)
+  verifyF2boption(f2boptions,'ban_time_increment', True)
+  verifyF2boption(f2boptions,'max_attempts', 10)
+  verifyF2boption(f2boptions,'retry_window', 600)
+  verifyF2boption(f2boptions,'netban_ipv4', 32)
+  verifyF2boption(f2boptions,'netban_ipv6', 128)
+
+def verifyF2boption(f2boptions, f2boption, f2bdefault):
+  f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault
+
 def refreshF2bregex():
   global f2bregex
   global quit_now
@@ -147,6 +159,7 @@ def ban(address):
   global lock
   refreshF2boptions()
   BAN_TIME = int(f2boptions['ban_time'])
+  BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
   MAX_ATTEMPTS = int(f2boptions['max_attempts'])
   RETRY_WINDOW = int(f2boptions['retry_window'])
   NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
@@ -174,20 +187,16 @@ def ban(address):
   net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
   net = str(net)
 
-  if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
-    bans[net] = { 'attempts': 0 }
-    active_window = RETRY_WINDOW
-  else:
-    active_window = time.time() - bans[net]['last_attempt']
+  if not net in bans:
+    bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0}
 
   bans[net]['attempts'] += 1
   bans[net]['last_attempt'] = time.time()
 
-  active_window = time.time() - bans[net]['last_attempt']
-
   if bans[net]['attempts'] >= MAX_ATTEMPTS:
     cur_time = int(round(time.time()))
-    logCrit('Banning %s for %d minutes' % (net, BAN_TIME / 60))
+    NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter']
+    logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
     if type(ip) is ipaddress.IPv4Address:
       with lock:
         chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
@@ -206,7 +215,7 @@ def ban(address):
         rule.target = target
         if rule not in chain.rules:
           chain.insert_rule(rule)
-    r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + BAN_TIME)
+    r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME)
   else:
     logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
 
@@ -238,7 +247,8 @@ def unban(net):
   r.hdel('F2B_ACTIVE_BANS', '%s' % net)
   r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
   if net in bans:
-    del bans[net]
+    bans[net]['attempts'] = 0
+    bans[net]['ban_counter'] += 1
 
 def permBan(net, unban=False):
   global lock
@@ -427,6 +437,8 @@ def autopurge():
     time.sleep(10)
     refreshF2boptions()
     BAN_TIME = int(f2boptions['ban_time'])
+    MAX_BAN_TIME = int(f2boptions['max_ban_time'])
+    BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
     MAX_ATTEMPTS = int(f2boptions['max_attempts'])
     QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
     if QUEUE_UNBAN:
@@ -434,7 +446,9 @@ def autopurge():
         unban(str(net))
     for net in bans.copy():
       if bans[net]['attempts'] >= MAX_ATTEMPTS:
-        if time.time() - bans[net]['last_attempt'] > BAN_TIME:
+        NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter']
+        TIME_SINCE_LAST_ATTEMPT = time.time() - bans[net]['last_attempt']
+        if TIME_SINCE_LAST_ATTEMPT > NET_BAN_TIME or TIME_SINCE_LAST_ATTEMPT > MAX_BAN_TIME:
           unban(net)
 
 def isIpNetwork(address):

+ 11 - 1
data/web/api/openapi.yaml

@@ -3176,8 +3176,10 @@ paths:
               example:
                 attr:
                   ban_time: "86400"
+                  ban_time_increment: "1"
                   blacklist: "10.100.6.5/32,10.100.8.4/32"
                   max_attempts: "5"
+                  max_ban_time: "86400"
                   netban_ipv4: "24"
                   netban_ipv6: "64"
                   retry_window: "600"
@@ -3191,11 +3193,17 @@ paths:
                       description: the backlisted ips or hostnames separated by comma
                       type: string
                     ban_time:
-                      description: the time a ip should be banned
+                      description: the time an ip should be banned
                       type: number
+                    ban_time_increment:
+                      description: if the time of the ban should increase each time
+                      type: boolean
                     max_attempts:
                       description: the maximum numbe of wrong logins before a ip is banned
                       type: number
+                    max_ban_time:
+                      description: the maximum time an ip should be banned
+                      type: number
                     netban_ipv4:
                       description: the networks mask to ban for ipv4
                       type: number
@@ -4113,10 +4121,12 @@ paths:
                 response:
                   value:
                     ban_time: 604800
+                    ban_time_increment: 1
                     blacklist: |-
                       45.82.153.37/32
                       92.118.38.52/32
                     max_attempts: 1
+                    max_ban_time: 604800
                     netban_ipv4: 32
                     netban_ipv6: 128
                     perm_bans:

+ 4 - 0
data/web/inc/functions.fail2ban.inc.php

@@ -239,7 +239,9 @@ function fail2ban($_action, $_data = null) {
       $is_now = fail2ban('get');
       if (!empty($is_now)) {
         $ban_time = intval((isset($_data['ban_time'])) ? $_data['ban_time'] : $is_now['ban_time']);
+        $ban_time_increment = (isset($_data['ban_time_increment']) && $_data['ban_time_increment'] == "1") ? 1 : 0;
         $max_attempts = intval((isset($_data['max_attempts'])) ? $_data['max_attempts'] : $is_now['max_attempts']);
+        $max_ban_time = intval((isset($_data['max_ban_time'])) ? $_data['max_ban_time'] : $is_now['max_ban_time']);
         $retry_window = intval((isset($_data['retry_window'])) ? $_data['retry_window'] : $is_now['retry_window']);
         $netban_ipv4 = intval((isset($_data['netban_ipv4'])) ? $_data['netban_ipv4'] : $is_now['netban_ipv4']);
         $netban_ipv6 = intval((isset($_data['netban_ipv6'])) ? $_data['netban_ipv6'] : $is_now['netban_ipv6']);
@@ -256,6 +258,8 @@ function fail2ban($_action, $_data = null) {
       }
       $f2b_options = array();
       $f2b_options['ban_time'] = ($ban_time < 60) ? 60 : $ban_time;
+      $f2b_options['ban_time_increment'] = ($ban_time_increment == 1) ? true : false;
+      $f2b_options['max_ban_time'] = ($max_ban_time < 60) ? 60 : $max_ban_time;
       $f2b_options['netban_ipv4'] = ($netban_ipv4 < 8) ? 8 : $netban_ipv4;
       $f2b_options['netban_ipv6'] = ($netban_ipv6 < 8) ? 8 : $netban_ipv6;
       $f2b_options['netban_ipv4'] = ($netban_ipv4 > 32) ? 32 : $netban_ipv4;

+ 2 - 0
data/web/lang/lang.de-de.json

@@ -175,10 +175,12 @@
         "empty": "Keine Einträge vorhanden",
         "excludes": "Diese Empfänger ausschließen",
         "f2b_ban_time": "Bannzeit in Sekunden",
+        "f2b_ban_time_increment": "Bannzeit erhöht sich mit jedem Bann",
         "f2b_blacklist": "Blacklist für Netzwerke und Hosts",
         "f2b_filter": "Regex-Filter",
         "f2b_list_info": "Ein Host oder Netzwerk auf der Blacklist wird immer eine Whitelist-Einheit überwiegen. <b>Die Aktualisierung der Liste dauert einige Sekunden.</b>",
         "f2b_max_attempts": "Max. Versuche",
+        "f2b_max_ban_time": "Maximale Bannzeit in Sekunden",
         "f2b_netban_ipv4": "Netzbereich für IPv4-Banns (8-32)",
         "f2b_netban_ipv6": "Netzbereich für IPv6-Banns (8-128)",
         "f2b_parameters": "Fail2ban-Parameter",

+ 2 - 0
data/web/lang/lang.en-gb.json

@@ -177,10 +177,12 @@
         "empty": "No results",
         "excludes": "Excludes these recipients",
         "f2b_ban_time": "Ban time (s)",
+        "f2b_ban_time_increment": "Ban time is incremented with each ban",
         "f2b_blacklist": "Blacklisted networks/hosts",
         "f2b_filter": "Regex filters",
         "f2b_list_info": "A blacklisted host or network will always outweigh a whitelist entity. <b>List updates will take a few seconds to be applied.</b>",
         "f2b_max_attempts": "Max. attempts",
+        "f2b_max_ban_time": "Max. ban time (s)",
         "f2b_netban_ipv4": "IPv4 subnet size to apply ban on (8-32)",
         "f2b_netban_ipv6": "IPv6 subnet size to apply ban on (8-128)",
         "f2b_parameters": "Fail2ban parameters",

+ 2 - 0
data/web/lang/lang.es-es.json

@@ -141,9 +141,11 @@
         "empty": "Sin resultados",
         "excludes": "Excluye a estos destinatarios",
         "f2b_ban_time": "Tiempo de restricción (s)",
+        "f2b_ban_time_increment": "Tiempo de restricción se incrementa con cada restricción",
         "f2b_blacklist": "Redes y hosts en lista negra",
         "f2b_list_info": "Un host o red en lista negra siempre superará a una entidad de la lista blanca. <b>Las actualizaciones de la lista tardarán unos segundos en aplicarse.</b>",
         "f2b_max_attempts": "Max num. de intentos",
+        "f2b_max_ban_time": "Max tiempo de restricción (s)",
         "f2b_netban_ipv4": "Tamaño de subred IPv4 para aplicar la restricción (8-32)",
         "f2b_netban_ipv6": "Tamaño de subred IPv6 para aplicar la restricción (8-128)",
         "f2b_parameters": "Parametros Fail2ban",

+ 3 - 1
data/web/lang/lang.fr-fr.json

@@ -172,11 +172,13 @@
         "edit": "Editer",
         "empty": "Aucun résultat",
         "excludes": "Exclure ces destinataires",
-        "f2b_ban_time": "Durée du bannissement(s)",
+        "f2b_ban_time": "Durée du bannissement (s)",
+        "f2b_ban_time_increment": "Durée du bannissement est augmentée à chaque bannissement",
         "f2b_blacklist": "Réseaux/Domaines sur Liste Noire",
         "f2b_filter": "Filtre(s) Regex",
         "f2b_list_info": "Un hôte ou un réseau sur liste noire l'emportera toujours sur une entité de liste blanche. <b>L'application des mises à jour de liste prendra quelques secondes.</b>",
         "f2b_max_attempts": "Nb max. de tentatives",
+        "f2b_max_ban_time": "Max. durée du bannissement (s)",
         "f2b_netban_ipv4": "Taille du sous-réseau IPv4 pour l'application du bannissement (8-32)",
         "f2b_netban_ipv6": "Taille du sous-réseau IPv6 pour l'application du bannissement (8-128)",
         "f2b_parameters": "Paramètres Fail2ban",

+ 2 - 0
data/web/lang/lang.it-it.json

@@ -175,10 +175,12 @@
         "empty": "Nessun risultato",
         "excludes": "Esclude questi destinatari",
         "f2b_ban_time": "Tempo di blocco (s)",
+        "f2b_ban_time_increment": "Tempo di blocco aumenta ad ogni blocco",
         "f2b_blacklist": "Host/reti in blacklist",
         "f2b_filter": "Filtri Regex",
         "f2b_list_info": "Un host oppure una rete in blacklist, avrà sempre un peso maggiore rispetto ad una in whitelist. <b>L'aggiornamento della lista richiede alcuni secondi per la sua entrata in azione.</b>",
         "f2b_max_attempts": "Tentativi massimi",
+        "f2b_max_ban_time": "Tempo massimo di blocco (s)",
         "f2b_netban_ipv4": "IPv4 subnet size to apply ban on (8-32)",
         "f2b_netban_ipv6": "IPv6 subnet size to apply ban on (8-128)",
         "f2b_parameters": "Parametri Fail2ban",

+ 2 - 0
data/web/lang/lang.nl-nl.json

@@ -168,10 +168,12 @@
         "empty": "Geen resultaten",
         "excludes": "Exclusief",
         "f2b_ban_time": "Verbanningstijd (s)",
+        "f2b_ban_time_increment": "Verbanningstijd wordt verhoogd met elk verbanning",
         "f2b_blacklist": "Netwerken/hosts op de blacklist",
         "f2b_filter": "Regex-filters",
         "f2b_list_info": "Een host of netwerk op de blacklist staat altijd boven eenzelfde op de whitelist. <b>Het doorvoeren van wijzigingen kan enkele seconden in beslag nemen.</b>",
         "f2b_max_attempts": "Maximaal aantal pogingen",
+        "f2b_max_ban_time": "Maximaal verbanningstijd (s)",
         "f2b_netban_ipv4": "Voer de IPv4-subnetgrootte in waar de verbanning van kracht moet zijn (8-32)",
         "f2b_netban_ipv6": "Voer de IPv6-subnetgrootte in waar de verbanning van kracht moet zijn (8-128)",
         "f2b_parameters": "Fail2ban",

+ 8 - 0
data/web/templates/admin/tab-config-f2b.twig

@@ -12,6 +12,14 @@
           <label for="f2b_ban_time">{{ lang.admin.f2b_ban_time }}:</label>
           <input type="number" class="form-control" id="f2b_ban_time" name="ban_time" value="{{ f2b_data.ban_time }}" required>
         </div>
+        <div class="mb-4">
+          <label for="f2b_max_ban_time">{{ lang.admin.f2b_max_ban_time }}:</label>
+          <input type="number" class="form-control" id="f2b_max_ban_time" name="max_ban_time" value="{{ f2b_data.max_ban_time }}" required>
+        </div>
+        <div class="mb-4">
+          <input class="form-check-input" type="checkbox" value="1" name="ban_time_increment" id="f2b_ban_time_increment" {% if f2b_data.ban_time_increment == 1 %}checked{% endif %}>
+          <label class="form-check-label" for="f2b_ban_time_increment">{{ lang.admin.f2b_ban_time_increment }}</label>
+        </div>
         <div class="mb-4">
           <label for="f2b_max_attempts">{{ lang.admin.f2b_max_attempts }}:</label>
           <input type="number" class="form-control" id="f2b_max_attempts" name="max_attempts" value="{{ f2b_data.max_attempts }}" required>