From 90bc70b28b5aaec61898b52a5554f22188d858eb Mon Sep 17 00:00:00 2001 From: Thuban <thuban@yeuxdelibad.net> Date: Tue, 11 Apr 2017 12:20:44 +0000 Subject: [PATCH] more debug and better cleaning --- vilain.py | 91 +++++++++++++++++++++++++++++---------------- 1 files changed, 58 insertions(+), 33 deletions(-) diff --git a/vilain.py b/vilain.py index a7c1b5a..27e1313 100755 --- a/vilain.py +++ b/vilain.py @@ -4,12 +4,12 @@ """ Author : thuban <thuban@yeuxdelibad.net> + Vincent <vincent.delft@gmail.com> Licence : MIT Require : python >= 3.5 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 @@ -28,8 +28,8 @@ import asyncio import time -configfile = "/etc/vilain.conf" -version = "0.4" +CONFIGFILE = "/etc/vilain.conf" +VERSION = "0.5" vilain_table = "vilain_bruteforce" logfile = "/var/log/daemon" @@ -43,25 +43,24 @@ format='%(asctime)s %(module)s:%(funcName)s:%(message)s', datefmt='%H:%M:%S') logger.setLevel(logging.INFO) -ch = logging.StreamHandler(sys.stdout) -logger.addHandler(ch) # functions def readconfig(): - if not os.path.isfile(configfile): + logger.info('Read config file: {}'.format(CONFIGFILE)) + if not os.path.isfile(CONFIGFILE): logging.error("Can't read config file, exiting...") sys.exit(1) config = configparser.ConfigParser() - config.read(configfile) + config.read(CONFIGFILE) return(config) def load_config(): c = readconfig() d = c.defaults() watch_while = int(d['watch_while']) - default_maxtries = int(d['maxtries']) vilain_table = d['vilain_table'] + default_maxtries = int(d['maxtries']) sleeptime = float(d['sleeptime']) ignore_ips = [] @@ -86,6 +85,7 @@ class Vilain(): def __init__(self): + logger.info('Start vilain version {}'.format(VERSION)) self.loop = asyncio.get_event_loop() self.watch_while, self.default_maxtries, self.vilain_table, self.ignore_ips, self.sleeptime = load_config() self.ip_seen_at = {} @@ -97,15 +97,17 @@ 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: + logger.warning("Failed to run pfctl -t {} -T show".format(self.vilain_table)) ret = "" for res in ret.split(): ip = res.strip().decode('utf-8') - logger.debug('Add existing banned IPs in your pf table: {}'.format(ip)) + logger.info('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} @@ -117,6 +119,7 @@ self.loop.close() finally: self.loop.close() + async def check_logs(self, logfile, maxtries, regex, reason): """ @@ -138,37 +141,35 @@ mtime = stat.st_mtime with open(logfile, "rb") as f: f.seek(size) - lines = f.readlines() - ul = [ u.decode() for u in lines ] - line = "".join(ul).strip() + for bline in f.readlines(): + line = bline.decode().strip() + ret = RE.match(line) + logger.debug('line:{}'.format(line)) + if ret: + bad_ip = ret.groups()[0] + if bad_ip not in self.ignore_ips : + logger.info('line match {} the {} rule'.format(bad_ip, reason)) + await self.bad_ip_queue.put({'ip' : bad_ip, 'maxtries': maxtries, 'reason' : reason}) + logger.debug('queue size: {}'.format(self.bad_ip_queue.qsize())) + else: + logger.info('line match {}. But IP in ingore list'.format(bad_ip)) - ret = RE.match(line) - logger.debug('line:{}'.format(line)) - if ret: - bad_ip = ret.groups()[0] - 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): """ record time when this IP has been seen in ip_seen_at = { ip:{'time':<time>,'count':<counter} } - - check old ip in ip_seen_at : remove older than watch_while + and ban with pf """ logger.info('ban_ips sarted with sleeptime={}'.format(self.sleeptime)) while True: - await asyncio.sleep(self.sleeptime) + # await asyncio.sleep(self.sleeptime) ip_item = await self.bad_ip_queue.get() logger.debug('ban_ips awake') ip = ip_item['ip'] reason = ip_item['reason'] maxtries = ip_item['maxtries'] - self.ip_seen_at.setdefault(ip,{'time':time.time(),'count':0}) + 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)) @@ -176,6 +177,16 @@ 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, data in self.ip_seen_at.items(): if time.time() - data['time'] >= self.watch_while: @@ -185,12 +196,7 @@ 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('ban_ips end:{}'.format(self.ip_seen_at)) - - - - - + logger.debug('clean_ips end:{}'.format(self.ip_seen_at)) @@ -202,7 +208,26 @@ return 0 if __name__ == '__main__': - main() + import argparse + parser = argparse.ArgumentParser(description="Vilain mimic fail2ban with pf for OpenBSD") + parser.add_argument('--debug','-d', action="store_true", help="run in debug mode") + parser.add_argument('--conf','-c', nargs="?", help="location of the config file") + parser.add_argument('--version','-v', action="store_true", help="Show the version and exit") + args = parser.parse_args() + if args.debug: + print("run in debug") + logger.setLevel(logging.DEBUG) + ch = logging.StreamHandler(sys.stdout) + logger.addHandler(ch) + if args.conf: + CONFIGFILE = args.conf + if args.version: + print("Version: ", VERSION) + sys.exit(0) + main() + + +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 -- Gitblit v1.9.3