Browse Source

Add whitelist function to Fail2ban

andryyy 8 years ago
parent
commit
b9ffcf2bf8
3 changed files with 62 additions and 5 deletions
  1. 32 5
      data/Dockerfiles/fail2ban/logwatch.py
  2. 4 0
      data/web/admin.php
  3. 26 0
      data/web/inc/functions.inc.php

+ 32 - 5
data/Dockerfiles/fail2ban/logwatch.py

@@ -12,7 +12,7 @@ import redis
 import time
 import json
 
-r = redis.StrictRedis(host='172.22.1.249', port=6379, db=0)
+r = redis.StrictRedis(host='172.22.1.249', decode_responses=True, port=6379, db=0)
 RULES = {
 	'mailcowdockerized_postfix-mailcow_1': 'warning: .*\[([0-9a-f\.:]+)\]: SASL .* authentication failed',
 	'mailcowdockerized_dovecot-mailcow_1': '-login: Disconnected \(auth failed, .*\): user=.*, method=.*, rip=([0-9a-f\.:]+),',
@@ -32,6 +32,7 @@ def ban(address):
 	BAN_TIME = int(r.get("F2B_BAN_TIME"))
 	MAX_ATTEMPTS = int(r.get("F2B_MAX_ATTEMPTS"))
 	RETRY_WINDOW = int(r.get("F2B_RETRY_WINDOW"))
+	WHITELIST = r.hgetall("F2B_WHITELIST")
 
 	ip = ipaddress.ip_address(address.decode('ascii'))
 	if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
@@ -39,7 +40,19 @@ def ban(address):
 		address = str(ip)
 	if ip.is_private or ip.is_loopback:
 		return
-	
+
+	self_network = ipaddress.ip_network(address.decode('ascii'))
+	if WHITELIST:
+		for wl_key in WHITELIST:
+			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"
+				log['message'] = "Address %s is whitelisted by rule %s" % (self_network, wl_net)
+				r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
+				print "Address %s is whitelisted by rule %s" % (self_network, wl_net)
+				return
+
 	net = ipaddress.ip_network((address + ('/24' if type(ip) is ipaddress.IPv4Address else '/64')).decode('ascii'), strict=False)
 	net = str(net)
 
@@ -48,10 +61,10 @@ def ban(address):
 		active_window = RETRY_WINDOW
 	else:
 		active_window = time.time() - bans[net]['last_attempt']
-	
+
 	bans[net]['attempts'] += 1
 	bans[net]['last_attempt'] = time.time()
-	
+
 	active_window = time.time() - bans[net]['last_attempt']
 
 	if bans[net]['attempts'] >= MAX_ATTEMPTS:
@@ -66,6 +79,7 @@ def ban(address):
 		else:
 			subprocess.call(["ip6tables", "-I", "INPUT", "-s", net, "-j", "REJECT"])
 			subprocess.call(["ip6tables", "-I", "FORWARD", "-s", net, "-j", "REJECT"])
+		r.hset("F2B_ACTIVE_BANS", "%s" % net, log['time'] + BAN_TIME)
 	else:
 		log['time'] = int(round(time.time()))
 		log['priority'] = "warn"
@@ -76,6 +90,13 @@ def ban(address):
 def unban(net):
 	log['time'] = int(round(time.time()))
 	log['priority'] = "info"
+	r.lpush("F2B_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("F2B_LOG", json.dumps(log, ensure_ascii=False))
+		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("F2B_LOG", json.dumps(log, ensure_ascii=False))
 	print "Unbanning %s" % net
@@ -85,6 +106,8 @@ def unban(net):
 	else:
 		subprocess.call(["ip6tables", "-D", "INPUT", "-s", net, "-j", "REJECT"])
 		subprocess.call(["ip6tables", "-D", "FORWARD", "-s", net, "-j", "REJECT"])
+	r.hdel("F2B_ACTIVE_BANS", "%s" % net)
+	r.hdel("F2B_QUEUE_UNBAN", "%s" % net)
 	del bans[net]
 
 def quit(signum, frame):
@@ -117,11 +140,15 @@ def autopurge():
 	while not quit_now:
 		BAN_TIME = int(r.get("F2B_BAN_TIME"))
 		MAX_ATTEMPTS = int(r.get("F2B_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:
 					unban(net)
-		time.sleep(60)
+		time.sleep(30)
 
 if __name__ == '__main__':
 	threads = []

+ 4 - 0
data/web/admin.php

@@ -289,6 +289,10 @@ $tfa_data = get_tfa();
             <label for="retry_window">Retry window (s) for max. attempts:</label>
             <input type="number" class="form-control" id="retry_window" name="retry_window" value="<?=$f2b_data['retry_window'];?>" required>
           </div>
+          <div class="form-group">
+            <label for="retry_window">Whitelisted networks/hosts</label>
+            <textarea class="form-control" id="whitelist" name="whitelist" rows="5"><?=$f2b_data['whitelist'];?></textarea>
+          </div>
           <button class="btn btn-default" id="add_item" data-id="f2b" data-api-url='edit/fail2ban' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
         </form>
       </div>

+ 26 - 0
data/web/inc/functions.inc.php

@@ -1446,6 +1446,16 @@ function get_f2b_parameters() {
     $data['ban_time'] = $redis->Get('F2B_BAN_TIME');
     $data['max_attempts'] = $redis->Get('F2B_MAX_ATTEMPTS');
     $data['retry_window'] = $redis->Get('F2B_RETRY_WINDOW');
+    $wl = $redis->hGetAll('F2B_WHITELIST');
+    if (is_array($wl)) {
+      foreach ($wl as $key => $value) {
+        $tmp_data[] = $key;
+      }
+      $data['whitelist'] = implode(PHP_EOL, $tmp_data);
+    }
+    else {
+      $data['whitelist'] = "";
+    }
   }
   catch (RedisException $e) {
     $_SESSION['return'] = array(
@@ -1479,6 +1489,7 @@ function edit_f2b_parameters($postarray) {
     );
     return false;
   }
+  $wl = $postarray['whitelist'];
   $ban_time = ($ban_time < 60) ? 60 : $ban_time;
   $max_attempts = ($max_attempts < 1) ? 1 : $max_attempts;
   $retry_window = ($retry_window < 1) ? 1 : $retry_window;
@@ -1486,6 +1497,21 @@ function edit_f2b_parameters($postarray) {
     $redis->Set('F2B_BAN_TIME', $ban_time);
     $redis->Set('F2B_MAX_ATTEMPTS', $max_attempts);
     $redis->Set('F2B_RETRY_WINDOW', $retry_window);
+    $redis->Del('F2B_WHITELIST');
+    if(!empty($wl)) {
+      $wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl));
+      if (is_array($wl_array)) {
+        foreach ($wl_array as $wl_item) {
+          $cidr = explode('/', $wl_item);
+          if (filter_var($cidr[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && (!isset($cidr[1]) || ($cidr[1] >= 8 && $cidr[1] <= 32))) {
+            $redis->hSet('F2B_WHITELIST', $wl_item, 1);
+          }
+          elseif (filter_var($cidr[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && (!isset($cidr[1]) || ($cidr[1] >= 16 && $cidr[1] <= 128))) {
+            $redis->hSet('F2B_WHITELIST', $wl_item, 1);
+          }
+        }
+      }
+    }
   }
   catch (RedisException $e) {
     $_SESSION['return'] = array(