#!/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
|