From 25237df6358b0d0d9899601f5ea17cdc6debf0cd Mon Sep 17 00:00:00 2001
From: Thuban <thuban@yeuxdelibad.net>
Date: Tue, 11 Apr 2017 08:24:31 +0000
Subject: [PATCH] debug

---
 vilain.py |  126 ++++++++++++++++++++++++-----------------
 1 files changed, 73 insertions(+), 53 deletions(-)

diff --git a/vilain.py b/vilain.py
index 24e2ff4..dcfb90e 100755
--- a/vilain.py
+++ b/vilain.py
@@ -9,13 +9,11 @@
 
 Description : Mimic fail2ban with pf for OpenBSD.
               Inspired from http://www.vincentdelft.be/post/post_20161106
+              with improvements of vincendelft
 
               In pf.conf, add : 
                     table <vilain_bruteforce> persist
                     block quick from <vilain_bruteforce> 
-
-              You might want to add a cron task to remove old banned IP. As example, to ban for one day max : 
-                    pfctl -t vilain_bruteforce -T expire 86400
 
               To see banned IP : 
                     pfctl -t vilain_bruteforce -T show
@@ -29,13 +27,11 @@
 import subprocess
 import asyncio
 import time
-from multiprocessing import Process
 
 configfile = "/etc/vilain.conf"
-version = "0.3"
+version = "0.4"
 vilain_table = "vilain_bruteforce"
 logfile = "/var/log/daemon"
-sleeptime = 0.5
 
 if os.geteuid() != 0:
     print("Only root can use this tool")
@@ -46,7 +42,7 @@
 logging.basicConfig(filename=logfile,
                     format='%(asctime)s %(module)s:%(funcName)s:%(message)s',
                     datefmt='%H:%M:%S')
-logger.setLevel(logging.DEBUG)
+logger.setLevel(logging.INFO)
 ch = logging.StreamHandler(sys.stdout)
 logger.addHandler(ch)
 
@@ -64,14 +60,14 @@
     c = readconfig()
     d = c.defaults()
     watch_while = int(d['watch_while'])
-    maxtries = int(d['maxtries'])
+    default_maxtries = int(d['maxtries'])
     vilain_table = d['vilain_table']
+    sleeptime = float(d['sleeptime'])
+    ignore_ips = []
 
     if c.has_section('ignoreip'):
-        ignoreip = [ i[1] for i in c.items('ignoreip') if i[0] not in c.defaults()]
-    else:
-        ignoreip = []
-    return(watch_while, maxtries, vilain_table, ignoreip)
+        ignoreips = [ i[1] for i in c.items('ignoreip') if i[0] not in c.defaults()]
+    return(watch_while, default_maxtries, vilain_table, ignoreips, sleeptime)
 
 def load_sections():
     c = readconfig()
@@ -79,21 +75,41 @@
         if c.has_option(s,'logfile'):
             logfile = c.get(s,'logfile')
             regex = c.get(s,'regex')
-            d = {'name' : s, 'logfile':logfile, 'regex':regex}
+            #we take the default value of maxtries
+            maxtries = c.defaults()['maxtries']
+            if c.has_option(s,'maxtries'):
+                #if we have a maxtries defined in the section, we overwrite the default
+                maxtries = int(c.get(s,'maxtries'))
+            d = {'name' : s, 'logfile':logfile, 'regex':regex, 'maxtries': maxtries}
             yield d
+
 
 class Vilain():
     def __init__(self):
         self.loop = asyncio.get_event_loop()
-        self.watch_while, self.maxtries, self.vilain_table, self.ignore_ip = load_config()
-        #self.bad_ip_queue = []
+        self.watch_while, self.default_maxtries, self.vilain_table, self.ignore_ips, self.sleeptime = load_config()
+        self.ip_seen_at = {}
+        self.load_bad_ips()
         self.bad_ip_queue = asyncio.Queue(loop=self.loop)
 
         for entry in load_sections():
             logger.info("Start vilain for {}".format(entry['name']))
-            asyncio.ensure_future(self.check_logs(entry['logfile'], entry['regex'], entry['name']))
+            asyncio.ensure_future(self.check_logs(entry['logfile'], entry['maxtries'], entry['regex'], entry['name']))
 
         asyncio.ensure_future(self.ban_ips())
+        asyncio.ensure_future(self.clean_ips())
+
+    def load_bad_ips(self):
+        try:
+            ret = subprocess.check_output(["pfctl", "-t", self.vilain_table, "-T", "show"])
+        except:
+            ret = ""
+        for res in ret.split():
+            ip = res.strip().decode('utf-8')
+            logger.debug('Add existing banned IPs in your pf table: {}'.format(ip))
+            #we assign the counter to 1, but for sure we don't know the real value 
+            self.ip_seen_at[ip]={'time':time.time(),'count':1}
+
 
     def start(self):
         try:
@@ -103,7 +119,8 @@
         finally:
             self.loop.close()
 
-    async def check_logs(self, logfile, regex, reason):
+
+    async def check_logs(self, logfile, maxtries, regex, reason):
         """
         worker who put in bad_ip_queue bruteforce IP
         """
@@ -116,9 +133,10 @@
             mtime = stat.st_mtime
             RE = re.compile(regex)
             while True:
-                await asyncio.sleep(sleeptime)
+                await asyncio.sleep(self.sleeptime)
                 stat = os.stat(logfile)
                 if mtime < stat.st_mtime:
+                    logger.debug("{} has been modified".format(logfile))
                     mtime = stat.st_mtime
                     with open(logfile, "rb") as f:
                         f.seek(size)
@@ -127,54 +145,57 @@
                         line = "".join(ul).strip()
 
                         ret = RE.match(line)
+                        logger.debug('line:{}'.format(line))
                         if ret:
                             bad_ip = ret.groups()[0]
-                            if bad_ip not in self.ignore_ip :
-                                #self.bad_ip_queue.append({'ip' : bad_ip, 'reason' : reason})
+                            if bad_ip not in self.ignore_ips :
+                                logger.info('line match {} because of rule : {}'.format(bad_ip, reason))
                                 await self.bad_ip_queue.put({'ip' : bad_ip, 'reason' : reason})
+                                logger.debug('queue size: {}'.format(self.bad_ip_queue.qsize()))
+                            else:
+                                logger.info('line match {}. But IP in ignore list'.format(bad_ip))
                     size = stat.st_size
 
     async def ban_ips(self):
         """
-        worker who ban IP on bad_ip_queue
-        add IP in bad_ips_list 
-        record time when this IP has been seen in ip_seen_at = { ip:time }
-
-        check number of occurence of the same ip in bad_ips_list
-        if more than 3 : ban and clean of list
-
-        check old ip in ip_seen_at : remove older than watch_while
+        record time when this IP has been seen in ip_seen_at = { ip:{'time':<time>,'count':<counter} }
         """
-
-        bad_ips_list = []
-        ip_seen_at = {}
+        logger.info('ban_ips sarted with sleeptime={}'.format(self.sleeptime))
         while True:
-            await asyncio.sleep(sleeptime)
-            #if not len(s#elf.bad_ip_queue) > 0:
-            #    continue
+            await asyncio.sleep(self.sleeptime)
             ip_item = await self.bad_ip_queue.get()
-            #ip_item = self.bad_ip_queue.pop()
+            logger.debug('ban_ips awake')
             ip = ip_item['ip']
             reason = ip_item['reason']
-            logger.info("{} detected, reason {}".format(ip, reason))
-            bad_ips_list.append(ip)
-            ip_seen_at[ip] = time.time()
-            n_ip = bad_ips_list.count(ip)
-            if n_ip >= self.maxtries:
-                logger.info("Blacklisting {}".format(ip))
-                subprocess.call(["pfctl", "-t", self.vilain_table, "-T", "add", ip])
-                ip_seen_at.pop(ip)
-                while ip in bad_ips_list:
-                    bad_ips_list.remove(ip)
+            maxtries = ip_item['maxtries']
+            self.ip_seen_at.setdefault(ip,{'time':time.time(),'count':0})
+            self.ip_seen_at[ip]['count'] += 1
+            n_ip = self.ip_seen_at[ip]['count']
+            logger.info("{} detected, reason {}, count: {}, maxtries: {}".format(ip, reason, n_ip, maxtries))
+            if n_ip >= maxtries:
+                ret = subprocess.call(["pfctl", "-t", self.vilain_table, "-T", "add", ip])
+                logger.info("Blacklisting {}, return code:{}".format(ip, ret))
+                self.ip_seen_at.pop(ip)
+            #for debugging, this line allow us to see if the script run until here
+            logger.debug('ban_ips end:{}'.format(self.ip_seen_at))
 
+    async def clean_ips(self):
+        """
+        check old ip in ip_seen_at : remove older than watch_while
+        """
+        logger.info('clean_ips sarted with sleeptime={}'.format(self.sleeptime))
+        while True:
+            await asyncio.sleep(self.sleeptime)
             to_remove = []
-            for recorded_ip, last_seen in ip_seen_at.items():
-                if time.time() - last_seen >= self.watch_while:
-                    logger.info("{} not seen since a long time, forgetting...".format(recorded_ip))
+            for recorded_ip, data in self.ip_seen_at.items():
+                if time.time() - data['time'] >= self.watch_while:
+                    ret = subprocess.call(["pfctl", "-t", self.vilain_table, "-T", "delete", recorded_ip])
+                    logger.info("{} not blocked any more, return code:{}".format(recorded_ip, ret))
                     to_remove.append(recorded_ip)
-            for i in to_remove:
-                ip_seen_at.pop(i)
-
+            for ip in to_remove:
+                self.ip_seen_at.pop(ip)
+            #for debugging, this line allow us to see if the script run until here
+            logger.debug('clean_ips end:{}'.format(self.ip_seen_at))
 
 
 
@@ -182,8 +203,7 @@
 def main():
     os.chdir(os.path.dirname(os.path.abspath(__file__)))
     v = Vilain()
-    p = Process(target=v.start())
-    p.start()
+    v.start()
     return 0
 
 if __name__ == '__main__':

--
Gitblit v1.9.3