Subversion Repositories configs

Rev

Rev 34 | Rev 71 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
34 - 1
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
2
# vi: set ft=python sts=4 ts=4 sw=4 noet :
3
 
4
# This file is part of Fail2Ban.
5
#
6
# Fail2Ban is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# Fail2Ban is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with Fail2Ban; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19
 
20
import sys
21
if sys.version_info < (2, 7):
22
	raise ImportError("badips.py action requires Python >= 2.7")
23
import json
24
from functools import partial
25
import threading
26
import logging
27
if sys.version_info >= (3, ):
28
	from urllib.request import Request, urlopen
29
	from urllib.parse import urlencode
30
	from urllib.error import HTTPError
31
else:
32
	from urllib2 import Request, urlopen, HTTPError
33
	from urllib import urlencode
34
 
35
from fail2ban.server.actions import ActionBase
36
from fail2ban.version import version as f2bVersion
37
 
39 - 38
 
34 - 39
class BadIPsAction(ActionBase):
40
	"""Fail2Ban action which reports bans to badips.com, and also
41
	blacklist bad IPs listed on badips.com by using another action's
42
	ban method.
43
 
44
	Parameters
45
	----------
46
	jail : Jail
47
		The jail which the action belongs to.
48
	name : str
49
		Name assigned to the action.
50
	category : str
51
		Valid badips.com category for reporting failures.
52
	score : int, optional
53
		Minimum score for bad IPs. Default 3.
54
	age : str, optional
55
		Age of last report for bad IPs, per badips.com syntax.
56
		Default "24h" (24 hours)
57
	key : str, optional
58
		Key issued by badips.com to report bans, for later retrieval
59
		of personalised content.
60
	banaction : str, optional
61
		Name of banaction to use for blacklisting bad IPs. If `None`,
62
		no blacklist of IPs will take place.
63
		Default `None`.
64
	bancategory : str, optional
65
		Name of category to use for blacklisting, which can differ
66
		from category used for reporting. e.g. may want to report
67
		"postfix", but want to use whole "mail" category for blacklist.
68
		Default `category`.
69
	bankey : str, optional
70
		Key issued by badips.com to blacklist IPs reported with the
71
		associated key.
72
	updateperiod : int, optional
73
		Time in seconds between updating bad IPs blacklist.
74
		Default 900 (15 minutes)
75
 
76
	Raises
77
	------
78
	ValueError
79
		If invalid `category`, `score`, `banaction` or `updateperiod`.
80
	"""
81
 
82
	_badips = "http://www.badips.com"
83
	_Request = partial(
84
		Request, headers={'User-Agent': "Fail2Ban %s" % f2bVersion})
85
 
86
	def __init__(self, jail, name, category, score=3, age="24h", key=None,
87
		banaction=None, bancategory=None, bankey=None, updateperiod=900):
88
		super(BadIPsAction, self).__init__(jail, name)
89
 
90
		self.category = category
91
		self.score = score
92
		self.age = age
93
		self.key = key
94
		self.banaction = banaction
95
		self.bancategory = bancategory or category
96
		self.bankey = bankey
97
		self.updateperiod = updateperiod
98
 
99
		self._bannedips = set()
100
		# Used later for threading.Timer for updating badips
101
		self._timer = None
102
 
103
	def getCategories(self, incParents=False):
104
		"""Get badips.com categories.
105
 
106
		Returns
107
		-------
108
		set
109
			Set of categories.
110
 
111
		Raises
112
		------
113
		HTTPError
114
			Any issues with badips.com request.
115
		ValueError
116
			If badips.com response didn't contain necessary information
117
		"""
118
		try:
119
			response = urlopen(
120
				self._Request("/".join([self._badips, "get", "categories"])))
121
		except HTTPError as response:
122
			messages = json.loads(response.read().decode('utf-8'))
123
			self._logSys.error(
124
				"Failed to fetch categories. badips.com response: '%s'",
125
				messages['err'])
126
			raise
127
		else:
128
			response_json = json.loads(response.read().decode('utf-8'))
129
			if not 'categories' in response_json:
130
				err = "badips.com response lacked categories specification. Response was: %s" \
131
				  % (response_json,)
132
				self._logSys.error(err)
133
				raise ValueError(err)
134
			categories = response_json['categories']
135
			categories_names = set(
136
				value['Name'] for value in categories)
137
			if incParents:
138
				categories_names.update(set(
139
					value['Parent'] for value in categories
140
					if "Parent" in value))
141
			return categories_names
142
 
143
	def getList(self, category, score, age, key=None):
144
		"""Get badips.com list of bad IPs.
145
 
146
		Parameters
147
		----------
148
		category : str
149
			Valid badips.com category.
150
		score : int
151
			Minimum score for bad IPs.
152
		age : str
153
			Age of last report for bad IPs, per badips.com syntax.
154
		key : str, optional
155
			Key issued by badips.com to fetch IPs reported with the
156
			associated key.
157
 
158
		Returns
159
		-------
160
		set
161
			Set of bad IPs.
162
 
163
		Raises
164
		------
165
		HTTPError
166
			Any issues with badips.com request.
167
		"""
168
		try:
169
			url = "?".join([
170
				"/".join([self._badips, "get", "list", category, str(score)]),
171
				urlencode({'age': age})])
172
			if key:
173
				url = "&".join([url, urlencode({'key': key})])
174
			response = urlopen(self._Request(url))
175
		except HTTPError as response:
176
			messages = json.loads(response.read().decode('utf-8'))
177
			self._logSys.error(
178
				"Failed to fetch bad IP list. badips.com response: '%s'",
179
				messages['err'])
180
			raise
181
		else:
182
			return set(response.read().decode('utf-8').split())
183
 
184
	@property
185
	def category(self):
186
		"""badips.com category for reporting IPs.
187
		"""
188
		return self._category
189
 
190
	@category.setter
191
	def category(self, category):
192
		if category not in self.getCategories():
193
			self._logSys.error("Category name '%s' not valid. "
194
				"see badips.com for list of valid categories",
195
				category)
196
			raise ValueError("Invalid category: %s" % category)
197
		self._category = category
198
 
199
	@property
200
	def bancategory(self):
201
		"""badips.com bancategory for fetching IPs.
202
		"""
203
		return self._bancategory
204
 
205
	@bancategory.setter
206
	def bancategory(self, bancategory):
207
		if bancategory not in self.getCategories(incParents=True):
208
			self._logSys.error("Category name '%s' not valid. "
209
				"see badips.com for list of valid categories",
210
				bancategory)
211
			raise ValueError("Invalid bancategory: %s" % bancategory)
212
		self._bancategory = bancategory
213
 
214
	@property
215
	def score(self):
216
		"""badips.com minimum score for fetching IPs.
217
		"""
218
		return self._score
219
 
220
	@score.setter
221
	def score(self, score):
222
		score = int(score)
223
		if 0 <= score <= 5:
224
			self._score = score
225
		else:
226
			raise ValueError("Score must be 0-5")
227
 
228
	@property
229
	def banaction(self):
230
		"""Jail action to use for banning/unbanning.
231
		"""
232
		return self._banaction
233
 
234
	@banaction.setter
235
	def banaction(self, banaction):
236
		if banaction is not None and banaction not in self._jail.actions:
237
			self._logSys.error("Action name '%s' not in jail '%s'",
238
				banaction, self._jail.name)
239
			raise ValueError("Invalid banaction")
240
		self._banaction = banaction
241
 
242
	@property
243
	def updateperiod(self):
244
		"""Period in seconds between banned bad IPs will be updated.
245
		"""
246
		return self._updateperiod
247
 
248
	@updateperiod.setter
249
	def updateperiod(self, updateperiod):
250
		updateperiod = int(updateperiod)
251
		if updateperiod > 0:
252
			self._updateperiod = updateperiod
253
		else:
254
			raise ValueError("Update period must be integer greater than 0")
255
 
256
	def _banIPs(self, ips):
257
		for ip in ips:
258
			try:
259
				self._jail.actions[self.banaction].ban({
260
					'ip': ip,
261
					'failures': 0,
262
					'matches': "",
263
					'ipmatches': "",
264
					'ipjailmatches': "",
265
				})
266
			except Exception as e:
267
				self._logSys.error(
268
					"Error banning IP %s for jail '%s' with action '%s': %s",
269
					ip, self._jail.name, self.banaction, e,
270
					exc_info=self._logSys.getEffectiveLevel()<=logging.DEBUG)
271
			else:
272
				self._bannedips.add(ip)
273
				self._logSys.info(
274
					"Banned IP %s for jail '%s' with action '%s'",
275
					ip, self._jail.name, self.banaction)
276
 
277
	def _unbanIPs(self, ips):
278
		for ip in ips:
279
			try:
280
				self._jail.actions[self.banaction].unban({
281
					'ip': ip,
282
					'failures': 0,
283
					'matches': "",
284
					'ipmatches': "",
285
					'ipjailmatches': "",
286
				})
287
			except Exception as e:
288
				self._logSys.info(
289
					"Error unbanning IP %s for jail '%s' with action '%s': %s",
290
					ip, self._jail.name, self.banaction, e,
291
					exc_info=self._logSys.getEffectiveLevel()<=logging.DEBUG)
292
			else:
293
				self._logSys.info(
294
					"Unbanned IP %s for jail '%s' with action '%s'",
295
					ip, self._jail.name, self.banaction)
296
			finally:
297
				self._bannedips.remove(ip)
298
 
299
	def start(self):
300
		"""If `banaction` set, blacklists bad IPs.
301
		"""
302
		if self.banaction is not None:
303
			self.update()
304
 
305
	def update(self):
306
		"""If `banaction` set, updates blacklisted IPs.
307
 
308
		Queries badips.com for list of bad IPs, removing IPs from the
309
		blacklist if no longer present, and adds new bad IPs to the
310
		blacklist.
311
		"""
312
		if self.banaction is not None:
313
			if self._timer:
314
				self._timer.cancel()
315
				self._timer = None
316
 
317
			try:
318
				ips = self.getList(
319
					self.bancategory, self.score, self.age, self.bankey)
320
				# Remove old IPs no longer listed
321
				self._unbanIPs(self._bannedips - ips)
322
				# Add new IPs which are now listed
323
				self._banIPs(ips - self._bannedips)
324
 
325
				self._logSys.info(
326
					"Updated IPs for jail '%s'. Update again in %i seconds",
327
					self._jail.name, self.updateperiod)
328
			finally:
329
				self._timer = threading.Timer(self.updateperiod, self.update)
330
				self._timer.start()
331
 
332
	def stop(self):
333
		"""If `banaction` set, clears blacklisted IPs.
334
		"""
335
		if self.banaction is not None:
336
			if self._timer:
337
				self._timer.cancel()
338
				self._timer = None
339
			self._unbanIPs(self._bannedips.copy())
340
 
341
	def ban(self, aInfo):
342
		"""Reports banned IP to badips.com.
343
 
344
		Parameters
345
		----------
346
		aInfo : dict
347
			Dictionary which includes information in relation to
348
			the ban.
349
 
350
		Raises
351
		------
352
		HTTPError
353
			Any issues with badips.com request.
354
		"""
355
		try:
356
			url = "/".join([self._badips, "add", self.category, aInfo['ip']])
357
			if self.key:
358
				url = "?".join([url, urlencode({'key': self.key})])
359
			response = urlopen(self._Request(url))
360
		except HTTPError as response:
361
			messages = json.loads(response.read().decode('utf-8'))
362
			self._logSys.error(
363
				"Response from badips.com report: '%s'",
364
				messages['err'])
365
			raise
366
		else:
367
			messages = json.loads(response.read().decode('utf-8'))
368
			self._logSys.info(
369
				"Response from badips.com report: '%s'",
370
				messages['suc'])
371
 
372
Action = BadIPsAction