Skip to content

Commit 087481a

Browse files
Merge pull request #5696 from mailcow/fix/netfilter
[Netfilter] add mailcow isolation rule to MAILCOW chain
2 parents b968695 + c941e80 commit 087481a

File tree

10 files changed

+314
-58
lines changed

10 files changed

+314
-58
lines changed

Diff for: .gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ data/conf/dovecot/acl_anyone
1313
data/conf/dovecot/dovecot-master.passwd
1414
data/conf/dovecot/dovecot-master.userdb
1515
data/conf/dovecot/extra.conf
16+
data/conf/dovecot/mail_replica.conf
1617
data/conf/dovecot/global_sieve_*
1718
data/conf/dovecot/last_login
1819
data/conf/dovecot/lua

Diff for: data/Dockerfiles/dovecot/docker-entrypoint.sh

+9
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,15 @@ sys.exit()
335335
EOF
336336
fi
337337

338+
# Set mail_replica for HA setups
339+
if [[ -n ${MAILCOW_REPLICA_IP} && -n ${DOVEADM_REPLICA_PORT} ]]; then
340+
cat <<EOF > /etc/dovecot/mail_replica.conf
341+
# Autogenerated by mailcow
342+
mail_replica = tcp:${MAILCOW_REPLICA_IP}:${DOVEADM_REPLICA_PORT}
343+
EOF
344+
fi
345+
346+
338347
# 401 is user dovecot
339348
if [[ ! -s /mail_crypt/ecprivkey.pem || ! -s /mail_crypt/ecpubkey.pem ]]; then
340349
openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem

Diff for: data/Dockerfiles/netfilter/main.py

+74-47
Original file line numberDiff line numberDiff line change
@@ -21,47 +21,17 @@
2121
from modules.NFTables import NFTables
2222

2323

24-
# connect to redis
25-
while True:
26-
try:
27-
redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
28-
redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '')
29-
if "".__eq__(redis_slaveof_ip):
30-
r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0)
31-
else:
32-
r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0)
33-
r.ping()
34-
except Exception as ex:
35-
print('%s - trying again in 3 seconds' % (ex))
36-
time.sleep(3)
37-
else:
38-
break
39-
pubsub = r.pubsub()
40-
41-
# rename fail2ban to netfilter
42-
if r.exists('F2B_LOG'):
43-
r.rename('F2B_LOG', 'NETFILTER_LOG')
44-
45-
4624
# globals
4725
WHITELIST = []
4826
BLACKLIST= []
4927
bans = {}
5028
quit_now = False
5129
exit_code = 0
5230
lock = Lock()
53-
54-
55-
# init Logger
56-
logger = Logger(r)
57-
# init backend
58-
backend = sys.argv[1]
59-
if backend == "nftables":
60-
logger.logInfo('Using NFTables backend')
61-
tables = NFTables("MAILCOW", logger)
62-
else:
63-
logger.logInfo('Using IPTables backend')
64-
tables = IPTables("MAILCOW", logger)
31+
chain_name = "MAILCOW"
32+
r = None
33+
pubsub = None
34+
clear_before_quit = False
6535

6636

6737
def refreshF2boptions():
@@ -250,17 +220,21 @@ def clear():
250220
with lock:
251221
tables.clearIPv4Table()
252222
tables.clearIPv6Table()
253-
r.delete('F2B_ACTIVE_BANS')
254-
r.delete('F2B_PERM_BANS')
255-
pubsub.unsubscribe()
223+
try:
224+
if r is not None:
225+
r.delete('F2B_ACTIVE_BANS')
226+
r.delete('F2B_PERM_BANS')
227+
except Exception as ex:
228+
logger.logWarn('Error clearing redis keys F2B_ACTIVE_BANS and F2B_PERM_BANS: %s' % ex)
256229

257230
def watch():
258-
logger.logInfo('Watching Redis channel F2B_CHANNEL')
259-
pubsub.subscribe('F2B_CHANNEL')
260-
231+
global pubsub
261232
global quit_now
262233
global exit_code
263234

235+
logger.logInfo('Watching Redis channel F2B_CHANNEL')
236+
pubsub.subscribe('F2B_CHANNEL')
237+
264238
while not quit_now:
265239
try:
266240
for item in pubsub.listen():
@@ -280,6 +254,7 @@ def watch():
280254
ban(addr)
281255
except Exception as ex:
282256
logger.logWarn('Error reading log line from pubsub: %s' % ex)
257+
pubsub = None
283258
quit_now = True
284259
exit_code = 2
285260

@@ -403,21 +378,76 @@ def blacklistUpdate():
403378
permBan(net=net, unban=True)
404379
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
405380

406-
def quit(signum, frame):
407-
global quit_now
408-
quit_now = True
381+
def sigterm_quit(signum, frame):
382+
global clear_before_quit
383+
clear_before_quit = True
384+
sys.exit(exit_code)
385+
386+
def berfore_quit():
387+
if clear_before_quit:
388+
clear()
389+
if pubsub is not None:
390+
pubsub.unsubscribe()
409391

410392

411393
if __name__ == '__main__':
412-
refreshF2boptions()
394+
atexit.register(berfore_quit)
395+
signal.signal(signal.SIGTERM, sigterm_quit)
396+
397+
# init Logger
398+
logger = Logger(None)
399+
400+
# init backend
401+
backend = sys.argv[1]
402+
if backend == "nftables":
403+
logger.logInfo('Using NFTables backend')
404+
tables = NFTables(chain_name, logger)
405+
else:
406+
logger.logInfo('Using IPTables backend')
407+
tables = IPTables(chain_name, logger)
408+
413409
# In case a previous session was killed without cleanup
414410
clear()
411+
415412
# Reinit MAILCOW chain
416413
# Is called before threads start, no locking
417414
logger.logInfo("Initializing mailcow netfilter chain")
418415
tables.initChainIPv4()
419416
tables.initChainIPv6()
420417

418+
if os.getenv("DISABLE_NETFILTER_ISOLATION_RULE").lower() in ("y", "yes"):
419+
logger.logInfo(f"Skipping {chain_name} isolation")
420+
else:
421+
logger.logInfo(f"Setting {chain_name} isolation")
422+
tables.create_mailcow_isolation_rule("br-mailcow", [3306, 6379, 8983, 12345], os.getenv("MAILCOW_REPLICA_IP"))
423+
424+
# connect to redis
425+
while True:
426+
try:
427+
redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
428+
redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '')
429+
if "".__eq__(redis_slaveof_ip):
430+
r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0)
431+
else:
432+
r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0)
433+
r.ping()
434+
pubsub = r.pubsub()
435+
except Exception as ex:
436+
print('%s - trying again in 3 seconds' % (ex))
437+
time.sleep(3)
438+
else:
439+
break
440+
Logger.r = r
441+
442+
# rename fail2ban to netfilter
443+
if r.exists('F2B_LOG'):
444+
r.rename('F2B_LOG', 'NETFILTER_LOG')
445+
# clear bans in redis
446+
r.delete('F2B_ACTIVE_BANS')
447+
r.delete('F2B_PERM_BANS')
448+
449+
refreshF2boptions()
450+
421451
watch_thread = Thread(target=watch)
422452
watch_thread.daemon = True
423453
watch_thread.start()
@@ -460,9 +490,6 @@ def quit(signum, frame):
460490
whitelistupdate_thread.daemon = True
461491
whitelistupdate_thread.start()
462492

463-
signal.signal(signal.SIGTERM, quit)
464-
atexit.register(clear)
465-
466493
while not quit_now:
467494
time.sleep(0.5)
468495

Diff for: data/Dockerfiles/netfilter/modules/IPTables.py

+39
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import iptc
22
import time
3+
import os
34

45
class IPTables:
56
def __init__(self, chain_name, logger):
@@ -211,3 +212,41 @@ def getSnat6Rule(self, snat_target, source):
211212
target = rule.create_target("SNAT")
212213
target.to_source = snat_target
213214
return rule
215+
216+
def create_mailcow_isolation_rule(self, _interface:str, _dports:list, _allow:str = ""):
217+
try:
218+
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name)
219+
220+
# insert mailcow isolation rule
221+
rule = iptc.Rule()
222+
rule.in_interface = f'! {_interface}'
223+
rule.out_interface = _interface
224+
rule.protocol = 'tcp'
225+
rule.create_target("DROP")
226+
match = rule.create_match("multiport")
227+
match.dports = ','.join(map(str, _dports))
228+
229+
if rule in chain.rules:
230+
chain.delete_rule(rule)
231+
chain.insert_rule(rule, position=0)
232+
233+
# insert mailcow isolation exception rule
234+
if _allow != "":
235+
rule = iptc.Rule()
236+
rule.src = _allow
237+
rule.in_interface = f'! {_interface}'
238+
rule.out_interface = _interface
239+
rule.protocol = 'tcp'
240+
rule.create_target("ACCEPT")
241+
match = rule.create_match("multiport")
242+
match.dports = ','.join(map(str, _dports))
243+
244+
if rule in chain.rules:
245+
chain.delete_rule(rule)
246+
chain.insert_rule(rule, position=0)
247+
248+
249+
return True
250+
except Exception as e:
251+
self.logger.logCrit(f"Error adding {self.chain_name} isolation: {e}")
252+
return False

Diff for: data/Dockerfiles/netfilter/modules/Logger.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ def log(self, priority, message):
1010
tolog['time'] = int(round(time.time()))
1111
tolog['priority'] = priority
1212
tolog['message'] = message
13-
self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
13+
if self.r:
14+
self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
1415
print(message)
1516

1617
def logWarn(self, message):

0 commit comments

Comments
 (0)