Merge pull request #5313 from mailcow/feat/f2b-banlist
[Web] add f2b_banlist endpoint
This commit is contained in:
commit
c2e5dfd933
@ -1,463 +1,469 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import atexit
|
import atexit
|
||||||
import signal
|
import signal
|
||||||
import ipaddress
|
import ipaddress
|
||||||
from collections import Counter
|
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 redis
|
||||||
import json
|
import json
|
||||||
import dns.resolver
|
import dns.resolver
|
||||||
import dns.exception
|
import dns.exception
|
||||||
from modules.Logger import Logger
|
import uuid
|
||||||
from modules.IPTables import IPTables
|
from modules.Logger import Logger
|
||||||
from modules.NFTables import NFTables
|
from modules.IPTables import IPTables
|
||||||
|
from modules.NFTables import NFTables
|
||||||
|
|
||||||
# connect to redis
|
|
||||||
while True:
|
# connect to redis
|
||||||
try:
|
while True:
|
||||||
redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
|
try:
|
||||||
redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '')
|
redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
|
||||||
if "".__eq__(redis_slaveof_ip):
|
redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '')
|
||||||
r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0)
|
if "".__eq__(redis_slaveof_ip):
|
||||||
else:
|
r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0)
|
||||||
r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0)
|
else:
|
||||||
r.ping()
|
r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0)
|
||||||
except Exception as ex:
|
r.ping()
|
||||||
print('%s - trying again in 3 seconds' % (ex))
|
except Exception as ex:
|
||||||
time.sleep(3)
|
print('%s - trying again in 3 seconds' % (ex))
|
||||||
else:
|
time.sleep(3)
|
||||||
break
|
else:
|
||||||
pubsub = r.pubsub()
|
break
|
||||||
|
pubsub = r.pubsub()
|
||||||
# rename fail2ban to netfilter
|
|
||||||
if r.exists('F2B_LOG'):
|
# rename fail2ban to netfilter
|
||||||
r.rename('F2B_LOG', 'NETFILTER_LOG')
|
if r.exists('F2B_LOG'):
|
||||||
|
r.rename('F2B_LOG', 'NETFILTER_LOG')
|
||||||
|
|
||||||
# globals
|
|
||||||
WHITELIST = []
|
# globals
|
||||||
BLACKLIST= []
|
WHITELIST = []
|
||||||
bans = {}
|
BLACKLIST= []
|
||||||
quit_now = False
|
bans = {}
|
||||||
exit_code = 0
|
quit_now = False
|
||||||
lock = Lock()
|
exit_code = 0
|
||||||
|
lock = Lock()
|
||||||
|
|
||||||
# init Logger
|
|
||||||
logger = Logger(r)
|
# init Logger
|
||||||
# init backend
|
logger = Logger(r)
|
||||||
backend = sys.argv[1]
|
# init backend
|
||||||
if backend == "nftables":
|
backend = sys.argv[1]
|
||||||
logger.logInfo('Using NFTables backend')
|
if backend == "nftables":
|
||||||
tables = NFTables("MAILCOW", logger)
|
logger.logInfo('Using NFTables backend')
|
||||||
else:
|
tables = NFTables("MAILCOW", logger)
|
||||||
logger.logInfo('Using IPTables backend')
|
else:
|
||||||
tables = IPTables("MAILCOW", logger)
|
logger.logInfo('Using IPTables backend')
|
||||||
|
tables = IPTables("MAILCOW", logger)
|
||||||
|
|
||||||
def refreshF2boptions():
|
|
||||||
global f2boptions
|
def refreshF2boptions():
|
||||||
global quit_now
|
global f2boptions
|
||||||
global exit_code
|
global quit_now
|
||||||
|
global exit_code
|
||||||
f2boptions = {}
|
|
||||||
|
f2boptions = {}
|
||||||
if not r.get('F2B_OPTIONS'):
|
|
||||||
f2boptions['ban_time'] = r.get('F2B_BAN_TIME')
|
if not r.get('F2B_OPTIONS'):
|
||||||
f2boptions['max_ban_time'] = r.get('F2B_MAX_BAN_TIME')
|
f2boptions['ban_time'] = r.get('F2B_BAN_TIME')
|
||||||
f2boptions['ban_time_increment'] = r.get('F2B_BAN_TIME_INCREMENT')
|
f2boptions['max_ban_time'] = r.get('F2B_MAX_BAN_TIME')
|
||||||
f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS')
|
f2boptions['ban_time_increment'] = r.get('F2B_BAN_TIME_INCREMENT')
|
||||||
f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW')
|
f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS')
|
||||||
f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4')
|
f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW')
|
||||||
f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6')
|
f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4')
|
||||||
else:
|
f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6')
|
||||||
try:
|
else:
|
||||||
f2boptions = json.loads(r.get('F2B_OPTIONS'))
|
try:
|
||||||
except ValueError:
|
f2boptions = json.loads(r.get('F2B_OPTIONS'))
|
||||||
logger.logCrit('Error loading F2B options: F2B_OPTIONS is not json')
|
except ValueError:
|
||||||
quit_now = True
|
logger.logCrit('Error loading F2B options: F2B_OPTIONS is not json')
|
||||||
exit_code = 2
|
quit_now = True
|
||||||
|
exit_code = 2
|
||||||
verifyF2boptions(f2boptions)
|
|
||||||
r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
|
verifyF2boptions(f2boptions)
|
||||||
|
r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
|
||||||
def verifyF2boptions(f2boptions):
|
|
||||||
verifyF2boption(f2boptions,'ban_time', 1800)
|
def verifyF2boptions(f2boptions):
|
||||||
verifyF2boption(f2boptions,'max_ban_time', 10000)
|
verifyF2boption(f2boptions,'ban_time', 1800)
|
||||||
verifyF2boption(f2boptions,'ban_time_increment', True)
|
verifyF2boption(f2boptions,'max_ban_time', 10000)
|
||||||
verifyF2boption(f2boptions,'max_attempts', 10)
|
verifyF2boption(f2boptions,'ban_time_increment', True)
|
||||||
verifyF2boption(f2boptions,'retry_window', 600)
|
verifyF2boption(f2boptions,'max_attempts', 10)
|
||||||
verifyF2boption(f2boptions,'netban_ipv4', 32)
|
verifyF2boption(f2boptions,'retry_window', 600)
|
||||||
verifyF2boption(f2boptions,'netban_ipv6', 128)
|
verifyF2boption(f2boptions,'netban_ipv4', 32)
|
||||||
|
verifyF2boption(f2boptions,'netban_ipv6', 128)
|
||||||
def verifyF2boption(f2boptions, f2boption, f2bdefault):
|
verifyF2boption(f2boptions,'banlist_id', str(uuid.uuid4()))
|
||||||
f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault
|
verifyF2boption(f2boptions,'manage_external', 0)
|
||||||
|
|
||||||
def refreshF2bregex():
|
def verifyF2boption(f2boptions, f2boption, f2bdefault):
|
||||||
global f2bregex
|
f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault
|
||||||
global quit_now
|
|
||||||
global exit_code
|
def refreshF2bregex():
|
||||||
if not r.get('F2B_REGEX'):
|
global f2bregex
|
||||||
f2bregex = {}
|
global quit_now
|
||||||
f2bregex[1] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
|
global exit_code
|
||||||
f2bregex[2] = 'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'
|
if not r.get('F2B_REGEX'):
|
||||||
f2bregex[3] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+'
|
f2bregex = {}
|
||||||
f2bregex[4] = 'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+'
|
f2bregex[1] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
|
||||||
f2bregex[5] = 'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+'
|
f2bregex[2] = 'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'
|
||||||
f2bregex[6] = '-login: Disconnected.+ \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
|
f2bregex[3] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+'
|
||||||
f2bregex[7] = '-login: Aborted login.+ \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
|
f2bregex[4] = 'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+'
|
||||||
f2bregex[8] = '-login: Aborted login.+ \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
|
f2bregex[5] = 'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+'
|
||||||
f2bregex[9] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
|
f2bregex[6] = '-login: Disconnected.+ \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
|
||||||
f2bregex[10] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
|
f2bregex[7] = '-login: Aborted login.+ \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
|
||||||
r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False))
|
f2bregex[8] = '-login: Aborted login.+ \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
|
||||||
else:
|
f2bregex[9] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
|
||||||
try:
|
f2bregex[10] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
|
||||||
f2bregex = {}
|
r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False))
|
||||||
f2bregex = json.loads(r.get('F2B_REGEX'))
|
else:
|
||||||
except ValueError:
|
try:
|
||||||
logger.logCrit('Error loading F2B options: F2B_REGEX is not json')
|
f2bregex = {}
|
||||||
quit_now = True
|
f2bregex = json.loads(r.get('F2B_REGEX'))
|
||||||
exit_code = 2
|
except ValueError:
|
||||||
|
logger.logCrit('Error loading F2B options: F2B_REGEX is not json')
|
||||||
def get_ip(address):
|
quit_now = True
|
||||||
ip = ipaddress.ip_address(address)
|
exit_code = 2
|
||||||
if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
|
|
||||||
ip = ip.ipv4_mapped
|
def get_ip(address):
|
||||||
if ip.is_private or ip.is_loopback:
|
ip = ipaddress.ip_address(address)
|
||||||
return False
|
if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
|
||||||
|
ip = ip.ipv4_mapped
|
||||||
return ip
|
if ip.is_private or ip.is_loopback:
|
||||||
|
return False
|
||||||
def ban(address):
|
|
||||||
global lock
|
return ip
|
||||||
|
|
||||||
refreshF2boptions()
|
def ban(address):
|
||||||
BAN_TIME = int(f2boptions['ban_time'])
|
global f2boptions
|
||||||
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
|
global lock
|
||||||
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
|
||||||
RETRY_WINDOW = int(f2boptions['retry_window'])
|
refreshF2boptions()
|
||||||
NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
|
BAN_TIME = int(f2boptions['ban_time'])
|
||||||
NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
|
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
|
||||||
|
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
||||||
ip = get_ip(address)
|
RETRY_WINDOW = int(f2boptions['retry_window'])
|
||||||
if not ip: return
|
NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
|
||||||
address = str(ip)
|
NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
|
||||||
self_network = ipaddress.ip_network(address)
|
|
||||||
|
ip = get_ip(address)
|
||||||
with lock:
|
if not ip: return
|
||||||
temp_whitelist = set(WHITELIST)
|
address = str(ip)
|
||||||
if temp_whitelist:
|
self_network = ipaddress.ip_network(address)
|
||||||
for wl_key in temp_whitelist:
|
|
||||||
wl_net = ipaddress.ip_network(wl_key, False)
|
with lock:
|
||||||
if wl_net.overlaps(self_network):
|
temp_whitelist = set(WHITELIST)
|
||||||
logger.logInfo('Address %s is whitelisted by rule %s' % (self_network, wl_net))
|
if temp_whitelist:
|
||||||
return
|
for wl_key in temp_whitelist:
|
||||||
|
wl_net = ipaddress.ip_network(wl_key, False)
|
||||||
net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
|
if wl_net.overlaps(self_network):
|
||||||
net = str(net)
|
logger.logInfo('Address %s is whitelisted by rule %s' % (self_network, wl_net))
|
||||||
|
return
|
||||||
if not net in bans:
|
|
||||||
bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0}
|
net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
|
||||||
|
net = str(net)
|
||||||
current_attempt = time.time()
|
|
||||||
if current_attempt - bans[net]['last_attempt'] > RETRY_WINDOW:
|
if not net in bans:
|
||||||
bans[net]['attempts'] = 0
|
bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0}
|
||||||
|
|
||||||
bans[net]['attempts'] += 1
|
current_attempt = time.time()
|
||||||
bans[net]['last_attempt'] = current_attempt
|
if current_attempt - bans[net]['last_attempt'] > RETRY_WINDOW:
|
||||||
|
bans[net]['attempts'] = 0
|
||||||
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
|
||||||
cur_time = int(round(time.time()))
|
bans[net]['attempts'] += 1
|
||||||
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter']
|
bans[net]['last_attempt'] = current_attempt
|
||||||
logger.logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
|
|
||||||
if type(ip) is ipaddress.IPv4Address:
|
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
||||||
with lock:
|
cur_time = int(round(time.time()))
|
||||||
tables.banIPv4(net)
|
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter']
|
||||||
else:
|
logger.logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
|
||||||
with lock:
|
if type(ip) is ipaddress.IPv4Address and int(f2boptions['manage_external']) != 1:
|
||||||
tables.banIPv6(net)
|
with lock:
|
||||||
|
tables.banIPv4(net)
|
||||||
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME)
|
elif int(f2boptions['manage_external']) != 1:
|
||||||
else:
|
with lock:
|
||||||
logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
|
tables.banIPv6(net)
|
||||||
|
|
||||||
def unban(net):
|
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME)
|
||||||
global lock
|
else:
|
||||||
|
logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
|
||||||
if not net in bans:
|
|
||||||
logger.logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net)
|
def unban(net):
|
||||||
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
global lock
|
||||||
return
|
|
||||||
|
if not net in bans:
|
||||||
logger.logInfo('Unbanning %s' % net)
|
logger.logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net)
|
||||||
if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
|
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
||||||
with lock:
|
return
|
||||||
tables.unbanIPv4(net)
|
|
||||||
else:
|
logger.logInfo('Unbanning %s' % net)
|
||||||
with lock:
|
if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
|
||||||
tables.unbanIPv6(net)
|
with lock:
|
||||||
|
tables.unbanIPv4(net)
|
||||||
r.hdel('F2B_ACTIVE_BANS', '%s' % net)
|
else:
|
||||||
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
with lock:
|
||||||
if net in bans:
|
tables.unbanIPv6(net)
|
||||||
bans[net]['attempts'] = 0
|
|
||||||
bans[net]['ban_counter'] += 1
|
r.hdel('F2B_ACTIVE_BANS', '%s' % net)
|
||||||
|
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
||||||
def permBan(net, unban=False):
|
if net in bans:
|
||||||
global lock
|
bans[net]['attempts'] = 0
|
||||||
|
bans[net]['ban_counter'] += 1
|
||||||
is_unbanned = False
|
|
||||||
is_banned = False
|
def permBan(net, unban=False):
|
||||||
if type(ipaddress.ip_network(net, strict=False)) is ipaddress.IPv4Network:
|
global f2boptions
|
||||||
with lock:
|
global lock
|
||||||
if unban:
|
|
||||||
is_unbanned = tables.unbanIPv4(net)
|
is_unbanned = False
|
||||||
else:
|
is_banned = False
|
||||||
is_banned = tables.banIPv4(net)
|
if type(ipaddress.ip_network(net, strict=False)) is ipaddress.IPv4Network:
|
||||||
else:
|
with lock:
|
||||||
with lock:
|
if unban:
|
||||||
if unban:
|
is_unbanned = tables.unbanIPv4(net)
|
||||||
is_unbanned = tables.unbanIPv6(net)
|
elif int(f2boptions['manage_external']) != 1:
|
||||||
else:
|
is_banned = tables.banIPv4(net)
|
||||||
is_banned = tables.banIPv6(net)
|
else:
|
||||||
|
with lock:
|
||||||
|
if unban:
|
||||||
if is_unbanned:
|
is_unbanned = tables.unbanIPv6(net)
|
||||||
r.hdel('F2B_PERM_BANS', '%s' % net)
|
elif int(f2boptions['manage_external']) != 1:
|
||||||
logger.logCrit('Removed host/network %s from blacklist' % net)
|
is_banned = tables.banIPv6(net)
|
||||||
elif is_banned:
|
|
||||||
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
|
|
||||||
logger.logCrit('Added host/network %s to blacklist' % net)
|
if is_unbanned:
|
||||||
|
r.hdel('F2B_PERM_BANS', '%s' % net)
|
||||||
def clear():
|
logger.logCrit('Removed host/network %s from blacklist' % net)
|
||||||
global lock
|
elif is_banned:
|
||||||
logger.logInfo('Clearing all bans')
|
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
|
||||||
for net in bans.copy():
|
logger.logCrit('Added host/network %s to blacklist' % net)
|
||||||
unban(net)
|
|
||||||
with lock:
|
def clear():
|
||||||
tables.clearIPv4Table()
|
global lock
|
||||||
tables.clearIPv6Table()
|
logger.logInfo('Clearing all bans')
|
||||||
r.delete('F2B_ACTIVE_BANS')
|
for net in bans.copy():
|
||||||
r.delete('F2B_PERM_BANS')
|
unban(net)
|
||||||
pubsub.unsubscribe()
|
with lock:
|
||||||
|
tables.clearIPv4Table()
|
||||||
def watch():
|
tables.clearIPv6Table()
|
||||||
logger.logInfo('Watching Redis channel F2B_CHANNEL')
|
r.delete('F2B_ACTIVE_BANS')
|
||||||
pubsub.subscribe('F2B_CHANNEL')
|
r.delete('F2B_PERM_BANS')
|
||||||
|
pubsub.unsubscribe()
|
||||||
global quit_now
|
|
||||||
global exit_code
|
def watch():
|
||||||
|
logger.logInfo('Watching Redis channel F2B_CHANNEL')
|
||||||
while not quit_now:
|
pubsub.subscribe('F2B_CHANNEL')
|
||||||
try:
|
|
||||||
for item in pubsub.listen():
|
global quit_now
|
||||||
refreshF2bregex()
|
global exit_code
|
||||||
for rule_id, rule_regex in f2bregex.items():
|
|
||||||
if item['data'] and item['type'] == 'message':
|
while not quit_now:
|
||||||
try:
|
try:
|
||||||
result = re.search(rule_regex, item['data'])
|
for item in pubsub.listen():
|
||||||
except re.error:
|
refreshF2bregex()
|
||||||
result = False
|
for rule_id, rule_regex in f2bregex.items():
|
||||||
if result:
|
if item['data'] and item['type'] == 'message':
|
||||||
addr = result.group(1)
|
try:
|
||||||
ip = ipaddress.ip_address(addr)
|
result = re.search(rule_regex, item['data'])
|
||||||
if ip.is_private or ip.is_loopback:
|
except re.error:
|
||||||
continue
|
result = False
|
||||||
logger.logWarn('%s matched rule id %s (%s)' % (addr, rule_id, item['data']))
|
if result:
|
||||||
ban(addr)
|
addr = result.group(1)
|
||||||
except Exception as ex:
|
ip = ipaddress.ip_address(addr)
|
||||||
logger.logWarn('Error reading log line from pubsub: %s' % ex)
|
if ip.is_private or ip.is_loopback:
|
||||||
quit_now = True
|
continue
|
||||||
exit_code = 2
|
logger.logWarn('%s matched rule id %s (%s)' % (addr, rule_id, item['data']))
|
||||||
|
ban(addr)
|
||||||
def snat4(snat_target):
|
except Exception as ex:
|
||||||
global lock
|
logger.logWarn('Error reading log line from pubsub: %s' % ex)
|
||||||
global quit_now
|
quit_now = True
|
||||||
|
exit_code = 2
|
||||||
while not quit_now:
|
|
||||||
time.sleep(10)
|
def snat4(snat_target):
|
||||||
with lock:
|
global lock
|
||||||
tables.snat4(snat_target, os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24')
|
global quit_now
|
||||||
|
|
||||||
def snat6(snat_target):
|
while not quit_now:
|
||||||
global lock
|
time.sleep(10)
|
||||||
global quit_now
|
with lock:
|
||||||
|
tables.snat4(snat_target, os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24')
|
||||||
while not quit_now:
|
|
||||||
time.sleep(10)
|
def snat6(snat_target):
|
||||||
with lock:
|
global lock
|
||||||
tables.snat6(snat_target, os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64'))
|
global quit_now
|
||||||
|
|
||||||
def autopurge():
|
while not quit_now:
|
||||||
while not quit_now:
|
time.sleep(10)
|
||||||
time.sleep(10)
|
with lock:
|
||||||
refreshF2boptions()
|
tables.snat6(snat_target, os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64'))
|
||||||
BAN_TIME = int(f2boptions['ban_time'])
|
|
||||||
MAX_BAN_TIME = int(f2boptions['max_ban_time'])
|
def autopurge():
|
||||||
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
|
while not quit_now:
|
||||||
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
time.sleep(10)
|
||||||
QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
|
refreshF2boptions()
|
||||||
if QUEUE_UNBAN:
|
BAN_TIME = int(f2boptions['ban_time'])
|
||||||
for net in QUEUE_UNBAN:
|
MAX_BAN_TIME = int(f2boptions['max_ban_time'])
|
||||||
unban(str(net))
|
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
|
||||||
for net in bans.copy():
|
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
||||||
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
|
||||||
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter']
|
if QUEUE_UNBAN:
|
||||||
TIME_SINCE_LAST_ATTEMPT = time.time() - bans[net]['last_attempt']
|
for net in QUEUE_UNBAN:
|
||||||
if TIME_SINCE_LAST_ATTEMPT > NET_BAN_TIME or TIME_SINCE_LAST_ATTEMPT > MAX_BAN_TIME:
|
unban(str(net))
|
||||||
unban(net)
|
for net in bans.copy():
|
||||||
|
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
||||||
def mailcowChainOrder():
|
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter']
|
||||||
global lock
|
TIME_SINCE_LAST_ATTEMPT = time.time() - bans[net]['last_attempt']
|
||||||
global quit_now
|
if TIME_SINCE_LAST_ATTEMPT > NET_BAN_TIME or TIME_SINCE_LAST_ATTEMPT > MAX_BAN_TIME:
|
||||||
global exit_code
|
unban(net)
|
||||||
while not quit_now:
|
|
||||||
time.sleep(10)
|
def mailcowChainOrder():
|
||||||
with lock:
|
global lock
|
||||||
quit_now, exit_code = tables.checkIPv4ChainOrder()
|
global quit_now
|
||||||
if quit_now: return
|
global exit_code
|
||||||
quit_now, exit_code = tables.checkIPv6ChainOrder()
|
while not quit_now:
|
||||||
|
time.sleep(10)
|
||||||
def isIpNetwork(address):
|
with lock:
|
||||||
try:
|
quit_now, exit_code = tables.checkIPv4ChainOrder()
|
||||||
ipaddress.ip_network(address, False)
|
if quit_now: return
|
||||||
except ValueError:
|
quit_now, exit_code = tables.checkIPv6ChainOrder()
|
||||||
return False
|
|
||||||
return True
|
def isIpNetwork(address):
|
||||||
|
try:
|
||||||
def genNetworkList(list):
|
ipaddress.ip_network(address, False)
|
||||||
resolver = dns.resolver.Resolver()
|
except ValueError:
|
||||||
hostnames = []
|
return False
|
||||||
networks = []
|
return True
|
||||||
for key in list:
|
|
||||||
if isIpNetwork(key):
|
def genNetworkList(list):
|
||||||
networks.append(key)
|
resolver = dns.resolver.Resolver()
|
||||||
else:
|
hostnames = []
|
||||||
hostnames.append(key)
|
networks = []
|
||||||
for hostname in hostnames:
|
for key in list:
|
||||||
hostname_ips = []
|
if isIpNetwork(key):
|
||||||
for rdtype in ['A', 'AAAA']:
|
networks.append(key)
|
||||||
try:
|
else:
|
||||||
answer = resolver.resolve(qname=hostname, rdtype=rdtype, lifetime=3)
|
hostnames.append(key)
|
||||||
except dns.exception.Timeout:
|
for hostname in hostnames:
|
||||||
logger.logInfo('Hostname %s timedout on resolve' % hostname)
|
hostname_ips = []
|
||||||
break
|
for rdtype in ['A', 'AAAA']:
|
||||||
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
try:
|
||||||
continue
|
answer = resolver.resolve(qname=hostname, rdtype=rdtype, lifetime=3)
|
||||||
except dns.exception.DNSException as dnsexception:
|
except dns.exception.Timeout:
|
||||||
logger.logInfo('%s' % dnsexception)
|
logger.logInfo('Hostname %s timedout on resolve' % hostname)
|
||||||
continue
|
break
|
||||||
for rdata in answer:
|
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
||||||
hostname_ips.append(rdata.to_text())
|
continue
|
||||||
networks.extend(hostname_ips)
|
except dns.exception.DNSException as dnsexception:
|
||||||
return set(networks)
|
logger.logInfo('%s' % dnsexception)
|
||||||
|
continue
|
||||||
def whitelistUpdate():
|
for rdata in answer:
|
||||||
global lock
|
hostname_ips.append(rdata.to_text())
|
||||||
global quit_now
|
networks.extend(hostname_ips)
|
||||||
global WHITELIST
|
return set(networks)
|
||||||
while not quit_now:
|
|
||||||
start_time = time.time()
|
def whitelistUpdate():
|
||||||
list = r.hgetall('F2B_WHITELIST')
|
global lock
|
||||||
new_whitelist = []
|
global quit_now
|
||||||
if list:
|
global WHITELIST
|
||||||
new_whitelist = genNetworkList(list)
|
while not quit_now:
|
||||||
with lock:
|
start_time = time.time()
|
||||||
if Counter(new_whitelist) != Counter(WHITELIST):
|
list = r.hgetall('F2B_WHITELIST')
|
||||||
WHITELIST = new_whitelist
|
new_whitelist = []
|
||||||
logger.logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))
|
if list:
|
||||||
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
|
new_whitelist = genNetworkList(list)
|
||||||
|
with lock:
|
||||||
def blacklistUpdate():
|
if Counter(new_whitelist) != Counter(WHITELIST):
|
||||||
global quit_now
|
WHITELIST = new_whitelist
|
||||||
global BLACKLIST
|
logger.logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))
|
||||||
while not quit_now:
|
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
|
||||||
start_time = time.time()
|
|
||||||
list = r.hgetall('F2B_BLACKLIST')
|
def blacklistUpdate():
|
||||||
new_blacklist = []
|
global quit_now
|
||||||
if list:
|
global BLACKLIST
|
||||||
new_blacklist = genNetworkList(list)
|
while not quit_now:
|
||||||
if Counter(new_blacklist) != Counter(BLACKLIST):
|
start_time = time.time()
|
||||||
addban = set(new_blacklist).difference(BLACKLIST)
|
list = r.hgetall('F2B_BLACKLIST')
|
||||||
delban = set(BLACKLIST).difference(new_blacklist)
|
new_blacklist = []
|
||||||
BLACKLIST = new_blacklist
|
if list:
|
||||||
logger.logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST))
|
new_blacklist = genNetworkList(list)
|
||||||
if addban:
|
if Counter(new_blacklist) != Counter(BLACKLIST):
|
||||||
for net in addban:
|
addban = set(new_blacklist).difference(BLACKLIST)
|
||||||
permBan(net=net)
|
delban = set(BLACKLIST).difference(new_blacklist)
|
||||||
if delban:
|
BLACKLIST = new_blacklist
|
||||||
for net in delban:
|
logger.logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST))
|
||||||
permBan(net=net, unban=True)
|
if addban:
|
||||||
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
|
for net in addban:
|
||||||
|
permBan(net=net)
|
||||||
def quit(signum, frame):
|
if delban:
|
||||||
global quit_now
|
for net in delban:
|
||||||
quit_now = True
|
permBan(net=net, unban=True)
|
||||||
|
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
def quit(signum, frame):
|
||||||
# In case a previous session was killed without cleanup
|
global quit_now
|
||||||
clear()
|
quit_now = True
|
||||||
# Reinit MAILCOW chain
|
|
||||||
# Is called before threads start, no locking
|
|
||||||
logger.logInfo("Initializing mailcow netfilter chain")
|
if __name__ == '__main__':
|
||||||
tables.initChainIPv4()
|
refreshF2boptions()
|
||||||
tables.initChainIPv6()
|
# In case a previous session was killed without cleanup
|
||||||
|
clear()
|
||||||
watch_thread = Thread(target=watch)
|
# Reinit MAILCOW chain
|
||||||
watch_thread.daemon = True
|
# Is called before threads start, no locking
|
||||||
watch_thread.start()
|
logger.logInfo("Initializing mailcow netfilter chain")
|
||||||
|
tables.initChainIPv4()
|
||||||
if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') != 'n':
|
tables.initChainIPv6()
|
||||||
try:
|
|
||||||
snat_ip = os.getenv('SNAT_TO_SOURCE')
|
watch_thread = Thread(target=watch)
|
||||||
snat_ipo = ipaddress.ip_address(snat_ip)
|
watch_thread.daemon = True
|
||||||
if type(snat_ipo) is ipaddress.IPv4Address:
|
watch_thread.start()
|
||||||
snat4_thread = Thread(target=snat4,args=(snat_ip,))
|
|
||||||
snat4_thread.daemon = True
|
if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') != 'n':
|
||||||
snat4_thread.start()
|
try:
|
||||||
except ValueError:
|
snat_ip = os.getenv('SNAT_TO_SOURCE')
|
||||||
print(os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address')
|
snat_ipo = ipaddress.ip_address(snat_ip)
|
||||||
|
if type(snat_ipo) is ipaddress.IPv4Address:
|
||||||
if os.getenv('SNAT6_TO_SOURCE') and os.getenv('SNAT6_TO_SOURCE') != 'n':
|
snat4_thread = Thread(target=snat4,args=(snat_ip,))
|
||||||
try:
|
snat4_thread.daemon = True
|
||||||
snat_ip = os.getenv('SNAT6_TO_SOURCE')
|
snat4_thread.start()
|
||||||
snat_ipo = ipaddress.ip_address(snat_ip)
|
except ValueError:
|
||||||
if type(snat_ipo) is ipaddress.IPv6Address:
|
print(os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address')
|
||||||
snat6_thread = Thread(target=snat6,args=(snat_ip,))
|
|
||||||
snat6_thread.daemon = True
|
if os.getenv('SNAT6_TO_SOURCE') and os.getenv('SNAT6_TO_SOURCE') != 'n':
|
||||||
snat6_thread.start()
|
try:
|
||||||
except ValueError:
|
snat_ip = os.getenv('SNAT6_TO_SOURCE')
|
||||||
print(os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address')
|
snat_ipo = ipaddress.ip_address(snat_ip)
|
||||||
|
if type(snat_ipo) is ipaddress.IPv6Address:
|
||||||
autopurge_thread = Thread(target=autopurge)
|
snat6_thread = Thread(target=snat6,args=(snat_ip,))
|
||||||
autopurge_thread.daemon = True
|
snat6_thread.daemon = True
|
||||||
autopurge_thread.start()
|
snat6_thread.start()
|
||||||
|
except ValueError:
|
||||||
mailcowchainwatch_thread = Thread(target=mailcowChainOrder)
|
print(os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address')
|
||||||
mailcowchainwatch_thread.daemon = True
|
|
||||||
mailcowchainwatch_thread.start()
|
autopurge_thread = Thread(target=autopurge)
|
||||||
|
autopurge_thread.daemon = True
|
||||||
blacklistupdate_thread = Thread(target=blacklistUpdate)
|
autopurge_thread.start()
|
||||||
blacklistupdate_thread.daemon = True
|
|
||||||
blacklistupdate_thread.start()
|
mailcowchainwatch_thread = Thread(target=mailcowChainOrder)
|
||||||
|
mailcowchainwatch_thread.daemon = True
|
||||||
whitelistupdate_thread = Thread(target=whitelistUpdate)
|
mailcowchainwatch_thread.start()
|
||||||
whitelistupdate_thread.daemon = True
|
|
||||||
whitelistupdate_thread.start()
|
blacklistupdate_thread = Thread(target=blacklistUpdate)
|
||||||
|
blacklistupdate_thread.daemon = True
|
||||||
signal.signal(signal.SIGTERM, quit)
|
blacklistupdate_thread.start()
|
||||||
atexit.register(clear)
|
|
||||||
|
whitelistupdate_thread = Thread(target=whitelistUpdate)
|
||||||
while not quit_now:
|
whitelistupdate_thread.daemon = True
|
||||||
time.sleep(0.5)
|
whitelistupdate_thread.start()
|
||||||
|
|
||||||
sys.exit(exit_code)
|
signal.signal(signal.SIGTERM, quit)
|
||||||
|
atexit.register(clear)
|
||||||
|
|
||||||
|
while not quit_now:
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
sys.exit(exit_code)
|
||||||
|
@ -85,6 +85,8 @@ $cors_settings = cors('get');
|
|||||||
$cors_settings['allowed_origins'] = str_replace(", ", "\n", $cors_settings['allowed_origins']);
|
$cors_settings['allowed_origins'] = str_replace(", ", "\n", $cors_settings['allowed_origins']);
|
||||||
$cors_settings['allowed_methods'] = explode(", ", $cors_settings['allowed_methods']);
|
$cors_settings['allowed_methods'] = explode(", ", $cors_settings['allowed_methods']);
|
||||||
|
|
||||||
|
$f2b_data = fail2ban('get');
|
||||||
|
|
||||||
$template = 'admin.twig';
|
$template = 'admin.twig';
|
||||||
$template_data = [
|
$template_data = [
|
||||||
'tfa_data' => $tfa_data,
|
'tfa_data' => $tfa_data,
|
||||||
@ -101,7 +103,8 @@ $template_data = [
|
|||||||
'domains' => $domains,
|
'domains' => $domains,
|
||||||
'all_domains' => $all_domains,
|
'all_domains' => $all_domains,
|
||||||
'mailboxes' => $mailboxes,
|
'mailboxes' => $mailboxes,
|
||||||
'f2b_data' => fail2ban('get'),
|
'f2b_data' => $f2b_data,
|
||||||
|
'f2b_banlist_url' => getBaseUrl() . "/api/v1/get/fail2ban/banlist/" . $f2b_data['banlist_id'],
|
||||||
'q_data' => quarantine('settings'),
|
'q_data' => quarantine('settings'),
|
||||||
'qn_data' => quota_notification('get'),
|
'qn_data' => quota_notification('get'),
|
||||||
'rsettings_map' => file_get_contents('http://nginx:8081/settings.php'),
|
'rsettings_map' => file_get_contents('http://nginx:8081/settings.php'),
|
||||||
@ -113,6 +116,7 @@ $template_data = [
|
|||||||
'password_complexity' => password_complexity('get'),
|
'password_complexity' => password_complexity('get'),
|
||||||
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
|
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
|
||||||
'cors_settings' => $cors_settings,
|
'cors_settings' => $cors_settings,
|
||||||
|
'is_https' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
|
||||||
'lang_admin' => json_encode($lang['admin']),
|
'lang_admin' => json_encode($lang['admin']),
|
||||||
'lang_datatables' => json_encode($lang['datatables'])
|
'lang_datatables' => json_encode($lang['datatables'])
|
||||||
];
|
];
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
function fail2ban($_action, $_data = null) {
|
function fail2ban($_action, $_data = null, $_extra = null) {
|
||||||
global $redis;
|
global $redis;
|
||||||
$_data_log = $_data;
|
$_data_log = $_data;
|
||||||
switch ($_action) {
|
switch ($_action) {
|
||||||
@ -247,6 +247,7 @@ function fail2ban($_action, $_data = null) {
|
|||||||
$netban_ipv6 = intval((isset($_data['netban_ipv6'])) ? $_data['netban_ipv6'] : $is_now['netban_ipv6']);
|
$netban_ipv6 = intval((isset($_data['netban_ipv6'])) ? $_data['netban_ipv6'] : $is_now['netban_ipv6']);
|
||||||
$wl = (isset($_data['whitelist'])) ? $_data['whitelist'] : $is_now['whitelist'];
|
$wl = (isset($_data['whitelist'])) ? $_data['whitelist'] : $is_now['whitelist'];
|
||||||
$bl = (isset($_data['blacklist'])) ? $_data['blacklist'] : $is_now['blacklist'];
|
$bl = (isset($_data['blacklist'])) ? $_data['blacklist'] : $is_now['blacklist'];
|
||||||
|
$manage_external = (isset($_data['manage_external'])) ? intval($_data['manage_external']) : 0;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
@ -266,6 +267,8 @@ function fail2ban($_action, $_data = null) {
|
|||||||
$f2b_options['netban_ipv6'] = ($netban_ipv6 > 128) ? 128 : $netban_ipv6;
|
$f2b_options['netban_ipv6'] = ($netban_ipv6 > 128) ? 128 : $netban_ipv6;
|
||||||
$f2b_options['max_attempts'] = ($max_attempts < 1) ? 1 : $max_attempts;
|
$f2b_options['max_attempts'] = ($max_attempts < 1) ? 1 : $max_attempts;
|
||||||
$f2b_options['retry_window'] = ($retry_window < 1) ? 1 : $retry_window;
|
$f2b_options['retry_window'] = ($retry_window < 1) ? 1 : $retry_window;
|
||||||
|
$f2b_options['banlist_id'] = $is_now['banlist_id'];
|
||||||
|
$f2b_options['manage_external'] = ($manage_external > 0) ? 1 : 0;
|
||||||
try {
|
try {
|
||||||
$redis->Set('F2B_OPTIONS', json_encode($f2b_options));
|
$redis->Set('F2B_OPTIONS', json_encode($f2b_options));
|
||||||
$redis->Del('F2B_WHITELIST');
|
$redis->Del('F2B_WHITELIST');
|
||||||
@ -329,5 +332,71 @@ function fail2ban($_action, $_data = null) {
|
|||||||
'msg' => 'f2b_modified'
|
'msg' => 'f2b_modified'
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case 'banlist':
|
||||||
|
try {
|
||||||
|
$f2b_options = json_decode($redis->Get('F2B_OPTIONS'), true);
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
|
||||||
|
'msg' => array('redis_error', $e)
|
||||||
|
);
|
||||||
|
http_response_code(500);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (is_array($_extra)) {
|
||||||
|
$_extra = $_extra[0];
|
||||||
|
}
|
||||||
|
if ($_extra != $f2b_options['banlist_id']){
|
||||||
|
http_response_code(404);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($_data) {
|
||||||
|
case 'get':
|
||||||
|
try {
|
||||||
|
$bl = $redis->hKeys('F2B_BLACKLIST');
|
||||||
|
$active_bans = $redis->hKeys('F2B_ACTIVE_BANS');
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
|
||||||
|
'msg' => array('redis_error', $e)
|
||||||
|
);
|
||||||
|
http_response_code(500);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$banlist = implode("\n", array_merge($bl, $active_bans));
|
||||||
|
return $banlist;
|
||||||
|
break;
|
||||||
|
case 'refresh':
|
||||||
|
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$f2b_options['banlist_id'] = uuid4();
|
||||||
|
try {
|
||||||
|
$redis->Set('F2B_OPTIONS', json_encode($f2b_options));
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
|
||||||
|
'msg' => array('redis_error', $e)
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'success',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
|
||||||
|
'msg' => 'f2b_banlist_refreshed'
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2246,6 +2246,21 @@ function cors($action, $data = null) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function getBaseURL() {
|
||||||
|
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
|
||||||
|
$host = $_SERVER['HTTP_HOST'];
|
||||||
|
$base_url = $protocol . '://' . $host;
|
||||||
|
|
||||||
|
return $base_url;
|
||||||
|
}
|
||||||
|
function uuid4() {
|
||||||
|
$data = openssl_random_pseudo_bytes(16);
|
||||||
|
|
||||||
|
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
|
||||||
|
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
|
||||||
|
|
||||||
|
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
|
||||||
|
}
|
||||||
|
|
||||||
function get_logs($application, $lines = false) {
|
function get_logs($application, $lines = false) {
|
||||||
if ($lines === false) {
|
if ($lines === false) {
|
||||||
|
@ -70,6 +70,8 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception $e) {
|
catch (Exception $e) {
|
||||||
|
// Stop when redis is not available
|
||||||
|
http_response_code(500);
|
||||||
?>
|
?>
|
||||||
<center style='font-family:sans-serif;'>Connection to Redis failed.<br /><br />The following error was reported:<br/><?=$e->getMessage();?></center>
|
<center style='font-family:sans-serif;'>Connection to Redis failed.<br /><br />The following error was reported:<br/><?=$e->getMessage();?></center>
|
||||||
<?php
|
<?php
|
||||||
@ -98,6 +100,7 @@ try {
|
|||||||
}
|
}
|
||||||
catch (PDOException $e) {
|
catch (PDOException $e) {
|
||||||
// Stop when SQL connection fails
|
// Stop when SQL connection fails
|
||||||
|
http_response_code(500);
|
||||||
?>
|
?>
|
||||||
<center style='font-family:sans-serif;'>Connection to database failed.<br /><br />The following error was reported:<br/> <?=$e->getMessage();?></center>
|
<center style='font-family:sans-serif;'>Connection to database failed.<br /><br />The following error was reported:<br/> <?=$e->getMessage();?></center>
|
||||||
<?php
|
<?php
|
||||||
@ -105,6 +108,7 @@ exit;
|
|||||||
}
|
}
|
||||||
// Stop when dockerapi is not available
|
// Stop when dockerapi is not available
|
||||||
if (fsockopen("tcp://dockerapi", 443, $errno, $errstr) === false) {
|
if (fsockopen("tcp://dockerapi", 443, $errno, $errstr) === false) {
|
||||||
|
http_response_code(500);
|
||||||
?>
|
?>
|
||||||
<center style='font-family:sans-serif;'>Connection to dockerapi container failed.<br /><br />The following error was reported:<br/><?=$errno;?> - <?=$errstr;?></center>
|
<center style='font-family:sans-serif;'>Connection to dockerapi container failed.<br /><br />The following error was reported:<br/><?=$errno;?> - <?=$errstr;?></center>
|
||||||
<?php
|
<?php
|
||||||
|
@ -391,3 +391,11 @@ function addTag(tagAddElem, tag = null){
|
|||||||
$(tagValuesElem).val(JSON.stringify(value_tags));
|
$(tagValuesElem).val(JSON.stringify(value_tags));
|
||||||
$(tagInputElem).val('');
|
$(tagInputElem).val('');
|
||||||
}
|
}
|
||||||
|
function copyToClipboard(id) {
|
||||||
|
var copyText = document.getElementById(id);
|
||||||
|
copyText.select();
|
||||||
|
copyText.setSelectionRange(0, 99999);
|
||||||
|
// only works with https connections
|
||||||
|
navigator.clipboard.writeText(copyText.value);
|
||||||
|
mailcow_alert_box(lang.copy_to_clipboard, "success");
|
||||||
|
}
|
@ -503,6 +503,16 @@ if (isset($_GET['query'])) {
|
|||||||
print(json_encode($getArgs));
|
print(json_encode($getArgs));
|
||||||
$_SESSION['challenge'] = $WebAuthn->getChallenge();
|
$_SESSION['challenge'] = $WebAuthn->getChallenge();
|
||||||
return;
|
return;
|
||||||
|
break;
|
||||||
|
case "fail2ban":
|
||||||
|
if (!isset($_SESSION['mailcow_cc_role'])){
|
||||||
|
switch ($object) {
|
||||||
|
case 'banlist':
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
echo fail2ban('banlist', 'get', $extra);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (isset($_SESSION['mailcow_cc_role'])) {
|
if (isset($_SESSION['mailcow_cc_role'])) {
|
||||||
@ -1324,6 +1334,10 @@ if (isset($_GET['query'])) {
|
|||||||
break;
|
break;
|
||||||
case "fail2ban":
|
case "fail2ban":
|
||||||
switch ($object) {
|
switch ($object) {
|
||||||
|
case 'banlist':
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
echo fail2ban('banlist', 'get', $extra);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
$data = fail2ban('get');
|
$data = fail2ban('get');
|
||||||
process_get_return($data);
|
process_get_return($data);
|
||||||
@ -1943,7 +1957,14 @@ if (isset($_GET['query'])) {
|
|||||||
process_edit_return(fwdhost('edit', array_merge(array('fwdhost' => $items), $attr)));
|
process_edit_return(fwdhost('edit', array_merge(array('fwdhost' => $items), $attr)));
|
||||||
break;
|
break;
|
||||||
case "fail2ban":
|
case "fail2ban":
|
||||||
process_edit_return(fail2ban('edit', array_merge(array('network' => $items), $attr)));
|
switch ($object) {
|
||||||
|
case 'banlist':
|
||||||
|
process_edit_return(fail2ban('banlist', 'refresh', $items));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
process_edit_return(fail2ban('edit', array_merge(array('network' => $items), $attr)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "ui_texts":
|
case "ui_texts":
|
||||||
process_edit_return(customize('edit', 'ui_texts', $attr));
|
process_edit_return(customize('edit', 'ui_texts', $attr));
|
||||||
|
@ -148,6 +148,7 @@
|
|||||||
"change_logo": "Logo ändern",
|
"change_logo": "Logo ändern",
|
||||||
"configuration": "Konfiguration",
|
"configuration": "Konfiguration",
|
||||||
"convert_html_to_text": "Konvertiere HTML zu reinem Text",
|
"convert_html_to_text": "Konvertiere HTML zu reinem Text",
|
||||||
|
"copy_to_clipboard": "Text wurde in die Zwischenablage kopiert!",
|
||||||
"cors_settings": "CORS Einstellungen",
|
"cors_settings": "CORS Einstellungen",
|
||||||
"credentials_transport_warning": "<b>Warnung</b>: Das Hinzufügen einer neuen Regel bewirkt die Aktualisierung der Authentifizierungsdaten aller vorhandenen Einträge mit identischem Next Hop.",
|
"credentials_transport_warning": "<b>Warnung</b>: Das Hinzufügen einer neuen Regel bewirkt die Aktualisierung der Authentifizierungsdaten aller vorhandenen Einträge mit identischem Next Hop.",
|
||||||
"customer_id": "Kunde",
|
"customer_id": "Kunde",
|
||||||
@ -181,6 +182,8 @@
|
|||||||
"f2b_blacklist": "Blacklist für Netzwerke und Hosts",
|
"f2b_blacklist": "Blacklist für Netzwerke und Hosts",
|
||||||
"f2b_filter": "Regex-Filter",
|
"f2b_filter": "Regex-Filter",
|
||||||
"f2b_list_info": "Ein Host oder Netzwerk auf der Blacklist wird immer eine Whitelist-Einheit überwiegen. <b>Die Aktualisierung der Liste dauert einige Sekunden.</b>",
|
"f2b_list_info": "Ein Host oder Netzwerk auf der Blacklist wird immer eine Whitelist-Einheit überwiegen. <b>Die Aktualisierung der Liste dauert einige Sekunden.</b>",
|
||||||
|
"f2b_manage_external": "Fail2Ban extern verwalten",
|
||||||
|
"f2b_manage_external_info": "Fail2ban wird die Banlist weiterhin pflegen, jedoch werden keine aktiven Regeln zum blockieren gesetzt. Die unten generierte Banlist, kann verwendet werden, um den Datenverkehr extern zu blockieren.",
|
||||||
"f2b_max_attempts": "Max. Versuche",
|
"f2b_max_attempts": "Max. Versuche",
|
||||||
"f2b_max_ban_time": "Maximale Bannzeit in Sekunden",
|
"f2b_max_ban_time": "Maximale Bannzeit in Sekunden",
|
||||||
"f2b_netban_ipv4": "Netzbereich für IPv4-Banns (8-32)",
|
"f2b_netban_ipv4": "Netzbereich für IPv4-Banns (8-32)",
|
||||||
@ -1035,6 +1038,7 @@
|
|||||||
"domain_removed": "Domain %s wurde entfernt",
|
"domain_removed": "Domain %s wurde entfernt",
|
||||||
"dovecot_restart_success": "Dovecot wurde erfolgreich neu gestartet",
|
"dovecot_restart_success": "Dovecot wurde erfolgreich neu gestartet",
|
||||||
"eas_reset": "ActiveSync Gerät des Benutzers %s wurde zurückgesetzt",
|
"eas_reset": "ActiveSync Gerät des Benutzers %s wurde zurückgesetzt",
|
||||||
|
"f2b_banlist_refreshed": "Banlist ID wurde erfolgreich erneuert.",
|
||||||
"f2b_modified": "Änderungen an Fail2ban-Parametern wurden gespeichert",
|
"f2b_modified": "Änderungen an Fail2ban-Parametern wurden gespeichert",
|
||||||
"forwarding_host_added": "Weiterleitungs-Host %s wurde hinzugefügt",
|
"forwarding_host_added": "Weiterleitungs-Host %s wurde hinzugefügt",
|
||||||
"forwarding_host_removed": "Weiterleitungs-Host %s wurde entfernt",
|
"forwarding_host_removed": "Weiterleitungs-Host %s wurde entfernt",
|
||||||
|
@ -154,6 +154,7 @@
|
|||||||
"logo_dark_label": "Inverted for dark mode",
|
"logo_dark_label": "Inverted for dark mode",
|
||||||
"configuration": "Configuration",
|
"configuration": "Configuration",
|
||||||
"convert_html_to_text": "Convert HTML to plain text",
|
"convert_html_to_text": "Convert HTML to plain text",
|
||||||
|
"copy_to_clipboard": "Text copied to clipboard!",
|
||||||
"cors_settings": "CORS Settings",
|
"cors_settings": "CORS Settings",
|
||||||
"credentials_transport_warning": "<b>Warning</b>: Adding a new transport map entry will update the credentials for all entries with a matching next hop column.",
|
"credentials_transport_warning": "<b>Warning</b>: Adding a new transport map entry will update the credentials for all entries with a matching next hop column.",
|
||||||
"customer_id": "Customer ID",
|
"customer_id": "Customer ID",
|
||||||
@ -187,6 +188,8 @@
|
|||||||
"f2b_blacklist": "Blacklisted networks/hosts",
|
"f2b_blacklist": "Blacklisted networks/hosts",
|
||||||
"f2b_filter": "Regex filters",
|
"f2b_filter": "Regex filters",
|
||||||
"f2b_list_info": "A blacklisted host or network will always outweigh a whitelist entity. <b>List updates will take a few seconds to be applied.</b>",
|
"f2b_list_info": "A blacklisted host or network will always outweigh a whitelist entity. <b>List updates will take a few seconds to be applied.</b>",
|
||||||
|
"f2b_manage_external": "Manage Fail2Ban externally",
|
||||||
|
"f2b_manage_external_info": "Fail2ban will still maintain the banlist, but it will not actively set rules to block traffic. Use the generated banlist below to externally block the traffic.",
|
||||||
"f2b_max_attempts": "Max. attempts",
|
"f2b_max_attempts": "Max. attempts",
|
||||||
"f2b_max_ban_time": "Max. ban time (s)",
|
"f2b_max_ban_time": "Max. ban time (s)",
|
||||||
"f2b_netban_ipv4": "IPv4 subnet size to apply ban on (8-32)",
|
"f2b_netban_ipv4": "IPv4 subnet size to apply ban on (8-32)",
|
||||||
@ -1046,6 +1049,7 @@
|
|||||||
"domain_removed": "Domain %s has been removed",
|
"domain_removed": "Domain %s has been removed",
|
||||||
"dovecot_restart_success": "Dovecot was restarted successfully",
|
"dovecot_restart_success": "Dovecot was restarted successfully",
|
||||||
"eas_reset": "ActiveSync devices for user %s were reset",
|
"eas_reset": "ActiveSync devices for user %s were reset",
|
||||||
|
"f2b_banlist_refreshed": "Banlist ID has been successfully refreshed.",
|
||||||
"f2b_modified": "Changes to Fail2ban parameters have been saved",
|
"f2b_modified": "Changes to Fail2ban parameters have been saved",
|
||||||
"forwarding_host_added": "Forwarding host %s has been added",
|
"forwarding_host_added": "Forwarding host %s has been added",
|
||||||
"forwarding_host_removed": "Forwarding host %s has been removed",
|
"forwarding_host_removed": "Forwarding host %s has been removed",
|
||||||
|
@ -42,6 +42,13 @@
|
|||||||
<input type="number" class="form-control" id="f2b_netban_ipv6" name="netban_ipv6" value="{{ f2b_data.netban_ipv6 }}" required>
|
<input type="number" class="form-control" id="f2b_netban_ipv6" name="netban_ipv6" value="{{ f2b_data.netban_ipv6 }}" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" id="f2b_manage_external" value="1" name="manage_external" {% if f2b_data.manage_external == 1 %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="f2b_manage_external">{{ lang.admin.f2b_manage_external }}</label>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted">{{ lang.admin.f2b_manage_external_info }}</p>
|
||||||
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<p class="text-muted">{{ lang.admin.f2b_list_info|raw }}</p>
|
<p class="text-muted">{{ lang.admin.f2b_list_info|raw }}</p>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
@ -90,6 +97,15 @@
|
|||||||
{% if not f2b_data.active_bans and not f2b_data.perm_bans %}
|
{% if not f2b_data.active_bans and not f2b_data.perm_bans %}
|
||||||
<i>{{ lang.admin.no_active_bans }}</i>
|
<i>{{ lang.admin.no_active_bans }}</i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<form class="form-inline" data-id="f2b_banlist" role="form" method="post">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input type="text" class="form-control" aria-label="Banlist url" value="{{ f2b_banlist_url}}" id="banlist_url">
|
||||||
|
{% if is_https %}
|
||||||
|
<button class="btn btn-secondary" type="button" onclick="copyToClipboard('banlist_url')"><i class="bi bi-clipboard"></i></button>
|
||||||
|
{% endif %}
|
||||||
|
<button class="btn btn-secondary" type="button" data-action="edit_selected" data-item="{{ f2b_data.banlist_id }}" data-id="f2b_banlist" data-api-url='edit/fail2ban/banlist' data-api-attr='{}'><i class="bi bi-arrow-clockwise"></i></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
{% for active_ban in f2b_data.active_bans %}
|
{% for active_ban in f2b_data.active_bans %}
|
||||||
<p>
|
<p>
|
||||||
<span class="badge fs-7 bg-info d-block d-sm-inline-block">
|
<span class="badge fs-7 bg-info d-block d-sm-inline-block">
|
||||||
|
Loading…
Reference in New Issue
Block a user