-
Notifications
You must be signed in to change notification settings - Fork 3
/
fail2ban-subnets.py
executable file
·277 lines (223 loc) · 7.63 KB
/
fail2ban-subnets.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
#!/usr/bin/env python
# encoding: utf-8
#
# fail2ban-subnets, ban subnets from which IP are repeat offenders
#
# Copyright (C) 2015 Raphaël Beamonte <raphael.beamonte@gmail.com>
#
# This file is part of TraktForVLC. TraktForVLC is free software: you can
# redistribute it and/or modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation, version 2.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
# or see <http://www.gnu.org/licenses/>.
from datetime import datetime
import glob
import gzip
import logging
import os.path
import re
import subprocess
import time
# Path of logfile to analyze (accepts jokers)
filepath = '/var/log/fail2ban.log*'
# Maximum time to look back for our analyze
findtime = 4 * 4 * 7 * 86400
# Maximum number of bans in the period before reporting the range
maxretry = 50
# Minimum number of ips in the incriminated range
min_ips = 5
# Time format used in fail2ban logs
timeformat = '%Y-%m-%d %H:%M:%S,%f'
# Whether or not to exclude from report already banned subnets.
# This prevents the 'already banned' messages in fail2ban logs,
# it could however let a delay before banning again those ranges
# after restarting fail2ban. A larger findtime option in the subnets
# jail is recommanded when this option is activated.
excludeban = True
# Jail used in fail2ban for this script
subnetsjail = 'subnets'
# List of jails we don't want to match (for instance if you use recidive)
donotmatchjail = [
'recidive',
]
# Fail regex to use to check if a line matches. Variables %(time)s
# and %(donotmatchjail)s can be used to insert previously mentionned
# parameters.
failregex = ('%(time)s fail2ban\.actions(?:[\s\t]*\[[0-9]+\])?:' +
'(?: [A-Z]+)?[\s\t]+\[(?!%(donotmatchjail)s\])(?P<JAIL>.*)\] ' +
'Ban (?P<HOST>(?:[0-9]{1,3}\.){3}[0-9]{1,3})$')
# Where to store this scripts' logfile
logfile = '/var/log/fail2ban-subnets.log'
######
###### END OF CONFIGURATION
######
## Version
__version_info__ = (0, 2, 0, '', 0)
__version__ = "%d.%d.%d%s" % __version_info__[:4]
## Initialize logging
logging.basicConfig(
format="%(asctime)s %(name)s: %(levelname)s %(message)s",
level=logging.INFO,
filename=logfile)
logger = logging.getLogger(os.path.basename(__file__))
# To humanize time
def human_readable_time(secs):
mins, secs = divmod(secs, 60)
hours, mins = divmod(mins, 60)
days, hours = divmod(hours, 24)
weeks, days = divmod(days, 7)
readable = []
for t, s in (
(weeks, 'weeks'),
(days, 'days'),
(hours, 'hours'),
(mins, 'mins'),
(secs, 'secs'),
):
if t:
readable.append('%d %s' % (t, s))
return ' '.join(readable)
logger.info("started with an analysis over %s" % human_readable_time(findtime))
# Function to log the subnets to ban
def logban(ipsubnet):
data = {
'ban': ipsubnet['ban'],
'subnet': '%s/%s' % ipsubnet['subnet'],
'ipn': ipsubnet['ipn'],
'iplist': ipsubnet['iplist'],
}
logger.warning("subnet %(subnet)s has been banned "
"%(ban)d times with %(ipn)d ips" % data)
## TIME REGEX PREPARATION
timereplace = {
'%Y': '[0-9]{4}',
'%m': '(?:0?[0-9]|1[0-2])',
'%d': '(?:[0-2]?[0-9]|3[0-1])',
'%H': '(?:[0-1]?[0-9]|2[0-3])',
'%M': '(?:[0-5]?[0-9])',
'%S': '(?:[0-5]?[0-9])',
'%f': '[0-9]{3}',
}
timeregex_txt = '(?P<TIME>%s)' % timeformat
for k, v in timereplace.items():
timeregex_txt = timeregex_txt.replace(k, v)
timeregex = re.compile(timeregex_txt)
## BAN REGEX PREPARATION
# Add the subnets jail to the jails not to match
donotmatchjail += [subnetsjail, ]
lineregex_txt = failregex % {
'time': timeregex_txt,
'donotmatchjail': ('(?:%s)' %
'|'.join([re.escape(j) for j in donotmatchjail]))
}
lineregex = re.compile(lineregex_txt)
## MINIMUM DATE TO CONSIDER ENTRIES
mintime = time.time() - findtime
## DICTIONNARY TO STORE RESULTS
# In an IP block, we'll store as key the
# block, and as value a value indicating
# the number of ban in this block and the
# list of ips matching
ipblocks = {}
## Convert an IP to int
def ip_to_int(a, b, c, d):
return (a << 24) + (b << 16) + (c << 8) + d
## Calculate the subnet to use
def get_subnet(iplist):
splittedlist = []
for ip in iplist:
splittedlist.append([int(chk) for chk in ip.split('.')])
# Get max and min IP
splittedlist.sort()
maxip = splittedlist[-1]
minip = splittedlist[0]
# Calculate mask
mask = 0xFFFFFFFF ^ ip_to_int(*minip) ^ ip_to_int(*maxip)
# CIDR
cidr = bin(mask)[2:].find('0')
# Calculate new mask according to CIDR
mask = int(bin(mask)[2:cidr + 2].ljust(32, '0'), 2)
# Calculate netmask
netmask = [(mask & (0xFF << (8 * n))) >> 8 * n for n in (3, 2, 1, 0)]
# Calculate network start
netstart = [minip[x] & netmask[x] for x in range(0, 4)]
return ('.'.join([str(chk) for chk in netstart]), cidr)
## LOGIC
for f in sorted(glob.glob(filepath), reverse=True):
if f.endswith('.gz'):
fh = gzip.open(f, 'rb')
else:
fh = open(f, 'rb')
for l in fh:
if isinstance(l, bytes):
l = l.decode()
m = lineregex.match(l)
if not m:
continue
dt = datetime.strptime(m.group('TIME'), timeformat)
fdt = float(dt.strftime('%s.%f'))
if fdt < mintime:
continue
ip = m.group('HOST')
ipb = '.'.join(ip.split('.')[:3])
if ipb in ipblocks:
ipblocks[ipb]['ban'] += 1
if ip not in ipblocks[ipb]['ip']:
ipblocks[ipb]['ip'].append(ip)
else:
ipblocks[ipb] = {
'ban': 1,
'ip': [ip, ]
}
fh.close()
# Filter then sort the offenders by order or higher offense
offenders = [
{
'ban': v['ban'],
'subnet': get_subnet(v['ip']),
'ipn': len(v['ip']),
'iplist': v['ip']
}
for k, v in ipblocks.items() if (
len(v['ip']) >= min_ips
and v['ban'] >= maxretry
)
]
if excludeban:
iptablesL = subprocess.Popen(
['iptables', '-n', '-L', 'fail2ban-%s' % subnetsjail],
stdout=subprocess.PIPE)
out = iptablesL.communicate()[0]
banList = dict(re.findall(
'\nDROP[^\n]*?'
'(?P<HOST>(?:[0-9]{1,3}\.){3}[0-9]{1,3})'
'/'
'(?P<CIDR>[0-9]{1,2})(?![0-9])', out))
for o in offenders:
if excludeban:
net, cidr = o['subnet']
if net in banList:
if int(banList[net]) > cidr:
# We have to remove the previous ban in order for the
# new one, larger, to be applied
iptablesD = subprocess.Popen(
['iptables', '-D', 'fail2ban-%s' % subnetsjail,
'-s', '%s/%s' % (net, banList[net]), '-j', 'DROP'],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
out = iptablesD.communicate()[0].rstrip()
if iptablesD.returncode != 0:
logger.error(
"while unbanning %s/%s: %s" % (
net, banList[net], out))
else:
# We just don't log this subnet
continue
logban(o)