Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Netfilter] Implemented exponentially incrementing bantime #5127

Merged
merged 5 commits into from
Mar 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 38 additions & 24 deletions data/Dockerfiles/netfilter/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,28 +64,40 @@ def refreshF2boptions():
global f2boptions
global quit_now
global exit_code

f2boptions = {}

if not r.get('F2B_OPTIONS'):
f2boptions = {}
f2boptions['ban_time'] = int
f2boptions['max_attempts'] = int
f2boptions['retry_window'] = int
f2boptions['netban_ipv4'] = int
f2boptions['netban_ipv6'] = int
f2boptions['ban_time'] = r.get('F2B_BAN_TIME') or 1800
f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS') or 10
f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW') or 600
f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4') or 32
f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6') or 128
r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
f2boptions['ban_time'] = r.get('F2B_BAN_TIME')
f2boptions['max_ban_time'] = r.get('F2B_MAX_BAN_TIME')
f2boptions['ban_time_increment'] = r.get('F2B_BAN_TIME_INCREMENT')
f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS')
f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW')
f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4')
f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6')
else:
try:
f2boptions = {}
f2boptions = json.loads(r.get('F2B_OPTIONS'))
except ValueError:
print('Error loading F2B options: F2B_OPTIONS is not json')
quit_now = True
exit_code = 2

verifyF2boptions(f2boptions)
r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))

def verifyF2boptions(f2boptions):
verifyF2boption(f2boptions,'ban_time', 1800)
verifyF2boption(f2boptions,'max_ban_time', 10000)
verifyF2boption(f2boptions,'ban_time_increment', True)
verifyF2boption(f2boptions,'max_attempts', 10)
verifyF2boption(f2boptions,'retry_window', 600)
verifyF2boption(f2boptions,'netban_ipv4', 32)
verifyF2boption(f2boptions,'netban_ipv6', 128)

def verifyF2boption(f2boptions, f2boption, f2bdefault):
f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault

def refreshF2bregex():
global f2bregex
global quit_now
Expand Down Expand Up @@ -147,6 +159,7 @@ def ban(address):
global lock
refreshF2boptions()
BAN_TIME = int(f2boptions['ban_time'])
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
RETRY_WINDOW = int(f2boptions['retry_window'])
NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
Expand Down Expand Up @@ -174,20 +187,16 @@ def ban(address):
net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
net = str(net)

if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
bans[net] = { 'attempts': 0 }
active_window = RETRY_WINDOW
else:
active_window = time.time() - bans[net]['last_attempt']
if not net in bans:
bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0}

bans[net]['attempts'] += 1
bans[net]['last_attempt'] = time.time()

active_window = time.time() - bans[net]['last_attempt']

if bans[net]['attempts'] >= MAX_ATTEMPTS:
cur_time = int(round(time.time()))
logCrit('Banning %s for %d minutes' % (net, BAN_TIME / 60))
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter']
logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
if type(ip) is ipaddress.IPv4Address:
with lock:
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
Expand All @@ -206,7 +215,7 @@ def ban(address):
rule.target = target
if rule not in chain.rules:
chain.insert_rule(rule)
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + BAN_TIME)
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME)
else:
logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))

Expand Down Expand Up @@ -238,7 +247,8 @@ def unban(net):
r.hdel('F2B_ACTIVE_BANS', '%s' % net)
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
if net in bans:
del bans[net]
bans[net]['attempts'] = 0
bans[net]['ban_counter'] += 1

def permBan(net, unban=False):
global lock
Expand Down Expand Up @@ -425,14 +435,18 @@ def autopurge():
time.sleep(10)
refreshF2boptions()
BAN_TIME = int(f2boptions['ban_time'])
MAX_BAN_TIME = int(f2boptions['max_ban_time'])
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
if QUEUE_UNBAN:
for net in QUEUE_UNBAN:
unban(str(net))
for net in bans.copy():
if bans[net]['attempts'] >= MAX_ATTEMPTS:
if time.time() - bans[net]['last_attempt'] > BAN_TIME:
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter']
TIME_SINCE_LAST_ATTEMPT = time.time() - bans[net]['last_attempt']
if TIME_SINCE_LAST_ATTEMPT > NET_BAN_TIME or TIME_SINCE_LAST_ATTEMPT > MAX_BAN_TIME:
unban(net)

def isIpNetwork(address):
Expand Down
12 changes: 11 additions & 1 deletion data/web/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3176,8 +3176,10 @@ paths:
example:
attr:
ban_time: "86400"
ban_time_increment: "1"
blacklist: "10.100.6.5/32,10.100.8.4/32"
max_attempts: "5"
max_ban_time: "86400"
netban_ipv4: "24"
netban_ipv6: "64"
retry_window: "600"
Expand All @@ -3191,11 +3193,17 @@ paths:
description: the backlisted ips or hostnames separated by comma
type: string
ban_time:
description: the time a ip should be banned
description: the time an ip should be banned
type: number
ban_time_increment:
description: if the time of the ban should increase each time
type: boolean
max_attempts:
description: the maximum numbe of wrong logins before a ip is banned
type: number
max_ban_time:
description: the maximum time an ip should be banned
type: number
netban_ipv4:
description: the networks mask to ban for ipv4
type: number
Expand Down Expand Up @@ -4113,10 +4121,12 @@ paths:
response:
value:
ban_time: 604800
ban_time_increment: 1
blacklist: |-
45.82.153.37/32
92.118.38.52/32
max_attempts: 1
max_ban_time: 604800
netban_ipv4: 32
netban_ipv6: 128
perm_bans:
Expand Down
4 changes: 4 additions & 0 deletions data/web/inc/functions.fail2ban.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,9 @@ function fail2ban($_action, $_data = null) {
$is_now = fail2ban('get');
if (!empty($is_now)) {
$ban_time = intval((isset($_data['ban_time'])) ? $_data['ban_time'] : $is_now['ban_time']);
$ban_time_increment = (isset($_data['ban_time_increment']) && $_data['ban_time_increment'] == "1") ? 1 : 0;
$max_attempts = intval((isset($_data['max_attempts'])) ? $_data['max_attempts'] : $is_now['max_attempts']);
$max_ban_time = intval((isset($_data['max_ban_time'])) ? $_data['max_ban_time'] : $is_now['max_ban_time']);
$retry_window = intval((isset($_data['retry_window'])) ? $_data['retry_window'] : $is_now['retry_window']);
$netban_ipv4 = intval((isset($_data['netban_ipv4'])) ? $_data['netban_ipv4'] : $is_now['netban_ipv4']);
$netban_ipv6 = intval((isset($_data['netban_ipv6'])) ? $_data['netban_ipv6'] : $is_now['netban_ipv6']);
Expand All @@ -256,6 +258,8 @@ function fail2ban($_action, $_data = null) {
}
$f2b_options = array();
$f2b_options['ban_time'] = ($ban_time < 60) ? 60 : $ban_time;
$f2b_options['ban_time_increment'] = ($ban_time_increment == 1) ? true : false;
$f2b_options['max_ban_time'] = ($max_ban_time < 60) ? 60 : $max_ban_time;
$f2b_options['netban_ipv4'] = ($netban_ipv4 < 8) ? 8 : $netban_ipv4;
$f2b_options['netban_ipv6'] = ($netban_ipv6 < 8) ? 8 : $netban_ipv6;
$f2b_options['netban_ipv4'] = ($netban_ipv4 > 32) ? 32 : $netban_ipv4;
Expand Down
2 changes: 2 additions & 0 deletions data/web/lang/lang.de-de.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,12 @@
"empty": "Keine Einträge vorhanden",
"excludes": "Diese Empfänger ausschließen",
"f2b_ban_time": "Bannzeit in Sekunden",
"f2b_ban_time_increment": "Bannzeit erhöht sich mit jedem Bann",
"f2b_blacklist": "Blacklist für Netzwerke und Hosts",
"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_max_attempts": "Max. Versuche",
"f2b_max_ban_time": "Maximale Bannzeit in Sekunden",
"f2b_netban_ipv4": "Netzbereich für IPv4-Banns (8-32)",
"f2b_netban_ipv6": "Netzbereich für IPv6-Banns (8-128)",
"f2b_parameters": "Fail2ban-Parameter",
Expand Down
2 changes: 2 additions & 0 deletions data/web/lang/lang.en-gb.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,12 @@
"empty": "No results",
"excludes": "Excludes these recipients",
"f2b_ban_time": "Ban time (s)",
"f2b_ban_time_increment": "Ban time is incremented with each ban",
"f2b_blacklist": "Blacklisted networks/hosts",
"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_max_attempts": "Max. attempts",
"f2b_max_ban_time": "Max. ban time (s)",
"f2b_netban_ipv4": "IPv4 subnet size to apply ban on (8-32)",
"f2b_netban_ipv6": "IPv6 subnet size to apply ban on (8-128)",
"f2b_parameters": "Fail2ban parameters",
Expand Down
2 changes: 2 additions & 0 deletions data/web/lang/lang.es-es.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,11 @@
"empty": "Sin resultados",
"excludes": "Excluye a estos destinatarios",
"f2b_ban_time": "Tiempo de restricción (s)",
"f2b_ban_time_increment": "Tiempo de restricción se incrementa con cada restricción",
"f2b_blacklist": "Redes y hosts en lista negra",
"f2b_list_info": "Un host o red en lista negra siempre superará a una entidad de la lista blanca. <b>Las actualizaciones de la lista tardarán unos segundos en aplicarse.</b>",
"f2b_max_attempts": "Max num. de intentos",
"f2b_max_ban_time": "Max tiempo de restricción (s)",
"f2b_netban_ipv4": "Tamaño de subred IPv4 para aplicar la restricción (8-32)",
"f2b_netban_ipv6": "Tamaño de subred IPv6 para aplicar la restricción (8-128)",
"f2b_parameters": "Parametros Fail2ban",
Expand Down
4 changes: 3 additions & 1 deletion data/web/lang/lang.fr-fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,13 @@
"edit": "Editer",
"empty": "Aucun résultat",
"excludes": "Exclure ces destinataires",
"f2b_ban_time": "Durée du bannissement(s)",
"f2b_ban_time": "Durée du bannissement (s)",
"f2b_ban_time_increment": "Durée du bannissement est augmentée à chaque bannissement",
"f2b_blacklist": "Réseaux/Domaines sur Liste Noire",
"f2b_filter": "Filtre(s) Regex",
"f2b_list_info": "Un hôte ou un réseau sur liste noire l'emportera toujours sur une entité de liste blanche. <b>L'application des mises à jour de liste prendra quelques secondes.</b>",
"f2b_max_attempts": "Nb max. de tentatives",
"f2b_max_ban_time": "Max. durée du bannissement (s)",
"f2b_netban_ipv4": "Taille du sous-réseau IPv4 pour l'application du bannissement (8-32)",
"f2b_netban_ipv6": "Taille du sous-réseau IPv6 pour l'application du bannissement (8-128)",
"f2b_parameters": "Paramètres Fail2ban",
Expand Down
2 changes: 2 additions & 0 deletions data/web/lang/lang.it-it.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,12 @@
"empty": "Nessun risultato",
"excludes": "Esclude questi destinatari",
"f2b_ban_time": "Tempo di blocco (s)",
"f2b_ban_time_increment": "Tempo di blocco aumenta ad ogni blocco",
"f2b_blacklist": "Host/reti in blacklist",
"f2b_filter": "Filtri Regex",
"f2b_list_info": "Un host oppure una rete in blacklist, avrà sempre un peso maggiore rispetto ad una in whitelist. <b>L'aggiornamento della lista richiede alcuni secondi per la sua entrata in azione.</b>",
"f2b_max_attempts": "Tentativi massimi",
"f2b_max_ban_time": "Tempo massimo di blocco (s)",
"f2b_netban_ipv4": "IPv4 subnet size to apply ban on (8-32)",
"f2b_netban_ipv6": "IPv6 subnet size to apply ban on (8-128)",
"f2b_parameters": "Parametri Fail2ban",
Expand Down
2 changes: 2 additions & 0 deletions data/web/lang/lang.nl-nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,12 @@
"empty": "Geen resultaten",
"excludes": "Exclusief",
"f2b_ban_time": "Verbanningstijd (s)",
"f2b_ban_time_increment": "Verbanningstijd wordt verhoogd met elk verbanning",
"f2b_blacklist": "Netwerken/hosts op de blacklist",
"f2b_filter": "Regex-filters",
"f2b_list_info": "Een host of netwerk op de blacklist staat altijd boven eenzelfde op de whitelist. <b>Het doorvoeren van wijzigingen kan enkele seconden in beslag nemen.</b>",
"f2b_max_attempts": "Maximaal aantal pogingen",
"f2b_max_ban_time": "Maximaal verbanningstijd (s)",
"f2b_netban_ipv4": "Voer de IPv4-subnetgrootte in waar de verbanning van kracht moet zijn (8-32)",
"f2b_netban_ipv6": "Voer de IPv6-subnetgrootte in waar de verbanning van kracht moet zijn (8-128)",
"f2b_parameters": "Fail2ban",
Expand Down
8 changes: 8 additions & 0 deletions data/web/templates/admin/tab-config-f2b.twig
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
<label for="f2b_ban_time">{{ lang.admin.f2b_ban_time }}:</label>
<input type="number" class="form-control" id="f2b_ban_time" name="ban_time" value="{{ f2b_data.ban_time }}" required>
</div>
<div class="mb-4">
<label for="f2b_max_ban_time">{{ lang.admin.f2b_max_ban_time }}:</label>
<input type="number" class="form-control" id="f2b_max_ban_time" name="max_ban_time" value="{{ f2b_data.max_ban_time }}" required>
</div>
<div class="mb-4">
<input class="form-check-input" type="checkbox" value="1" name="ban_time_increment" id="f2b_ban_time_increment" {% if f2b_data.ban_time_increment == 1 %}checked{% endif %}>
<label class="form-check-label" for="f2b_ban_time_increment">{{ lang.admin.f2b_ban_time_increment }}</label>
</div>
<div class="mb-4">
<label for="f2b_max_attempts">{{ lang.admin.f2b_max_attempts }}:</label>
<input type="number" class="form-control" id="f2b_max_attempts" name="max_attempts" value="{{ f2b_data.max_attempts }}" required>
Expand Down