From 3a37e640ba952bbaf726b38077f8d0adf6850fec Mon Sep 17 00:00:00 2001
From: Thuban <thuban@yeuxdelibad.net>
Date: Wed, 29 Mar 2017 12:13:09 +0000
Subject: [PATCH] major update, better init script

---
 vilain.conf |   34 +++
 vilain.py   |  194 +++++++++++++++++++++
 Makefile    |   36 ++++
 vilain.rc   |   13 +
 vilain      |  188 +-------------------
 vilain.1    |   41 ++++
 README.md   |   18 +
 7 files changed, 336 insertions(+), 188 deletions(-)

diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..e153e0b
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,36 @@
+# vilain - anti-bruteforce for OpenBSD
+# See LICENSE file for copyright and license details.
+#
+# vilain version
+VERSION = 0.3
+
+# Customize below to fit your system
+# paths
+PREFIX = /usr/local
+MANPREFIX = ${PREFIX}/man/man1/
+
+install: 
+	@echo installing executable file to ${DESTDIR}${PREFIX}/bin
+	@mkdir -p ${DESTDIR}${PREFIX}/bin
+	@cp -f vilain ${DESTDIR}${PREFIX}/bin
+	@echo installing script file to ${DESTDIR}${PREFIX}/sbin
+	@cp -f vilain.py ${DESTDIR}${PREFIX}/sbin
+	@chmod 755 ${DESTDIR}${PREFIX}/bin/vilain
+	@chmod 644 ${DESTDIR}${PREFIX}/sbin/vilain.py
+	@echo installing init script in /etc/rc.d
+	@cp -f vilain.rc /etc/rc.d/vilain
+	@chmod 755 /etc/rc.d/vilain
+	@echo installing manual page to ${DESTDIR}${MANPREFIX}/man1
+	@mkdir -p ${DESTDIR}${MANPREFIX}/
+	@cp -f vilain.1 ${DESTDIR}${MANPREFIX}/vilain.1
+	@chmod 644 ${DESTDIR}${MANPREFIX}/vilain.1
+
+
+uninstall:
+	@echo removing executable file from ${DESTDIR}${PREFIX}/bin
+	@rm -f ${DESTDIR}${PREFIX}/bin/vilain
+	@rm -f ${DESTDIR}${PREFIX}/sbin/vilain.py
+	@echo removing manual page to ${DESTDIR}${MANPREFIX}/
+	@rm -f ${DESTDIR}${MANPREFIX}/vilain.1
+
+.PHONY: install uninstall 
diff --git a/README.md b/README.md
index 821e44e..2c90965 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,9 @@
 # vilain
-
-
 Mimic fail2ban with pf for OpenBSD.
 
 Inspired from http://www.vincentdelft.be/post/post_20161106
 
-This repository is just for work.
-See here for last vilain "stable" version : http://git.yeuxdelibad.net/vilain/  
-
-
-In pf.conf, add : 
+In pf.conf, add according to your configuration : 
 
     table <vilain_bruteforce> persist
     block quick from <vilain_bruteforce> 
@@ -23,4 +17,14 @@
     pfctl -t vilain_bruteforce -T show
 
 
+To start vilain at boot, add this in ``/etc/rc.local``
 
+```
+/usr/bin/tmux new -s vilain -d /usr/local/bin/vilain
+```
+
+Then, to attach to the tmux session, run : 
+
+```
+tmux a -t vilain
+```
diff --git a/vilain b/vilain
index f0ea83e..3282278 100755
--- a/vilain
+++ b/vilain
@@ -1,179 +1,9 @@
-#!/usr/bin/env python3.4
-# -*- coding:Utf-8 -*- 
-
-
-"""
-Author :      thuban <thuban@yeuxdelibad.net>  
-Licence :     MIT
-
-Description : Mimic fail2ban with pf for OpenBSD.
-              Inspired from http://www.vincentdelft.be/post/post_20161106
-
-              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
-
-"""
-
-import sys
-import os
-import configparser
-import re
-import time
-import logging
-import subprocess
-from multiprocessing import Process, Queue, TimeoutError
-
-configfile = "/etc/vilain.conf"
-version = "0.1"
-vilain_table = "vilain_bruteforce"
-logfile = "/var/log/daemon"
-
-if os.geteuid() != 0:
-    print("Only root can use this tool")
-    sys.exit()
-
-# Configure logging
-logger = logging.getLogger(__name__)
-logging.basicConfig(filename=logfile,
-                    format='%(asctime)s %(module)s:%(funcName)s:%(message)s',
-                    datefmt='%H:%M:%S')
-logger.setLevel(logging.DEBUG)
-ch = logging.StreamHandler(sys.stdout)
-logger.addHandler(ch)
-
-# functions
-def readconfig():
-    if not os.path.isfile(configfile):
-        logging.error("Can't read config file, exiting...")
-        sys.exit(1)
-
-    config = configparser.ConfigParser()
-    config.read(configfile)
-    return(config)
-
-def load_config():
-    c = readconfig()
-    d = c.defaults()
-    watch_while = int(d['watch_while'])
-    maxtries = int(d['maxtries'])
-    vilain_table = d['vilain_table']
-    return(watch_while, maxtries, vilain_table)
-
-def load_sections():
-    c = readconfig()
-    for s in c.sections():
-        logfile = c.get(s,'logfile')
-        regex = c.get(s,'regex')
-        d = {'name' : s, 'logfile':logfile, 'regex':regex}
-        yield d
-
-    
-def check_logs(logfile, regex, bad_ip_queue):
-    """
-    worker who put in bad_ip_queue bruteforce IP
-    """
-    if not os.path.isfile(logfile) :
-        logger.warning("{} doesn't exist".format(logfile))
-        return
-    # Watch the file for changes
-    stat = os.stat(logfile)
-    size = stat.st_size
-    mtime = stat.st_mtime
-    RE = re.compile(regex)
-    while True:
-        time.sleep(0.5)
-        stat = os.stat(logfile)
-        if mtime < stat.st_mtime:
-            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()
-
-                ret = RE.match(line)
-                if ret:
-                    bad_ip = ret.groups()[0]
-                    bad_ip_queue.put(bad_ip)
-            size = stat.st_size
-
-def ban_ips(bad_ip_queue, watch_while, maxtries, vilain_table):
-    """
-    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
-    """
-
-    bad_ips_list = []
-    ip_seen_at = {}
-    while True:
-        try:
-            ip = bad_ip_queue.get(0.5)
-            logger.info("{} detected".format(ip))
-            bad_ips_list.append(ip)
-            ip_seen_at[ip] = time.time()
-
-            n_ip = bad_ips_list.count(ip)
-            if n_ip >= maxtries:
-                logger.info("Blacklisting {}".format(ip))
-                subprocess.call(["pfctl", "-t", vilain_table, "-T", "add", ip])
-                ip_seen_at.pop(ip)
-                while ip in bad_ips_list:
-                    bad_ips_list.remove(ip)
-
-            to_remove = []
-            for recorded_ip, last_seen in ip_seen_at.items():
-                if time.time() - last_seen >= watch_while:
-                    logger.info("{} not seen since a long time, forgetting...".format(recorded_ip))
-                    to_remove.append(recorded_ip)
-            for i in to_remove:
-                ip_seen_at.pop(i)
-
-        except TimeoutError:
-            pass
-        except KeyboardInterrupt:
-            sys.exit(0)
-
-def main():
-    os.chdir(os.path.dirname(os.path.abspath(__file__)))
-
-    watch_while, maxtries, vilain_table = load_config()
-    bad_ip_queue = Queue()
-    workers = []
-
-    for entry in load_sections():
-        logger.info("Start vilain for {}".format(entry['name']))
-        worker = Process(target=check_logs,
-                        args=(entry['logfile'], entry['regex'], bad_ip_queue))
-        workers.append(worker)
-        worker.daemon = True
-        worker.start()
-
-    ban_worker = Process(target=ban_ips, args=(bad_ip_queue, watch_while, maxtries))
-    workers.append(ban_worker)
-    ban_worker.daemon = True
-    ban_worker.start()
-
-    for w in workers:
-        w.join()
-
-    return 0
-
-if __name__ == '__main__':
-	main()
-
-
-# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
-
+#!/bin/sh
+PYTHONVERSION=$(ls -l /usr/local/bin/python3.* |grep -Eo "3\.[0-9]" |tail -n1)
+PYTHON="/usr/local/bin/python$PYTHONVERSION"
+if [ -x $PYTHON ]; then
+	$PYTHON /usr/local/sbin/vilain.py >> /var/log/daemon 2>&1 &
+else
+	echo "Error : no python3 executable found"
+fi
+exit
diff --git a/vilain.1 b/vilain.1
new file mode 100644
index 0000000..7977626
--- /dev/null
+++ b/vilain.1
@@ -0,0 +1,41 @@
+.
+.TH vilain 28 "March 2017" "" "Fail2ban-like for OpenBSD"
+.SH NAME
+vilain \- fail2ban-like for OpenBSD
+.
+.SH SYNOPSIS
+.RS
+To use vilain : 
+  1. Copy configuration file in /etc/vilain.conf and configure according to your needs.
+  2. Configure pf according to the example below
+  3. start vilain via rcctl
+You only need python >= 3.5 to use it.
+.RE
+
+.SH DESCRIPTION
+.RS
+In pf.conf, add according to your configuration : 
+
+    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
+.RE
+
+.SH LOGS
+.RS
+Watch vilain logs in /var/log/daemon.
+.RE
+
+
+.SH BUGS
+.RS
+Please let me know : <thuban@yeuxdelibad.net>
+.RE
+
diff --git a/vilain.conf b/vilain.conf
index 8fe5ffb..255438e 100644
--- a/vilain.conf
+++ b/vilain.conf
@@ -2,12 +2,17 @@
 # 24h + 5min
 # Time to keep banned a bad ip
 watch_while = 86700 
-# Max tries before being bannes
+# Max tries before being banned
 maxtries = 3
 # pf table to keep bad IP.
 # remember to clean the table with this command in a cron job :
-#     pfctl -t vilain_bruteforce -T expire 86400
+#     /sbin/pfctl -t vilain_bruteforce -T expire 86400
 vilain_table = vilain_bruteforce
+
+### Ip ignored ###
+[ignoreip]
+ip1 = 92.150.160.157
+ip2 = 92.150.160.156
 
 ### Guardians
 #[name of the guardian]
@@ -22,3 +27,28 @@
 logfile = /var/log/authlog
 regex = .* Connection closed by ([\S]+) .*
 
+#[http404]
+#logfile = /var/www/logs/access.log
+#regex = (?:\S+\s){1}(\S+).*\s404\s.*
+
+[http401]
+logfile = /var/www/logs/access.log
+regex = (?:\S+\s){1}(\S+).*\s401\s.*
+
+[http403]
+logfile = /var/www/logs/access.log
+regex = (?:\S+\s){1}(\S+).*\s403\s.*
+
+[smtp]
+logfile = /var/log/maillog
+regex = .* event=failed-command address=([\S]+) .*
+
+[dovecot]
+logfile = /var/log/maillog
+regex = .*auth failed.*rip=([\S]+),.*
+
+[wordpress]
+# don't use if you have wordpress
+logfile = /var/www/logs/access.log
+regex = (?:\S+\s){1}(\S+).*wp-login.php.*
+
diff --git a/vilain.py b/vilain.py
new file mode 100755
index 0000000..24e2ff4
--- /dev/null
+++ b/vilain.py
@@ -0,0 +1,194 @@
+#!/usr/bin/env python3
+# -*- coding:Utf-8 -*- 
+
+
+"""
+Author :      thuban <thuban@yeuxdelibad.net>  
+Licence :     MIT
+Require : python >= 3.5
+
+Description : Mimic fail2ban with pf for OpenBSD.
+              Inspired from http://www.vincentdelft.be/post/post_20161106
+
+              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
+"""
+
+import sys
+import os
+import configparser
+import re
+import logging
+import subprocess
+import asyncio
+import time
+from multiprocessing import Process
+
+configfile = "/etc/vilain.conf"
+version = "0.3"
+vilain_table = "vilain_bruteforce"
+logfile = "/var/log/daemon"
+sleeptime = 0.5
+
+if os.geteuid() != 0:
+    print("Only root can use this tool")
+    sys.exit()
+
+# Configure logging
+logger = logging.getLogger(__name__)
+logging.basicConfig(filename=logfile,
+                    format='%(asctime)s %(module)s:%(funcName)s:%(message)s',
+                    datefmt='%H:%M:%S')
+logger.setLevel(logging.DEBUG)
+ch = logging.StreamHandler(sys.stdout)
+logger.addHandler(ch)
+
+# functions
+def readconfig():
+    if not os.path.isfile(configfile):
+        logging.error("Can't read config file, exiting...")
+        sys.exit(1)
+
+    config = configparser.ConfigParser()
+    config.read(configfile)
+    return(config)
+
+def load_config():
+    c = readconfig()
+    d = c.defaults()
+    watch_while = int(d['watch_while'])
+    maxtries = int(d['maxtries'])
+    vilain_table = d['vilain_table']
+
+    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)
+
+def load_sections():
+    c = readconfig()
+    for s in c.sections():
+        if c.has_option(s,'logfile'):
+            logfile = c.get(s,'logfile')
+            regex = c.get(s,'regex')
+            d = {'name' : s, 'logfile':logfile, 'regex':regex}
+            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.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.ban_ips())
+
+    def start(self):
+        try:
+            self.loop.run_forever()
+        except KeyboardInterrupt:
+            self.loop.close()
+        finally:
+            self.loop.close()
+
+    async def check_logs(self, logfile, regex, reason):
+        """
+        worker who put in bad_ip_queue bruteforce IP
+        """
+        if not os.path.isfile(logfile) :
+            logger.warning("{} doesn't exist".format(logfile))
+        else :
+            # Watch the file for changes
+            stat = os.stat(logfile)
+            size = stat.st_size
+            mtime = stat.st_mtime
+            RE = re.compile(regex)
+            while True:
+                await asyncio.sleep(sleeptime)
+                stat = os.stat(logfile)
+                if mtime < stat.st_mtime:
+                    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()
+
+                        ret = RE.match(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})
+                                await self.bad_ip_queue.put({'ip' : bad_ip, 'reason' : reason})
+                    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
+        """
+
+        bad_ips_list = []
+        ip_seen_at = {}
+        while True:
+            await asyncio.sleep(sleeptime)
+            #if not len(s#elf.bad_ip_queue) > 0:
+            #    continue
+            ip_item = await self.bad_ip_queue.get()
+            #ip_item = self.bad_ip_queue.pop()
+            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)
+
+            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))
+                    to_remove.append(recorded_ip)
+            for i in to_remove:
+                ip_seen_at.pop(i)
+
+
+
+
+
+def main():
+    os.chdir(os.path.dirname(os.path.abspath(__file__)))
+    v = Vilain()
+    p = Process(target=v.start())
+    p.start()
+    return 0
+
+if __name__ == '__main__':
+	main()
+
+
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
+
diff --git a/vilain.rc b/vilain.rc
new file mode 100755
index 0000000..e3f73bf
--- /dev/null
+++ b/vilain.rc
@@ -0,0 +1,13 @@
+#!/bin/sh
+#
+# $OpenBSD: vilain.rc,v 1.0 2017/03/28 17:58:46 Thuban$
+
+daemon="/usr/local/bin/vilain"
+
+. /etc/rc.d/rc.subr
+
+pexp="python3.*vilain\.py"
+rc_reload=NO
+
+rc_cmd $1
+

--
Gitblit v1.9.3