Subversion Repositories configs

Rev

Rev 41 | Go to most recent revision | Details | Last modification | View Log | RSS feed

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