This commit is contained in:
Vicente 2023-09-13 21:00:47 +02:00 committed by GitHub
commit fad707f279
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 734 additions and 167 deletions

View File

@ -1,6 +1,8 @@
FROM alpine:3.17 FROM alpine:3.17
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
WORKDIR /app
ENV XTABLES_LIBDIR /usr/lib/xtables ENV XTABLES_LIBDIR /usr/lib/xtables
ENV PYTHON_IPTABLES_XTABLES_VERSION 12 ENV PYTHON_IPTABLES_XTABLES_VERSION 12
ENV IPTABLES_LIBDIR /usr/lib ENV IPTABLES_LIBDIR /usr/lib
@ -14,10 +16,13 @@ RUN apk add --virtual .build-deps \
iptables \ iptables \
ip6tables \ ip6tables \
xtables-addons \ xtables-addons \
nftables \
tzdata \ tzdata \
py3-pip \ py3-pip \
py3-nftables \
musl-dev \ musl-dev \
&& pip3 install --ignore-installed --upgrade pip \ && pip3 install --ignore-installed --upgrade pip \
jsonschema \
python-iptables \ python-iptables \
redis \ redis \
ipaddress \ ipaddress \
@ -26,5 +31,9 @@ RUN apk add --virtual .build-deps \
# && pip3 install --upgrade pip python-iptables==0.13.0 redis ipaddress dnspython \ # && pip3 install --upgrade pip python-iptables==0.13.0 redis ipaddress dnspython \
COPY server.py / COPY server.py /app/
CMD ["python3", "-u", "/server.py"] COPY ./netfilter.sh /app/
RUN chmod +x /app/netfilter.sh
CMD ["/bin/sh", "-c", "/app/netfilter.sh"]

View File

@ -0,0 +1,29 @@
#!/bin/sh
backend=iptables
nft list table ip filter &>/dev/null
nftables_found=$?
iptables -L &>/dev/null
iptables_found=$?
if [ $nftables_found -lt $iptables_found ]; then
backend=nftables
fi
if [ $nftables_found -gt $iptables_found ]; then
backend=iptables
fi
if [ $nftables_found -eq 0 ] && [ $nftables_found -eq $iptables_found ]; then
nftables_lines=$(nft list ruleset | wc -l)
iptables_lines=$(iptables-save | wc -l)
if [ $nftables_lines -gt $iptables_lines ]; then
backend=nftables
else
backend=iptables
fi
fi
exec python -u server.py $backend

View File

@ -11,8 +11,9 @@ from collections import Counter
from random import randint from random import randint
from threading import Thread from threading import Thread
from threading import Lock from threading import Lock
import redis
import json import json
import redis
import nftables
import iptc import iptc
import dns.resolver import dns.resolver
import dns.exception import dns.exception
@ -43,6 +44,10 @@ quit_now = False
exit_code = 0 exit_code = 0
lock = Lock() lock = Lock()
backend = sys.argv[1]
nft = None
nft_chain_names = {}
def log(priority, message): def log(priority, message):
tolog = {} tolog = {}
tolog['time'] = int(round(time.time())) tolog['time'] = int(round(time.time()))
@ -60,6 +65,17 @@ def logCrit(message):
def logInfo(message): def logInfo(message):
log('info', message) log('info', message)
#nftables
if backend == 'nftables':
logInfo('Using Nftables backend')
nft = nftables.Nftables()
nft.set_json_output(True)
nft.set_handle_output(True)
nft_chain_names = {'ip': {'filter': {'input': '', 'forward': ''}, 'nat': {'postrouting': ''} },
'ip6': {'filter': {'input': '', 'forward': ''}, 'nat': {'postrouting': ''} } }
else:
logInfo('Using Iptables backend')
def refreshF2boptions(): def refreshF2boptions():
global f2boptions global f2boptions
global quit_now global quit_now
@ -127,33 +143,430 @@ def refreshF2bregex():
if r.exists('F2B_LOG'): if r.exists('F2B_LOG'):
r.rename('F2B_LOG', 'NETFILTER_LOG') r.rename('F2B_LOG', 'NETFILTER_LOG')
# Nftables functions
def nft_exec_dict(query: dict):
global nft
if not query: return False
rc, output, error = nft.json_cmd(query)
if rc != 0:
#logCrit(f"Nftables Error: {error}")
return False
# Prevent returning False or empty string on commands that do not produce output
if rc == 0 and len(output) == 0:
return True
return output
def get_base_dict():
return {'nftables': [{ 'metainfo': { 'json_schema_version': 1} } ] }
def search_current_chains():
global nft_chain_names
nft_chain_priority = {'ip': {'filter': {'input': None, 'forward': None}, 'nat': {'postrouting': None} },
'ip6': {'filter': {'input': None, 'forward': None}, 'nat': {'postrouting': None} } }
# Command: 'nft list chains'
_list = {'list' : {'chains': 'null'} }
command = get_base_dict()
command['nftables'].append(_list)
kernel_ruleset = nft_exec_dict(command)
if kernel_ruleset:
for _object in kernel_ruleset['nftables']:
chain = _object.get("chain")
if not chain: continue
_family = chain['family']
_table = chain['table']
_hook = chain.get("hook")
_priority = chain.get("prio")
_name = chain['name']
if _family not in nft_chain_names: continue
if _table not in nft_chain_names[_family]: continue
if _hook not in nft_chain_names[_family][_table]: continue
if _priority is None: continue
_saved_priority = nft_chain_priority[_family][_table][_hook]
if _saved_priority is None or _priority < _saved_priority:
# at this point, we know the chain has:
# hook and priority set
# and it has the lowest priority
nft_chain_priority[_family][_table][_hook] = _priority
nft_chain_names[_family][_table][_hook] = _name
def search_for_chain(kernel_ruleset: dict, chain_name: str):
found = False
for _object in kernel_ruleset["nftables"]:
chain = _object.get("chain")
if not chain:
continue
ch_name = chain.get("name")
if ch_name == chain_name:
found = True
break
return found
def get_chain_dict(_family: str, _name: str):
# nft (add | create) chain [<family>] <table> <name>
_chain_opts = {'family': _family, 'table': 'filter', 'name': _name }
_add = {'add': {'chain': _chain_opts} }
final_chain = get_base_dict()
final_chain["nftables"].append(_add)
return final_chain
def get_mailcow_jump_rule_dict(_family: str, _chain: str):
_jump_rule = get_base_dict()
_expr_opt=[]
_expr_counter = {'family': _family, 'table': 'filter', 'packets': 0, 'bytes': 0}
_counter_dict = {'counter': _expr_counter}
_expr_opt.append(_counter_dict)
_jump_opts = {'jump': {'target': 'MAILCOW'} }
_expr_opt.append(_jump_opts)
_rule_params = {'family': _family,
'table': 'filter',
'chain': _chain,
'expr': _expr_opt,
'comment': "mailcow" }
_add_rule = {'insert': {'rule': _rule_params} }
_jump_rule["nftables"].append(_add_rule)
return _jump_rule
def insert_mailcow_chains(_family: str):
nft_input_chain = nft_chain_names[_family]['filter']['input']
nft_forward_chain = nft_chain_names[_family]['filter']['forward']
# Command: 'nft list table <family> filter'
_table_opts = {'family': _family, 'name': 'filter'}
_list = {'list': {'table': _table_opts} }
command = get_base_dict()
command['nftables'].append(_list)
kernel_ruleset = nft_exec_dict(command)
if kernel_ruleset:
# MAILCOW chain
if not search_for_chain(kernel_ruleset, "MAILCOW"):
cadena = get_chain_dict(_family, "MAILCOW")
if nft_exec_dict(cadena):
logInfo(f"MAILCOW {_family} chain created successfully.")
input_jump_found, forward_jump_found = False, False
for _object in kernel_ruleset["nftables"]:
if not _object.get("rule"):
continue
rule = _object["rule"]
if nft_input_chain and rule["chain"] == nft_input_chain:
if rule.get("comment") and rule["comment"] == "mailcow":
input_jump_found = True
if nft_forward_chain and rule["chain"] == nft_forward_chain:
if rule.get("comment") and rule["comment"] == "mailcow":
forward_jump_found = True
if not input_jump_found:
command = get_mailcow_jump_rule_dict(_family, nft_input_chain)
nft_exec_dict(command)
if not forward_jump_found:
command = get_mailcow_jump_rule_dict(_family, nft_forward_chain)
nft_exec_dict(command)
def delete_nat_rule(_family:str, _chain: str, _handle:str):
delete_command = get_base_dict()
_rule_opts = {'family': _family,
'table': 'nat',
'chain': _chain,
'handle': _handle }
_delete = {'delete': {'rule': _rule_opts} }
delete_command["nftables"].append(_delete)
return nft_exec_dict(delete_command)
def snat_rule(_family: str, snat_target: str):
chain_name = nft_chain_names[_family]['nat']['postrouting']
# no postrouting chain, may occur if docker has ipv6 disabled.
if not chain_name: return
# Command: nft list chain <family> nat <chain_name>
_chain_opts = {'family': _family, 'table': 'nat', 'name': chain_name}
_list = {'list':{'chain': _chain_opts} }
command = get_base_dict()
command['nftables'].append(_list)
kernel_ruleset = nft_exec_dict(command)
if not kernel_ruleset:
return
rule_position = 0
rule_handle = None
rule_found = False
for _object in kernel_ruleset["nftables"]:
if not _object.get("rule"):
continue
rule = _object["rule"]
if not rule.get("comment") or not rule["comment"] == "mailcow":
rule_position +=1
continue
rule_found = True
rule_handle = rule["handle"]
break
if _family == "ip":
source_address = os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24'
else:
source_address = os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64')
dest_net = ipaddress.ip_network(source_address)
target_net = ipaddress.ip_network(snat_target)
if rule_found:
saddr_ip = rule["expr"][0]["match"]["right"]["prefix"]["addr"]
saddr_len = int(rule["expr"][0]["match"]["right"]["prefix"]["len"])
daddr_ip = rule["expr"][1]["match"]["right"]["prefix"]["addr"]
daddr_len = int(rule["expr"][1]["match"]["right"]["prefix"]["len"])
target_ip = rule["expr"][3]["snat"]["addr"]
saddr_net = ipaddress.ip_network(saddr_ip + '/' + str(saddr_len))
daddr_net = ipaddress.ip_network(daddr_ip + '/' + str(daddr_len))
current_target_net = ipaddress.ip_network(target_ip)
match = all((
dest_net == saddr_net,
dest_net == daddr_net,
target_net == current_target_net
))
try:
if rule_position == 0:
if not match:
# Position 0 , it is a mailcow rule , but it does not have the same parameters
if delete_nat_rule(_family, chain_name, rule_handle):
logInfo(f'Remove rule for source network {saddr_net} to SNAT target {target_net} from {_family} nat {chain_name} chain, rule does not match configured parameters')
else:
# Position > 0 and is mailcow rule
if delete_nat_rule(_family, chain_name, rule_handle):
logInfo(f'Remove rule for source network {saddr_net} to SNAT target {target_net} from {_family} nat {chain_name} chain, rule is at position {rule_position}')
except:
logCrit(f"Error running SNAT on {_family}, retrying..." )
else:
# rule not found
json_command = get_base_dict()
try:
snat_dict = {'snat': {'addr': str(target_net.network_address)} }
expr_counter = {'family': _family, 'table': 'nat', 'packets': 0, 'bytes': 0}
counter_dict = {'counter': expr_counter}
prefix_dict = {'prefix': {'addr': str(dest_net.network_address), 'len': int(dest_net.prefixlen)} }
payload_dict = {'payload': {'protocol': _family, 'field': "saddr"} }
match_dict1 = {'match': {'op': '==', 'left': payload_dict, 'right': prefix_dict} }
payload_dict2 = {'payload': {'protocol': _family, 'field': "daddr"} }
match_dict2 = {'match': {'op': '!=', 'left': payload_dict2, 'right': prefix_dict } }
expr_list = [
match_dict1,
match_dict2,
counter_dict,
snat_dict
]
rule_fields = {'family': _family,
'table': 'nat',
'chain': chain_name,
'comment': "mailcow",
'expr': expr_list }
insert_dict = {'insert': {'rule': rule_fields} }
json_command["nftables"].append(insert_dict)
if nft_exec_dict(json_command):
logInfo(f'Added {_family} nat {chain_name} rule for source network {dest_net} to {target_net}')
except:
logCrit(f"Error running SNAT on {_family}, retrying...")
def get_chain_handle(_family: str, _table: str, chain_name: str):
chain_handle = None
# Command: 'nft list chains {family}'
_list = {'list': {'chains': {'family': _family} } }
command = get_base_dict()
command['nftables'].append(_list)
kernel_ruleset = nft_exec_dict(command)
if kernel_ruleset:
for _object in kernel_ruleset["nftables"]:
if not _object.get("chain"):
continue
chain = _object["chain"]
if chain["family"] == _family and chain["table"] == _table and chain["name"] == chain_name:
chain_handle = chain["handle"]
break
return chain_handle
def get_rules_handle(_family: str, _table: str, chain_name: str):
rule_handle = []
# Command: 'nft list chain {family} {table} {chain_name}'
_chain_opts = {'family': _family, 'table': _table, 'name': chain_name}
_list = {'list': {'chain': _chain_opts} }
command = get_base_dict()
command['nftables'].append(_list)
kernel_ruleset = nft_exec_dict(command)
if kernel_ruleset:
for _object in kernel_ruleset["nftables"]:
if not _object.get("rule"):
continue
rule = _object["rule"]
if rule["family"] == _family and rule["table"] == _table and rule["chain"] == chain_name:
if rule.get("comment") and rule["comment"] == "mailcow":
rule_handle.append(rule["handle"])
return rule_handle
def get_ban_ip_dict(ipaddr: str, _family: str):
json_command = get_base_dict()
expr_opt = []
ipaddr_net = ipaddress.ip_network(ipaddr)
right_dict = {'prefix': {'addr': str(ipaddr_net.network_address), 'len': int(ipaddr_net.prefixlen) } }
left_dict = {'payload': {'protocol': _family, 'field': 'saddr'} }
match_dict = {'op': '==', 'left': left_dict, 'right': right_dict }
expr_opt.append({'match': match_dict})
counter_dict = {'counter': {'family': _family, 'table': "filter", 'packets': 0, 'bytes': 0} }
expr_opt.append(counter_dict)
expr_opt.append({'drop': "null"})
rule_dict = {'family': _family, 'table': "filter", 'chain': "MAILCOW", 'expr': expr_opt}
base_dict = {'insert': {'rule': rule_dict} }
json_command["nftables"].append(base_dict)
return json_command
def get_unban_ip_dict(ipaddr:str, _family: str):
json_command = get_base_dict()
# Command: 'nft list chain {s_family} filter MAILCOW'
_chain_opts = {'family': _family, 'table': 'filter', 'name': 'MAILCOW'}
_list = {'list': {'chain': _chain_opts} }
command = get_base_dict()
command['nftables'].append(_list)
kernel_ruleset = nft_exec_dict(command)
rule_handle = None
if kernel_ruleset:
for _object in kernel_ruleset["nftables"]:
if not _object.get("rule"):
continue
rule = _object["rule"]["expr"][0]["match"]
left_opt = rule["left"]["payload"]
if not left_opt["protocol"] == _family:
continue
if not left_opt["field"] =="saddr":
continue
# ip currently banned
rule_right = rule["right"]
if isinstance(rule_right, dict):
current_rule_ip = rule_right["prefix"]["addr"] + '/' + str(rule_right["prefix"]["len"])
else:
current_rule_ip = rule_right
current_rule_net = ipaddress.ip_network(current_rule_ip)
# ip to ban
candidate_net = ipaddress.ip_network(ipaddr)
if current_rule_net == candidate_net:
rule_handle = _object["rule"]["handle"]
break
if rule_handle is not None:
mailcow_rule = {'family': _family, 'table': 'filter', 'chain': 'MAILCOW', 'handle': rule_handle}
delete_rule = {'delete': {'rule': mailcow_rule} }
json_command["nftables"].append(delete_rule)
else:
return False
return json_command
def check_mailcow_chains(family: str, chain: str):
position = 0
rule_found = False
chain_name = nft_chain_names[family]['filter'][chain]
if not chain_name: return None
_chain_opts = {'family': family, 'table': 'filter', 'name': chain_name}
_list = {'list': {'chain': _chain_opts}}
command = get_base_dict()
command['nftables'].append(_list)
kernel_ruleset = nft_exec_dict(command)
if kernel_ruleset:
for _object in kernel_ruleset["nftables"]:
if not _object.get("rule"):
continue
rule = _object["rule"]
if rule.get("comment") and rule["comment"] == "mailcow":
rule_found = True
break
position+=1
return position if rule_found else False
# Mailcow
def mailcowChainOrder(): def mailcowChainOrder():
global lock global lock
global quit_now global quit_now
global exit_code global exit_code
while not quit_now: while not quit_now:
time.sleep(10) time.sleep(10)
with lock: with lock:
filter4_table = iptc.Table(iptc.Table.FILTER) if backend == 'iptables':
filter6_table = iptc.Table6(iptc.Table6.FILTER) filter4_table = iptc.Table(iptc.Table.FILTER)
filter4_table.refresh() filter6_table = iptc.Table6(iptc.Table6.FILTER)
filter6_table.refresh() filter4_table.refresh()
for f in [filter4_table, filter6_table]: filter6_table.refresh()
forward_chain = iptc.Chain(f, 'FORWARD') for f in [filter4_table, filter6_table]:
input_chain = iptc.Chain(f, 'INPUT') forward_chain = iptc.Chain(f, 'FORWARD')
for chain in [forward_chain, input_chain]: input_chain = iptc.Chain(f, 'INPUT')
target_found = False for chain in [forward_chain, input_chain]:
for position, item in enumerate(chain.rules): target_found = False
if item.target.name == 'MAILCOW': for position, item in enumerate(chain.rules):
target_found = True if item.target.name == 'MAILCOW':
if position > 2: target_found = True
logCrit('Error in %s chain order: MAILCOW on position %d, restarting container' % (chain.name, position)) if position > 2:
quit_now = True logCrit(f'MAILCOW target is in position {position} in the {chain.name} chain, restarting container to fix it...')
exit_code = 2 quit_now = True
if not target_found: exit_code = 2
logCrit('Error in %s chain: MAILCOW target not found, restarting container' % (chain.name)) if not target_found:
quit_now = True logCrit(f'MAILCOW target not found in {chain.name} chain, restarting container to fix it...')
exit_code = 2 quit_now = True
exit_code = 2
else:
for family in ["ip", "ip6"]:
for chain in ['input', 'forward']:
chain_position = check_mailcow_chains(family, chain)
if chain_position is None: continue
if chain_position is False:
logCrit(f'MAILCOW target not found in {family} {chain} table, restarting container to fix it...')
quit_now = True
exit_code = 2
if chain_position > 0:
logCrit(f'MAILCOW target is in position {chain_position} in the {family} {chain} table, restarting container to fix it...')
quit_now = True
exit_code = 2
def ban(address): def ban(address):
global lock global lock
@ -187,8 +600,11 @@ def ban(address):
net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False) net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
net = str(net) net = str(net)
if not net in bans: if net not in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0} bans[net] = { 'attempts': 0 }
active_window = RETRY_WINDOW
else:
active_window = time.time() - bans[net]['last_attempt']
bans[net]['attempts'] += 1 bans[net]['attempts'] += 1
bans[net]['last_attempt'] = time.time() bans[net]['last_attempt'] = time.time()
@ -199,22 +615,31 @@ def ban(address):
logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 )) logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
if type(ip) is ipaddress.IPv4Address: if type(ip) is ipaddress.IPv4Address:
with lock: with lock:
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW') if backend == 'iptables':
rule = iptc.Rule() chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
rule.src = net rule = iptc.Rule()
target = iptc.Target(rule, "REJECT") rule.src = net
rule.target = target target = iptc.Target(rule, "REJECT")
if rule not in chain.rules: rule.target = target
chain.insert_rule(rule) if rule not in chain.rules:
chain.insert_rule(rule)
else:
ban_dict = get_ban_ip_dict(net, "ip")
nft_exec_dict(ban_dict)
else: else:
with lock: with lock:
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW') if backend == 'iptables':
rule = iptc.Rule6() chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
rule.src = net rule = iptc.Rule6()
target = iptc.Target(rule, "REJECT") rule.src = net
rule.target = target target = iptc.Target(rule, "REJECT")
if rule not in chain.rules: rule.target = target
chain.insert_rule(rule) if rule not in chain.rules:
chain.insert_rule(rule)
else:
ban_dict = get_ban_ip_dict(net, "ip6")
nft_exec_dict(ban_dict)
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME) r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME)
else: else:
logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)) logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
@ -228,22 +653,35 @@ def unban(net):
logInfo('Unbanning %s' % net) logInfo('Unbanning %s' % net)
if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network: if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
with lock: with lock:
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW') if backend == 'iptables':
rule = iptc.Rule() chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
rule.src = net rule = iptc.Rule()
target = iptc.Target(rule, "REJECT") rule.src = net
rule.target = target target = iptc.Target(rule, "REJECT")
if rule in chain.rules: rule.target = target
chain.delete_rule(rule) if rule in chain.rules:
chain.delete_rule(rule)
else:
dict_unban = get_unban_ip_dict(net, "ip")
if dict_unban:
if nft_exec_dict(dict_unban):
logInfo(f"Unbanned ip: {net}")
else: else:
with lock: with lock:
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW') if backend == 'iptables':
rule = iptc.Rule6() chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
rule.src = net rule = iptc.Rule6()
target = iptc.Target(rule, "REJECT") rule.src = net
rule.target = target target = iptc.Target(rule, "REJECT")
if rule in chain.rules: rule.target = target
chain.delete_rule(rule) if rule in chain.rules:
chain.delete_rule(rule)
else:
dict_unban = get_unban_ip_dict(net, "ip6")
if dict_unban:
if nft_exec_dict(dict_unban):
logInfo(f"Unbanned ip6: {net}")
r.hdel('F2B_ACTIVE_BANS', '%s' % net) r.hdel('F2B_ACTIVE_BANS', '%s' % net)
r.hdel('F2B_QUEUE_UNBAN', '%s' % net) r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
if net in bans: if net in bans:
@ -254,34 +692,60 @@ def permBan(net, unban=False):
global lock global lock
if type(ipaddress.ip_network(net, strict=False)) is ipaddress.IPv4Network: if type(ipaddress.ip_network(net, strict=False)) is ipaddress.IPv4Network:
with lock: with lock:
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW') if backend == 'iptables':
rule = iptc.Rule() chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
rule.src = net rule = iptc.Rule()
target = iptc.Target(rule, "REJECT") rule.src = net
rule.target = target target = iptc.Target(rule, "REJECT")
if rule not in chain.rules and not unban: rule.target = target
logCrit('Add host/network %s to blacklist' % net) if rule not in chain.rules and not unban:
chain.insert_rule(rule) logCrit('Add host/network %s to blacklist' % net)
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time()))) chain.insert_rule(rule)
elif rule in chain.rules and unban: r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
logCrit('Remove host/network %s from blacklist' % net) elif rule in chain.rules and unban:
chain.delete_rule(rule) logCrit('Remove host/network %s from blacklist' % net)
r.hdel('F2B_PERM_BANS', '%s' % net) chain.delete_rule(rule)
r.hdel('F2B_PERM_BANS', '%s' % net)
else:
if not unban:
ban_dict = get_ban_ip_dict(net, "ip")
if nft_exec_dict(ban_dict):
logCrit('Add host/network %s to blacklist' % net)
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
elif unban:
dict_unban = get_unban_ip_dict(net, "ip")
if dict_unban:
if nft_exec_dict(dict_unban):
logCrit('Remove host/network %s from blacklist' % net)
r.hdel('F2B_PERM_BANS', '%s' % net)
else: else:
with lock: with lock:
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW') if backend == 'iptables':
rule = iptc.Rule6() chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
rule.src = net rule = iptc.Rule6()
target = iptc.Target(rule, "REJECT") rule.src = net
rule.target = target target = iptc.Target(rule, "REJECT")
if rule not in chain.rules and not unban: rule.target = target
logCrit('Add host/network %s to blacklist' % net) if rule not in chain.rules and not unban:
chain.insert_rule(rule) logCrit('Add host/network %s to blacklist' % net)
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time()))) chain.insert_rule(rule)
elif rule in chain.rules and unban: r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
logCrit('Remove host/network %s from blacklist' % net) elif rule in chain.rules and unban:
chain.delete_rule(rule) logCrit('Remove host/network %s from blacklist' % net)
r.hdel('F2B_PERM_BANS', '%s' % net) chain.delete_rule(rule)
r.hdel('F2B_PERM_BANS', '%s' % net)
else:
if not unban:
ban_dict = get_ban_ip_dict(net, "ip6")
if nft_exec_dict(ban_dict):
logCrit('Add host/network %s to blacklist' % net)
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
elif unban:
dict_unban = get_unban_ip_dict(net, "ip6")
if dict_unban:
if nft_exec_dict(dict_unban):
logCrit('Remove host/network %s from blacklist' % net)
r.hdel('F2B_PERM_BANS', '%s' % net)
def quit(signum, frame): def quit(signum, frame):
global quit_now global quit_now
@ -293,26 +757,73 @@ def clear():
for net in bans.copy(): for net in bans.copy():
unban(net) unban(net)
with lock: with lock:
filter4_table = iptc.Table(iptc.Table.FILTER) if backend == 'iptables':
filter6_table = iptc.Table6(iptc.Table6.FILTER) filter4_table = iptc.Table(iptc.Table.FILTER)
for filter_table in [filter4_table, filter6_table]: filter6_table = iptc.Table6(iptc.Table6.FILTER)
filter_table.autocommit = False for filter_table in [filter4_table, filter6_table]:
forward_chain = iptc.Chain(filter_table, "FORWARD") filter_table.autocommit = False
input_chain = iptc.Chain(filter_table, "INPUT") forward_chain = iptc.Chain(filter_table, "FORWARD")
mailcow_chain = iptc.Chain(filter_table, "MAILCOW") input_chain = iptc.Chain(filter_table, "INPUT")
if mailcow_chain in filter_table.chains: mailcow_chain = iptc.Chain(filter_table, "MAILCOW")
for rule in mailcow_chain.rules: if mailcow_chain in filter_table.chains:
mailcow_chain.delete_rule(rule) for rule in mailcow_chain.rules:
for rule in forward_chain.rules: mailcow_chain.delete_rule(rule)
if rule.target.name == 'MAILCOW': for rule in forward_chain.rules:
forward_chain.delete_rule(rule) if rule.target.name == 'MAILCOW':
for rule in input_chain.rules: forward_chain.delete_rule(rule)
if rule.target.name == 'MAILCOW': for rule in input_chain.rules:
input_chain.delete_rule(rule) if rule.target.name == 'MAILCOW':
filter_table.delete_chain("MAILCOW") input_chain.delete_rule(rule)
filter_table.commit() filter_table.delete_chain("MAILCOW")
filter_table.refresh() filter_table.commit()
filter_table.autocommit = True filter_table.refresh()
filter_table.autocommit = True
else:
for _family in ["ip", "ip6"]:
is_empty_dict = True
json_command = get_base_dict()
chain_handle = get_chain_handle(_family, "filter", "MAILCOW")
# if no handle, the chain doesn't exists
if chain_handle is not None:
is_empty_dict = False
# flush chain MAILCOW
mailcow_chain = {'family': _family, 'table': 'filter', 'name': 'MAILCOW'}
flush_chain = {'flush': {'chain': mailcow_chain}}
json_command["nftables"].append(flush_chain)
# remove rule in forward chain
# remove rule in input chain
chains_family = [nft_chain_names[_family]['filter']['input'],
nft_chain_names[_family]['filter']['forward'] ]
for chain_base in chains_family:
if not chain_base: continue
rules_handle = get_rules_handle(_family, "filter", chain_base)
if rules_handle is not None:
for r_handle in rules_handle:
is_empty_dict = False
mailcow_rule = {'family':_family,
'table': 'filter',
'chain': chain_base,
'handle': r_handle }
delete_rules = {'delete': {'rule': mailcow_rule} }
json_command["nftables"].append(delete_rules)
# remove chain MAILCOW
# after delete all rules referencing this chain
if chain_handle is not None:
mc_chain_handle = {'family':_family,
'table': 'filter',
'name': 'MAILCOW',
'handle': chain_handle }
delete_chain = {'delete': {'chain': mc_chain_handle} }
json_command["nftables"].append(delete_chain)
if is_empty_dict == False:
if nft_exec_dict(json_command):
logInfo(f"Clear completed: {_family}")
r.delete('F2B_ACTIVE_BANS') r.delete('F2B_ACTIVE_BANS')
r.delete('F2B_PERM_BANS') r.delete('F2B_PERM_BANS')
pubsub.unsubscribe() pubsub.unsubscribe()
@ -364,37 +875,38 @@ def snat4(snat_target):
time.sleep(10) time.sleep(10)
with lock: with lock:
try: try:
table = iptc.Table('nat') if backend == 'iptables':
table.refresh() table = iptc.Table('nat')
chain = iptc.Chain(table, 'POSTROUTING') table.refresh()
table.autocommit = False chain = iptc.Chain(table, 'POSTROUTING')
new_rule = get_snat4_rule() table.autocommit = False
new_rule = get_snat4_rule()
if not chain.rules: if not chain.rules:
# if there are no rules in the chain, insert the new rule directly # 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}') logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
chain.insert_rule(new_rule) chain.insert_rule(new_rule)
else:
for position, rule in enumerate(chain.rules):
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
else: else:
for position, rule in enumerate(chain.rules): snat_rule("ip", snat_target)
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: except:
print('Error running SNAT4, retrying...') print('Error running SNAT4, retrying...')
@ -414,21 +926,31 @@ def snat6(snat_target):
time.sleep(10) time.sleep(10)
with lock: with lock:
try: try:
table = iptc.Table6('nat') if backend == 'iptables':
table.refresh() table = iptc.Table6('nat')
chain = iptc.Chain(table, 'POSTROUTING') table.refresh()
table.autocommit = False chain = iptc.Chain(table, 'POSTROUTING')
if get_snat6_rule() not in chain.rules: table.autocommit = False
logInfo('Added POSTROUTING rule for source network %s to SNAT target %s' % (get_snat6_rule().src, snat_target)) new_rule = get_snat6_rule()
chain.insert_rule(get_snat6_rule()) for position, rule in enumerate(chain.rules):
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.commit()
table.autocommit = True
else: else:
for position, item in enumerate(chain.rules): snat_rule("ip6", snat_target)
if item == get_snat6_rule():
if position != 0:
chain.delete_rule(get_snat6_rule())
table.commit()
table.autocommit = True
except: except:
print('Error running SNAT6, retrying...') print('Error running SNAT6, retrying...')
@ -458,7 +980,6 @@ def isIpNetwork(address):
return False return False
return True return True
def genNetworkList(list): def genNetworkList(list):
resolver = dns.resolver.Resolver() resolver = dns.resolver.Resolver()
hostnames = [] hostnames = []
@ -527,33 +1048,41 @@ def blacklistUpdate():
def initChain(): def initChain():
# Is called before threads start, no locking # Is called before threads start, no locking
print("Initializing mailcow netfilter chain") print("Initializing mailcow netfilter chain")
# IPv4 if backend == 'iptables':
if not iptc.Chain(iptc.Table(iptc.Table.FILTER), "MAILCOW") in iptc.Table(iptc.Table.FILTER).chains: # IPv4
iptc.Table(iptc.Table.FILTER).create_chain("MAILCOW") if not iptc.Chain(iptc.Table(iptc.Table.FILTER), "MAILCOW") in iptc.Table(iptc.Table.FILTER).chains:
for c in ['FORWARD', 'INPUT']: iptc.Table(iptc.Table.FILTER).create_chain("MAILCOW")
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c) for c in ['FORWARD', 'INPUT']:
rule = iptc.Rule() chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c)
rule.src = '0.0.0.0/0' rule = iptc.Rule()
rule.dst = '0.0.0.0/0' rule.src = '0.0.0.0/0'
target = iptc.Target(rule, "MAILCOW") rule.dst = '0.0.0.0/0'
rule.target = target target = iptc.Target(rule, "MAILCOW")
if rule not in chain.rules: rule.target = target
chain.insert_rule(rule) if rule not in chain.rules:
# IPv6 chain.insert_rule(rule)
if not iptc.Chain(iptc.Table6(iptc.Table6.FILTER), "MAILCOW") in iptc.Table6(iptc.Table6.FILTER).chains: # IPv6
iptc.Table6(iptc.Table6.FILTER).create_chain("MAILCOW") if not iptc.Chain(iptc.Table6(iptc.Table6.FILTER), "MAILCOW") in iptc.Table6(iptc.Table6.FILTER).chains:
for c in ['FORWARD', 'INPUT']: iptc.Table6(iptc.Table6.FILTER).create_chain("MAILCOW")
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c) for c in ['FORWARD', 'INPUT']:
rule = iptc.Rule6() chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c)
rule.src = '::/0' rule = iptc.Rule6()
rule.dst = '::/0' rule.src = '::/0'
target = iptc.Target(rule, "MAILCOW") rule.dst = '::/0'
rule.target = target target = iptc.Target(rule, "MAILCOW")
if rule not in chain.rules: rule.target = target
chain.insert_rule(rule) if rule not in chain.rules:
chain.insert_rule(rule)
else:
for family in ["ip", "ip6"]:
insert_mailcow_chains(family)
if __name__ == '__main__': if __name__ == '__main__':
if backend == 'nftables':
search_current_chains()
# In case a previous session was killed without cleanup # In case a previous session was killed without cleanup
clear() clear()
# Reinit MAILCOW chain # Reinit MAILCOW chain