git subrepo commit (merge) mailcow/src/mailcow-dockerized
subrepo: subdir: "mailcow/src/mailcow-dockerized"
merged: "2866fb80"
upstream: origin: "https://github.com/mailcow/mailcow-dockerized.git"
branch: "master"
commit: "a366494c"
git-subrepo: version: "0.4.6"
origin: "???"
commit: "???"
Change-Id: I26ce31f84c1ff9e905669570f9fc7eb754ce6c1c
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/acme/acme.sh b/mailcow/src/mailcow-dockerized/data/Dockerfiles/acme/acme.sh
index a45f5a5..1cd456a 100755
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/acme/acme.sh
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/acme/acme.sh
@@ -219,6 +219,8 @@
IPV4=$(get_ipv4)
IPV6=$(get_ipv6)
log_f "OK: ${IPV4}, ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}"
+ fi
+
#########################################
# IP and webroot challenge verification #
SQL_DOMAINS=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain WHERE backupmx=0 and active=1" -Bs)
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/clamd/Dockerfile b/mailcow/src/mailcow-dockerized/data/Dockerfiles/clamd/Dockerfile
index 1fbcfda..31a332d 100644
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/clamd/Dockerfile
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/clamd/Dockerfile
@@ -1,7 +1,7 @@
FROM clamav/clamav:1.0.3_base
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
- && apk add --update --no-cache \
+
RUN apk upgrade --no-cache \
&& apk add --update --no-cache \
rsync \
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/dockerapi/modules/DockerApi.py b/mailcow/src/mailcow-dockerized/data/Dockerfiles/dockerapi/modules/DockerApi.py
index ea1c104..3ca4560 100644
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/dockerapi/modules/DockerApi.py
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/dockerapi/modules/DockerApi.py
@@ -159,7 +159,7 @@
postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
# todo: check each exit code
res = { 'type': 'success', 'msg': 'Scheduled immediate delivery'}
- return Response(content=json.dumps(res, indent=4), media_type="application/json")
+ return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: mailq - task: list
def container_post__exec__mailq__list(self, request_json, **kwargs):
if 'container_id' in kwargs:
@@ -231,43 +231,10 @@
return "0,0,0,0,0,0"
# api call: container_post - post_action: exec - cmd: system - task: mysql_upgrade
def container_post__exec__system__mysql_upgrade(self, request_json, **kwargs):
- if 'container_id' in kwargs:
- filters = {"id": kwargs['container_id']}
- elif 'container_name' in kwargs:
- filters = {"name": kwargs['container_name']}
-
- for container in self.sync_docker_client.containers.list(filters=filters):
- sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"], user='mysql')
- if sql_return.exit_code == 0:
- matched = False
- for line in sql_return.output.decode('utf-8').split("\n"):
- if 'is already upgraded to' in line:
- matched = True
- if matched:
- res = { 'type': 'success', 'msg':'mysql_upgrade: already upgraded', 'text': sql_return.output.decode('utf-8')}
- return Response(content=json.dumps(res, indent=4), media_type="application/json")
- else:
- container.restart()
- res = { 'type': 'warning', 'msg':'mysql_upgrade: upgrade was applied', 'text': sql_return.output.decode('utf-8')}
- return Response(content=json.dumps(res, indent=4), media_type="application/json")
- else:
- res = { 'type': 'error', 'msg': 'mysql_upgrade: error running command', 'text': sql_return.output.decode('utf-8')}
- return Response(content=json.dumps(res, indent=4), media_type="application/json")
+ return Response(content=json.dumps(dict(type='success', msg='mysql_upgrade: not touching fake MySQL', text=''), indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: system - task: mysql_tzinfo_to_sql
def container_post__exec__system__mysql_tzinfo_to_sql(self, request_json, **kwargs):
- if 'container_id' in kwargs:
- filters = {"id": kwargs['container_id']}
- elif 'container_name' in kwargs:
- filters = {"name": kwargs['container_name']}
-
- for container in self.sync_docker_client.containers.list(filters=filters):
- sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_tzinfo_to_sql /usr/share/zoneinfo | /bin/sed 's/Local time zone must be set--see zic manual page/FCTY/' | /usr/bin/mysql -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "' mysql \n"], user='mysql')
- if sql_return.exit_code == 0:
- res = { 'type': 'info', 'msg': 'mysql_tzinfo_to_sql: command completed successfully', 'text': sql_return.output.decode('utf-8')}
- return Response(content=json.dumps(res, indent=4), media_type="application/json")
- else:
- res = { 'type': 'error', 'msg': 'mysql_tzinfo_to_sql: error running command', 'text': sql_return.output.decode('utf-8')}
- return Response(content=json.dumps(res, indent=4), media_type="application/json")
+ return Response(content=json.dumps(dict(type='success', msg='mysql_tzinfo_to_sql: not touching fake MySQL', text=''), indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: reload - task: dovecot
def container_post__exec__reload__dovecot(self, request_json, **kwargs):
if 'container_id' in kwargs:
@@ -318,7 +285,7 @@
if 'username' in request_json and 'script_name' in request_json:
for container in self.sync_docker_client.containers.list(filters=filters):
- cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"]
+ cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"]
sieve_return = container.exec_run(cmd)
return self.exec_run_handler('utf8_text_only', sieve_return)
# api call: container_post - post_action: exec - cmd: maildir - task: cleanup
@@ -462,7 +429,7 @@
except:
pass
return ''.join(total_data)
-
+
try :
socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock
if not cmd.endswith("\n"):
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/dovecot/Dockerfile b/mailcow/src/mailcow-dockerized/data/Dockerfiles/dovecot/Dockerfile
index 6249302..90a6af9 100644
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/dovecot/Dockerfile
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/dovecot/Dockerfile
@@ -7,7 +7,6 @@
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG GOSU_VERSION=1.16
ENV LC_ALL C
-ENV GOSU_VERSION 1.14
# Add groups and users before installing Dovecot to not break compatibility
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/netfilter/Dockerfile b/mailcow/src/mailcow-dockerized/data/Dockerfiles/netfilter/Dockerfile
index 1ebee4c..4fcb5ee 100644
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/netfilter/Dockerfile
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/netfilter/Dockerfile
@@ -22,8 +22,6 @@
redis \
ipaddress \
dnspython \
- ipaddress \
- dnspython \
&& apk del .build-deps
# && pip3 install --upgrade pip python-iptables==0.13.0 redis ipaddress dnspython \
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/netfilter/server.py b/mailcow/src/mailcow-dockerized/data/Dockerfiles/netfilter/server.py
index 9767994..942b258 100644
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/netfilter/server.py
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/netfilter/server.py
@@ -1,531 +1,523 @@
-#!/usr/bin/env python3
-
-import re
-import os
-import sys
-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
-exit_code = 0
-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
- global exit_code
-
- f2boptions = {}
-
- if not r.get('F2B_OPTIONS'):
- 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 = 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
- global exit_code
- if not r.get('F2B_REGEX'):
- f2bregex = {}
- f2bregex[1] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
- f2bregex[2] = 'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'
- f2bregex[3] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+'
- f2bregex[4] = 'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+'
- f2bregex[5] = 'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+'
- f2bregex[6] = '-login: Disconnected.+ \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
- f2bregex[7] = '-login: Aborted login.+ \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
- f2bregex[8] = '-login: Aborted login.+ \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
- f2bregex[9] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
- f2bregex[10] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
- 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
- exit_code = 2
-
-if r.exists('F2B_LOG'):
- r.rename('F2B_LOG', 'NETFILTER_LOG')
-
-def mailcowChainOrder():
- global lock
- global quit_now
- global exit_code
- 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
- exit_code = 2
- if not target_found:
- logCrit('Error in %s chain: MAILCOW target not found, restarting container' % (chain.name))
- quit_now = True
- exit_code = 2
-
-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'])
- 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:
- bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0}
-
- bans[net]['attempts'] += 1
- bans[net]['last_attempt'] = time.time()
-
- if bans[net]['attempts'] >= MAX_ATTEMPTS:
- cur_time = int(round(time.time()))
- 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')
- 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 + 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))
-
-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:
- bans[net]['attempts'] = 0
- bans[net]['ban_counter'] += 1
-
-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
- r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
-
-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')
-
- global quit_now
- global exit_code
-
- while not quit_now:
- try:
- 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)
- except Exception as ex:
- logWarn('Error reading log line from pubsub: %s' % ex)
- quit_now = True
- exit_code = 2
-
-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
- match = rule.create_match("comment")
- match.comment = f'{int(round(time.time()))}'
- 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
- new_rule = get_snat4_rule()
-
- if not chain.rules:
- # if there are no rules in the chain, insert the new rule directly
- logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
- chain.insert_rule(new_rule)
- new_rule.target.name == rule.target.name
- ))
- if position == 0:
- if not match:
- logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
- chain.insert_rule(new_rule)
- else:
- for position, rule in enumerate(chain.rules):
- if not hasattr(rule.target, 'parameter'):
- continue
- match = all((
- new_rule.get_src() == rule.get_src(),
- new_rule.get_dst() == rule.get_dst(),
- new_rule.target.parameters == rule.target.parameters,
- new_rule.target.name == rule.target.name
- ))
- if position == 0:
- if not match:
- logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
- chain.insert_rule(new_rule)
- else:
- if match:
- logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}')
- chain.delete_rule(rule)
-
- table.commit()
- table.autocommit = True
- except:
- print('Error running SNAT4, retrying...')
-
- print('Error running SNAT6, retrying...')
-def autopurge():
- while not quit_now:
- 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:
- for net in QUEUE_UNBAN:
- unban(str(net))
- for net in bans.copy():
- if bans[net]['attempts'] >= MAX_ATTEMPTS:
- 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):
- 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)
-
- sys.exit(exit_code)
+#!/usr/bin/env python3
+
+import re
+import os
+import sys
+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
+exit_code = 0
+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
+ global exit_code
+
+ f2boptions = {}
+
+ if not r.get('F2B_OPTIONS'):
+ 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 = 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
+ global exit_code
+ if not r.get('F2B_REGEX'):
+ f2bregex = {}
+ f2bregex[1] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
+ f2bregex[2] = 'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'
+ f2bregex[3] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+'
+ f2bregex[4] = 'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+'
+ f2bregex[5] = 'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+'
+ f2bregex[6] = '-login: Disconnected.+ \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
+ f2bregex[7] = '-login: Aborted login.+ \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
+ f2bregex[8] = '-login: Aborted login.+ \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
+ f2bregex[9] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
+ f2bregex[10] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
+ 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
+ exit_code = 2
+
+if r.exists('F2B_LOG'):
+ r.rename('F2B_LOG', 'NETFILTER_LOG')
+
+def mailcowChainOrder():
+ global lock
+ global quit_now
+ global exit_code
+ 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
+ exit_code = 2
+ if not target_found:
+ logCrit('Error in %s chain: MAILCOW target not found, restarting container' % (chain.name))
+ quit_now = True
+ exit_code = 2
+
+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'])
+ 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:
+ bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0}
+
+ bans[net]['attempts'] += 1
+ bans[net]['last_attempt'] = time.time()
+
+ if bans[net]['attempts'] >= MAX_ATTEMPTS:
+ cur_time = int(round(time.time()))
+ 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')
+ 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 + 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))
+
+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:
+ bans[net]['attempts'] = 0
+ bans[net]['ban_counter'] += 1
+
+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')
+
+ global quit_now
+ global exit_code
+
+ while not quit_now:
+ try:
+ 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)
+ except Exception as ex:
+ logWarn('Error reading log line from pubsub: %s' % ex)
+ quit_now = True
+ exit_code = 2
+
+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
+ match = rule.create_match("comment")
+ match.comment = f'{int(round(time.time()))}'
+ 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
+ new_rule = get_snat4_rule()
+
+ if not chain.rules:
+ # if there are no rules in the chain, insert the new rule directly
+ logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
+ chain.insert_rule(new_rule)
+ else:
+ for position, rule in enumerate(chain.rules):
+ if not hasattr(rule.target, 'parameter'):
+ continue
+ match = all((
+ new_rule.get_src() == rule.get_src(),
+ new_rule.get_dst() == rule.get_dst(),
+ new_rule.target.parameters == rule.target.parameters,
+ new_rule.target.name == rule.target.name
+ ))
+ if position == 0:
+ if not match:
+ logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
+ chain.insert_rule(new_rule)
+ else:
+ if match:
+ logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}')
+ chain.delete_rule(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_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:
+ for net in QUEUE_UNBAN:
+ unban(str(net))
+ for net in bans.copy():
+ if bans[net]['attempts'] >= MAX_ATTEMPTS:
+ 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):
+ 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)
+
+ sys.exit(exit_code)
diff --git a/mailcow/src/mailcow-dockerized/data/Dockerfiles/watchdog/watchdog.sh b/mailcow/src/mailcow-dockerized/data/Dockerfiles/watchdog/watchdog.sh
index 086e326..231d0ec 100755
--- a/mailcow/src/mailcow-dockerized/data/Dockerfiles/watchdog/watchdog.sh
+++ b/mailcow/src/mailcow-dockerized/data/Dockerfiles/watchdog/watchdog.sh
@@ -269,8 +269,8 @@
touch /tmp/unbound-mailcow; echo "$(tail -50 /tmp/unbound-mailcow)" > /tmp/unbound-mailcow
host_ip=$(get_container_ip unbound-mailcow)
err_c_cur=${err_count}
- /usr/bin/nslookup -sil stackoverflow.com "${host_ip}" 2>> /tmp/unbound-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
- DNSSEC=$(dig com +dnssec "@${host_ip}" | egrep 'flags:.+ad')
+ /usr/lib/nagios/plugins/check_dns -s ${host_ip} -H stackoverflow.com 2>> /tmp/unbound-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
+ DNSSEC=$(dig com +dnssec | egrep 'flags:.+ad')
if [[ -z ${DNSSEC} ]]; then
echo "DNSSEC failure" 2>> /tmp/unbound-mailcow 1>&2
err_count=$(( ${err_count} + 1))