Merge 7b1c53d35d
into 0303dbc1d2
This commit is contained in:
commit
fad707f279
|
@ -1,6 +1,8 @@
|
|||
FROM alpine:3.17
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV XTABLES_LIBDIR /usr/lib/xtables
|
||||
ENV PYTHON_IPTABLES_XTABLES_VERSION 12
|
||||
ENV IPTABLES_LIBDIR /usr/lib
|
||||
|
@ -14,10 +16,13 @@ RUN apk add --virtual .build-deps \
|
|||
iptables \
|
||||
ip6tables \
|
||||
xtables-addons \
|
||||
nftables \
|
||||
tzdata \
|
||||
py3-pip \
|
||||
py3-nftables \
|
||||
musl-dev \
|
||||
&& pip3 install --ignore-installed --upgrade pip \
|
||||
jsonschema \
|
||||
python-iptables \
|
||||
redis \
|
||||
ipaddress \
|
||||
|
@ -26,5 +31,9 @@ RUN apk add --virtual .build-deps \
|
|||
|
||||
# && pip3 install --upgrade pip python-iptables==0.13.0 redis ipaddress dnspython \
|
||||
|
||||
COPY server.py /
|
||||
CMD ["python3", "-u", "/server.py"]
|
||||
COPY server.py /app/
|
||||
COPY ./netfilter.sh /app/
|
||||
|
||||
RUN chmod +x /app/netfilter.sh
|
||||
|
||||
CMD ["/bin/sh", "-c", "/app/netfilter.sh"]
|
||||
|
|
|
@ -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
|
|
@ -11,8 +11,9 @@ from collections import Counter
|
|||
from random import randint
|
||||
from threading import Thread
|
||||
from threading import Lock
|
||||
import redis
|
||||
import json
|
||||
import redis
|
||||
import nftables
|
||||
import iptc
|
||||
import dns.resolver
|
||||
import dns.exception
|
||||
|
@ -43,6 +44,10 @@ quit_now = False
|
|||
exit_code = 0
|
||||
lock = Lock()
|
||||
|
||||
backend = sys.argv[1]
|
||||
nft = None
|
||||
nft_chain_names = {}
|
||||
|
||||
def log(priority, message):
|
||||
tolog = {}
|
||||
tolog['time'] = int(round(time.time()))
|
||||
|
@ -60,6 +65,17 @@ def logCrit(message):
|
|||
def logInfo(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():
|
||||
global f2boptions
|
||||
global quit_now
|
||||
|
@ -127,13 +143,395 @@ def refreshF2bregex():
|
|||
if r.exists('F2B_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():
|
||||
global lock
|
||||
global quit_now
|
||||
global exit_code
|
||||
|
||||
while not quit_now:
|
||||
time.sleep(10)
|
||||
with lock:
|
||||
if backend == 'iptables':
|
||||
filter4_table = iptc.Table(iptc.Table.FILTER)
|
||||
filter6_table = iptc.Table6(iptc.Table6.FILTER)
|
||||
filter4_table.refresh()
|
||||
|
@ -147,11 +545,26 @@ def mailcowChainOrder():
|
|||
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))
|
||||
logCrit(f'MAILCOW target is in position {position} in the {chain.name} chain, restarting container to fix it...')
|
||||
quit_now = True
|
||||
exit_code = 2
|
||||
if not target_found:
|
||||
logCrit('Error in %s chain: MAILCOW target not found, restarting container' % (chain.name))
|
||||
logCrit(f'MAILCOW target not found in {chain.name} chain, restarting container to fix it...')
|
||||
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
|
||||
|
||||
|
@ -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 = str(net)
|
||||
|
||||
if not net in bans:
|
||||
bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0}
|
||||
if net not 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()
|
||||
|
@ -199,6 +615,7 @@ def ban(address):
|
|||
logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
|
||||
if type(ip) is ipaddress.IPv4Address:
|
||||
with lock:
|
||||
if backend == 'iptables':
|
||||
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
|
||||
rule = iptc.Rule()
|
||||
rule.src = net
|
||||
|
@ -206,8 +623,12 @@ def ban(address):
|
|||
rule.target = target
|
||||
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:
|
||||
with lock:
|
||||
if backend == 'iptables':
|
||||
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
|
||||
rule = iptc.Rule6()
|
||||
rule.src = net
|
||||
|
@ -215,6 +636,10 @@ def ban(address):
|
|||
rule.target = target
|
||||
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)
|
||||
else:
|
||||
logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
|
||||
|
@ -228,6 +653,7 @@ def unban(net):
|
|||
logInfo('Unbanning %s' % net)
|
||||
if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
|
||||
with lock:
|
||||
if backend == 'iptables':
|
||||
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
|
||||
rule = iptc.Rule()
|
||||
rule.src = net
|
||||
|
@ -235,8 +661,14 @@ def unban(net):
|
|||
rule.target = target
|
||||
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:
|
||||
with lock:
|
||||
if backend == 'iptables':
|
||||
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
|
||||
rule = iptc.Rule6()
|
||||
rule.src = net
|
||||
|
@ -244,6 +676,12 @@ def unban(net):
|
|||
rule.target = target
|
||||
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_QUEUE_UNBAN', '%s' % net)
|
||||
if net in bans:
|
||||
|
@ -254,6 +692,7 @@ def permBan(net, unban=False):
|
|||
global lock
|
||||
if type(ipaddress.ip_network(net, strict=False)) is ipaddress.IPv4Network:
|
||||
with lock:
|
||||
if backend == 'iptables':
|
||||
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
|
||||
rule = iptc.Rule()
|
||||
rule.src = net
|
||||
|
@ -267,8 +706,21 @@ def permBan(net, unban=False):
|
|||
logCrit('Remove host/network %s from blacklist' % 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:
|
||||
with lock:
|
||||
if backend == 'iptables':
|
||||
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
|
||||
rule = iptc.Rule6()
|
||||
rule.src = net
|
||||
|
@ -282,6 +734,18 @@ def permBan(net, unban=False):
|
|||
logCrit('Remove host/network %s from blacklist' % 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):
|
||||
global quit_now
|
||||
|
@ -293,6 +757,7 @@ def clear():
|
|||
for net in bans.copy():
|
||||
unban(net)
|
||||
with lock:
|
||||
if backend == 'iptables':
|
||||
filter4_table = iptc.Table(iptc.Table.FILTER)
|
||||
filter6_table = iptc.Table6(iptc.Table6.FILTER)
|
||||
for filter_table in [filter4_table, filter6_table]:
|
||||
|
@ -313,6 +778,52 @@ def clear():
|
|||
filter_table.commit()
|
||||
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_PERM_BANS')
|
||||
pubsub.unsubscribe()
|
||||
|
@ -364,6 +875,7 @@ def snat4(snat_target):
|
|||
time.sleep(10)
|
||||
with lock:
|
||||
try:
|
||||
if backend == 'iptables':
|
||||
table = iptc.Table('nat')
|
||||
table.refresh()
|
||||
chain = iptc.Chain(table, 'POSTROUTING')
|
||||
|
@ -376,8 +888,6 @@ def snat4(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(),
|
||||
|
@ -395,6 +905,8 @@ def snat4(snat_target):
|
|||
|
||||
table.commit()
|
||||
table.autocommit = True
|
||||
else:
|
||||
snat_rule("ip", snat_target)
|
||||
except:
|
||||
print('Error running SNAT4, retrying...')
|
||||
|
||||
|
@ -414,21 +926,31 @@ def snat6(snat_target):
|
|||
time.sleep(10)
|
||||
with lock:
|
||||
try:
|
||||
if backend == 'iptables':
|
||||
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()
|
||||
new_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:
|
||||
for position, item in enumerate(chain.rules):
|
||||
if item == get_snat6_rule():
|
||||
if position != 0:
|
||||
chain.delete_rule(get_snat6_rule())
|
||||
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:
|
||||
snat_rule("ip6", snat_target)
|
||||
except:
|
||||
print('Error running SNAT6, retrying...')
|
||||
|
||||
|
@ -458,7 +980,6 @@ def isIpNetwork(address):
|
|||
return False
|
||||
return True
|
||||
|
||||
|
||||
def genNetworkList(list):
|
||||
resolver = dns.resolver.Resolver()
|
||||
hostnames = []
|
||||
|
@ -527,6 +1048,7 @@ def blacklistUpdate():
|
|||
def initChain():
|
||||
# Is called before threads start, no locking
|
||||
print("Initializing mailcow netfilter chain")
|
||||
if backend == 'iptables':
|
||||
# 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")
|
||||
|
@ -551,9 +1073,16 @@ def initChain():
|
|||
rule.target = target
|
||||
if rule not in chain.rules:
|
||||
chain.insert_rule(rule)
|
||||
else:
|
||||
for family in ["ip", "ip6"]:
|
||||
insert_mailcow_chains(family)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if backend == 'nftables':
|
||||
search_current_chains()
|
||||
|
||||
# In case a previous session was killed without cleanup
|
||||
clear()
|
||||
# Reinit MAILCOW chain
|
||||
|
|
Loading…
Reference in New Issue