[Netfilter] Prevent crashes by locking threads

[Netfilter] SNAT6
master
André 2018-07-11 19:41:04 +02:00
parent 055183257d
commit 1e59816665
4 changed files with 201 additions and 115 deletions

View File

@ -1,11 +1,11 @@
FROM alpine:3.6 FROM alpine:3.8
LABEL maintainer "Andre Peters <andre.peters@servercow.de>" LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
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
RUN apk add -U python2 python-dev py-pip gcc musl-dev iptables ip6tables \ RUN apk add -U python2 python-dev py-pip gcc musl-dev iptables ip6tables tzdata \
&& pip2 install --upgrade python-iptables==0.13.0 redis ipaddress \ && pip2 install --upgrade python-iptables==0.13.0 redis ipaddress \
&& apk del python-dev py2-pip gcc && apk del python-dev py2-pip gcc

View File

@ -6,8 +6,9 @@ import time
import atexit import atexit
import signal import signal
import ipaddress import ipaddress
import subprocess from random import randint
from threading import Thread from threading import Thread
from threading import Lock
import redis import redis
import time import time
import json import json
@ -27,6 +28,7 @@ RULES[6] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
bans = {} bans = {}
log = {} log = {}
quit_now = False quit_now = False
lock = Lock()
def refreshF2boptions(): def refreshF2boptions():
global f2boptions global f2boptions
@ -55,38 +57,41 @@ def refreshF2boptions():
if r.exists('F2B_LOG'): if r.exists('F2B_LOG'):
r.rename('F2B_LOG', 'NETFILTER_LOG') r.rename('F2B_LOG', 'NETFILTER_LOG')
def checkChainOrder(): def mailcowChainOrder():
global lock
global quit_now global quit_now
while not quit_now: while not quit_now:
time.sleep(20) time.sleep(10)
filter4_table = iptc.Table(iptc.Table.FILTER) with lock:
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 != 0: target_found = True
log['time'] = int(round(time.time())) if position != 0:
log['priority'] = 'crit' log['time'] = int(round(time.time()))
log['message'] = 'Error in ' + chain.name + ' chain order, restarting container' log['priority'] = 'crit'
r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) log['message'] = 'Error in ' + chain.name + ' chain order, restarting container'
print log['message'] r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
quit_now = True print log['message']
if not target_found: quit_now = True
log['time'] = int(round(time.time())) if not target_found:
log['priority'] = 'crit' log['time'] = int(round(time.time()))
log['message'] = 'Error in ' + chain.name + ' chain: target not found, restarting container' log['priority'] = 'crit'
r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) log['message'] = 'Error in ' + chain.name + ' chain: MAILCOW target not found, restarting container'
print log['message'] r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
quit_now = True print log['message']
quit_now = True
def ban(address): def ban(address):
global lock
refreshF2boptions() refreshF2boptions()
BAN_TIME = int(f2boptions['ban_time']) BAN_TIME = int(f2boptions['ban_time'])
MAX_ATTEMPTS = int(f2boptions['max_attempts']) MAX_ATTEMPTS = int(f2boptions['max_attempts'])
@ -135,21 +140,23 @@ def ban(address):
r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
print 'Banning %s for %d minutes' % (net, BAN_TIME / 60) print 'Banning %s for %d minutes' % (net, BAN_TIME / 60)
if type(ip) is ipaddress.IPv4Address: if type(ip) is ipaddress.IPv4Address:
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW') with lock:
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: else:
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW') with lock:
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)
r.hset('F2B_ACTIVE_BANS', '%s' % net, log['time'] + BAN_TIME) r.hset('F2B_ACTIVE_BANS', '%s' % net, log['time'] + BAN_TIME)
else: else:
log['time'] = int(round(time.time())) log['time'] = int(round(time.time()))
@ -159,6 +166,7 @@ def ban(address):
print '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net) print '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
def unban(net): def unban(net):
global lock
log['time'] = int(round(time.time())) log['time'] = int(round(time.time()))
log['priority'] = 'info' log['priority'] = 'info'
r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
@ -172,21 +180,23 @@ def unban(net):
r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
print 'Unbanning %s' % net print 'Unbanning %s' % net
if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network: if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network:
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW') with lock:
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: else:
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW') with lock:
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)
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:
@ -197,6 +207,7 @@ def quit(signum, frame):
quit_now = True quit_now = True
def clear(): def clear():
global lock
log['time'] = int(round(time.time())) log['time'] = int(round(time.time()))
log['priority'] = 'info' log['priority'] = 'info'
log['message'] = 'Clearing all bans' log['message'] = 'Clearing all bans'
@ -204,29 +215,30 @@ def clear():
print 'Clearing all bans' print 'Clearing all bans'
for net in bans.copy(): for net in bans.copy():
unban(net) unban(net)
filter4_table = iptc.Table(iptc.Table.FILTER) with lock:
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()
r.delete('F2B_ACTIVE_BANS') filter_table.autocommit = True
r.delete('F2B_PERM_BANS') r.delete('F2B_ACTIVE_BANS')
pubsub.unsubscribe() r.delete('F2B_PERM_BANS')
pubsub.unsubscribe()
def watch(): def watch():
log['time'] = int(round(time.time())) log['time'] = int(round(time.time()))
@ -235,6 +247,7 @@ def watch():
r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
pubsub.subscribe('F2B_CHANNEL') pubsub.subscribe('F2B_CHANNEL')
print 'Subscribing to Redis channel F2B_CHANNEL' print 'Subscribing to Redis channel F2B_CHANNEL'
while not quit_now: while not quit_now:
for item in pubsub.listen(): for item in pubsub.listen():
for rule_id, rule_regex in RULES.iteritems(): for rule_id, rule_regex in RULES.iteritems():
@ -252,34 +265,81 @@ def watch():
r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
ban(addr) ban(addr)
def snat(snat_target): def snat4(snat_target):
def get_snat_rule(): global lock
global quit_now
def get_snat4_rule():
rule = iptc.Rule() rule = iptc.Rule()
rule.src = os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24' rule.src = os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24'
rule.dst = '!' + rule.src rule.dst = '!' + rule.src
target = rule.create_target("SNAT") target = rule.create_target("SNAT")
target.to_source = snat_target target.to_source = snat_target
return rule return rule
while not quit_now: while not quit_now:
time.sleep(5) time.sleep(10)
table = iptc.Table('nat') with lock:
table.refresh() try:
table.autocommit = False table = iptc.Table('nat')
chain = iptc.Chain(table, 'POSTROUTING') table.refresh()
if get_snat_rule() not in chain.rules: chain = iptc.Chain(table, 'POSTROUTING')
log['time'] = int(round(time.time())) table.autocommit = False
log['priority'] = 'info' if get_snat4_rule() not in chain.rules:
log['message'] = 'Added POSTROUTING rule for source network ' + get_snat_rule().src + ' to SNAT target ' + snat_target log['time'] = int(round(time.time()))
r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) log['priority'] = 'info'
print log['message'] log['message'] = 'Added POSTROUTING rule for source network ' + get_snat4_rule().src + ' to SNAT target ' + snat_target
chain.insert_rule(get_snat_rule()) r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
table.commit() print log['message']
else: chain.insert_rule(get_snat4_rule())
for position, item in enumerate(chain.rules): table.commit()
if item == get_snat_rule(): else:
if position != 0: for position, item in enumerate(chain.rules):
chain.delete_rule(get_snat_rule()) if item == get_snat4_rule():
table.commit() if position != 0:
chain.delete_rule(get_snat4_rule())
table.commit()
table.autocommit = True
except:
print 'Error running SNAT4, retrying...'
def snat6(snat_target):
global lock
global quit_now
def get_snat6_rule():
rule = iptc.Rule6()
rule.src = os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64')
rule.dst = '!' + rule.src
target = rule.create_target("SNAT")
target.to_source = snat_target
return rule
while not quit_now:
time.sleep(10)
with lock:
try:
table = iptc.Table6('nat')
table.refresh()
chain = iptc.Chain(table, 'POSTROUTING')
table.autocommit = False
if get_snat6_rule() not in chain.rules:
log['time'] = int(round(time.time()))
log['priority'] = 'info'
log['message'] = 'Added POSTROUTING rule for source network ' + get_snat6_rule().src + ' to SNAT target ' + snat_target
r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
print log['message']
chain.insert_rule(get_snat6_rule())
table.commit()
else:
for position, item in enumerate(chain.rules):
if item == get_snat6_rule():
if position != 0:
chain.delete_rule(get_snat6_rule())
table.commit()
table.autocommit = True
except:
print 'Error running SNAT6, retrying...'
def autopurge(): def autopurge():
while not quit_now: while not quit_now:
@ -297,6 +357,7 @@ def autopurge():
unban(net) unban(net)
def initChain(): def initChain():
# Is called before threads start, no locking
print "Initializing mailcow netfilter chain" print "Initializing mailcow netfilter chain"
# IPv4 # IPv4
if not iptc.Chain(iptc.Table(iptc.Table.FILTER), "MAILCOW") in iptc.Table(iptc.Table.FILTER).chains: if not iptc.Chain(iptc.Table(iptc.Table.FILTER), "MAILCOW") in iptc.Table(iptc.Table.FILTER).chains:
@ -355,7 +416,6 @@ def initChain():
chain.insert_rule(rule) chain.insert_rule(rule)
r.hset('F2B_PERM_BANS', '%s' % bl_key, int(round(time.time()))) r.hset('F2B_PERM_BANS', '%s' % bl_key, int(round(time.time())))
if __name__ == '__main__': if __name__ == '__main__':
# In case a previous session was killed without cleanup # In case a previous session was killed without cleanup
@ -372,19 +432,30 @@ if __name__ == '__main__':
snat_ip = os.getenv('SNAT_TO_SOURCE').decode('ascii') snat_ip = os.getenv('SNAT_TO_SOURCE').decode('ascii')
snat_ipo = ipaddress.ip_address(snat_ip) snat_ipo = ipaddress.ip_address(snat_ip)
if type(snat_ipo) is ipaddress.IPv4Address: if type(snat_ipo) is ipaddress.IPv4Address:
snat_thread = Thread(target=snat,args=(snat_ip,)) snat4_thread = Thread(target=snat4,args=(snat_ip,))
snat_thread.daemon = True snat4_thread.daemon = True
snat_thread.start() snat4_thread.start()
except ValueError: except ValueError:
print os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address' print os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address'
if os.getenv('SNAT6_TO_SOURCE') and os.getenv('SNAT6_TO_SOURCE') is not 'n':
try:
snat_ip = os.getenv('SNAT6_TO_SOURCE').decode('ascii')
snat_ipo = ipaddress.ip_address(snat_ip)
if type(snat_ipo) is ipaddress.IPv6Address:
snat6_thread = Thread(target=snat6,args=(snat_ip,))
snat6_thread.daemon = True
snat6_thread.start()
except ValueError:
print os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address'
autopurge_thread = Thread(target=autopurge) autopurge_thread = Thread(target=autopurge)
autopurge_thread.daemon = True autopurge_thread.daemon = True
autopurge_thread.start() autopurge_thread.start()
chainwatch_thread = Thread(target=checkChainOrder) mailcowchainwatch_thread = Thread(target=mailcowChainOrder)
chainwatch_thread.daemon = True mailcowchainwatch_thread.daemon = True
chainwatch_thread.start() mailcowchainwatch_thread.start()
signal.signal(signal.SIGTERM, quit) signal.signal(signal.SIGTERM, quit)
atexit.register(clear) atexit.register(clear)

View File

@ -23,9 +23,14 @@ if [[ -f mailcow.conf ]]; then
esac esac
fi fi
if [ -z "$MAILCOW_HOSTNAME" ]; then while [ -z "${MAILCOW_HOSTNAME}" ]; do
read -p "Hostname (FQDN - example.org is not a valid FQDN): " -ei "mx.example.org" MAILCOW_HOSTNAME read -p "Hostname (FQDN): " -ei "mx.example.org" MAILCOW_HOSTNAME
fi DOTS=${MAILCOW_HOSTNAME//[^.]};
if [ ${#DOTS} -lt 2 ]; then
echo "${MAILCOW_HOSTNAME} is not a FQDN"
MAILCOW_HOSTNAME=
fi
done
if [[ -a /etc/timezone ]]; then if [[ -a /etc/timezone ]]; then
TZ=$(cat /etc/timezone) TZ=$(cat /etc/timezone)
@ -122,9 +127,12 @@ IPV4_NETWORK=172.22.1
# Internal IPv6 subnet in fc00::/7 # Internal IPv6 subnet in fc00::/7
IPV6_NETWORK=fd4d:6169:6c63:6f77::/64 IPV6_NETWORK=fd4d:6169:6c63:6f77::/64
# Use this IP for outgoing connections (SNAT) # Use this IPv4 for outgoing connections (SNAT)
#SNAT_TO_SOURCE= #SNAT_TO_SOURCE=
# Use this IPv6 for outgoing connections (SNAT)
#SNAT6_TO_SOURCE=
# Disable IPv6 # Disable IPv6
# mailcow-network will still be created as IPv6 enabled, all containers will be created # mailcow-network will still be created as IPv6 enabled, all containers will be created
# without IPv6 support. # without IPv6 support.

View File

@ -48,6 +48,7 @@ CONFIG_ARRAY=(
"IPV6_NETWORK" "IPV6_NETWORK"
"LOG_LINES" "LOG_LINES"
"SNAT_TO_SOURCE" "SNAT_TO_SOURCE"
"SNAT6_TO_SOURCE"
"SYSCTL_IPV6_DISABLED" "SYSCTL_IPV6_DISABLED"
"COMPOSE_PROJECT_NAME" "COMPOSE_PROJECT_NAME"
"SQL_PORT" "SQL_PORT"
@ -125,9 +126,15 @@ for option in ${CONFIG_ARRAY[@]}; do
elif [[ ${option} == "SNAT_TO_SOURCE" ]]; then elif [[ ${option} == "SNAT_TO_SOURCE" ]]; then
if ! grep -q ${option} mailcow.conf; then if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf" echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Use this IP for outgoing connections (SNAT)' >> mailcow.conf echo '# Use this IPv4 for outgoing connections (SNAT)' >> mailcow.conf
echo "#SNAT_TO_SOURCE=" >> mailcow.conf echo "#SNAT_TO_SOURCE=" >> mailcow.conf
fi fi
elif [[ ${option} == "SNAT6_TO_SOURCE" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Use this IPv6 for outgoing connections (SNAT)' >> mailcow.conf
echo "#SNAT6_TO_SOURCE=" >> mailcow.conf
fi
elif ! grep -q ${option} mailcow.conf; then elif ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf" echo "Adding new option \"${option}\" to mailcow.conf"
echo "${option}=n" >> mailcow.conf echo "${option}=n" >> mailcow.conf