Mailcow: Remove any traces of IPv6.

In the Kata container pod that is used to run Mailcow, ip6tables fails
with messages such as:

    ipv6nat-mailcow_1    running [/sbin/ip6tables -t filter -S --wait]: exit status 3: ip6tables v1.8.4 (legacy): can't initialize ip6tables table `filter': Table does not exist (do you need to insmod?)
    ipv6nat-mailcow_1    Perhaps ip6tables or your kernel needs to be upgraded.

This is causing the Netfilter service to fail, breaking fail2ban.
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/netfilter/server.py b/mailcow/src/mailcow-dockerized/data/Dockerfiles/netfilter/server.py
index 36565db..5ec9269 100644
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/netfilter/server.py
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/netfilter/server.py
@@ -1,557 +1,470 @@
-#!/usr/bin/env python3

-

-import re

-import os

-import time

-import atexit

-import signal

-import ipaddress

-from collections import Counter

-from random import randint

-from threading import Thread

-from threading import Lock

-import redis

-import json

-import iptc

-import dns.resolver

-import dns.exception

-

-while True:

-  try:

-    redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')

-    redis_slaveof_port = os.getenv('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)

-    else:

-      r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0)

-    r.ping()

-  except Exception as ex:

-    print('%s - trying again in 3 seconds'  % (ex))

-    time.sleep(3)

-  else:

-    break

-

-pubsub = r.pubsub()

-

-WHITELIST = []

-BLACKLIST= []

-

-bans = {}

-

-quit_now = False

-lock = Lock()

-

-def log(priority, message):

-  tolog = {}

-  tolog['time'] = int(round(time.time()))

-  tolog['priority'] = priority

-  tolog['message'] = message

-  r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))

-  print(message)

-

-def logWarn(message):

-  log('warn', message)

-

-def logCrit(message):

-  log('crit', message)

-

-def logInfo(message):

-  log('info', message)

-

-def refreshF2boptions():

-  global f2boptions

-  global quit_now

-  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))

-  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

-

-def refreshF2bregex():

-  global f2bregex

-  global quit_now

-  if not r.get('F2B_REGEX'):

-    f2bregex = {}

-    f2bregex[1] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed'

-    f2bregex[2] = '-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'

-    f2bregex[3] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'

-    f2bregex[4] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'

-    f2bregex[5] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'

-    f2bregex[6] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'

-    f2bregex[7] = 'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'

-    f2bregex[8] = '-login: Aborted login \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'

-    r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False))

-  else:

-    try:

-      f2bregex = {}

-      f2bregex = json.loads(r.get('F2B_REGEX'))

-    except ValueError:

-      print('Error loading F2B options: F2B_REGEX is not json')

-      quit_now = True

-

-if r.exists('F2B_LOG'):

-  r.rename('F2B_LOG', 'NETFILTER_LOG')

-

-def mailcowChainOrder():

-  global lock

-  global quit_now

-  while not quit_now:

-    time.sleep(10)

-    with lock:

-      filter4_table = iptc.Table(iptc.Table.FILTER)

-      filter6_table = iptc.Table6(iptc.Table6.FILTER)

-      filter4_table.refresh()

-      filter6_table.refresh()

-      for f in [filter4_table, filter6_table]:

-        forward_chain = iptc.Chain(f, 'FORWARD')

-        input_chain = iptc.Chain(f, 'INPUT')

-        for chain in [forward_chain, input_chain]:

-          target_found = False

-          for position, item in enumerate(chain.rules):

-            if item.target.name == 'MAILCOW':

-              target_found = True

-              if position > 2:

-                logCrit('Error in %s chain order: MAILCOW on position %d, restarting container' % (chain.name, position))

-                quit_now = True

-          if not target_found:

-            logCrit('Error in %s chain: MAILCOW target not found, restarting container' % (chain.name))

-            quit_now = True

-

-def ban(address):

-  global lock

-  refreshF2boptions()

-  BAN_TIME = int(f2boptions['ban_time'])

-  MAX_ATTEMPTS = int(f2boptions['max_attempts'])

-  RETRY_WINDOW = int(f2boptions['retry_window'])

-  NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])

-  NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])

-

-  ip = ipaddress.ip_address(address)

-  if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:

-    ip = ip.ipv4_mapped

-    address = str(ip)

-  if ip.is_private or ip.is_loopback:

-    return

-

-  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):

-        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)

-  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']

-

-  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))

-    if type(ip) is ipaddress.IPv4Address:

-      with lock:

-        chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')

-        rule = iptc.Rule()

-        rule.src = net

-        target = iptc.Target(rule, "REJECT")

-        rule.target = target

-        if rule not in chain.rules:

-          chain.insert_rule(rule)

-    else:

-      with lock:

-        chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')

-        rule = iptc.Rule6()

-        rule.src = net

-        target = iptc.Target(rule, "REJECT")

-        rule.target = target

-        if rule not in chain.rules:

-          chain.insert_rule(rule)

-    r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + BAN_TIME)

-  else:

-    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

-  if not net in bans:

-   logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net)

-   r.hdel('F2B_QUEUE_UNBAN', '%s' % net)

-   return

-  logInfo('Unbanning %s' % net)

-  if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:

-    with lock:

-      chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')

-      rule = iptc.Rule()

-      rule.src = net

-      target = iptc.Target(rule, "REJECT")

-      rule.target = target

-      if rule in chain.rules:

-        chain.delete_rule(rule)

-  else:

-    with lock:

-      chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')

-      rule = iptc.Rule6()

-      rule.src = net

-      target = iptc.Target(rule, "REJECT")

-      rule.target = target

-      if rule in chain.rules:

-        chain.delete_rule(rule)

-  r.hdel('F2B_ACTIVE_BANS', '%s' % net)

-  r.hdel('F2B_QUEUE_UNBAN', '%s' % net)

-  if net in bans:

-    del bans[net]

-

-def permBan(net, unban=False):

-  global lock

-  if type(ipaddress.ip_network(net, strict=False)) is ipaddress.IPv4Network:

-    with lock:

-      chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')

-      rule = iptc.Rule()

-      rule.src = net

-      target = iptc.Target(rule, "REJECT")

-      rule.target = target

-      if rule not in chain.rules and not unban:

-        logCrit('Add host/network %s to blacklist' % net)

-        chain.insert_rule(rule)

-        r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time()))) 

-      elif rule in chain.rules and unban:

-        logCrit('Remove host/network %s from blacklist' % net)

-        chain.delete_rule(rule)

-        r.hdel('F2B_PERM_BANS', '%s' % net)

-  else:

-    with lock:

-      chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')

-      rule = iptc.Rule6()

-      rule.src = net

-      target = iptc.Target(rule, "REJECT")

-      rule.target = target

-      if rule not in chain.rules and not unban:

-        logCrit('Add host/network %s to blacklist' % net)

-        chain.insert_rule(rule)

-        r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time()))) 

-      elif rule in chain.rules and unban:

-        logCrit('Remove host/network %s from blacklist' % net)

-        chain.delete_rule(rule)

-        r.hdel('F2B_PERM_BANS', '%s' % net)

-

-def quit(signum, frame):

-  global quit_now

-  quit_now = True

-

-def clear():

-  global lock

-  logInfo('Clearing all bans')

-  for net in bans.copy():

-    unban(net)

-  with lock:

-    filter4_table = iptc.Table(iptc.Table.FILTER)

-    filter6_table = iptc.Table6(iptc.Table6.FILTER)

-    for filter_table in [filter4_table, filter6_table]:

-      filter_table.autocommit = False

-      forward_chain = iptc.Chain(filter_table, "FORWARD")

-      input_chain = iptc.Chain(filter_table, "INPUT")

-      mailcow_chain = iptc.Chain(filter_table, "MAILCOW")

-      if mailcow_chain in filter_table.chains:

-        for rule in mailcow_chain.rules:

-          mailcow_chain.delete_rule(rule)

-        for rule in forward_chain.rules:

-          if rule.target.name == 'MAILCOW':

-            forward_chain.delete_rule(rule)

-        for rule in input_chain.rules:

-          if rule.target.name == 'MAILCOW':

-            input_chain.delete_rule(rule)

-        filter_table.delete_chain("MAILCOW")

-      filter_table.commit()

-      filter_table.refresh()

-      filter_table.autocommit = True

-    r.delete('F2B_ACTIVE_BANS')

-    r.delete('F2B_PERM_BANS')

-    pubsub.unsubscribe()

-

-def watch():

-  logInfo('Watching Redis channel F2B_CHANNEL')

-  pubsub.subscribe('F2B_CHANNEL')

-

-  while not quit_now:

-    for item in pubsub.listen():

-      refreshF2bregex()

-      for rule_id, rule_regex in f2bregex.items():

-        if item['data'] and item['type'] == 'message':

-          try:

-            result = re.search(rule_regex, item['data'])

-          except re.error:

-            result = False

-          if result:

-            addr = result.group(1)

-            ip = ipaddress.ip_address(addr)

-            if ip.is_private or ip.is_loopback:

-              continue

-            logWarn('%s matched rule id %s (%s)' % (addr, rule_id, item['data']))

-            ban(addr)

-

-def snat4(snat_target):

-  global lock

-  global quit_now

-

-  def get_snat4_rule():

-    rule = iptc.Rule()

-    rule.src = os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24'

-    rule.dst = '!' + rule.src

-    target = rule.create_target("SNAT")

-    target.to_source = snat_target

-    return rule

-

-  while not quit_now:

-    time.sleep(10)

-    with lock:

-      try:

-        table = iptc.Table('nat')

-        table.refresh()

-        chain = iptc.Chain(table, 'POSTROUTING')

-        table.autocommit = False

-        if get_snat4_rule() not in chain.rules:

-          logCrit('Added POSTROUTING rule for source network %s to SNAT target %s' % (get_snat4_rule().src, snat_target))

-          chain.insert_rule(get_snat4_rule())

-          table.commit()

-        else:

-          for position, item in enumerate(chain.rules):

-            if item == get_snat4_rule():

-              if position != 0:

-                chain.delete_rule(get_snat4_rule())

-          table.commit()

-        table.autocommit = True

-      except:

-        print('Error running SNAT4, retrying...') 

-

-def snat6(snat_target):

-  global lock

-  global quit_now

-

-  def get_snat6_rule():

-    rule = iptc.Rule6()

-    rule.src = os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64')

-    rule.dst = '!' + rule.src

-    target = rule.create_target("SNAT")

-    target.to_source = snat_target

-    return rule

-

-  while not quit_now:

-    time.sleep(10)

-    with lock:

-      try:

-        table = iptc.Table6('nat')

-        table.refresh()

-        chain = iptc.Chain(table, 'POSTROUTING')

-        table.autocommit = False

-        if get_snat6_rule() not in chain.rules:

-          logInfo('Added POSTROUTING rule for source network %s to SNAT target %s' % (get_snat6_rule().src, snat_target))

-          chain.insert_rule(get_snat6_rule())

-          table.commit()

-        else:

-          for position, item in enumerate(chain.rules):

-            if item == get_snat6_rule():

-              if position != 0:

-                chain.delete_rule(get_snat6_rule())

-          table.commit()

-        table.autocommit = True

-      except:

-        print('Error running SNAT6, retrying...') 

-

-def autopurge():

-  while not quit_now:

-    time.sleep(10)

-    refreshF2boptions()

-    BAN_TIME = int(f2boptions['ban_time'])

-    MAX_ATTEMPTS = int(f2boptions['max_attempts'])

-    QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')

-    if QUEUE_UNBAN:

-      for net in QUEUE_UNBAN:

-        unban(str(net))

-    for net in bans.copy():

-      if bans[net]['attempts'] >= MAX_ATTEMPTS:

-        if time.time() - bans[net]['last_attempt'] > BAN_TIME:

-          unban(net)

-

-def isIpNetwork(address):

-  try:

-    ipaddress.ip_network(address, False)

-  except ValueError:

-    return False

-  return True

-

-

-def genNetworkList(list):

-  resolver = dns.resolver.Resolver()

-  hostnames = []

-  networks = []

-  for key in list:

-    if isIpNetwork(key):

-      networks.append(key)

-    else:

-      hostnames.append(key)

-  for hostname in hostnames:

-    hostname_ips = []

-    for rdtype in ['A', 'AAAA']:

-      try:

-        answer = resolver.resolve(qname=hostname, rdtype=rdtype, lifetime=3)

-      except dns.exception.Timeout:

-        logInfo('Hostname %s timedout on resolve' % hostname)

-        break

-      except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):

-        continue

-      except dns.exception.DNSException as dnsexception:

-        logInfo('%s' % dnsexception)

-        continue

-      for rdata in answer:

-        hostname_ips.append(rdata.to_text())

-    networks.extend(hostname_ips)

-  return set(networks)

-

-def whitelistUpdate():

-  global lock

-  global quit_now

-  global WHITELIST

-  while not quit_now:

-    start_time = time.time()

-    list = r.hgetall('F2B_WHITELIST')

-    new_whitelist = []

-    if list:

-      new_whitelist = genNetworkList(list)

-    with lock:

-      if Counter(new_whitelist) != Counter(WHITELIST):

-        WHITELIST = new_whitelist

-        logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))

-    time.sleep(60.0 - ((time.time() - start_time) % 60.0)) 

-

-def blacklistUpdate():

-  global quit_now

-  global BLACKLIST

-  while not quit_now:

-    start_time = time.time()

-    list = r.hgetall('F2B_BLACKLIST')

-    new_blacklist = []

-    if list:

-      new_blacklist = genNetworkList(list)

-    if Counter(new_blacklist) != Counter(BLACKLIST): 

-      addban = set(new_blacklist).difference(BLACKLIST)

-      delban = set(BLACKLIST).difference(new_blacklist)

-      BLACKLIST = new_blacklist

-      logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST))

-      if addban:

-        for net in addban:

-          permBan(net=net)

-      if delban:

-        for net in delban:

-          permBan(net=net, unban=True)

-    time.sleep(60.0 - ((time.time() - start_time) % 60.0)) 

-

-def initChain():

-  # Is called before threads start, no locking

-  print("Initializing mailcow netfilter chain")

-  # IPv4

-  if not iptc.Chain(iptc.Table(iptc.Table.FILTER), "MAILCOW") in iptc.Table(iptc.Table.FILTER).chains:

-    iptc.Table(iptc.Table.FILTER).create_chain("MAILCOW")

-  for c in ['FORWARD', 'INPUT']:

-    chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c)

-    rule = iptc.Rule()

-    rule.src = '0.0.0.0/0'

-    rule.dst = '0.0.0.0/0'

-    target = iptc.Target(rule, "MAILCOW")

-    rule.target = target

-    if rule not in chain.rules:

-      chain.insert_rule(rule)

-  # IPv6

-  if not iptc.Chain(iptc.Table6(iptc.Table6.FILTER), "MAILCOW") in iptc.Table6(iptc.Table6.FILTER).chains:

-    iptc.Table6(iptc.Table6.FILTER).create_chain("MAILCOW")

-  for c in ['FORWARD', 'INPUT']:

-    chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c)

-    rule = iptc.Rule6()

-    rule.src = '::/0'

-    rule.dst = '::/0'

-    target = iptc.Target(rule, "MAILCOW")

-    rule.target = target

-    if rule not in chain.rules:

-      chain.insert_rule(rule)

-

-if __name__ == '__main__':

-

-  # In case a previous session was killed without cleanup

-  clear()

-  # Reinit MAILCOW chain

-  initChain()

-

-  watch_thread = Thread(target=watch)

-  watch_thread.daemon = True

-  watch_thread.start()

-

-  if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') != 'n':

-    try:

-      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.daemon = True

-        snat4_thread.start()

-    except ValueError:

-      print(os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address')

-

-  if os.getenv('SNAT6_TO_SOURCE') and os.getenv('SNAT6_TO_SOURCE') != 'n':

-    try:

-      snat_ip = os.getenv('SNAT6_TO_SOURCE')

-      snat_ipo = ipaddress.ip_address(snat_ip)

-      if type(snat_ipo) is ipaddress.IPv6Address:

-        snat6_thread = Thread(target=snat6,args=(snat_ip,))

-        snat6_thread.daemon = True

-        snat6_thread.start()

-    except ValueError:

-      print(os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address')

-

-  autopurge_thread = Thread(target=autopurge)

-  autopurge_thread.daemon = True

-  autopurge_thread.start()

-

-  mailcowchainwatch_thread = Thread(target=mailcowChainOrder)

-  mailcowchainwatch_thread.daemon = True

-  mailcowchainwatch_thread.start()

-

-  blacklistupdate_thread = Thread(target=blacklistUpdate)

-  blacklistupdate_thread.daemon = True

-  blacklistupdate_thread.start()

-

-  whitelistupdate_thread = Thread(target=whitelistUpdate)

-  whitelistupdate_thread.daemon = True

-  whitelistupdate_thread.start()

-

-  signal.signal(signal.SIGTERM, quit)

-  atexit.register(clear)

-

-  while not quit_now:

-    time.sleep(0.5)

+#!/usr/bin/env python3
+
+import re
+import os
+import time
+import atexit
+import signal
+import ipaddress
+from collections import Counter
+from random import randint
+from threading import Thread
+from threading import Lock
+import redis
+import json
+import iptc
+import dns.resolver
+import dns.exception
+
+while True:
+  try:
+    redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
+    redis_slaveof_port = os.getenv('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)
+    else:
+      r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0)
+    r.ping()
+  except Exception as ex:
+    print('%s - trying again in 3 seconds'  % (ex))
+    time.sleep(3)
+  else:
+    break
+
+pubsub = r.pubsub()
+
+WHITELIST = []
+BLACKLIST= []
+
+bans = {}
+
+quit_now = False
+lock = Lock()
+
+def log(priority, message):
+  tolog = {}
+  tolog['time'] = int(round(time.time()))
+  tolog['priority'] = priority
+  tolog['message'] = message
+  r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
+  print(message)
+
+def logWarn(message):
+  log('warn', message)
+
+def logCrit(message):
+  log('crit', message)
+
+def logInfo(message):
+  log('info', message)
+
+def refreshF2boptions():
+  global f2boptions
+  global quit_now
+  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))
+  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
+
+def refreshF2bregex():
+  global f2bregex
+  global quit_now
+  if not r.get('F2B_REGEX'):
+    f2bregex = {}
+    f2bregex[1] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed'
+    f2bregex[2] = '-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
+    f2bregex[3] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
+    f2bregex[4] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
+    f2bregex[5] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
+    f2bregex[6] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
+    f2bregex[7] = 'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'
+    f2bregex[8] = '-login: Aborted login \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
+    r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False))
+  else:
+    try:
+      f2bregex = {}
+      f2bregex = json.loads(r.get('F2B_REGEX'))
+    except ValueError:
+      print('Error loading F2B options: F2B_REGEX is not json')
+      quit_now = True
+
+if r.exists('F2B_LOG'):
+  r.rename('F2B_LOG', 'NETFILTER_LOG')
+
+def mailcowChainOrder():
+  global lock
+  global quit_now
+  while not quit_now:
+    time.sleep(10)
+    with lock:
+      filter4_table = iptc.Table(iptc.Table.FILTER)
+      filter4_table.refresh()
+      for f in [filter4_table]:
+        forward_chain = iptc.Chain(f, 'FORWARD')
+        input_chain = iptc.Chain(f, 'INPUT')
+        for chain in [forward_chain, input_chain]:
+          target_found = False
+          for position, item in enumerate(chain.rules):
+            if item.target.name == 'MAILCOW':
+              target_found = True
+              if position > 2:
+                logCrit('Error in %s chain order: MAILCOW on position %d, restarting container' % (chain.name, position))
+                quit_now = True
+          if not target_found:
+            logCrit('Error in %s chain: MAILCOW target not found, restarting container' % (chain.name))
+            quit_now = True
+
+def ban(address):
+  global lock
+  refreshF2boptions()
+  BAN_TIME = int(f2boptions['ban_time'])
+  MAX_ATTEMPTS = int(f2boptions['max_attempts'])
+  RETRY_WINDOW = int(f2boptions['retry_window'])
+  NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
+  NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
+
+  ip = ipaddress.ip_address(address)
+  if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
+    ip = ip.ipv4_mapped
+    address = str(ip)
+  if ip.is_private or ip.is_loopback:
+    return
+
+  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):
+        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)
+  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']
+
+  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))
+    if type(ip) is ipaddress.IPv4Address:
+      with lock:
+        chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
+        rule = iptc.Rule()
+        rule.src = net
+        target = iptc.Target(rule, "REJECT")
+        rule.target = target
+        if rule not in chain.rules:
+          chain.insert_rule(rule)
+    else:
+      pass
+    r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + BAN_TIME)
+  else:
+    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
+  if not net in bans:
+   logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net)
+   r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
+   return
+  logInfo('Unbanning %s' % net)
+  if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
+    with lock:
+      chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
+      rule = iptc.Rule()
+      rule.src = net
+      target = iptc.Target(rule, "REJECT")
+      rule.target = target
+      if rule in chain.rules:
+        chain.delete_rule(rule)
+  else:
+    pass
+  r.hdel('F2B_ACTIVE_BANS', '%s' % net)
+  r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
+  if net in bans:
+    del bans[net]
+
+def permBan(net, unban=False):
+  global lock
+  if type(ipaddress.ip_network(net, strict=False)) is ipaddress.IPv4Network:
+    with lock:
+      chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
+      rule = iptc.Rule()
+      rule.src = net
+      target = iptc.Target(rule, "REJECT")
+      rule.target = target
+      if rule not in chain.rules and not unban:
+        logCrit('Add host/network %s to blacklist' % net)
+        chain.insert_rule(rule)
+        r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
+      elif rule in chain.rules and unban:
+        logCrit('Remove host/network %s from blacklist' % net)
+        chain.delete_rule(rule)
+        r.hdel('F2B_PERM_BANS', '%s' % net)
+  else:
+    pass
+
+def quit(signum, frame):
+  global quit_now
+  quit_now = True
+
+def clear():
+  global lock
+  logInfo('Clearing all bans')
+  for net in bans.copy():
+    unban(net)
+  with lock:
+    filter4_table = iptc.Table(iptc.Table.FILTER)
+    for filter_table in [filter4_table]:
+      filter_table.autocommit = False
+      forward_chain = iptc.Chain(filter_table, "FORWARD")
+      input_chain = iptc.Chain(filter_table, "INPUT")
+      mailcow_chain = iptc.Chain(filter_table, "MAILCOW")
+      if mailcow_chain in filter_table.chains:
+        for rule in mailcow_chain.rules:
+          mailcow_chain.delete_rule(rule)
+        for rule in forward_chain.rules:
+          if rule.target.name == 'MAILCOW':
+            forward_chain.delete_rule(rule)
+        for rule in input_chain.rules:
+          if rule.target.name == 'MAILCOW':
+            input_chain.delete_rule(rule)
+        filter_table.delete_chain("MAILCOW")
+      filter_table.commit()
+      filter_table.refresh()
+      filter_table.autocommit = True
+    r.delete('F2B_ACTIVE_BANS')
+    r.delete('F2B_PERM_BANS')
+    pubsub.unsubscribe()
+
+def watch():
+  logInfo('Watching Redis channel F2B_CHANNEL')
+  pubsub.subscribe('F2B_CHANNEL')
+
+  while not quit_now:
+    for item in pubsub.listen():
+      refreshF2bregex()
+      for rule_id, rule_regex in f2bregex.items():
+        if item['data'] and item['type'] == 'message':
+          try:
+            result = re.search(rule_regex, item['data'])
+          except re.error:
+            result = False
+          if result:
+            addr = result.group(1)
+            ip = ipaddress.ip_address(addr)
+            if ip.is_private or ip.is_loopback:
+              continue
+            logWarn('%s matched rule id %s (%s)' % (addr, rule_id, item['data']))
+            ban(addr)
+
+def snat4(snat_target):
+  global lock
+  global quit_now
+
+  def get_snat4_rule():
+    rule = iptc.Rule()
+    rule.src = os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24'
+    rule.dst = '!' + rule.src
+    target = rule.create_target("SNAT")
+    target.to_source = snat_target
+    return rule
+
+  while not quit_now:
+    time.sleep(10)
+    with lock:
+      try:
+        table = iptc.Table('nat')
+        table.refresh()
+        chain = iptc.Chain(table, 'POSTROUTING')
+        table.autocommit = False
+        if get_snat4_rule() not in chain.rules:
+          logCrit('Added POSTROUTING rule for source network %s to SNAT target %s' % (get_snat4_rule().src, snat_target))
+          chain.insert_rule(get_snat4_rule())
+          table.commit()
+        else:
+          for position, item in enumerate(chain.rules):
+            if item == get_snat4_rule():
+              if position != 0:
+                chain.delete_rule(get_snat4_rule())
+          table.commit()
+        table.autocommit = True
+      except:
+        print('Error running SNAT4, retrying...')
+
+def autopurge():
+  while not quit_now:
+    time.sleep(10)
+    refreshF2boptions()
+    BAN_TIME = int(f2boptions['ban_time'])
+    MAX_ATTEMPTS = int(f2boptions['max_attempts'])
+    QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
+    if QUEUE_UNBAN:
+      for net in QUEUE_UNBAN:
+        unban(str(net))
+    for net in bans.copy():
+      if bans[net]['attempts'] >= MAX_ATTEMPTS:
+        if time.time() - bans[net]['last_attempt'] > BAN_TIME:
+          unban(net)
+
+def isIpNetwork(address):
+  try:
+    ipaddress.ip_network(address, False)
+  except ValueError:
+    return False
+  return True
+
+
+def genNetworkList(list):
+  resolver = dns.resolver.Resolver()
+  hostnames = []
+  networks = []
+  for key in list:
+    if isIpNetwork(key):
+      networks.append(key)
+    else:
+      hostnames.append(key)
+  for hostname in hostnames:
+    hostname_ips = []
+    for rdtype in ['A', 'AAAA']:
+      try:
+        answer = resolver.resolve(qname=hostname, rdtype=rdtype, lifetime=3)
+      except dns.exception.Timeout:
+        logInfo('Hostname %s timedout on resolve' % hostname)
+        break
+      except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
+        continue
+      except dns.exception.DNSException as dnsexception:
+        logInfo('%s' % dnsexception)
+        continue
+      for rdata in answer:
+        hostname_ips.append(rdata.to_text())
+    networks.extend(hostname_ips)
+  return set(networks)
+
+def whitelistUpdate():
+  global lock
+  global quit_now
+  global WHITELIST
+  while not quit_now:
+    start_time = time.time()
+    list = r.hgetall('F2B_WHITELIST')
+    new_whitelist = []
+    if list:
+      new_whitelist = genNetworkList(list)
+    with lock:
+      if Counter(new_whitelist) != Counter(WHITELIST):
+        WHITELIST = new_whitelist
+        logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))
+    time.sleep(60.0 - ((time.time() - start_time) % 60.0))
+
+def blacklistUpdate():
+  global quit_now
+  global BLACKLIST
+  while not quit_now:
+    start_time = time.time()
+    list = r.hgetall('F2B_BLACKLIST')
+    new_blacklist = []
+    if list:
+      new_blacklist = genNetworkList(list)
+    if Counter(new_blacklist) != Counter(BLACKLIST):
+      addban = set(new_blacklist).difference(BLACKLIST)
+      delban = set(BLACKLIST).difference(new_blacklist)
+      BLACKLIST = new_blacklist
+      logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST))
+      if addban:
+        for net in addban:
+          permBan(net=net)
+      if delban:
+        for net in delban:
+          permBan(net=net, unban=True)
+    time.sleep(60.0 - ((time.time() - start_time) % 60.0))
+
+def initChain():
+  # Is called before threads start, no locking
+  print("Initializing mailcow netfilter chain")
+  # IPv4
+  if not iptc.Chain(iptc.Table(iptc.Table.FILTER), "MAILCOW") in iptc.Table(iptc.Table.FILTER).chains:
+    iptc.Table(iptc.Table.FILTER).create_chain("MAILCOW")
+  for c in ['FORWARD', 'INPUT']:
+    chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c)
+    rule = iptc.Rule()
+    rule.src = '0.0.0.0/0'
+    rule.dst = '0.0.0.0/0'
+    target = iptc.Target(rule, "MAILCOW")
+    rule.target = target
+    if rule not in chain.rules:
+      chain.insert_rule(rule)
+
+if __name__ == '__main__':
+
+  # In case a previous session was killed without cleanup
+  clear()
+  # Reinit MAILCOW chain
+  initChain()
+
+  watch_thread = Thread(target=watch)
+  watch_thread.daemon = True
+  watch_thread.start()
+
+  if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') != 'n':
+    try:
+      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.daemon = True
+        snat4_thread.start()
+    except ValueError:
+      print(os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address')
+
+  autopurge_thread = Thread(target=autopurge)
+  autopurge_thread.daemon = True
+  autopurge_thread.start()
+
+  mailcowchainwatch_thread = Thread(target=mailcowChainOrder)
+  mailcowchainwatch_thread.daemon = True
+  mailcowchainwatch_thread.start()
+
+  blacklistupdate_thread = Thread(target=blacklistUpdate)
+  blacklistupdate_thread.daemon = True
+  blacklistupdate_thread.start()
+
+  whitelistupdate_thread = Thread(target=whitelistUpdate)
+  whitelistupdate_thread.daemon = True
+  whitelistupdate_thread.start()
+
+  signal.signal(signal.SIGTERM, quit)
+  atexit.register(clear)
+
+  while not quit_now:
+    time.sleep(0.5)
diff --git a/mailcow/src/mailcow-dockerized/docker-compose.yml b/mailcow/src/mailcow-dockerized/docker-compose.yml
index ff1e5eb..72c8a20 100644
--- a/mailcow/src/mailcow-dockerized/docker-compose.yml
+++ b/mailcow/src/mailcow-dockerized/docker-compose.yml
@@ -382,7 +382,7 @@
             - acme
 
     netfilter-mailcow:
-      image: mailcow/netfilter:1.38
+      build: ./data/Dockerfiles/netfilter
       stop_grace_period: 30s
       depends_on:
         - dovecot-mailcow
@@ -517,36 +517,6 @@
           aliases:
             - olefy
 
-    ipv6nat-mailcow:
-      depends_on:
-        - unbound-mailcow
-        - mysql-mailcow
-        - redis-mailcow
-        - clamd-mailcow
-        - rspamd-mailcow
-        - php-fpm-mailcow
-        - sogo-mailcow
-        - dovecot-mailcow
-        - postfix-mailcow
-        - memcached-mailcow
-        - nginx-mailcow
-        - acme-mailcow
-        - netfilter-mailcow
-        - watchdog-mailcow
-        - dockerapi-mailcow
-        - solr-mailcow
-      environment:
-        - TZ=${TZ}
-      image: robbertkl/ipv6nat
-      security_opt:
-        - label=disable
-      restart: always
-      privileged: true
-      network_mode: "host"
-      volumes:
-        - /var/run/docker.sock:/var/run/docker.sock:ro
-        - /lib/modules:/lib/modules:ro
-
 networks:
   mailcow-network:
     driver: bridge