From 4cc63ceeb7af654c4b1cbd7ef796919c9429c4fc Mon Sep 17 00:00:00 2001 From: Kraeutergarten Date: Fri, 17 May 2019 19:38:34 +0200 Subject: [PATCH 1/8] Allow hostnames for fail2ban whitelist. --- data/Dockerfiles/netfilter/server.py | 21 +++++++++++++++++++++ data/web/inc/functions.fail2ban.inc.php | 7 ++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/data/Dockerfiles/netfilter/server.py b/data/Dockerfiles/netfilter/server.py index 910679c6..a7de6393 100644 --- a/data/Dockerfiles/netfilter/server.py +++ b/data/Dockerfiles/netfilter/server.py @@ -5,6 +5,7 @@ import os import time import atexit import signal +import socket import ipaddress from random import randint from threading import Thread @@ -39,6 +40,13 @@ log = {} quit_now = False lock = Lock() +def is_ip_network(address): + try: + ipaddress.ip_network(address.decode('ascii'), False) + except ValueError: + return False + return True + def refreshF2boptions(): global f2boptions global quit_now @@ -119,6 +127,19 @@ def ban(address): self_network = ipaddress.ip_network(address.decode('ascii')) if WHITELIST: for wl_key in WHITELIST: + if not is_ip_network(wl_key): + hostname = wl_key + try: + wl_key = socket.gethostbyname(hostname) + except socket.gaierror as err: + continue + + log['time'] = int(round(time.time())) + log['priority'] = 'info' + log['message'] = 'Hostname %s is resolved to %s' % (hostname, wl_key) + r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) + print 'Hostname %s is resolved to %s' % (hostname, wl_key) + wl_net = ipaddress.ip_network(wl_key.decode('ascii'), False) if wl_net.overlaps(self_network): log['time'] = int(round(time.time())) diff --git a/data/web/inc/functions.fail2ban.inc.php b/data/web/inc/functions.fail2ban.inc.php index 1cde10d3..bf492490 100644 --- a/data/web/inc/functions.fail2ban.inc.php +++ b/data/web/inc/functions.fail2ban.inc.php @@ -9,6 +9,11 @@ function valid_network($network) { } return false; } + +function valid_hostname($hostname) { + return filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME); +} + function fail2ban($_action, $_data = null) { global $redis; global $lang; @@ -188,7 +193,7 @@ function fail2ban($_action, $_data = null) { $wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl)); if (is_array($wl_array)) { foreach ($wl_array as $wl_item) { - if (valid_network($wl_item)) { + if (valid_network($wl_item) || valid_hostname($wl_item)) { $redis->hSet('F2B_WHITELIST', $wl_item, 1); } } From 51f5f66c912bf84aa1e56b6b87f326fce9795fb4 Mon Sep 17 00:00:00 2001 From: Kraeutergarten Date: Sat, 18 May 2019 12:04:11 +0200 Subject: [PATCH 2/8] low response timeout add ipv6 support add multiple record support --- data/Dockerfiles/netfilter/Dockerfile | 2 +- data/Dockerfiles/netfilter/server.py | 55 +++++++++++++++++++++------ 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/data/Dockerfiles/netfilter/Dockerfile b/data/Dockerfiles/netfilter/Dockerfile index 92eaa39f..472abb59 100644 --- a/data/Dockerfiles/netfilter/Dockerfile +++ b/data/Dockerfiles/netfilter/Dockerfile @@ -6,7 +6,7 @@ ENV PYTHON_IPTABLES_XTABLES_VERSION 12 ENV IPTABLES_LIBDIR /usr/lib 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 dnspython \ && apk del python-dev py2-pip gcc COPY server.py / diff --git a/data/Dockerfiles/netfilter/server.py b/data/Dockerfiles/netfilter/server.py index a7de6393..4fd1e3b0 100644 --- a/data/Dockerfiles/netfilter/server.py +++ b/data/Dockerfiles/netfilter/server.py @@ -5,7 +5,6 @@ import os import time import atexit import signal -import socket import ipaddress from random import randint from threading import Thread @@ -13,6 +12,8 @@ from threading import Lock import redis import json import iptc +import dns.resolver +import dns.exception while True: try: @@ -26,6 +27,8 @@ while True: pubsub = r.pubsub() +resolver = dns.resolver.Resolver() + RULES = {} RULES[1] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed' RULES[2] = '-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),' @@ -126,21 +129,51 @@ def ban(address): self_network = ipaddress.ip_network(address.decode('ascii')) if WHITELIST: + wl_hostnames=[] + wl_networks=[] + for wl_key in WHITELIST: - if not is_ip_network(wl_key): - hostname = wl_key + if is_ip_network(wl_key): + wl_networks.append(wl_key) + else: + wl_hostnames.append(wl_key) + + for w1_hostname in wl_hostnames: + hostname_ips = [] + for rdtype in ['A', 'AAAA']: try: - wl_key = socket.gethostbyname(hostname) - except socket.gaierror as err: + answer = resolver.query(qname=w1_hostname, rdtype=rdtype, lifetime=1) + except dns.exception.Timeout as timout: + log['time'] = int(round(time.time())) + log['priority'] = 'info' + log['message'] = 'Hostname %s timedout on resolve' % (w1_hostname) + r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) + print 'Hostname %s timedout on resolve' % (w1_hostname) + break + except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): + continue + except dns.exception.DNSException as dnsexception: + log['time'] = int(round(time.time())) + log['priority'] = 'info' + log['message'] = '%s' % (dnsexception) + r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) + print '%s' % (dnsexception) continue - log['time'] = int(round(time.time())) - log['priority'] = 'info' - log['message'] = 'Hostname %s is resolved to %s' % (hostname, wl_key) - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print 'Hostname %s is resolved to %s' % (hostname, wl_key) - + for rdata in answer: + hostname_ips.append(rdata.to_text()) + + wl_networks.extend(hostname_ips) + + log['time'] = int(round(time.time())) + log['priority'] = 'info' + log['message'] = 'Hostname %s is resolved to %s' % (w1_hostname, hostname_ips) + r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) + print 'Hostname %s is resolved to %s' % (w1_hostname, hostname_ips) + + for wl_key in wl_networks: wl_net = ipaddress.ip_network(wl_key.decode('ascii'), False) + if wl_net.overlaps(self_network): log['time'] = int(round(time.time())) log['priority'] = 'info' From 5ed113c47ff4de0c0ab29fda2d95c51820bbf856 Mon Sep 17 00:00:00 2001 From: Kraeutergarten Date: Sun, 19 May 2019 09:48:10 +0200 Subject: [PATCH 3/8] resolving whitelist every minute --- data/Dockerfiles/netfilter/server.py | 127 ++++++++++++++++----------- 1 file changed, 76 insertions(+), 51 deletions(-) diff --git a/data/Dockerfiles/netfilter/server.py b/data/Dockerfiles/netfilter/server.py index 4fd1e3b0..18d98d81 100644 --- a/data/Dockerfiles/netfilter/server.py +++ b/data/Dockerfiles/netfilter/server.py @@ -6,6 +6,7 @@ import time import atexit import signal import ipaddress +from collections import Counter from random import randint from threading import Thread from threading import Lock @@ -38,18 +39,13 @@ RULES[5] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)' RULES[6] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+' #RULES[7] = '-login: Aborted login \(no auth .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' +WHITELIST = [] + bans = {} log = {} quit_now = False lock = Lock() -def is_ip_network(address): - try: - ipaddress.ip_network(address.decode('ascii'), False) - except ValueError: - return False - return True - def refreshF2boptions(): global f2boptions global quit_now @@ -118,7 +114,6 @@ def ban(address): RETRY_WINDOW = int(f2boptions['retry_window']) NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4']) NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6']) - WHITELIST = r.hgetall('F2B_WHITELIST') ip = ipaddress.ip_address(address.decode('ascii')) if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped: @@ -128,50 +123,9 @@ def ban(address): return self_network = ipaddress.ip_network(address.decode('ascii')) - if WHITELIST: - wl_hostnames=[] - wl_networks=[] - - for wl_key in WHITELIST: - if is_ip_network(wl_key): - wl_networks.append(wl_key) - else: - wl_hostnames.append(wl_key) - for w1_hostname in wl_hostnames: - hostname_ips = [] - for rdtype in ['A', 'AAAA']: - try: - answer = resolver.query(qname=w1_hostname, rdtype=rdtype, lifetime=1) - except dns.exception.Timeout as timout: - log['time'] = int(round(time.time())) - log['priority'] = 'info' - log['message'] = 'Hostname %s timedout on resolve' % (w1_hostname) - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print 'Hostname %s timedout on resolve' % (w1_hostname) - break - except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): - continue - except dns.exception.DNSException as dnsexception: - log['time'] = int(round(time.time())) - log['priority'] = 'info' - log['message'] = '%s' % (dnsexception) - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print '%s' % (dnsexception) - continue - - for rdata in answer: - hostname_ips.append(rdata.to_text()) - - wl_networks.extend(hostname_ips) - - log['time'] = int(round(time.time())) - log['priority'] = 'info' - log['message'] = 'Hostname %s is resolved to %s' % (w1_hostname, hostname_ips) - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print 'Hostname %s is resolved to %s' % (w1_hostname, hostname_ips) - - for wl_key in wl_networks: + if WHITELIST: + for wl_key in WHITELIST: wl_net = ipaddress.ip_network(wl_key.decode('ascii'), False) if wl_net.overlaps(self_network): @@ -419,6 +373,73 @@ def autopurge(): if time.time() - bans[net]['last_attempt'] > BAN_TIME: unban(net) +def isIpNetwork(address): + try: + ipaddress.ip_network(address.decode('ascii'), False) + except ValueError: + return False + return True + + +def genNetworkList(list): + hostnames = [] + networks = [] + + for key in list: + if isIpNetwork(key): + networks.append(key.encode("utf-8")) + else: + hostnames.append(key) + + for hostname in hostnames: + hostname_ips = [] + for rdtype in ['A', 'AAAA']: + try: + answer = resolver.query(qname=hostname, rdtype=rdtype, lifetime=10) + except dns.exception.Timeout: + log['time'] = int(round(time.time())) + log['priority'] = 'info' + log['message'] = 'Hostname %s timedout on resolve' % (hostname) + r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) + print 'Hostname %s timedout on resolve' % (hostname) + break + except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): + continue + except dns.exception.DNSException as dnsexception: + log['time'] = int(round(time.time())) + log['priority'] = 'info' + log['message'] = '%s' % (dnsexception) + r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) + print '%s' % (dnsexception) + continue + + for rdata in answer: + hostname_ips.append(rdata.to_text().encode("utf-8")) + + networks.extend(hostname_ips) + + return networks + +def whitelistUpdate(): + global lock + global quit_now + global WHITELIST + + while not quit_now: + start_time = time.time() + list = r.hgetall('F2B_WHITELIST') + if list: + new_whitelist = genNetworkList(list) + if Counter(new_whitelist) != Counter(WHITELIST): + WHITELIST = new_whitelist + log['time'] = int(round(time.time())) + log['priority'] = 'info' + log['message'] = 'New entrys for whitelist %s' % (WHITELIST) + r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) + print 'New entrys for whitelist %s' % (WHITELIST) + + time.sleep(60.0 - ((time.time() - start_time) % 60.0)) + def initChain(): # Is called before threads start, no locking print "Initializing mailcow netfilter chain" @@ -520,6 +541,10 @@ if __name__ == '__main__': mailcowchainwatch_thread.daemon = True mailcowchainwatch_thread.start() + whitelistupdate_thread = Thread(target=whitelistUpdate) + whitelistupdate_thread.daemon = True + whitelistupdate_thread.start() + signal.signal(signal.SIGTERM, quit) atexit.register(clear) From d6af494789a543746c85e5bb223de6721959669e Mon Sep 17 00:00:00 2001 From: Kraeutergarten Date: Sun, 19 May 2019 09:55:49 +0200 Subject: [PATCH 4/8] update to python3 --- data/Dockerfiles/netfilter/Dockerfile | 6 +-- data/Dockerfiles/netfilter/server.py | 54 +++++++++++++-------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/data/Dockerfiles/netfilter/Dockerfile b/data/Dockerfiles/netfilter/Dockerfile index 472abb59..e62b7dfa 100644 --- a/data/Dockerfiles/netfilter/Dockerfile +++ b/data/Dockerfiles/netfilter/Dockerfile @@ -5,9 +5,9 @@ ENV XTABLES_LIBDIR /usr/lib/xtables ENV PYTHON_IPTABLES_XTABLES_VERSION 12 ENV IPTABLES_LIBDIR /usr/lib -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 dnspython \ - && apk del python-dev py2-pip gcc +RUN apk add -U python3 python3-dev gcc musl-dev iptables ip6tables tzdata \ + && pip3 install --upgrade python-iptables==0.13.0 redis ipaddress dnspython \ + && apk del python3-dev gcc COPY server.py / CMD ["python2", "-u", "/server.py"] diff --git a/data/Dockerfiles/netfilter/server.py b/data/Dockerfiles/netfilter/server.py index 18d98d81..c3b76984 100644 --- a/data/Dockerfiles/netfilter/server.py +++ b/data/Dockerfiles/netfilter/server.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import re import os @@ -21,7 +21,7 @@ while True: r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0) r.ping() except Exception as ex: - print '%s - trying again in 3 seconds' % (ex) + print('%s - trying again in 3 seconds' % (ex)) time.sleep(3) else: break @@ -66,8 +66,8 @@ def refreshF2boptions(): try: f2boptions = {} f2boptions = json.loads(r.get('F2B_OPTIONS')) - except ValueError, e: - print 'Error loading F2B options: F2B_OPTIONS is not json' + except ValueError as e: + print('Error loading F2B options: F2B_OPTIONS is not json') quit_now = True if r.exists('F2B_LOG'): @@ -96,14 +96,14 @@ def mailcowChainOrder(): log['priority'] = 'crit' log['message'] = 'Error in ' + chain.name + ' chain order, restarting container' r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print log['message'] + print(log['message']) quit_now = True if not target_found: log['time'] = int(round(time.time())) log['priority'] = 'crit' log['message'] = 'Error in ' + chain.name + ' chain: MAILCOW target not found, restarting container' r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print log['message'] + print(log['message']) quit_now = True def ban(address): @@ -133,7 +133,7 @@ def ban(address): log['priority'] = 'info' log['message'] = 'Address %s is whitelisted by rule %s' % (self_network, wl_net) r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print 'Address %s is whitelisted by rule %s' % (self_network, wl_net) + print('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)).decode('ascii'), strict=False) @@ -155,7 +155,7 @@ def ban(address): log['priority'] = 'crit' log['message'] = 'Banning %s' % net 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: with lock: chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW') @@ -180,7 +180,7 @@ def ban(address): log['priority'] = 'warn' log['message'] = '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net) r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - 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): global lock @@ -190,12 +190,12 @@ def unban(net): if not net in bans: log['message'] = '%s is not banned, skipping unban and deleting from queue (if any)' % net r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print '%s is not banned, skipping unban and deleting from queue (if any)' % net + print('%s is not banned, skipping unban and deleting from queue (if any)' % net) r.hdel('F2B_QUEUE_UNBAN', '%s' % net) return log['message'] = 'Unbanning %s' % net 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: with lock: chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW') @@ -229,7 +229,7 @@ def clear(): log['priority'] = 'info' log['message'] = 'Clearing all bans' r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print 'Clearing all bans' + print('Clearing all bans') for net in bans.copy(): unban(net) with lock: @@ -263,11 +263,11 @@ def watch(): log['message'] = 'Watching Redis channel F2B_CHANNEL' r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) pubsub.subscribe('F2B_CHANNEL') - print 'Subscribing to Redis channel F2B_CHANNEL' + print('Subscribing to Redis channel F2B_CHANNEL') while not quit_now: for item in pubsub.listen(): - for rule_id, rule_regex in RULES.iteritems(): + for rule_id, rule_regex in RULES.items(): if item['data'] and item['type'] == 'message': result = re.search(rule_regex, item['data']) if result: @@ -275,7 +275,7 @@ def watch(): ip = ipaddress.ip_address(addr.decode('ascii')) if ip.is_private or ip.is_loopback: continue - print '%s matched rule id %d' % (addr, rule_id) + print('%s matched rule id %d' % (addr, rule_id)) log['time'] = int(round(time.time())) log['priority'] = 'warn' log['message'] = '%s matched rule id %d' % (addr, rule_id) @@ -307,7 +307,7 @@ def snat4(snat_target): log['priority'] = 'info' log['message'] = 'Added POSTROUTING rule for source network ' + get_snat4_rule().src + ' to SNAT target ' + snat_target r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print log['message'] + print(log['message']) chain.insert_rule(get_snat4_rule()) table.commit() else: @@ -318,7 +318,7 @@ def snat4(snat_target): table.commit() table.autocommit = True except: - print 'Error running SNAT4, retrying...' + print('Error running SNAT4, retrying...') def snat6(snat_target): global lock @@ -345,7 +345,7 @@ def snat6(snat_target): 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'] + print(log['message']) chain.insert_rule(get_snat6_rule()) table.commit() else: @@ -356,7 +356,7 @@ def snat6(snat_target): table.commit() table.autocommit = True except: - print 'Error running SNAT6, retrying...' + print('Error running SNAT6, retrying...') def autopurge(): while not quit_now: @@ -401,7 +401,7 @@ def genNetworkList(list): log['priority'] = 'info' log['message'] = 'Hostname %s timedout on resolve' % (hostname) r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print 'Hostname %s timedout on resolve' % (hostname) + print('Hostname %s timedout on resolve' % (hostname)) break except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): continue @@ -410,7 +410,7 @@ def genNetworkList(list): log['priority'] = 'info' log['message'] = '%s' % (dnsexception) r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print '%s' % (dnsexception) + print('%s' % (dnsexception)) continue for rdata in answer: @@ -436,13 +436,13 @@ def whitelistUpdate(): log['priority'] = 'info' log['message'] = 'New entrys for whitelist %s' % (WHITELIST) r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print 'New entrys for whitelist %s' % (WHITELIST) + print('New entrys for whitelist %s' % (WHITELIST)) time.sleep(60.0 - ((time.time() - start_time) % 60.0)) def initChain(): # Is called before threads start, no locking - print "Initializing mailcow netfilter chain" + 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") @@ -482,7 +482,7 @@ def initChain(): log['priority'] = 'crit' log['message'] = 'Blacklisting host/network %s' % bl_key r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print log['message'] + print(log['message']) chain.insert_rule(rule) r.hset('F2B_PERM_BANS', '%s' % bl_key, int(round(time.time()))) else: @@ -496,7 +496,7 @@ def initChain(): log['priority'] = 'crit' log['message'] = 'Blacklisting host/network %s' % bl_key r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print log['message'] + print(log['message']) chain.insert_rule(rule) r.hset('F2B_PERM_BANS', '%s' % bl_key, int(round(time.time()))) @@ -520,7 +520,7 @@ if __name__ == '__main__': snat4_thread.daemon = True snat4_thread.start() 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: @@ -531,7 +531,7 @@ if __name__ == '__main__': snat6_thread.daemon = True snat6_thread.start() except ValueError: - print os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address' + print(os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address') autopurge_thread = Thread(target=autopurge) autopurge_thread.daemon = True From 5af250398c85638811057ede8739b93eed32e017 Mon Sep 17 00:00:00 2001 From: Kraeutergarten Date: Sun, 19 May 2019 10:36:16 +0200 Subject: [PATCH 5/8] Redo complete logging. Do some other fixes caused by python3 --- data/Dockerfiles/netfilter/server.py | 147 +++++++++------------------ 1 file changed, 49 insertions(+), 98 deletions(-) diff --git a/data/Dockerfiles/netfilter/server.py b/data/Dockerfiles/netfilter/server.py index c3b76984..ac8556c7 100644 --- a/data/Dockerfiles/netfilter/server.py +++ b/data/Dockerfiles/netfilter/server.py @@ -42,10 +42,27 @@ RULES[6] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+' WHITELIST = [] bans = {} -log = {} + quit_now = False 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 @@ -92,18 +109,10 @@ def mailcowChainOrder(): if item.target.name == 'MAILCOW': target_found = True if position != 0: - log['time'] = int(round(time.time())) - log['priority'] = 'crit' - log['message'] = 'Error in ' + chain.name + ' chain order, restarting container' - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print(log['message']) + logCrit('Error in %s chain order, restarting container' % (chain.name)) quit_now = True if not target_found: - log['time'] = int(round(time.time())) - log['priority'] = 'crit' - log['message'] = 'Error in ' + chain.name + ' chain: MAILCOW target not found, restarting container' - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print(log['message']) + logCrit('Error in %s chain: MAILCOW target not found, restarting container' % (chain.name)) quit_now = True def ban(address): @@ -115,28 +124,24 @@ def ban(address): NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4']) NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6']) - ip = ipaddress.ip_address(address.decode('ascii')) + 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.decode('ascii')) + self_network = ipaddress.ip_network(address) if WHITELIST: for wl_key in WHITELIST: - wl_net = ipaddress.ip_network(wl_key.decode('ascii'), False) + wl_net = ipaddress.ip_network(wl_key, False) if wl_net.overlaps(self_network): - log['time'] = int(round(time.time())) - log['priority'] = 'info' - log['message'] = 'Address %s is whitelisted by rule %s' % (self_network, wl_net) - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print('Address %s is whitelisted by rule %s' % (self_network, wl_net)) + 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)).decode('ascii'), strict=False) + 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 or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW: @@ -151,11 +156,8 @@ def ban(address): active_window = time.time() - bans[net]['last_attempt'] if bans[net]['attempts'] >= MAX_ATTEMPTS: - log['time'] = int(round(time.time())) - log['priority'] = 'crit' - log['message'] = 'Banning %s' % net - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print('Banning %s for %d minutes' % (net, BAN_TIME / 60)) + cur_time = int(round(time.time())) + logCrit('Banning %s for %d minutes' % (net, BAN_TIME / 60)) if type(ip) is ipaddress.IPv4Address: with lock: chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW') @@ -174,29 +176,18 @@ def ban(address): rule.target = target 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, cur_time + BAN_TIME) else: - log['time'] = int(round(time.time())) - log['priority'] = 'warn' - log['message'] = '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net) - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print('%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)) def unban(net): global lock - log['time'] = int(round(time.time())) - log['priority'] = 'info' - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) if not net in bans: - log['message'] = '%s is not banned, skipping unban and deleting from queue (if any)' % net - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print('%s is not banned, skipping unban and deleting from queue (if any)' % net) + logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net) r.hdel('F2B_QUEUE_UNBAN', '%s' % net) return - log['message'] = 'Unbanning %s' % net - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print('Unbanning %s' % net) - if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network: + 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() @@ -225,11 +216,7 @@ def quit(signum, frame): def clear(): global lock - log['time'] = int(round(time.time())) - log['priority'] = 'info' - log['message'] = 'Clearing all bans' - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print('Clearing all bans') + logInfo('Clearing all bans') for net in bans.copy(): unban(net) with lock: @@ -258,12 +245,8 @@ def clear(): pubsub.unsubscribe() def watch(): - log['time'] = int(round(time.time())) - log['priority'] = 'info' - log['message'] = 'Watching Redis channel F2B_CHANNEL' - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) + logInfo('Watching Redis channel F2B_CHANNEL') pubsub.subscribe('F2B_CHANNEL') - print('Subscribing to Redis channel F2B_CHANNEL') while not quit_now: for item in pubsub.listen(): @@ -272,14 +255,10 @@ def watch(): result = re.search(rule_regex, item['data']) if result: addr = result.group(1) - ip = ipaddress.ip_address(addr.decode('ascii')) + ip = ipaddress.ip_address(addr) if ip.is_private or ip.is_loopback: continue - print('%s matched rule id %d' % (addr, rule_id)) - log['time'] = int(round(time.time())) - log['priority'] = 'warn' - log['message'] = '%s matched rule id %d' % (addr, rule_id) - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) + logWarn('%s matched rule id %d' % (addr, rule_id)) ban(addr) def snat4(snat_target): @@ -303,11 +282,7 @@ def snat4(snat_target): chain = iptc.Chain(table, 'POSTROUTING') table.autocommit = False if get_snat4_rule() not in chain.rules: - log['time'] = int(round(time.time())) - log['priority'] = 'info' - log['message'] = 'Added POSTROUTING rule for source network ' + get_snat4_rule().src + ' to SNAT target ' + snat_target - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print(log['message']) + logCrit('Added POSTROUTING rule for source network %s to SNAT target %s' % (get_snat4_rule().src, snat_target)) chain.insert_rule(get_snat4_rule()) table.commit() else: @@ -341,11 +316,7 @@ def snat6(snat_target): 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']) + 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() else: @@ -375,7 +346,7 @@ def autopurge(): def isIpNetwork(address): try: - ipaddress.ip_network(address.decode('ascii'), False) + ipaddress.ip_network(address, False) except ValueError: return False return True @@ -387,7 +358,7 @@ def genNetworkList(list): for key in list: if isIpNetwork(key): - networks.append(key.encode("utf-8")) + networks.append(key) else: hostnames.append(key) @@ -397,24 +368,16 @@ def genNetworkList(list): try: answer = resolver.query(qname=hostname, rdtype=rdtype, lifetime=10) except dns.exception.Timeout: - log['time'] = int(round(time.time())) - log['priority'] = 'info' - log['message'] = 'Hostname %s timedout on resolve' % (hostname) - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print('Hostname %s timedout on resolve' % (hostname)) + logInfo('Hostname %s timedout on resolve' % hostname) break except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): continue except dns.exception.DNSException as dnsexception: - log['time'] = int(round(time.time())) - log['priority'] = 'info' - log['message'] = '%s' % (dnsexception) - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print('%s' % (dnsexception)) + logInfo('%s' % dnsexception) continue for rdata in answer: - hostname_ips.append(rdata.to_text().encode("utf-8")) + hostname_ips.append(rdata.to_text()) networks.extend(hostname_ips) @@ -432,11 +395,7 @@ def whitelistUpdate(): new_whitelist = genNetworkList(list) if Counter(new_whitelist) != Counter(WHITELIST): WHITELIST = new_whitelist - log['time'] = int(round(time.time())) - log['priority'] = 'info' - log['message'] = 'New entrys for whitelist %s' % (WHITELIST) - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print('New entrys for whitelist %s' % (WHITELIST)) + logInfo('New entrys for whitelist %s' % WHITELIST) time.sleep(60.0 - ((time.time() - start_time) % 60.0)) @@ -471,18 +430,14 @@ def initChain(): BLACKLIST = r.hgetall('F2B_BLACKLIST') if BLACKLIST: for bl_key in BLACKLIST: - if type(ipaddress.ip_network(bl_key.decode('ascii'), strict=False)) is ipaddress.IPv4Network: + if type(ipaddress.ip_network(bl_key, strict=False)) is ipaddress.IPv4Network: chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW') rule = iptc.Rule() rule.src = bl_key target = iptc.Target(rule, "REJECT") rule.target = target if rule not in chain.rules: - log['time'] = int(round(time.time())) - log['priority'] = 'crit' - log['message'] = 'Blacklisting host/network %s' % bl_key - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print(log['message']) + logCrit('Blacklisting host/network %s' % bl_key) chain.insert_rule(rule) r.hset('F2B_PERM_BANS', '%s' % bl_key, int(round(time.time()))) else: @@ -492,11 +447,7 @@ def initChain(): target = iptc.Target(rule, "REJECT") rule.target = target if rule not in chain.rules: - log['time'] = int(round(time.time())) - log['priority'] = 'crit' - log['message'] = 'Blacklisting host/network %s' % bl_key - r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False)) - print(log['message']) + logCrit('Blacklisting host/network %s' % bl_key) chain.insert_rule(rule) r.hset('F2B_PERM_BANS', '%s' % bl_key, int(round(time.time()))) @@ -513,7 +464,7 @@ if __name__ == '__main__': if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') is not 'n': try: - snat_ip = os.getenv('SNAT_TO_SOURCE').decode('ascii') + 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,)) @@ -524,7 +475,7 @@ if __name__ == '__main__': 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_ip = os.getenv('SNAT6_TO_SOURCE') snat_ipo = ipaddress.ip_address(snat_ip) if type(snat_ipo) is ipaddress.IPv6Address: snat6_thread = Thread(target=snat6,args=(snat_ip,)) From 9b02c9272eb11324c914c179689a1f6e6bb5a615 Mon Sep 17 00:00:00 2001 From: Kraeutergarten Date: Sun, 19 May 2019 10:55:11 +0200 Subject: [PATCH 6/8] clear whitelist, if it gets cleard. --- data/Dockerfiles/netfilter/server.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/data/Dockerfiles/netfilter/server.py b/data/Dockerfiles/netfilter/server.py index ac8556c7..c0e35696 100644 --- a/data/Dockerfiles/netfilter/server.py +++ b/data/Dockerfiles/netfilter/server.py @@ -391,12 +391,16 @@ def whitelistUpdate(): while not quit_now: start_time = time.time() list = r.hgetall('F2B_WHITELIST') + + gen_whitelist = [] + if list: - new_whitelist = genNetworkList(list) - if Counter(new_whitelist) != Counter(WHITELIST): - WHITELIST = new_whitelist - logInfo('New entrys for whitelist %s' % WHITELIST) - + gen_whitelist = genNetworkList(list) + + if Counter(gen_whitelist) != Counter(WHITELIST): + WHITELIST = gen_whitelist + logInfo('New entrys for whitelist %s' % WHITELIST) + time.sleep(60.0 - ((time.time() - start_time) % 60.0)) def initChain(): From e6de9c299d3c869d5ee5ab57eac0963354dbda76 Mon Sep 17 00:00:00 2001 From: Kraeutergarten Date: Mon, 20 May 2019 07:02:42 +0200 Subject: [PATCH 7/8] Fix wrong python version. --- data/Dockerfiles/netfilter/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/Dockerfiles/netfilter/Dockerfile b/data/Dockerfiles/netfilter/Dockerfile index e62b7dfa..7ed44192 100644 --- a/data/Dockerfiles/netfilter/Dockerfile +++ b/data/Dockerfiles/netfilter/Dockerfile @@ -10,4 +10,4 @@ RUN apk add -U python3 python3-dev gcc musl-dev iptables ip6tables tzdata \ && apk del python3-dev gcc COPY server.py / -CMD ["python2", "-u", "/server.py"] +CMD ["python3", "-u", "/server.py"] From b862ce2bfba24e8423169fbb02dee10f2111f9ac Mon Sep 17 00:00:00 2001 From: Kraeutergarten Date: Mon, 20 May 2019 09:02:40 +0200 Subject: [PATCH 8/8] Add hostnames for blacklist. --- data/Dockerfiles/netfilter/server.py | 122 +++++++++++++++++------- data/web/inc/functions.fail2ban.inc.php | 2 +- 2 files changed, 87 insertions(+), 37 deletions(-) diff --git a/data/Dockerfiles/netfilter/server.py b/data/Dockerfiles/netfilter/server.py index c0e35696..68652c28 100644 --- a/data/Dockerfiles/netfilter/server.py +++ b/data/Dockerfiles/netfilter/server.py @@ -28,8 +28,6 @@ while True: pubsub = r.pubsub() -resolver = dns.resolver.Resolver() - RULES = {} RULES[1] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed' RULES[2] = '-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),' @@ -40,6 +38,7 @@ RULES[6] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+' #RULES[7] = '-login: Aborted login \(no auth .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' WHITELIST = [] +BLACKLIST= [] bans = {} @@ -83,7 +82,7 @@ def refreshF2boptions(): try: f2boptions = {} f2boptions = json.loads(r.get('F2B_OPTIONS')) - except ValueError as e: + except ValueError: print('Error loading F2B options: F2B_OPTIONS is not json') quit_now = True @@ -132,9 +131,12 @@ def ban(address): return self_network = ipaddress.ip_network(address) + + with lock: + temp_whitelist = set(WHITELIST) - if WHITELIST: - for wl_key in WHITELIST: + if temp_whitelist: + for wl_key in temp_whitelist: wl_net = ipaddress.ip_network(wl_key, False) if wl_net.overlaps(self_network): @@ -210,6 +212,40 @@ def unban(net): if net in bans: del bans[net] +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: + with lock: + chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW') + rule = iptc.Rule6() + 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) + def quit(signum, frame): global quit_now quit_now = True @@ -353,6 +389,7 @@ def isIpNetwork(address): def genNetworkList(list): + resolver = dns.resolver.Resolver() hostnames = [] networks = [] @@ -366,7 +403,7 @@ def genNetworkList(list): hostname_ips = [] for rdtype in ['A', 'AAAA']: try: - answer = resolver.query(qname=hostname, rdtype=rdtype, lifetime=10) + answer = resolver.query(qname=hostname, rdtype=rdtype, lifetime=3) except dns.exception.Timeout: logInfo('Hostname %s timedout on resolve' % hostname) break @@ -381,7 +418,7 @@ def genNetworkList(list): networks.extend(hostname_ips) - return networks + return set(networks) def whitelistUpdate(): global lock @@ -392,16 +429,48 @@ def whitelistUpdate(): start_time = time.time() list = r.hgetall('F2B_WHITELIST') - gen_whitelist = [] + new_whitelist = [] if list: - gen_whitelist = genNetworkList(list) + new_whitelist = genNetworkList(list) - if Counter(gen_whitelist) != Counter(WHITELIST): - WHITELIST = gen_whitelist - logInfo('New entrys for whitelist %s' % WHITELIST) + 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 @@ -430,30 +499,7 @@ def initChain(): rule.target = target if rule not in chain.rules: chain.insert_rule(rule) - # Apply blacklist - BLACKLIST = r.hgetall('F2B_BLACKLIST') - if BLACKLIST: - for bl_key in BLACKLIST: - if type(ipaddress.ip_network(bl_key, strict=False)) is ipaddress.IPv4Network: - chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW') - rule = iptc.Rule() - rule.src = bl_key - target = iptc.Target(rule, "REJECT") - rule.target = target - if rule not in chain.rules: - logCrit('Blacklisting host/network %s' % bl_key) - chain.insert_rule(rule) - r.hset('F2B_PERM_BANS', '%s' % bl_key, int(round(time.time()))) - else: - chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW') - rule = iptc.Rule6() - rule.src = bl_key - target = iptc.Target(rule, "REJECT") - rule.target = target - if rule not in chain.rules: - logCrit('Blacklisting host/network %s' % bl_key) - chain.insert_rule(rule) - r.hset('F2B_PERM_BANS', '%s' % bl_key, int(round(time.time()))) + if __name__ == '__main__': @@ -495,6 +541,10 @@ if __name__ == '__main__': 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 diff --git a/data/web/inc/functions.fail2ban.inc.php b/data/web/inc/functions.fail2ban.inc.php index bf492490..d6440d1c 100644 --- a/data/web/inc/functions.fail2ban.inc.php +++ b/data/web/inc/functions.fail2ban.inc.php @@ -203,7 +203,7 @@ function fail2ban($_action, $_data = null) { $bl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $bl)); if (is_array($bl_array)) { foreach ($bl_array as $bl_item) { - if (valid_network($bl_item)) { + if (valid_network($bl_item) || valid_hostname($bl_item)) { $redis->hSet('F2B_BLACKLIST', $bl_item, 1); } }