diff --git a/data/web/inc/functions.fail2ban.inc.php b/data/web/inc/functions.fail2ban.inc.php
index 5962237fc..cce7304b2 100644
--- a/data/web/inc/functions.fail2ban.inc.php
+++ b/data/web/inc/functions.fail2ban.inc.php
@@ -60,13 +60,14 @@ function fail2ban($_action, $_data = null, $_extra = null) {
$queue_unban = $redis->hGetAll('F2B_QUEUE_UNBAN');
if (is_array($active_bans)) {
foreach ($active_bans as $network => $banned_until) {
- $queued_for_unban = (isset($queue_unban[$network]) && $queue_unban[$network] == 1) ? 1 : 0;
- $difference = $banned_until - time();
+ // Expired bans are lifted by netfilter within seconds
+ $difference = max(0, intval($banned_until) - time());
+ $queued_for_unban = ($difference == 0 || (isset($queue_unban[$network]) && $queue_unban[$network] == 1)) ? 1 : 0;
$f2b_options['active_bans'][] = array(
'queued_for_unban' => $queued_for_unban,
'network' => $network,
'ip' => strtok($network,'/'),
- 'banned_until' => sprintf('%02dh %02dm %02ds', ($difference/3600), ($difference/60%60), $difference%60)
+ 'banned_until' => sprintf('%02dh %02dm %02ds', intdiv($difference, 3600), intdiv($difference, 60) % 60, $difference % 60)
);
}
}
@@ -108,16 +109,15 @@ function fail2ban($_action, $_data = null, $_extra = null) {
);
return false;
}
- // Rules will also be recreated on log events, but rules may seem empty for a second in the UI
- docker('post', 'netfilter-mailcow', 'restart');
+ // netfilter recreates the default rules within 10 seconds
$fail_count = 0;
$regex_result = json_decode($redis->Get('F2B_REGEX'), true);
- while (empty($regex_result) && $fail_count < 10) {
+ while (empty($regex_result) && $fail_count < 15) {
$regex_result = json_decode($redis->Get('F2B_REGEX'), true);
$fail_count++;
sleep(1);
}
- if ($fail_count >= 10) {
+ if (empty($regex_result)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
@@ -141,7 +141,7 @@ function fail2ban($_action, $_data = null, $_extra = null) {
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => array('object_modified', htmlspecialchars($network))
+ 'msg' => 'f2b_modified'
);
return true;
}
@@ -172,7 +172,7 @@ function fail2ban($_action, $_data = null, $_extra = null) {
if (valid_network($network)) {
try {
$redis->hSet('F2B_WHITELIST', $network, 1);
- $redis->hDel('F2B_BLACKLIST', $network, 1);
+ $redis->hDel('F2B_BLACKLIST', $network);
$redis->hSet('F2B_QUEUE_UNBAN', $network, 1);
}
catch (RedisException $e) {
@@ -199,14 +199,13 @@ function fail2ban($_action, $_data = null, $_extra = null) {
if (valid_network($network) && !in_array($network, array(
'0.0.0.0',
'0.0.0.0/0',
- getenv('IPV4_NETWORK') . '0/24',
- getenv('IPV4_NETWORK') . '0',
+ getenv('IPV4_NETWORK') . '.0/24',
+ getenv('IPV4_NETWORK') . '.0',
getenv('IPV6_NETWORK')
))) {
try {
$redis->hSet('F2B_BLACKLIST', $network, 1);
- $redis->hDel('F2B_WHITELIST', $network, 1);
- //$response = docker('post', 'netfilter-mailcow', 'restart');
+ $redis->hDel('F2B_WHITELIST', $network);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
@@ -261,62 +260,60 @@ function fail2ban($_action, $_data = null, $_extra = null) {
$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;
- $f2b_options['netban_ipv6'] = ($netban_ipv6 > 128) ? 128 : $netban_ipv6;
+ $f2b_options['netban_ipv4'] = min(32, max(8, $netban_ipv4));
+ $f2b_options['netban_ipv6'] = min(128, max(8, $netban_ipv6));
$f2b_options['max_attempts'] = ($max_attempts < 1) ? 1 : $max_attempts;
$f2b_options['retry_window'] = ($retry_window < 1) ? 1 : $retry_window;
- $f2b_options['banlist_id'] = $is_now['banlist_id'];
+ $f2b_options['banlist_id'] = (!empty($is_now['banlist_id'])) ? $is_now['banlist_id'] : uuid4();
$f2b_options['manage_external'] = ($manage_external > 0) ? 1 : 0;
+ $wl_items = array();
+ if (!empty($wl)) {
+ foreach (array_filter(array_map('trim', preg_split("/( |,|;|\n)/", $wl))) as $wl_item) {
+ if (valid_network($wl_item) || valid_hostname($wl_item)) {
+ $wl_items[$wl_item] = 1;
+ }
+ else {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('network_host_invalid', $wl_item)
+ );
+ }
+ }
+ }
+ $bl_items = array();
+ if (!empty($bl)) {
+ foreach (array_filter(array_map('trim', preg_split("/( |,|;|\n)/", $bl))) as $bl_item) {
+ if (valid_network($bl_item) && !in_array($bl_item, array(
+ '0.0.0.0',
+ '0.0.0.0/0',
+ getenv('IPV4_NETWORK') . '.0/24',
+ getenv('IPV4_NETWORK') . '.0',
+ getenv('IPV6_NETWORK')
+ ))) {
+ $bl_items[$bl_item] = 1;
+ }
+ else {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('network_host_invalid', $bl_item)
+ );
+ }
+ }
+ }
try {
+ // One transaction, netfilter must never see empty lists in between
+ $redis->multi();
$redis->Set('F2B_OPTIONS', json_encode($f2b_options));
- $redis->Del('F2B_WHITELIST');
- $redis->Del('F2B_BLACKLIST');
- if(!empty($wl)) {
- $wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl));
- $wl_array = array_filter($wl_array);
- if (is_array($wl_array)) {
- foreach ($wl_array as $wl_item) {
- if (valid_network($wl_item) || valid_hostname($wl_item)) {
- $redis->hSet('F2B_WHITELIST', $wl_item, 1);
- }
- else {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => array('network_host_invalid', $wl_item)
- );
- continue;
- }
- }
- }
+ $redis->Del('F2B_WHITELIST', 'F2B_BLACKLIST');
+ if (!empty($wl_items)) {
+ $redis->hMSet('F2B_WHITELIST', $wl_items);
}
- if(!empty($bl)) {
- $bl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $bl));
- $bl_array = array_filter($bl_array);
- if (is_array($bl_array)) {
- foreach ($bl_array as $bl_item) {
- if (valid_network($bl_item) && !in_array($bl_item, array(
- '0.0.0.0',
- '0.0.0.0/0',
- getenv('IPV4_NETWORK') . '0/24',
- getenv('IPV4_NETWORK') . '0',
- getenv('IPV6_NETWORK')
- ))) {
- $redis->hSet('F2B_BLACKLIST', $bl_item, 1);
- }
- else {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => array('network_host_invalid', $bl_item)
- );
- continue;
- }
- }
- }
+ if (!empty($bl_items)) {
+ $redis->hMSet('F2B_BLACKLIST', $bl_items);
}
+ $redis->exec();
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
@@ -348,7 +345,9 @@ function fail2ban($_action, $_data = null, $_extra = null) {
if (is_array($_extra)) {
$_extra = $_extra[0];
}
- if ($_extra != $f2b_options['banlist_id']){
+ // Timing safe, a missing or empty id never matches
+ $banlist_id = (string)($f2b_options['banlist_id'] ?? '');
+ if ($banlist_id === '' || !is_string($_extra) || !hash_equals($banlist_id, $_extra)) {
http_response_code(404);
return false;
}
diff --git a/data/web/templates/admin/tab-config-f2b.twig b/data/web/templates/admin/tab-config-f2b.twig
index 29535b723..1b5eb96b7 100644
--- a/data/web/templates/admin/tab-config-f2b.twig
+++ b/data/web/templates/admin/tab-config-f2b.twig
@@ -119,7 +119,7 @@
{% if active_ban.queued_for_unban == 0 %}
[{{ lang.admin.queue_unban }}]
[allowlist]
- [denylist ({{ lang.admin.needs_restart }})]
+ [denylist]
{% else %}
{{ lang.admin.unban_pending }}
{% endif %}