git subrepo clone https://github.com/mailcow/mailcow-dockerized.git mailcow/src/mailcow-dockerized
subrepo: subdir: "mailcow/src/mailcow-dockerized"
merged: "a832becb"
upstream: origin: "https://github.com/mailcow/mailcow-dockerized.git"
branch: "master"
commit: "a832becb"
git-subrepo: version: "0.4.3"
origin: "???"
commit: "???"
Change-Id: If5be2d621a211e164c9b6577adaa7884449f16b5
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/functions.fail2ban.inc.php b/mailcow/src/mailcow-dockerized/data/web/inc/functions.fail2ban.inc.php
new file mode 100644
index 0000000..6f6b024
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/functions.fail2ban.inc.php
@@ -0,0 +1,332 @@
+<?php
+function fail2ban($_action, $_data = null) {
+ global $redis;
+ $_data_log = $_data;
+ switch ($_action) {
+ case 'get':
+ $f2b_options = array();
+ if ($_SESSION['mailcow_cc_role'] != "admin") {
+ return false;
+ }
+ try {
+ $f2b_options = json_decode($redis->Get('F2B_OPTIONS'), true);
+ $f2b_options['regex'] = json_decode($redis->Get('F2B_REGEX'), true);
+ $wl = $redis->hGetAll('F2B_WHITELIST');
+ if (is_array($wl)) {
+ foreach ($wl as $key => $value) {
+ $tmp_wl_data[] = $key;
+ }
+ if (isset($tmp_wl_data)) {
+ natsort($tmp_wl_data);
+ $f2b_options['whitelist'] = implode(PHP_EOL, $tmp_wl_data);
+ }
+ else {
+ $f2b_options['whitelist'] = "";
+ }
+ }
+ else {
+ $f2b_options['whitelist'] = "";
+ }
+ $bl = $redis->hGetAll('F2B_BLACKLIST');
+ if (is_array($bl)) {
+ foreach ($bl as $key => $value) {
+ $tmp_bl_data[] = $key;
+ }
+ if (isset($tmp_bl_data)) {
+ natsort($tmp_bl_data);
+ $f2b_options['blacklist'] = implode(PHP_EOL, $tmp_bl_data);
+ }
+ else {
+ $f2b_options['blacklist'] = "";
+ }
+ }
+ else {
+ $f2b_options['blacklist'] = "";
+ }
+ $pb = $redis->hGetAll('F2B_PERM_BANS');
+ if (is_array($pb)) {
+ foreach ($pb as $key => $value) {
+ $f2b_options['perm_bans'][] = $key;
+ }
+ }
+ else {
+ $f2b_options['perm_bans'] = "";
+ }
+ $active_bans = $redis->hGetAll('F2B_ACTIVE_BANS');
+ $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();
+ $f2b_options['active_bans'][] = array(
+ 'queued_for_unban' => $queued_for_unban,
+ 'network' => $network,
+ 'banned_until' => sprintf('%02dh %02dm %02ds', ($difference/3600), ($difference/60%60), $difference%60)
+ );
+ }
+ }
+ else {
+ $f2b_options['active_bans'] = "";
+ }
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('redis_error', $e)
+ );
+ return false;
+ }
+ return $f2b_options;
+ break;
+ case 'edit':
+ if ($_SESSION['mailcow_cc_role'] != "admin") {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+ // Start to read actions, if any
+ if (isset($_data['action'])) {
+ // Reset regex filters
+ if ($_data['action'] == "reset-regex") {
+ try {
+ $redis->Del('F2B_REGEX');
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('redis_error', $e)
+ );
+ 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');
+ $fail_count = 0;
+ $regex_result = json_decode($redis->Get('F2B_REGEX'), true);
+ while (empty($regex_result) && $fail_count < 10) {
+ $regex_result = json_decode($redis->Get('F2B_REGEX'), true);
+ $fail_count++;
+ sleep(1);
+ }
+ if ($fail_count >= 10) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('reset_f2b_regex')
+ );
+ return false;
+ }
+ }
+ elseif ($_data['action'] == "edit-regex") {
+ if (!empty($_data['regex'])) {
+ $rule_id = 1;
+ $regex_array = array();
+ foreach($_data['regex'] as $regex) {
+ $regex_array[$rule_id] = $regex;
+ $rule_id++;
+ }
+ if (!empty($regex_array)) {
+ $redis->Set('F2B_REGEX', json_encode($regex_array, JSON_UNESCAPED_SLASHES));
+ }
+ }
+ else {
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => print_r($_data, true)
+ );
+ return false;
+ }
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('object_modified', htmlspecialchars($network))
+ );
+ return true;
+ }
+
+ // Start actions in dependency of network
+ if (!empty($_data['network'])) {
+ $networks = (array)$_data['network'];
+ foreach ($networks as $network) {
+ // Unban network
+ if ($_data['action'] == "unban") {
+ if (valid_network($network)) {
+ try {
+ $redis->hSet('F2B_QUEUE_UNBAN', $network, 1);
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('redis_error', $e)
+ );
+ continue;
+ }
+ }
+ }
+ // Whitelist network
+ elseif ($_data['action'] == "whitelist") {
+ if (empty($network)) { continue; }
+ if (valid_network($network)) {
+ try {
+ $redis->hSet('F2B_WHITELIST', $network, 1);
+ $redis->hDel('F2B_BLACKLIST', $network, 1);
+ $redis->hSet('F2B_QUEUE_UNBAN', $network, 1);
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('redis_error', $e)
+ );
+ continue;
+ }
+ }
+ else {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('network_host_invalid', $network)
+ );
+ continue;
+ }
+ }
+ // Blacklist network
+ elseif ($_data['action'] == "blacklist") {
+ if (empty($network)) { continue; }
+ 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('IPV6_NETWORK')
+ ))) {
+ try {
+ $redis->hSet('F2B_BLACKLIST', $network, 1);
+ $redis->hDel('F2B_WHITELIST', $network, 1);
+ //$response = docker('post', 'netfilter-mailcow', 'restart');
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('redis_error', $e)
+ );
+ continue;
+ }
+ }
+ else {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('network_host_invalid', $network)
+ );
+ continue;
+ }
+ }
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('object_modified', htmlspecialchars($network))
+ );
+ }
+ return true;
+ }
+ }
+ // Start default edit without specific action
+ $is_now = fail2ban('get');
+ if (!empty($is_now)) {
+ $ban_time = intval((isset($_data['ban_time'])) ? $_data['ban_time'] : $is_now['ban_time']);
+ $max_attempts = intval((isset($_data['max_attempts'])) ? $_data['max_attempts'] : $is_now['max_attempts']);
+ $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']);
+ $wl = (isset($_data['whitelist'])) ? $_data['whitelist'] : $is_now['whitelist'];
+ $bl = (isset($_data['blacklist'])) ? $_data['blacklist'] : $is_now['blacklist'];
+ }
+ else {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+ $f2b_options = array();
+ $f2b_options['ban_time'] = ($ban_time < 60) ? 60 : $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['max_attempts'] = ($max_attempts < 1) ? 1 : $max_attempts;
+ $f2b_options['retry_window'] = ($retry_window < 1) ? 1 : $retry_window;
+ try {
+ $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;
+ }
+ }
+ }
+ }
+ 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;
+ }
+ }
+ }
+ }
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('redis_error', $e)
+ );
+ return false;
+ }
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => 'f2b_modified'
+ );
+ break;
+ }
+}