| | |
| | | #!/usr/bin/env python3.4 |
| | | # -*- coding:Utf-8 -*- |
| | | #!/bin/sh |
| | | # script to launch vilain with the latest python3 version available |
| | | |
| | | |
| | | """ |
| | | 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 |
| | | |
| | | 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 >/dev/null 2>&1 & |
| | | else |
| | | echo "Error : no python3 executable found" |
| | | fi |
| | | exit |