2
0
Эх сурвалжийг харах

[netfilter] fix negative timer, no unbanning of IPs (#6575)

* [netfilter] added debug logs and updated autopurge

* updated "Allow/Blacklist" terms

* netfilter: bumped compose version

* netfilter: changed black/whitelist terms in code

---------

Co-authored-by: Denis Evers <git@evers.sh>
Co-authored-by: DerLinkman <niklas.meyer@servercow.de>
Denis Evers 1 сар өмнө
parent
commit
95eb350f15

+ 103 - 62
data/Dockerfiles/netfilter/main.py

@@ -1,5 +1,7 @@
 #!/usr/bin/env python3
 
+DEBUG = False
+
 import re
 import os
 import sys
@@ -20,10 +22,13 @@ from modules.Logger import Logger
 from modules.IPTables import IPTables
 from modules.NFTables import NFTables
 
+def logdebug(msg):
+  if DEBUG:
+    logger.logInfo("DEBUG: %s" % msg)
 
-# globals
+# Globals
 WHITELIST = []
-BLACKLIST= []
+BLACKLIST = []
 bans = {}
 quit_now = False
 exit_code = 0
@@ -33,12 +38,10 @@ r = None
 pubsub = None
 clear_before_quit = False
 
-
 def refreshF2boptions():
   global f2boptions
   global quit_now
   global exit_code
-
   f2boptions = {}
 
   if not r.get('F2B_OPTIONS'):
@@ -52,8 +55,9 @@ def refreshF2boptions():
   else:
     try:
       f2boptions = json.loads(r.get('F2B_OPTIONS'))
-    except ValueError:
-      logger.logCrit('Error loading F2B options: F2B_OPTIONS is not json')
+    except ValueError as e:
+      logger.logCrit(
+        'Error loading F2B options: F2B_OPTIONS is not json. Exception: %s' % e)
       quit_now = True
       exit_code = 2
 
@@ -61,15 +65,15 @@ def refreshF2boptions():
   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)
-  verifyF2boption(f2boptions,'banlist_id', str(uuid.uuid4()))
-  verifyF2boption(f2boptions,'manage_external', 0)
+  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)
+  verifyF2boption(f2boptions, 'banlist_id', str(uuid.uuid4()))
+  verifyF2boption(f2boptions, 'manage_external', 0)
 
 def verifyF2boption(f2boptions, f2boption, f2bdefault):
   f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault
@@ -111,7 +115,7 @@ def get_ip(address):
 def ban(address):
   global f2boptions
   global lock
-
+  logdebug("ban() called with address=%s" % address)
   refreshF2boptions()
   MAX_ATTEMPTS = int(f2boptions['max_attempts'])
   RETRY_WINDOW = int(f2boptions['retry_window'])
@@ -119,31 +123,43 @@ def ban(address):
   NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
 
   ip = get_ip(address)
-  if not ip: return
+  if not ip:
+    logdebug("No valid IP -- skipping ban()")
+    return
   address = str(ip)
   self_network = ipaddress.ip_network(address)
 
   with lock:
     temp_whitelist = set(WHITELIST)
-  if temp_whitelist:
-    for wl_key in temp_whitelist:
-      wl_net = ipaddress.ip_network(wl_key, False)
-      if wl_net.overlaps(self_network):
-        logger.logInfo('Address %s is whitelisted by rule %s' % (self_network, wl_net))
-        return
-
-  net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
+    logdebug("Checking if %s overlaps with any WHITELIST entries" % self_network)
+    if temp_whitelist:
+      for wl_key in temp_whitelist:
+        wl_net = ipaddress.ip_network(wl_key, False)
+        logdebug("Checking overlap between %s and %s" % (self_network, wl_net))
+        if wl_net.overlaps(self_network):
+          logger.logInfo(
+            'Address %s is allowlisted by rule %s' % (self_network, wl_net))
+          return
+
+  net = ipaddress.ip_network(
+    (address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
   net = str(net)
+  logdebug("Ban net: %s" % net)
 
   if not net in bans:
     bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0}
+    logdebug("Initing new ban counter for %s" % net)
 
   current_attempt = time.time()
+  logdebug("Current attempt ts=%s, previous: %s, retry_window: %s" %
+           (current_attempt, bans[net]['last_attempt'], RETRY_WINDOW))
   if current_attempt - bans[net]['last_attempt'] > RETRY_WINDOW:
     bans[net]['attempts'] = 0
+    logdebug("Ban counter for %s reset as window expired" % net)
 
   bans[net]['attempts'] += 1
   bans[net]['last_attempt'] = current_attempt
+  logdebug("%s attempts now %d" % (net, bans[net]['attempts']))
 
   if bans[net]['attempts'] >= MAX_ATTEMPTS:
     cur_time = int(round(time.time()))
@@ -151,34 +167,41 @@ def ban(address):
     logger.logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
     if type(ip) is ipaddress.IPv4Address and int(f2boptions['manage_external']) != 1:
       with lock:
+        logdebug("Calling tables.banIPv4(%s)" % net)
         tables.banIPv4(net)
     elif int(f2boptions['manage_external']) != 1:
       with lock:
+        logdebug("Calling tables.banIPv6(%s)" % net)
         tables.banIPv6(net)
 
+    logdebug("Updating F2B_ACTIVE_BANS[%s]=%d" %
+              (net, cur_time + NET_BAN_TIME))
     r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME)
   else:
-    logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
+    logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % (
+      MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
 
 def unban(net):
   global lock
-
+  logdebug("Calling unban() with net=%s" % net)
   if not net in bans:
-   logger.logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net)
-   r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
-   return
-
+    logger.logInfo(
+      '%s is not banned, skipping unban and deleting from queue (if any)' % net)
+    r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
+    return
   logger.logInfo('Unbanning %s' % net)
   if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
     with lock:
+      logdebug("Calling tables.unbanIPv4(%s)" % net)
       tables.unbanIPv4(net)
   else:
     with lock:
+      logdebug("Calling tables.unbanIPv6(%s)" % net)
       tables.unbanIPv6(net)
-
   r.hdel('F2B_ACTIVE_BANS', '%s' % net)
   r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
   if net in bans:
+    logdebug("Unban for %s, setting attempts=0, ban_counter+=1" % net)
     bans[net]['attempts'] = 0
     bans[net]['ban_counter'] += 1
 
@@ -204,17 +227,19 @@ def permBan(net, unban=False):
 
   if is_unbanned:
     r.hdel('F2B_PERM_BANS', '%s' % net)
-    logger.logCrit('Removed host/network %s from blacklist' % net)
+    logger.logCrit('Removed host/network %s from denylist' % net)
   elif is_banned:
     r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
-    logger.logCrit('Added host/network %s to blacklist' % net)
+    logger.logCrit('Added host/network %s to denylist' % net)
 
 def clear():
   global lock
   logger.logInfo('Clearing all bans')
   for net in bans.copy():
+    logdebug("Unbanning net: %s" % net)
     unban(net)
   with lock:
+    logdebug("Clearing IPv4/IPv6 table")
     tables.clearIPv4Table()
     tables.clearIPv6Table()
     try:
@@ -275,21 +300,35 @@ def snat6(snat_target):
 
 def autopurge():
   global f2boptions
-
+  logdebug("autopurge thread started")
   while not quit_now:
+    logdebug("autopurge tick")
     time.sleep(10)
     refreshF2boptions()
     MAX_ATTEMPTS = int(f2boptions['max_attempts'])
     QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
+    logdebug("QUEUE_UNBAN: %s" % QUEUE_UNBAN)
     if QUEUE_UNBAN:
       for net in QUEUE_UNBAN:
+        logdebug("Autopurge: unbanning queued net: %s" % net)
         unban(str(net))
-    for net in bans.copy():
-      if bans[net]['attempts'] >= MAX_ATTEMPTS:
-        NET_BAN_TIME = calcNetBanTime(bans[net]['ban_counter'])
-        TIME_SINCE_LAST_ATTEMPT = time.time() - bans[net]['last_attempt']
-        if TIME_SINCE_LAST_ATTEMPT > NET_BAN_TIME:
-          unban(net)
+    # Only check expiry for actively banned IPs:
+    active_bans = r.hgetall('F2B_ACTIVE_BANS')
+    now = time.time()
+    for net_str, expire_str in active_bans.items():
+      logdebug("Checking ban expiry for (actively banned): %s" % net_str)
+      # Defensive: always process if timer missing or expired
+      try:
+        expire = float(expire_str)
+      except Exception:
+        logdebug("Invalid expire time for %s; unbanning" % net_str)
+        unban(net_str)
+        continue
+      time_left = expire - now
+      logdebug("Time left for %s: %.1f seconds" % (net_str, time_left))
+      if time_left <= 0:
+        logdebug("Ban expired for %s" % net_str)
+        unban(net_str)
 
 def mailcowChainOrder():
   global lock
@@ -359,7 +398,7 @@ def whitelistUpdate():
     with lock:
       if Counter(new_whitelist) != Counter(WHITELIST):
         WHITELIST = new_whitelist
-        logger.logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))
+        logger.logInfo('Allowlist was changed, it has %s entries' % len(WHITELIST))
     time.sleep(60.0 - ((time.time() - start_time) % 60.0))
 
 def blacklistUpdate():
@@ -375,7 +414,7 @@ def blacklistUpdate():
       addban = set(new_blacklist).difference(BLACKLIST)
       delban = set(BLACKLIST).difference(new_blacklist)
       BLACKLIST = new_blacklist
-      logger.logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST))
+      logger.logInfo('Denylist was changed, it has %s entries' % len(BLACKLIST))
       if addban:
         for net in addban:
           permBan(net=net)
@@ -386,25 +425,25 @@ def blacklistUpdate():
 
 def sigterm_quit(signum, frame):
   global clear_before_quit
+  logdebug("SIGTERM received, setting clear_before_quit to True and exiting")
   clear_before_quit = True
   sys.exit(exit_code)
 
-def berfore_quit():
+def before_quit():
+  logdebug("before_quit called, clear_before_quit=%s" % clear_before_quit)
   if clear_before_quit:
     clear()
   if pubsub is not None:
     pubsub.unsubscribe()
 
-
 if __name__ == '__main__':
-  atexit.register(berfore_quit)
-  signal.signal(signal.SIGTERM, sigterm_quit)
-
-  # init Logger
   logger = Logger()
+  logdebug("Sys.argv: %s" % sys.argv)
+  atexit.register(before_quit)
+  signal.signal(signal.SIGTERM, sigterm_quit)
 
-  # init backend
   backend = sys.argv[1]
+  logdebug("Backend: %s" % backend)
   if backend == "nftables":
     logger.logInfo('Using NFTables backend')
     tables = NFTables(chain_name, logger)
@@ -412,16 +451,12 @@ if __name__ == '__main__':
     logger.logInfo('Using IPTables backend')
     tables = IPTables(chain_name, logger)
 
-  # In case a previous session was killed without cleanup
   clear()
-
-  # Reinit MAILCOW chain
-  # Is called before threads start, no locking
   logger.logInfo("Initializing mailcow netfilter chain")
   tables.initChainIPv4()
   tables.initChainIPv6()
 
-  if os.getenv("DISABLE_NETFILTER_ISOLATION_RULE").lower() in ("y", "yes"):
+  if os.getenv("DISABLE_NETFILTER_ISOLATION_RULE", "").lower() in ("y", "yes"):
     logger.logInfo(f"Skipping {chain_name} isolation")
   else:
     logger.logInfo(f"Setting {chain_name} isolation")
@@ -432,23 +467,28 @@ if __name__ == '__main__':
     try:
       redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
       redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '')
+      logdebug(
+        "Connecting redis (SLAVEOF_IP:%s, PORT:%s)" % (redis_slaveof_ip, redis_slaveof_port))
       if "".__eq__(redis_slaveof_ip):
-        r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0, password=os.environ['REDISPASS'])
+        r = redis.StrictRedis(
+          host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0, password=os.environ['REDISPASS'])
       else:
-        r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0, password=os.environ['REDISPASS'])
+        r = redis.StrictRedis(
+          host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0, password=os.environ['REDISPASS'])
       r.ping()
       pubsub = r.pubsub()
     except Exception as ex:
-      print('%s - trying again in 3 seconds'  % (ex))
+      logdebug(
+        'Redis connection failed: %s - trying again in 3 seconds' % (ex))
       time.sleep(3)
     else:
       break
   logger.set_redis(r)
+  logdebug("Redis connection established, setting up F2B keys")
 
-  # rename fail2ban to netfilter
   if r.exists('F2B_LOG'):
+    logdebug("Renaming F2B_LOG to NETFILTER_LOG")
     r.rename('F2B_LOG', 'NETFILTER_LOG')
-  # clear bans in redis
   r.delete('F2B_ACTIVE_BANS')
   r.delete('F2B_PERM_BANS')
 
@@ -463,7 +503,7 @@ if __name__ == '__main__':
       snat_ip = os.getenv('SNAT_TO_SOURCE')
       snat_ipo = ipaddress.ip_address(snat_ip)
       if type(snat_ipo) is ipaddress.IPv4Address:
-        snat4_thread = Thread(target=snat4,args=(snat_ip,))
+        snat4_thread = Thread(target=snat4, args=(snat_ip,))
         snat4_thread.daemon = True
         snat4_thread.start()
     except ValueError:
@@ -499,4 +539,5 @@ if __name__ == '__main__':
   while not quit_now:
     time.sleep(0.5)
 
-  sys.exit(exit_code)
+  logdebug("Exiting with code %s" % exit_code)
+  sys.exit(exit_code)

+ 6 - 6
data/web/inc/vars.inc.php

@@ -238,12 +238,12 @@ $FIDO2_FORMATS = array('apple', 'android-key', 'android-safetynet', 'fido-u2f',
 // Set visible Rspamd maps in mailcow UI, do not change unless you know what you are doing
 $RSPAMD_MAPS = array(
   'regex' => array(
-    'Header-From: Blacklist' => 'global_mime_from_blacklist.map',
-    'Header-From: Whitelist' => 'global_mime_from_whitelist.map',
-    'Envelope Sender Blacklist' => 'global_smtp_from_blacklist.map',
-    'Envelope Sender Whitelist' => 'global_smtp_from_whitelist.map',
-    'Recipient Blacklist' => 'global_rcpt_blacklist.map',
-    'Recipient Whitelist' => 'global_rcpt_whitelist.map',
+    'Header-From: Denylist' => 'global_mime_from_blacklist.map',
+    'Header-From: Allowlist' => 'global_mime_from_whitelist.map',
+    'Envelope Sender Denylist' => 'global_smtp_from_blacklist.map',
+    'Envelope Sender Allowlist' => 'global_smtp_from_whitelist.map',
+    'Recipient Denylist' => 'global_rcpt_blacklist.map',
+    'Recipient Allowlist' => 'global_rcpt_whitelist.map',
     'Fishy TLDS (only fired in combination with bad words)' => 'fishy_tlds.map',
     'Bad Words (only fired in combination with fishy TLDs)' => 'bad_words.map',
     'Bad Words DE (only fired in combination with fishy TLDs)' => 'bad_words_de.map',

+ 12 - 11
data/web/lang/lang.de-de.json

@@ -25,7 +25,7 @@
         "sogo_access": "Verwalten des SOGo-Zugriffsrechts erlauben",
         "sogo_profile_reset": "SOGo-Profil zurücksetzen",
         "spam_alias": "Temporäre E-Mail-Aliasse",
-        "spam_policy": "Blacklist/Whitelist",
+        "spam_policy": "Deny/Allowlist",
         "spam_score": "Spam-Bewertung",
         "syncjobs": "Sync Jobs",
         "tls_policy": "Verschlüsselungsrichtlinie",
@@ -147,7 +147,7 @@
         "arrival_time": "Ankunftszeit (Serverzeit)",
         "authed_user": "Auth. Benutzer",
         "ays": "Soll der Vorgang wirklich ausgeführt werden?",
-        "ban_list_info": "Übersicht ausgesperrter Netzwerke: <b>Netzwerk (verbleibende Bannzeit) - [Aktionen]</b>.<br />IPs, die zum Entsperren eingereiht werden, verlassen die Liste aktiver Banns nach wenigen Sekunden.<br />Rote Labels sind Indikatoren für aktive Blacklist-Einträge.",
+        "ban_list_info": "Übersicht ausgesperrter Netzwerke: <b>Netzwerk (verbleibende Bannzeit) - [Aktionen]</b>.<br />IPs, die zum Entsperren eingereiht werden, verlassen die Liste aktiver Banns nach wenigen Sekunden.<br />Rote Labels sind Indikatoren für aktive Allowlist-Einträge.",
         "change_logo": "Logo ändern",
         "configuration": "Konfiguration",
         "convert_html_to_text": "Konvertiere HTML zu reinem Text",
@@ -184,9 +184,9 @@
         "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_blacklist": "Denyliste 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_list_info": "Ein Host oder Netzwerk auf der Denyliste wird immer eine Allowlist-Einheit überwiegen. <b>Die Aktualisierung der Liste dauert einige Sekunden.</b>",
         "f2b_manage_external": "Fail2Ban extern verwalten",
         "f2b_manage_external_info": "Fail2ban wird die Banlist weiterhin pflegen, jedoch werden keine aktiven Regeln zum blockieren gesetzt. Die unten generierte Banlist, kann verwendet werden, um den Datenverkehr extern zu blockieren.",
         "f2b_max_attempts": "Max. Versuche",
@@ -196,7 +196,7 @@
         "f2b_parameters": "Fail2ban-Parameter",
         "f2b_regex_info": "Berücksichtigte Logs: SOGo, Postfix, Dovecot, PHP-FPM.",
         "f2b_retry_window": "Wiederholungen im Zeitraum von (s)",
-        "f2b_whitelist": "Whitelist für Netzwerke und Hosts",
+        "f2b_whitelist": "Allowliste für Netzwerke und Hosts",
         "filter_table": "Tabelle filtern",
         "force_sso_text": "Wenn ein externer OIDC-Provider konfiguriert ist, blendet diese Option die mailcow Loginform aus und zeigt nur den Single Sign-On-Button an.",
         "force_sso": "mailcow Login deaktivieren und nur Single Sign-On anzeigen",
@@ -272,6 +272,7 @@
         "message": "Nachricht",
         "message_size": "Nachrichtengröße",
         "nexthop": "Next Hop",
+        "needs_restart": "benötigt Neustart",
         "no": "&#10005;",
         "no_active_bans": "Keine aktiven Banns",
         "no_new_rows": "Keine weiteren Zeilen vorhanden",
@@ -354,8 +355,8 @@
         "rspamd_com_settings": "Ein Name wird automatisch generiert. Beispielinhalte zur Einsicht stehen nachstehend bereit. Siehe auch <a href=\"https://rspamd.com/doc/configuration/settings.html#settings-structure\" target=\"_blank\">Rspamd docs</a>",
         "rspamd_global_filters": "Globale Filter-Maps",
         "rspamd_global_filters_agree": "Ich werde vorsichtig sein!",
-        "rspamd_global_filters_info": "Globale Filter-Maps steuern globales White- und Blacklisting dieses Servers.",
-        "rspamd_global_filters_regex": "Die akzeptierte Form für Einträge sind <b>ausschließlich</b> Regular Expressions.\r\n  Trotz rudimentärer Überprüfung der Map, kann es zu fehlerhaften Einträgen kommen, die Rspamd im schlechtesten Fall mit unvorhersehbarer Funktionalität bestraft.<br>\r\n  Das korrekte Format lautet \"/pattern/options\" (Beispiel: <code>/.+@domain\\.tld/i</code>).<br>\r\n  Der Name der Map beschreibt die jeweilige Funktion.<br>\r\n  Rspamd versucht die Maps umgehend aufzulösen. Bei Problemen sollte <a href=\"\" data-toggle=\"modal\" data-container=\"rspamd-mailcow\" data-target=\"#RestartContainer\">Rspamd manuell neugestartet werden</a>.<br>Elemente auf Blacklists sind von der Quarantäne ausgeschlossen.",
+        "rspamd_global_filters_info": "Globale Filter-Maps steuern globales Allow- und Denylisting dieses Servers.",
+        "rspamd_global_filters_regex": "Die akzeptierte Form für Einträge sind <b>ausschließlich</b> Regular Expressions.\r\n  Trotz rudimentärer Überprüfung der Map, kann es zu fehlerhaften Einträgen kommen, die Rspamd im schlechtesten Fall mit unvorhersehbarer Funktionalität bestraft.<br>\r\n  Das korrekte Format lautet \"/pattern/options\" (Beispiel: <code>/.+@domain\\.tld/i</code>).<br>\r\n  Der Name der Map beschreibt die jeweilige Funktion.<br>\r\n  Rspamd versucht die Maps umgehend aufzulösen. Bei Problemen sollte <a href=\"\" data-toggle=\"modal\" data-container=\"rspamd-mailcow\" data-target=\"#RestartContainer\">Rspamd manuell neugestartet werden</a>.<br>Elemente auf Denylisten sind von der Quarantäne ausgeschlossen.",
         "rspamd_settings_map": "Rspamd-Settings-Map",
         "sal_level": "Moo-Level",
         "save": "Änderungen speichern",
@@ -747,7 +748,7 @@
         "sogo_visible_info": "Diese Option hat lediglich Einfluss auf Objekte, die in SOGo darstellbar sind (geteilte oder nicht-geteilte Alias-Adressen mit dem Ziel mindestens einer lokalen Mailbox).",
         "spam_alias": "Anpassen temporärer Alias-Adressen",
         "spam_filter": "Spamfilter",
-        "spam_policy": "Hinzufügen und Entfernen von Einträgen in White- und Blacklists",
+        "spam_policy": "Hinzufügen und Entfernen von Einträgen in Allow- und Denylisten",
         "spam_score": "Einen benutzerdefiniterten Spam-Score festlegen",
         "subfolder2": "Ziel-Ordner<br><small>(leer = kein Unterordner)</small>",
         "syncjob": "Sync-Job bearbeiten",
@@ -1037,7 +1038,7 @@
         "notified": "Benachrichtigt",
         "qhandler_success": "Aktion wurde an das System übergeben. Sie dürfen dieses Fenster nun schließen.",
         "qid": "Rspamd QID",
-        "qinfo": "Das Quarantänesystem speichert abgelehnte Nachrichten in der Datenbank (dem Sender wird <em>nicht</em> signalisiert, dass seine E-Mail zugestellt wurde) als auch diese, die als Kopie in den Junk-Ordner der jeweiligen Mailbox zugestellt wurden.\r\n  <br>\"Als Spam lernen und löschen\" lernt Nachrichten nach bayesscher Statistik als Spam und erstellt Fuzzy Hashes ausgehend von der jeweiligen Nachricht, um ähnliche Inhalte zukünftig zu unterbinden.\r\n  <br>Der Prozess des Lernens kann abhängig vom System zeitintensiv sein.<br>Auf Blacklists vorkommende Elemente sind von der Quarantäne ausgeschlossen.",
+        "qinfo": "Das Quarantänesystem speichert abgelehnte Nachrichten in der Datenbank (dem Sender wird <em>nicht</em> signalisiert, dass seine E-Mail zugestellt wurde) als auch diese, die als Kopie in den Junk-Ordner der jeweiligen Mailbox zugestellt wurden.\r\n  <br>\"Als Spam lernen und löschen\" lernt Nachrichten nach bayesscher Statistik als Spam und erstellt Fuzzy Hashes ausgehend von der jeweiligen Nachricht, um ähnliche Inhalte zukünftig zu unterbinden.\r\n  <br>Der Prozess des Lernens kann abhängig vom System zeitintensiv sein.<br>Auf Denylisten vorkommende Elemente sind von der Quarantäne ausgeschlossen.",
         "qitem": "Quarantäneeintrag",
         "quarantine": "Quarantäne",
         "quick_actions": "Aktionen",
@@ -1326,7 +1327,7 @@
         "spam_score_reset": "Auf Server-Standard zurücksetzen",
         "spamfilter": "Spamfilter",
         "spamfilter_behavior": "Bewertung",
-        "spamfilter_bl": "Blacklist",
+        "spamfilter_bl": "Denyliste",
         "spamfilter_bl_desc": "Für E-Mail-Adressen, die vom Spamfilter <b>immer</b> als Spam erfasst und abgelehnt werden. Die Quarantäne-Funktion ist für diese Nachrichten deaktiviert. Die Verwendung von Wildcards ist gestattet. Ein Filter funktioniert lediglich für direkte nicht-\"Catch All\" Alias-Adressen (Alias-Adressen mit lediglich einer Mailbox als Ziel-Adresse) sowie die Mailbox-Adresse selbst.",
         "spamfilter_default_score": "Standardwert",
         "spamfilter_green": "Grün: Die Nachricht ist kein Spam",
@@ -1338,7 +1339,7 @@
         "spamfilter_table_empty": "Keine Einträge vorhanden",
         "spamfilter_table_remove": "Entfernen",
         "spamfilter_table_rule": "Regel",
-        "spamfilter_wl": "Whitelist",
+        "spamfilter_wl": "Allowliste",
         "spamfilter_wl_desc": "Für E-Mail-Adressen, die vom Spamfilter <b>nicht</b> erfasst werden sollen. Die Verwendung von Wildcards ist gestattet. Ein Filter funktioniert lediglich für direkte nicht-\"Catch All\" Alias-Adressen (Alias-Adressen mit lediglich einer Mailbox als Ziel-Adresse) sowie die Mailbox-Adresse selbst.",
         "spamfilter_yellow": "Gelb: Die Nachricht ist vielleicht Spam, wird als Spam markiert und in den Junk-Ordner verschoben",
         "status": "Status",

+ 14 - 13
data/web/lang/lang.en-gb.json

@@ -25,7 +25,7 @@
         "sogo_access": "Allow management of SOGo access",
         "sogo_profile_reset": "Reset SOGo profile",
         "spam_alias": "Temporary aliases",
-        "spam_policy": "Blacklist/Whitelist",
+        "spam_policy": "Denylist/Allowlist",
         "spam_score": "Spam score",
         "syncjobs": "Sync jobs",
         "tls_policy": "TLS policy",
@@ -151,7 +151,7 @@
         "arrival_time": "Arrival time (server time)",
         "authed_user": "Auth. user",
         "ays": "Are you sure you want to proceed?",
-        "ban_list_info": "See a list of banned IPs below: <b>network (remaining ban time) - [actions]</b>.<br />IPs queued to be unbanned will be removed from the active ban list within a few seconds.<br />Red labels indicate active permanent bans by blacklisting.",
+        "ban_list_info": "See a list of banned IPs below: <b>network (remaining ban time) - [actions]</b>.<br />IPs queued to be unbanned will be removed from the active ban list within a few seconds.<br />Red labels indicate active permanent bans by denylisting.",
         "change_logo": "Change logo",
         "logo_normal_label": "Normal",
         "logo_dark_label": "Inverted for dark mode",
@@ -190,9 +190,9 @@
         "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_blacklist": "Denylisted 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_list_info": "A denylisted host or network will always outweigh a allowlist entity. <b>List updates will take a few seconds to be applied.</b>",
         "f2b_manage_external": "Manage Fail2Ban externally",
         "f2b_manage_external_info": "Fail2ban will still maintain the banlist, but it will not actively set rules to block traffic. Use the generated banlist below to externally block the traffic.",
         "f2b_max_attempts": "Max. attempts",
@@ -202,7 +202,7 @@
         "f2b_parameters": "Fail2ban parameters",
         "f2b_regex_info": "Logs taken into consideration: SOGo, Postfix, Dovecot, PHP-FPM.",
         "f2b_retry_window": "Retry window (s) for max. attempts",
-        "f2b_whitelist": "Whitelisted networks/hosts",
+        "f2b_whitelist": "Allowlisted networks/hosts",
         "filter": "Filter",
         "filter_table": "Filter table",
         "force_sso_text": "If an external OIDC provider is configured, this option hides the default mailcow login forms and only shows the Single Sign-On button",
@@ -279,6 +279,7 @@
         "message": "Message",
         "message_size": "Message size",
         "nexthop": "Next hop",
+        "needs_restart": "needs restart",
         "no": "&#10005;",
         "no_active_bans": "No active bans",
         "no_new_rows": "No further rows available",
@@ -364,8 +365,8 @@
         "rspamd_com_settings": "A setting name will be auto-generated, please see the example presets below. For more details see <a href=\"https://rspamd.com/doc/configuration/settings.html#settings-structure\" target=\"_blank\">Rspamd docs</a>",
         "rspamd_global_filters": "Global filter maps",
         "rspamd_global_filters_agree": "I will be careful!",
-        "rspamd_global_filters_info": "Global filter maps contain different kind of global black and whitelists.",
-        "rspamd_global_filters_regex": "Their names explain their purpose. All content must contain valid regular expression in the format of \"/pattern/options\" (e.g. <code>/.+@domain\\.tld/i</code>).<br>\r\n  Although rudimentary checks are being executed on each line of regex, Rspamds functionality can be broken, if it fails to read the syntax correctly.<br>\r\n  Rspamd will try to read the map content when changed. If you experience problems, <a href=\"\" data-toggle=\"modal\" data-container=\"rspamd-mailcow\" data-target=\"#RestartContainer\">restart Rspamd</a> to enforce a map reload.<br>Blacklisted elements are excluded from quarantine.",
+        "rspamd_global_filters_info": "Global filter maps contain different kind of global deny and allowlists.",
+        "rspamd_global_filters_regex": "Their names explain their purpose. All content must contain valid regular expression in the format of \"/pattern/options\" (e.g. <code>/.+@domain\\.tld/i</code>).<br>\r\n  Although rudimentary checks are being executed on each line of regex, Rspamds functionality can be broken, if it fails to read the syntax correctly.<br>\r\n  Rspamd will try to read the map content when changed. If you experience problems, <a href=\"\" data-toggle=\"modal\" data-container=\"rspamd-mailcow\" data-target=\"#RestartContainer\">restart Rspamd</a> to enforce a map reload.<br>Denylisted elements are excluded from quarantine.",
         "rspamd_settings_map": "Rspamd settings map",
         "sal_level": "Moo level",
         "save": "Save changes",
@@ -750,7 +751,7 @@
         "sogo_visible_info": "This option only affects objects, that can be displayed in SOGo (shared or non-shared alias addresses pointing to at least one local mailbox). If hidden, an alias will not appear as selectable sender in SOGo.",
         "spam_alias": "Create or change time limited alias addresses",
         "spam_filter": "Spam filter",
-        "spam_policy": "Add or remove items to white-/blacklist",
+        "spam_policy": "Add or remove items to allow-/denylist",
         "spam_score": "Set a custom spam score",
         "subfolder2": "Sync into subfolder on destination<br><small>(empty = do not use subfolder)</small>",
         "syncjob": "Edit sync job",
@@ -1039,7 +1040,7 @@
         "notified": "Notified",
         "qhandler_success": "Request successfully sent to the system. You can now close the window.",
         "qid": "Rspamd QID",
-        "qinfo": "The quarantine system will save rejected mail to the database (the sender will <em>not</em> be given the impression of a delivered mail) as well as mail, that is delivered as copy into the Junk folder of a mailbox.\r\n  <br>\"Learn as spam and delete\" will learn a message as spam via Bayesian theorem and also calculate fuzzy hashes to deny similar messages in the future.\r\n  <br>Please be aware that learning multiple messages can be - depending on your system - time consuming.<br>Blacklisted elements are excluded from the quarantine.",
+        "qinfo": "The quarantine system will save rejected mail to the database (the sender will <em>not</em> be given the impression of a delivered mail) as well as mail, that is delivered as copy into the Junk folder of a mailbox.\r\n  <br>\"Learn as spam and delete\" will learn a message as spam via Bayesian theorem and also calculate fuzzy hashes to deny similar messages in the future.\r\n  <br>Please be aware that learning multiple messages can be - depending on your system - time consuming.<br>Denylisted elements are excluded from the quarantine.",
         "qitem": "Quarantine item",
         "quarantine": "Quarantine",
         "quick_actions": "Actions",
@@ -1337,8 +1338,8 @@
         "spam_score_reset": "Reset to server default",
         "spamfilter": "Spam filter",
         "spamfilter_behavior": "Rating",
-        "spamfilter_bl": "Blacklist",
-        "spamfilter_bl_desc": "Blacklisted email addresses to <b>always</b> classify as spam and reject. Rejected mail will <b>not</b> be copied to quarantine. Wildcards may be used. A filter is only applied to direct aliases (aliases with a single target mailbox) excluding catch-all aliases and a mailbox itself.",
+        "spamfilter_bl": "Denylist",
+        "spamfilter_bl_desc": "Denylisted email addresses to <b>always</b> classify as spam and reject. Rejected mail will <b>not</b> be copied to quarantine. Wildcards may be used. A filter is only applied to direct aliases (aliases with a single target mailbox) excluding catch-all aliases and a mailbox itself.",
         "spamfilter_default_score": "Default values",
         "spamfilter_green": "Green: this message is not spam",
         "spamfilter_hint": "The first value describes the \"low spam score\", the second represents the \"high spam score\".",
@@ -1349,8 +1350,8 @@
         "spamfilter_table_empty": "No data to display",
         "spamfilter_table_remove": "remove",
         "spamfilter_table_rule": "Rule",
-        "spamfilter_wl": "Whitelist",
-        "spamfilter_wl_desc": "Whitelisted email addresses are programmed to <b>never</b> classify as spam. Wildcards may be used. A filter is only applied to direct aliases (aliases with a single target mailbox) excluding catch-all aliases and a mailbox itself.",
+        "spamfilter_wl": "Allowlist",
+        "spamfilter_wl_desc": "Allowlisted email addresses are programmed to <b>never</b> classify as spam. Wildcards may be used. A filter is only applied to direct aliases (aliases with a single target mailbox) excluding catch-all aliases and a mailbox itself.",
         "spamfilter_yellow": "Yellow: this message may be spam, will be tagged as spam and moved to your junk folder",
         "status": "Status",
         "sync_jobs": "Sync jobs",

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

@@ -118,8 +118,8 @@
           <span class="d-none d-sm-inline"> - </span>
             {% if active_ban.queued_for_unban == 0 %}
             <a data-action="edit_selected" data-item="{{ active_ban.network }}" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"unban"}' href="#">[{{ lang.admin.queue_unban }}]</a>
-            <a data-action="edit_selected" data-item="{{ active_ban.network }}" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"whitelist"}' href="#">[whitelist]</a>
-            <a data-action="edit_selected" data-item="{{ active_ban.network }}" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"blacklist"}' href="#">[blacklist (<b>needs restart</b>)]</a>
+            <a data-action="edit_selected" data-item="{{ active_ban.network }}" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"whitelist"}' href="#">[allowlist]</a>
+            <a data-action="edit_selected" data-item="{{ active_ban.network }}" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"blacklist"}' href="#">[denylist (<b>{{ lang.admin.needs_restart }}</b>)]</a>
             {% else %}
             <i>{{ lang.admin.unban_pending }}</i>
             {% endif %}

+ 1 - 1
docker-compose.yml

@@ -477,7 +477,7 @@ services:
             - acme
 
     netfilter-mailcow:
-      image: ghcr.io/mailcow/netfilter:1.61
+      image: ghcr.io/mailcow/netfilter:1.62
       stop_grace_period: 30s
       restart: always
       privileged: true