Thuban
2017-01-08 6090031806326b21d97ed090872c2b416146e8b5
commit | author | age
77f371 1 #!/usr/bin/env python3.4
T 2 # -*- coding:Utf-8 -*- 
3
4
5 """
6 Author :      thuban <thuban@yeuxdelibad.net>  
7 Licence :     MIT
8
9 Description : Mimic fail2ban with pf for OpenBSD.
10               Inspired from http://www.vincentdelft.be/post/post_20161106
11
12               In pf.conf, add : 
13                     table <vilain_bruteforce> persist
14                     block quick from <vilain_bruteforce> 
15
16               You might want to add a cron task to remove old banned IP. As example, to ban for one day max : 
17                     pfctl -t vilain_bruteforce -T expire 86400
18
19               To see banned IP : 
20                     pfctl -t vilain_bruteforce -T show
21
22 """
23
24 import sys
25 import os
26 import configparser
27 import re
28 import time
29 import logging
30 import subprocess
31 from multiprocessing import Process, Queue, TimeoutError
32
33 configfile = "/etc/vilain.conf"
34 version = "0.1"
35 vilain_table = "vilain_bruteforce"
36 logfile = "/var/log/daemon"
37
38 if os.geteuid() != 0:
39     print("Only root can use this tool")
40     sys.exit()
41
42 # Configure logging
43 logger = logging.getLogger(__name__)
44 logging.basicConfig(filename=logfile,
45                     format='%(asctime)s %(module)s:%(funcName)s:%(message)s',
46                     datefmt='%H:%M:%S')
47 logger.setLevel(logging.DEBUG)
48 ch = logging.StreamHandler(sys.stdout)
49 logger.addHandler(ch)
50
51 # functions
52 def readconfig():
53     if not os.path.isfile(configfile):
54         logging.error("Can't read config file, exiting...")
55         sys.exit(1)
56
57     config = configparser.ConfigParser()
58     config.read(configfile)
59     return(config)
60
61 def load_config():
62     c = readconfig()
63     d = c.defaults()
64     watch_while = int(d['watch_while'])
65     maxtries = int(d['maxtries'])
66     vilain_table = d['vilain_table']
67     return(watch_while, maxtries, vilain_table)
68
69 def load_sections():
70     c = readconfig()
71     for s in c.sections():
72         logfile = c.get(s,'logfile')
73         regex = c.get(s,'regex')
74         d = {'name' : s, 'logfile':logfile, 'regex':regex}
75         yield d
76
77     
78 def check_logs(logfile, regex, bad_ip_queue):
79     """
80     worker who put in bad_ip_queue bruteforce IP
81     """
82     if not os.path.isfile(logfile) :
83         logger.warning("{} doesn't exist".format(logfile))
84         return
85     # Watch the file for changes
86     stat = os.stat(logfile)
87     size = stat.st_size
88     mtime = stat.st_mtime
89     RE = re.compile(regex)
90     while True:
91         time.sleep(0.5)
92         stat = os.stat(logfile)
93         if mtime < stat.st_mtime:
94             mtime = stat.st_mtime
95             with open(logfile, "rb") as f:
96                 f.seek(size)
97                 lines = f.readlines()
98                 ul = [ u.decode() for u in lines ]
99                 line = "".join(ul).strip()
100
101                 ret = RE.match(line)
102                 if ret:
103                     bad_ip = ret.groups()[0]
104                     bad_ip_queue.put(bad_ip)
105             size = stat.st_size
106
107 def ban_ips(bad_ip_queue, watch_while, maxtries, vilain_table):
108     """
109     worker who ban IP on bad_ip_queue
110     add IP in bad_ips_list 
111     record time when this IP has been seen in ip_seen_at = { ip:time }
112
113     check number of occurence of the same ip in bad_ips_list
114     if more than 3 : ban and clean of list
115
116     check old ip in ip_seen_at : remove older than watch_while
117     """
118
119     bad_ips_list = []
120     ip_seen_at = {}
121     while True:
122         try:
123             ip = bad_ip_queue.get(0.5)
124             logger.info("{} detected".format(ip))
125             bad_ips_list.append(ip)
126             ip_seen_at[ip] = time.time()
127
128             n_ip = bad_ips_list.count(ip)
129             if n_ip >= maxtries:
130                 logger.info("Blacklisting {}".format(ip))
131                 subprocess.call(["pfctl", "-t", vilain_table, "-T", "add", ip])
132                 ip_seen_at.pop(ip)
133                 while ip in bad_ips_list:
134                     bad_ips_list.remove(ip)
135
136             to_remove = []
137             for recorded_ip, last_seen in ip_seen_at.items():
138                 if time.time() - last_seen >= watch_while:
139                     logger.info("{} not seen since a long time, forgetting...".format(recorded_ip))
140                     to_remove.append(recorded_ip)
141             for i in to_remove:
142                 ip_seen_at.pop(i)
143
144         except TimeoutError:
145             pass
146         except KeyboardInterrupt:
147             sys.exit(0)
148
149 def main():
150     os.chdir(os.path.dirname(os.path.abspath(__file__)))
151
152     watch_while, maxtries, vilain_table = load_config()
153     bad_ip_queue = Queue()
154     workers = []
155
156     for entry in load_sections():
157         logger.info("Start vilain for {}".format(entry['name']))
158         worker = Process(target=check_logs,
159                         args=(entry['logfile'], entry['regex'], bad_ip_queue))
160         workers.append(worker)
161         worker.daemon = True
162         worker.start()
163
164     ban_worker = Process(target=ban_ips, args=(bad_ip_queue, watch_while, maxtries))
165     workers.append(ban_worker)
166     ban_worker.daemon = True
167     ban_worker.start()
168
169     for w in workers:
170         w.join()
171
172     return 0
173
174 if __name__ == '__main__':
175     main()
176
177
178 # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
179