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