daily autocommit
This commit is contained in:
@@ -1,391 +0,0 @@
|
||||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban 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; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban 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 Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import sys
|
||||
if sys.version_info < (2, 7): # pragma: no cover
|
||||
raise ImportError("badips.py action requires Python >= 2.7")
|
||||
import json
|
||||
import threading
|
||||
import logging
|
||||
if sys.version_info >= (3, ): # pragma: 2.x no cover
|
||||
from urllib.request import Request, urlopen
|
||||
from urllib.parse import urlencode
|
||||
from urllib.error import HTTPError
|
||||
else: # pragma: 3.x no cover
|
||||
from urllib2 import Request, urlopen, HTTPError
|
||||
from urllib import urlencode
|
||||
|
||||
from fail2ban.server.actions import Actions, ActionBase, BanTicket
|
||||
from fail2ban.helpers import splitwords, str2LogLevel
|
||||
|
||||
|
||||
|
||||
class BadIPsAction(ActionBase): # pragma: no cover - may be unavailable
|
||||
"""Fail2Ban action which reports bans to badips.com, and also
|
||||
blacklist bad IPs listed on badips.com by using another action's
|
||||
ban method.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jail : Jail
|
||||
The jail which the action belongs to.
|
||||
name : str
|
||||
Name assigned to the action.
|
||||
category : str
|
||||
Valid badips.com category for reporting failures.
|
||||
score : int, optional
|
||||
Minimum score for bad IPs. Default 3.
|
||||
age : str, optional
|
||||
Age of last report for bad IPs, per badips.com syntax.
|
||||
Default "24h" (24 hours)
|
||||
banaction : str, optional
|
||||
Name of banaction to use for blacklisting bad IPs. If `None`,
|
||||
no blacklist of IPs will take place.
|
||||
Default `None`.
|
||||
bancategory : str, optional
|
||||
Name of category to use for blacklisting, which can differ
|
||||
from category used for reporting. e.g. may want to report
|
||||
"postfix", but want to use whole "mail" category for blacklist.
|
||||
Default `category`.
|
||||
bankey : str, optional
|
||||
Key issued by badips.com to retrieve personal list
|
||||
of blacklist IPs.
|
||||
updateperiod : int, optional
|
||||
Time in seconds between updating bad IPs blacklist.
|
||||
Default 900 (15 minutes)
|
||||
loglevel : int/str, optional
|
||||
Log level of the message when an IP is (un)banned.
|
||||
Default `DEBUG`.
|
||||
Can be also supplied as two-value list (comma- or space separated) to
|
||||
provide level of the summary message when a group of IPs is (un)banned.
|
||||
Example `DEBUG,INFO`.
|
||||
agent : str, optional
|
||||
User agent transmitted to server.
|
||||
Default `Fail2Ban/ver.`
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If invalid `category`, `score`, `banaction` or `updateperiod`.
|
||||
"""
|
||||
|
||||
TIMEOUT = 10
|
||||
_badips = "https://www.badips.com"
|
||||
def _Request(self, url, **argv):
|
||||
return Request(url, headers={'User-Agent': self.agent}, **argv)
|
||||
|
||||
def __init__(self, jail, name, category, score=3, age="24h",
|
||||
banaction=None, bancategory=None, bankey=None, updateperiod=900,
|
||||
loglevel='DEBUG', agent="Fail2Ban", timeout=TIMEOUT):
|
||||
super(BadIPsAction, self).__init__(jail, name)
|
||||
|
||||
self.timeout = timeout
|
||||
self.agent = agent
|
||||
self.category = category
|
||||
self.score = score
|
||||
self.age = age
|
||||
self.banaction = banaction
|
||||
self.bancategory = bancategory or category
|
||||
self.bankey = bankey
|
||||
loglevel = splitwords(loglevel)
|
||||
self.sumloglevel = str2LogLevel(loglevel[-1])
|
||||
self.loglevel = str2LogLevel(loglevel[0])
|
||||
self.updateperiod = updateperiod
|
||||
|
||||
self._bannedips = set()
|
||||
# Used later for threading.Timer for updating badips
|
||||
self._timer = None
|
||||
|
||||
@staticmethod
|
||||
def isAvailable(timeout=1):
|
||||
try:
|
||||
response = urlopen(Request("/".join([BadIPsAction._badips]),
|
||||
headers={'User-Agent': "Fail2Ban"}), timeout=timeout)
|
||||
return True, ''
|
||||
except Exception as e: # pragma: no cover
|
||||
return False, e
|
||||
|
||||
def logError(self, response, what=''): # pragma: no cover - sporadical (502: Bad Gateway, etc)
|
||||
messages = {}
|
||||
try:
|
||||
messages = json.loads(response.read().decode('utf-8'))
|
||||
except:
|
||||
pass
|
||||
self._logSys.error(
|
||||
"%s. badips.com response: '%s'", what,
|
||||
messages.get('err', 'Unknown'))
|
||||
|
||||
def getCategories(self, incParents=False):
|
||||
"""Get badips.com categories.
|
||||
|
||||
Returns
|
||||
-------
|
||||
set
|
||||
Set of categories.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPError
|
||||
Any issues with badips.com request.
|
||||
ValueError
|
||||
If badips.com response didn't contain necessary information
|
||||
"""
|
||||
try:
|
||||
response = urlopen(
|
||||
self._Request("/".join([self._badips, "get", "categories"])), timeout=self.timeout)
|
||||
except HTTPError as response: # pragma: no cover
|
||||
self.logError(response, "Failed to fetch categories")
|
||||
raise
|
||||
else:
|
||||
response_json = json.loads(response.read().decode('utf-8'))
|
||||
if not 'categories' in response_json:
|
||||
err = "badips.com response lacked categories specification. Response was: %s" \
|
||||
% (response_json,)
|
||||
self._logSys.error(err)
|
||||
raise ValueError(err)
|
||||
categories = response_json['categories']
|
||||
categories_names = set(
|
||||
value['Name'] for value in categories)
|
||||
if incParents:
|
||||
categories_names.update(set(
|
||||
value['Parent'] for value in categories
|
||||
if "Parent" in value))
|
||||
return categories_names
|
||||
|
||||
def getList(self, category, score, age, key=None):
|
||||
"""Get badips.com list of bad IPs.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
category : str
|
||||
Valid badips.com category.
|
||||
score : int
|
||||
Minimum score for bad IPs.
|
||||
age : str
|
||||
Age of last report for bad IPs, per badips.com syntax.
|
||||
key : str, optional
|
||||
Key issued by badips.com to fetch IPs reported with the
|
||||
associated key.
|
||||
|
||||
Returns
|
||||
-------
|
||||
set
|
||||
Set of bad IPs.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPError
|
||||
Any issues with badips.com request.
|
||||
"""
|
||||
try:
|
||||
url = "?".join([
|
||||
"/".join([self._badips, "get", "list", category, str(score)]),
|
||||
urlencode({'age': age})])
|
||||
if key:
|
||||
url = "&".join([url, urlencode({'key': key})])
|
||||
self._logSys.debug('badips.com: get list, url: %r', url)
|
||||
response = urlopen(self._Request(url), timeout=self.timeout)
|
||||
except HTTPError as response: # pragma: no cover
|
||||
self.logError(response, "Failed to fetch bad IP list")
|
||||
raise
|
||||
else:
|
||||
return set(response.read().decode('utf-8').split())
|
||||
|
||||
@property
|
||||
def category(self):
|
||||
"""badips.com category for reporting IPs.
|
||||
"""
|
||||
return self._category
|
||||
|
||||
@category.setter
|
||||
def category(self, category):
|
||||
if category not in self.getCategories():
|
||||
self._logSys.error("Category name '%s' not valid. "
|
||||
"see badips.com for list of valid categories",
|
||||
category)
|
||||
raise ValueError("Invalid category: %s" % category)
|
||||
self._category = category
|
||||
|
||||
@property
|
||||
def bancategory(self):
|
||||
"""badips.com bancategory for fetching IPs.
|
||||
"""
|
||||
return self._bancategory
|
||||
|
||||
@bancategory.setter
|
||||
def bancategory(self, bancategory):
|
||||
if bancategory != "any" and bancategory not in self.getCategories(incParents=True):
|
||||
self._logSys.error("Category name '%s' not valid. "
|
||||
"see badips.com for list of valid categories",
|
||||
bancategory)
|
||||
raise ValueError("Invalid bancategory: %s" % bancategory)
|
||||
self._bancategory = bancategory
|
||||
|
||||
@property
|
||||
def score(self):
|
||||
"""badips.com minimum score for fetching IPs.
|
||||
"""
|
||||
return self._score
|
||||
|
||||
@score.setter
|
||||
def score(self, score):
|
||||
score = int(score)
|
||||
if 0 <= score <= 5:
|
||||
self._score = score
|
||||
else:
|
||||
raise ValueError("Score must be 0-5")
|
||||
|
||||
@property
|
||||
def banaction(self):
|
||||
"""Jail action to use for banning/unbanning.
|
||||
"""
|
||||
return self._banaction
|
||||
|
||||
@banaction.setter
|
||||
def banaction(self, banaction):
|
||||
if banaction is not None and banaction not in self._jail.actions:
|
||||
self._logSys.error("Action name '%s' not in jail '%s'",
|
||||
banaction, self._jail.name)
|
||||
raise ValueError("Invalid banaction")
|
||||
self._banaction = banaction
|
||||
|
||||
@property
|
||||
def updateperiod(self):
|
||||
"""Period in seconds between banned bad IPs will be updated.
|
||||
"""
|
||||
return self._updateperiod
|
||||
|
||||
@updateperiod.setter
|
||||
def updateperiod(self, updateperiod):
|
||||
updateperiod = int(updateperiod)
|
||||
if updateperiod > 0:
|
||||
self._updateperiod = updateperiod
|
||||
else:
|
||||
raise ValueError("Update period must be integer greater than 0")
|
||||
|
||||
def _banIPs(self, ips):
|
||||
for ip in ips:
|
||||
try:
|
||||
ai = Actions.ActionInfo(BanTicket(ip), self._jail)
|
||||
self._jail.actions[self.banaction].ban(ai)
|
||||
except Exception as e:
|
||||
self._logSys.error(
|
||||
"Error banning IP %s for jail '%s' with action '%s': %s",
|
||||
ip, self._jail.name, self.banaction, e,
|
||||
exc_info=self._logSys.getEffectiveLevel()<=logging.DEBUG)
|
||||
else:
|
||||
self._bannedips.add(ip)
|
||||
self._logSys.log(self.loglevel,
|
||||
"Banned IP %s for jail '%s' with action '%s'",
|
||||
ip, self._jail.name, self.banaction)
|
||||
|
||||
def _unbanIPs(self, ips):
|
||||
for ip in ips:
|
||||
try:
|
||||
ai = Actions.ActionInfo(BanTicket(ip), self._jail)
|
||||
self._jail.actions[self.banaction].unban(ai)
|
||||
except Exception as e:
|
||||
self._logSys.error(
|
||||
"Error unbanning IP %s for jail '%s' with action '%s': %s",
|
||||
ip, self._jail.name, self.banaction, e,
|
||||
exc_info=self._logSys.getEffectiveLevel()<=logging.DEBUG)
|
||||
else:
|
||||
self._logSys.log(self.loglevel,
|
||||
"Unbanned IP %s for jail '%s' with action '%s'",
|
||||
ip, self._jail.name, self.banaction)
|
||||
finally:
|
||||
self._bannedips.remove(ip)
|
||||
|
||||
def start(self):
|
||||
"""If `banaction` set, blacklists bad IPs.
|
||||
"""
|
||||
if self.banaction is not None:
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
"""If `banaction` set, updates blacklisted IPs.
|
||||
|
||||
Queries badips.com for list of bad IPs, removing IPs from the
|
||||
blacklist if no longer present, and adds new bad IPs to the
|
||||
blacklist.
|
||||
"""
|
||||
if self.banaction is not None:
|
||||
if self._timer:
|
||||
self._timer.cancel()
|
||||
self._timer = None
|
||||
|
||||
try:
|
||||
ips = self.getList(
|
||||
self.bancategory, self.score, self.age, self.bankey)
|
||||
# Remove old IPs no longer listed
|
||||
s = self._bannedips - ips
|
||||
m = len(s)
|
||||
self._unbanIPs(s)
|
||||
# Add new IPs which are now listed
|
||||
s = ips - self._bannedips
|
||||
p = len(s)
|
||||
self._banIPs(s)
|
||||
if m != 0 or p != 0:
|
||||
self._logSys.log(self.sumloglevel,
|
||||
"Updated IPs for jail '%s' (-%d/+%d)",
|
||||
self._jail.name, m, p)
|
||||
self._logSys.debug(
|
||||
"Next update for jail '%' in %i seconds",
|
||||
self._jail.name, self.updateperiod)
|
||||
finally:
|
||||
self._timer = threading.Timer(self.updateperiod, self.update)
|
||||
self._timer.start()
|
||||
|
||||
def stop(self):
|
||||
"""If `banaction` set, clears blacklisted IPs.
|
||||
"""
|
||||
if self.banaction is not None:
|
||||
if self._timer:
|
||||
self._timer.cancel()
|
||||
self._timer = None
|
||||
self._unbanIPs(self._bannedips.copy())
|
||||
|
||||
def ban(self, aInfo):
|
||||
"""Reports banned IP to badips.com.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
aInfo : dict
|
||||
Dictionary which includes information in relation to
|
||||
the ban.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPError
|
||||
Any issues with badips.com request.
|
||||
"""
|
||||
try:
|
||||
url = "/".join([self._badips, "add", self.category, str(aInfo['ip'])])
|
||||
self._logSys.debug('badips.com: ban, url: %r', url)
|
||||
response = urlopen(self._Request(url), timeout=self.timeout)
|
||||
except HTTPError as response: # pragma: no cover
|
||||
self.logError(response, "Failed to ban")
|
||||
raise
|
||||
else:
|
||||
messages = json.loads(response.read().decode('utf-8'))
|
||||
self._logSys.debug(
|
||||
"Response from badips.com report: '%s'",
|
||||
messages['suc'])
|
||||
|
||||
Action = BadIPsAction
|
||||
@@ -1,377 +0,0 @@
|
||||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban 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; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban 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 Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import sys
|
||||
if sys.version_info < (2, 7):
|
||||
raise ImportError("badips.py action requires Python >= 2.7")
|
||||
import json
|
||||
import threading
|
||||
import logging
|
||||
if sys.version_info >= (3, ):
|
||||
from urllib.request import Request, urlopen
|
||||
from urllib.parse import urlencode
|
||||
from urllib.error import HTTPError
|
||||
else:
|
||||
from urllib2 import Request, urlopen, HTTPError
|
||||
from urllib import urlencode
|
||||
|
||||
from fail2ban.server.actions import ActionBase
|
||||
|
||||
|
||||
class BadIPsAction(ActionBase):
|
||||
"""Fail2Ban action which reports bans to badips.com, and also
|
||||
blacklist bad IPs listed on badips.com by using another action's
|
||||
ban method.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jail : Jail
|
||||
The jail which the action belongs to.
|
||||
name : str
|
||||
Name assigned to the action.
|
||||
category : str
|
||||
Valid badips.com category for reporting failures.
|
||||
score : int, optional
|
||||
Minimum score for bad IPs. Default 3.
|
||||
age : str, optional
|
||||
Age of last report for bad IPs, per badips.com syntax.
|
||||
Default "24h" (24 hours)
|
||||
key : str, optional
|
||||
Key issued by badips.com to report bans, for later retrieval
|
||||
of personalised content.
|
||||
banaction : str, optional
|
||||
Name of banaction to use for blacklisting bad IPs. If `None`,
|
||||
no blacklist of IPs will take place.
|
||||
Default `None`.
|
||||
bancategory : str, optional
|
||||
Name of category to use for blacklisting, which can differ
|
||||
from category used for reporting. e.g. may want to report
|
||||
"postfix", but want to use whole "mail" category for blacklist.
|
||||
Default `category`.
|
||||
bankey : str, optional
|
||||
Key issued by badips.com to blacklist IPs reported with the
|
||||
associated key.
|
||||
updateperiod : int, optional
|
||||
Time in seconds between updating bad IPs blacklist.
|
||||
Default 900 (15 minutes)
|
||||
agent : str, optional
|
||||
User agent transmitted to server.
|
||||
Default `Fail2Ban/ver.`
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If invalid `category`, `score`, `banaction` or `updateperiod`.
|
||||
"""
|
||||
|
||||
TIMEOUT = 10
|
||||
_badips = "http://www.badips.com"
|
||||
def _Request(self, url, **argv):
|
||||
return Request(url, headers={'User-Agent': self.agent}, **argv)
|
||||
|
||||
def __init__(self, jail, name, category, score=3, age="24h", key=None,
|
||||
banaction=None, bancategory=None, bankey=None, updateperiod=900, agent="Fail2Ban",
|
||||
timeout=TIMEOUT):
|
||||
super(BadIPsAction, self).__init__(jail, name)
|
||||
|
||||
self.timeout = timeout
|
||||
self.agent = agent
|
||||
self.category = category
|
||||
self.score = score
|
||||
self.age = age
|
||||
self.key = key
|
||||
self.banaction = banaction
|
||||
self.bancategory = bancategory or category
|
||||
self.bankey = bankey
|
||||
self.updateperiod = updateperiod
|
||||
|
||||
self._bannedips = set()
|
||||
# Used later for threading.Timer for updating badips
|
||||
self._timer = None
|
||||
|
||||
def getCategories(self, incParents=False):
|
||||
"""Get badips.com categories.
|
||||
|
||||
Returns
|
||||
-------
|
||||
set
|
||||
Set of categories.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPError
|
||||
Any issues with badips.com request.
|
||||
ValueError
|
||||
If badips.com response didn't contain necessary information
|
||||
"""
|
||||
try:
|
||||
response = urlopen(
|
||||
self._Request("/".join([self._badips, "get", "categories"])), timeout=self.timeout)
|
||||
except HTTPError as response:
|
||||
messages = json.loads(response.read().decode('utf-8'))
|
||||
self._logSys.error(
|
||||
"Failed to fetch categories. badips.com response: '%s'",
|
||||
messages['err'])
|
||||
raise
|
||||
else:
|
||||
response_json = json.loads(response.read().decode('utf-8'))
|
||||
if not 'categories' in response_json:
|
||||
err = "badips.com response lacked categories specification. Response was: %s" \
|
||||
% (response_json,)
|
||||
self._logSys.error(err)
|
||||
raise ValueError(err)
|
||||
categories = response_json['categories']
|
||||
categories_names = set(
|
||||
value['Name'] for value in categories)
|
||||
if incParents:
|
||||
categories_names.update(set(
|
||||
value['Parent'] for value in categories
|
||||
if "Parent" in value))
|
||||
return categories_names
|
||||
|
||||
def getList(self, category, score, age, key=None):
|
||||
"""Get badips.com list of bad IPs.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
category : str
|
||||
Valid badips.com category.
|
||||
score : int
|
||||
Minimum score for bad IPs.
|
||||
age : str
|
||||
Age of last report for bad IPs, per badips.com syntax.
|
||||
key : str, optional
|
||||
Key issued by badips.com to fetch IPs reported with the
|
||||
associated key.
|
||||
|
||||
Returns
|
||||
-------
|
||||
set
|
||||
Set of bad IPs.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPError
|
||||
Any issues with badips.com request.
|
||||
"""
|
||||
try:
|
||||
url = "?".join([
|
||||
"/".join([self._badips, "get", "list", category, str(score)]),
|
||||
urlencode({'age': age})])
|
||||
if key:
|
||||
url = "&".join([url, urlencode({'key': key})])
|
||||
response = urlopen(self._Request(url), timeout=self.timeout)
|
||||
except HTTPError as response:
|
||||
messages = json.loads(response.read().decode('utf-8'))
|
||||
self._logSys.error(
|
||||
"Failed to fetch bad IP list. badips.com response: '%s'",
|
||||
messages['err'])
|
||||
raise
|
||||
else:
|
||||
return set(response.read().decode('utf-8').split())
|
||||
|
||||
@property
|
||||
def category(self):
|
||||
"""badips.com category for reporting IPs.
|
||||
"""
|
||||
return self._category
|
||||
|
||||
@category.setter
|
||||
def category(self, category):
|
||||
if category not in self.getCategories():
|
||||
self._logSys.error("Category name '%s' not valid. "
|
||||
"see badips.com for list of valid categories",
|
||||
category)
|
||||
raise ValueError("Invalid category: %s" % category)
|
||||
self._category = category
|
||||
|
||||
@property
|
||||
def bancategory(self):
|
||||
"""badips.com bancategory for fetching IPs.
|
||||
"""
|
||||
return self._bancategory
|
||||
|
||||
@bancategory.setter
|
||||
def bancategory(self, bancategory):
|
||||
if bancategory not in self.getCategories(incParents=True):
|
||||
self._logSys.error("Category name '%s' not valid. "
|
||||
"see badips.com for list of valid categories",
|
||||
bancategory)
|
||||
raise ValueError("Invalid bancategory: %s" % bancategory)
|
||||
self._bancategory = bancategory
|
||||
|
||||
@property
|
||||
def score(self):
|
||||
"""badips.com minimum score for fetching IPs.
|
||||
"""
|
||||
return self._score
|
||||
|
||||
@score.setter
|
||||
def score(self, score):
|
||||
score = int(score)
|
||||
if 0 <= score <= 5:
|
||||
self._score = score
|
||||
else:
|
||||
raise ValueError("Score must be 0-5")
|
||||
|
||||
@property
|
||||
def banaction(self):
|
||||
"""Jail action to use for banning/unbanning.
|
||||
"""
|
||||
return self._banaction
|
||||
|
||||
@banaction.setter
|
||||
def banaction(self, banaction):
|
||||
if banaction is not None and banaction not in self._jail.actions:
|
||||
self._logSys.error("Action name '%s' not in jail '%s'",
|
||||
banaction, self._jail.name)
|
||||
raise ValueError("Invalid banaction")
|
||||
self._banaction = banaction
|
||||
|
||||
@property
|
||||
def updateperiod(self):
|
||||
"""Period in seconds between banned bad IPs will be updated.
|
||||
"""
|
||||
return self._updateperiod
|
||||
|
||||
@updateperiod.setter
|
||||
def updateperiod(self, updateperiod):
|
||||
updateperiod = int(updateperiod)
|
||||
if updateperiod > 0:
|
||||
self._updateperiod = updateperiod
|
||||
else:
|
||||
raise ValueError("Update period must be integer greater than 0")
|
||||
|
||||
def _banIPs(self, ips):
|
||||
for ip in ips:
|
||||
try:
|
||||
self._jail.actions[self.banaction].ban({
|
||||
'ip': ip,
|
||||
'failures': 0,
|
||||
'matches': "",
|
||||
'ipmatches': "",
|
||||
'ipjailmatches': "",
|
||||
})
|
||||
except Exception as e:
|
||||
self._logSys.error(
|
||||
"Error banning IP %s for jail '%s' with action '%s': %s",
|
||||
ip, self._jail.name, self.banaction, e,
|
||||
exc_info=self._logSys.getEffectiveLevel()<=logging.DEBUG)
|
||||
else:
|
||||
self._bannedips.add(ip)
|
||||
self._logSys.info(
|
||||
"Banned IP %s for jail '%s' with action '%s'",
|
||||
ip, self._jail.name, self.banaction)
|
||||
|
||||
def _unbanIPs(self, ips):
|
||||
for ip in ips:
|
||||
try:
|
||||
self._jail.actions[self.banaction].unban({
|
||||
'ip': ip,
|
||||
'failures': 0,
|
||||
'matches': "",
|
||||
'ipmatches': "",
|
||||
'ipjailmatches': "",
|
||||
})
|
||||
except Exception as e:
|
||||
self._logSys.info(
|
||||
"Error unbanning IP %s for jail '%s' with action '%s': %s",
|
||||
ip, self._jail.name, self.banaction, e,
|
||||
exc_info=self._logSys.getEffectiveLevel()<=logging.DEBUG)
|
||||
else:
|
||||
self._logSys.info(
|
||||
"Unbanned IP %s for jail '%s' with action '%s'",
|
||||
ip, self._jail.name, self.banaction)
|
||||
finally:
|
||||
self._bannedips.remove(ip)
|
||||
|
||||
def start(self):
|
||||
"""If `banaction` set, blacklists bad IPs.
|
||||
"""
|
||||
if self.banaction is not None:
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
"""If `banaction` set, updates blacklisted IPs.
|
||||
|
||||
Queries badips.com for list of bad IPs, removing IPs from the
|
||||
blacklist if no longer present, and adds new bad IPs to the
|
||||
blacklist.
|
||||
"""
|
||||
if self.banaction is not None:
|
||||
if self._timer:
|
||||
self._timer.cancel()
|
||||
self._timer = None
|
||||
|
||||
try:
|
||||
ips = self.getList(
|
||||
self.bancategory, self.score, self.age, self.bankey)
|
||||
# Remove old IPs no longer listed
|
||||
self._unbanIPs(self._bannedips - ips)
|
||||
# Add new IPs which are now listed
|
||||
self._banIPs(ips - self._bannedips)
|
||||
|
||||
self._logSys.info(
|
||||
"Updated IPs for jail '%s'. Update again in %i seconds",
|
||||
self._jail.name, self.updateperiod)
|
||||
finally:
|
||||
self._timer = threading.Timer(self.updateperiod, self.update)
|
||||
self._timer.start()
|
||||
|
||||
def stop(self):
|
||||
"""If `banaction` set, clears blacklisted IPs.
|
||||
"""
|
||||
if self.banaction is not None:
|
||||
if self._timer:
|
||||
self._timer.cancel()
|
||||
self._timer = None
|
||||
self._unbanIPs(self._bannedips.copy())
|
||||
|
||||
def ban(self, aInfo):
|
||||
"""Reports banned IP to badips.com.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
aInfo : dict
|
||||
Dictionary which includes information in relation to
|
||||
the ban.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPError
|
||||
Any issues with badips.com request.
|
||||
"""
|
||||
try:
|
||||
url = "/".join([self._badips, "add", self.category, aInfo['ip']])
|
||||
if self.key:
|
||||
url = "?".join([url, urlencode({'key': self.key})])
|
||||
response = urlopen(self._Request(url), timeout=self.timeout)
|
||||
except HTTPError as response:
|
||||
messages = json.loads(response.read().decode('utf-8'))
|
||||
self._logSys.error(
|
||||
"Response from badips.com report: '%s'",
|
||||
messages['err'])
|
||||
raise
|
||||
else:
|
||||
messages = json.loads(response.read().decode('utf-8'))
|
||||
self._logSys.info(
|
||||
"Response from badips.com report: '%s'",
|
||||
messages['suc'])
|
||||
|
||||
Action = BadIPsAction
|
||||
@@ -1,93 +0,0 @@
|
||||
# Fail2Ban configuration file
|
||||
#
|
||||
# Author: Daniel Black
|
||||
#
|
||||
# This is a included configuration file and includes the definitions for the iptables
|
||||
# used in all iptables based actions by default.
|
||||
#
|
||||
# The user can override the defaults in iptables-common.local
|
||||
#
|
||||
# Modified: Alexander Koeppe <format_c@online.de>, Serg G. Brester <serg.brester@sebres.de>
|
||||
# made config file IPv6 capable (see new section Init?family=inet6)
|
||||
|
||||
[INCLUDES]
|
||||
|
||||
after = iptables-blocktype.local
|
||||
iptables-common.local
|
||||
# iptables-blocktype.local is obsolete
|
||||
|
||||
[Definition]
|
||||
|
||||
# Option: actionflush
|
||||
# Notes.: command executed once to flush IPS, by shutdown (resp. by stop of the jail or this action)
|
||||
# Values: CMD
|
||||
#
|
||||
actionflush = <iptables> -F f2b-<name>
|
||||
|
||||
|
||||
[Init]
|
||||
|
||||
# Option: chain
|
||||
# Notes specifies the iptables chain to which the Fail2Ban rules should be
|
||||
# added
|
||||
# Values: STRING Default: INPUT
|
||||
chain = INPUT
|
||||
|
||||
# Default name of the chain
|
||||
#
|
||||
name = default
|
||||
|
||||
# Option: port
|
||||
# Notes.: specifies port to monitor
|
||||
# Values: [ NUM | STRING ] Default:
|
||||
#
|
||||
port = ssh
|
||||
|
||||
# Option: protocol
|
||||
# Notes.: internally used by config reader for interpolations.
|
||||
# Values: [ tcp | udp | icmp | all ] Default: tcp
|
||||
#
|
||||
protocol = tcp
|
||||
|
||||
# Option: blocktype
|
||||
# Note: This is what the action does with rules. This can be any jump target
|
||||
# as per the iptables man page (section 8). Common values are DROP
|
||||
# REJECT, REJECT --reject-with icmp-port-unreachable
|
||||
# Values: STRING
|
||||
#blocktype = REJECT --reject-with icmp-port-unreachable
|
||||
blocktype = DROP
|
||||
|
||||
# Option: returntype
|
||||
# Note: This is the default rule on "actionstart". This should be RETURN
|
||||
# in all (blocking) actions, except REJECT in allowing actions.
|
||||
# Values: STRING
|
||||
returntype = RETURN
|
||||
|
||||
# Option: lockingopt
|
||||
# Notes.: Option was introduced to iptables to prevent multiple instances from
|
||||
# running concurrently and causing irratic behavior. -w was introduced
|
||||
# in iptables 1.4.20, so might be absent on older systems
|
||||
# See https://github.com/fail2ban/fail2ban/issues/1122
|
||||
# Values: STRING
|
||||
lockingopt = -w
|
||||
|
||||
# Option: iptables
|
||||
# Notes.: Actual command to be executed, including common to all calls options
|
||||
# Values: STRING
|
||||
iptables = iptables <lockingopt>
|
||||
|
||||
|
||||
[Init?family=inet6]
|
||||
|
||||
# Option: blocktype (ipv6)
|
||||
# Note: This is what the action does with rules. This can be any jump target
|
||||
# as per the iptables man page (section 8). Common values are DROP
|
||||
# REJECT, REJECT --reject-with icmp6-port-unreachable
|
||||
# Values: STRING
|
||||
blocktype = REJECT --reject-with icmp6-port-unreachable
|
||||
|
||||
# Option: iptables (ipv6)
|
||||
# Notes.: Actual command to be executed, including common to all calls options
|
||||
# Values: STRING
|
||||
iptables = ip6tables <lockingopt>
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
# Fail2Ban configuration file
|
||||
#
|
||||
# Authors: Sergey G Brester (sebres), Daniel Black, Alexander Koeppe
|
||||
#
|
||||
# This is for ipset protocol 6 (and hopefully later) (ipset v6.14).
|
||||
# Use ipset -V to see the protocol and version. Version 4 should use
|
||||
# iptables-ipset-proto4.conf.
|
||||
#
|
||||
# This requires the program ipset which is normally in package called ipset.
|
||||
#
|
||||
# IPset was a feature introduced in the linux kernel 2.6.39 and 3.0.0 kernels.
|
||||
#
|
||||
# If you are running on an older kernel you make need to patch in external
|
||||
# modules.
|
||||
#
|
||||
|
||||
[INCLUDES]
|
||||
|
||||
before = iptables.conf
|
||||
|
||||
[Definition]
|
||||
|
||||
# Option: actionstart
|
||||
# Notes.: command executed on demand at the first ban (or at the start of Fail2Ban if actionstart_on_demand is set to false).
|
||||
# Values: CMD
|
||||
#
|
||||
actionstart = ipset -exist create <ipmset> hash:ip timeout <default-ipsettime> <familyopt>
|
||||
<_ipt_add_rules>
|
||||
|
||||
# Option: actionflush
|
||||
# Notes.: command executed once to flush IPS, by shutdown (resp. by stop of the jail or this action)
|
||||
# Values: CMD
|
||||
#
|
||||
actionflush = ipset flush <ipmset>
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
|
||||
# Values: CMD
|
||||
#
|
||||
actionstop = <_ipt_del_rules>
|
||||
<actionflush>
|
||||
ipset destroy <ipmset>
|
||||
|
||||
# Option: actionban
|
||||
# Notes.: command executed when banning an IP. Take care that the
|
||||
# command is executed with Fail2Ban user rights.
|
||||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionban = ipset -exist add <ipmset> <ip> timeout <ipsettime>
|
||||
|
||||
# actionprolong = %(actionban)s
|
||||
|
||||
# Option: actionunban
|
||||
# Notes.: command executed when unbanning an IP. Take care that the
|
||||
# command is executed with Fail2Ban user rights.
|
||||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionunban = ipset -exist del <ipmset> <ip>
|
||||
|
||||
# Several capabilities used internaly:
|
||||
|
||||
rule-jump = -m set --match-set <ipmset> src -j <blocktype>
|
||||
|
||||
|
||||
[Init]
|
||||
|
||||
# Option: default-ipsettime
|
||||
# Notes: specifies default timeout in seconds (handled default ipset timeout only)
|
||||
# Values: [ NUM ] Default: 0 (no timeout, managed by fail2ban by unban)
|
||||
default-ipsettime = 0
|
||||
|
||||
# Option: ipsettime
|
||||
# Notes: specifies ticket timeout (handled ipset timeout only)
|
||||
# Values: [ NUM ] Default: 0 (managed by fail2ban by unban)
|
||||
ipsettime = 0
|
||||
|
||||
# expresion to caclulate timeout from bantime, example:
|
||||
# banaction = %(known/banaction)s[ipsettime='<timeout-bantime>']
|
||||
timeout-bantime = $([ "<bantime>" -le 2147483 ] && echo "<bantime>" || echo 0)
|
||||
|
||||
ipmset = f2b-<name>
|
||||
familyopt =
|
||||
|
||||
|
||||
[Init?family=inet6]
|
||||
|
||||
ipmset = f2b-<name>6
|
||||
familyopt = family inet6
|
||||
@@ -1,62 +0,0 @@
|
||||
# Fail2Ban configuration file
|
||||
#
|
||||
# Author: Cyril Jaquier
|
||||
#
|
||||
#
|
||||
|
||||
[Definition]
|
||||
|
||||
# Option: actionstart
|
||||
# Notes.: command executed once at the start of Fail2Ban.
|
||||
# Values: CMD
|
||||
#
|
||||
actionstart = printf %%b "Hi,\n
|
||||
The jail <name> has been started successfully.\n
|
||||
Regards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: started on `uname -n`" <dest>
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed once at the end of Fail2Ban
|
||||
# Values: CMD
|
||||
#
|
||||
actionstop = printf %%b "Hi,\n
|
||||
The jail <name> has been stopped.\n
|
||||
Regards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: stopped on `uname -n`" <dest>
|
||||
|
||||
# Option: actioncheck
|
||||
# Notes.: command executed once before each actionban command
|
||||
# Values: CMD
|
||||
#
|
||||
actioncheck =
|
||||
|
||||
# Option: actionban
|
||||
# Notes.: command executed when banning an IP. Take care that the
|
||||
# command is executed with Fail2Ban user rights.
|
||||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionban = printf %%b "Hi,\n
|
||||
The IP <ip> has just been banned by Fail2Ban after
|
||||
<failures> attempts against <name>.\n
|
||||
Regards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: banned <ip> from `uname -n`" <dest>
|
||||
|
||||
# Option: actionunban
|
||||
# Notes.: command executed when unbanning an IP. Take care that the
|
||||
# command is executed with Fail2Ban user rights.
|
||||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionunban =
|
||||
|
||||
[Init]
|
||||
|
||||
# Default name of the chain
|
||||
#
|
||||
name = default
|
||||
|
||||
# Destination/Addressee of the mail
|
||||
#
|
||||
dest = bogdan@898.ro
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban 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; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban 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 Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import socket
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formatdate, formataddr
|
||||
|
||||
from fail2ban.server.actions import ActionBase, CallingMap
|
||||
|
||||
messages = {}
|
||||
messages['start'] = \
|
||||
"""Hi,
|
||||
|
||||
The jail %(jailname)s has been started successfully.
|
||||
|
||||
Regards,
|
||||
Fail2Ban"""
|
||||
|
||||
messages['stop'] = \
|
||||
"""Hi,
|
||||
|
||||
The jail %(jailname)s has been stopped.
|
||||
|
||||
Regards,
|
||||
Fail2Ban"""
|
||||
|
||||
messages['ban'] = {}
|
||||
messages['ban']['head'] = \
|
||||
"""Hi,
|
||||
|
||||
The IP %(ip)s has just been banned for %(bantime)i seconds
|
||||
by Fail2Ban after %(failures)i attempts against %(jailname)s.
|
||||
"""
|
||||
messages['ban']['tail'] = \
|
||||
"""
|
||||
Regards,
|
||||
Fail2Ban"""
|
||||
messages['ban']['matches'] = \
|
||||
"""
|
||||
Matches for this ban:
|
||||
%(matches)s
|
||||
"""
|
||||
messages['ban']['ipmatches'] = \
|
||||
"""
|
||||
Matches for %(ip)s:
|
||||
%(ipmatches)s
|
||||
"""
|
||||
messages['ban']['ipjailmatches'] = \
|
||||
"""
|
||||
Matches for %(ip)s for jail %(jailname)s:
|
||||
%(ipjailmatches)s
|
||||
"""
|
||||
|
||||
|
||||
class SMTPAction(ActionBase):
|
||||
"""Fail2Ban action which sends emails to inform on jail starting,
|
||||
stopping and bans.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, jail, name, host="localhost", user=None, password=None,
|
||||
sendername="Fail2Ban", sender="fail2ban", dest="root", matches=None):
|
||||
"""Initialise action.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jail : Jail
|
||||
The jail which the action belongs to.
|
||||
name : str
|
||||
Named assigned to the action.
|
||||
host : str, optional
|
||||
SMTP host, of host:port format. Default host "localhost" and
|
||||
port "25"
|
||||
user : str, optional
|
||||
Username used for authentication with SMTP server.
|
||||
password : str, optional
|
||||
Password used for authentication with SMTP server.
|
||||
sendername : str, optional
|
||||
Name to use for from address in email. Default "Fail2Ban".
|
||||
sender : str, optional
|
||||
Email address to use for from address in email.
|
||||
Default "fail2ban".
|
||||
dest : str, optional
|
||||
Email addresses of intended recipient(s) in comma space ", "
|
||||
delimited format. Default "root".
|
||||
matches : str, optional
|
||||
Type of matches to be included from ban in email. Can be one
|
||||
of "matches", "ipmatches" or "ipjailmatches". Default None
|
||||
(see man jail.conf.5).
|
||||
"""
|
||||
|
||||
super(SMTPAction, self).__init__(jail, name)
|
||||
|
||||
self.host = host
|
||||
#TODO: self.ssl = ssl
|
||||
|
||||
self.user = user
|
||||
self.password =password
|
||||
|
||||
self.fromname = sendername
|
||||
self.fromaddr = sender
|
||||
self.toaddr = dest
|
||||
|
||||
self.matches = matches
|
||||
|
||||
self.message_values = CallingMap(
|
||||
jailname = self._jail.name,
|
||||
hostname = socket.gethostname,
|
||||
bantime = lambda: self._jail.actions.getBanTime(),
|
||||
)
|
||||
|
||||
# bypass ban/unban for restored tickets
|
||||
self.norestored = 1
|
||||
|
||||
def _sendMessage(self, subject, text):
|
||||
"""Sends message based on arguments and instance's properties.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
subject : str
|
||||
Subject of the email.
|
||||
text : str
|
||||
Body of the email.
|
||||
|
||||
Raises
|
||||
------
|
||||
SMTPConnectionError
|
||||
Error on connecting to host.
|
||||
SMTPAuthenticationError
|
||||
Error authenticating with SMTP server.
|
||||
SMTPException
|
||||
See Python `smtplib` for full list of other possible
|
||||
exceptions.
|
||||
"""
|
||||
msg = MIMEText(text)
|
||||
msg['Subject'] = subject
|
||||
msg['From'] = formataddr((self.fromname, self.fromaddr))
|
||||
msg['To'] = self.toaddr
|
||||
msg['Date'] = formatdate()
|
||||
|
||||
smtp = smtplib.SMTP()
|
||||
try:
|
||||
self._logSys.debug("Connected to SMTP '%s', response: %i: %s",
|
||||
self.host, *smtp.connect(self.host))
|
||||
if self.user and self.password: # pragma: no cover (ATM no tests covering that)
|
||||
smtp.login(self.user, self.password)
|
||||
failed_recipients = smtp.sendmail(
|
||||
self.fromaddr, self.toaddr.split(", "), msg.as_string())
|
||||
except smtplib.SMTPConnectError: # pragma: no cover
|
||||
self._logSys.error("Error connecting to host '%s'", self.host)
|
||||
raise
|
||||
except smtplib.SMTPAuthenticationError: # pragma: no cover
|
||||
self._logSys.error(
|
||||
"Failed to authenticate with host '%s' user '%s'",
|
||||
self.host, self.user)
|
||||
raise
|
||||
except smtplib.SMTPException: # pragma: no cover
|
||||
self._logSys.error(
|
||||
"Error sending mail to host '%s' from '%s' to '%s'",
|
||||
self.host, self.fromaddr, self.toaddr)
|
||||
raise
|
||||
else:
|
||||
if failed_recipients: # pragma: no cover
|
||||
self._logSys.warning(
|
||||
"Email to '%s' failed to following recipients: %r",
|
||||
self.toaddr, failed_recipients)
|
||||
self._logSys.debug("Email '%s' successfully sent", subject)
|
||||
finally:
|
||||
try:
|
||||
self._logSys.debug("Disconnected from '%s', response %i: %s",
|
||||
self.host, *smtp.quit())
|
||||
except smtplib.SMTPServerDisconnected: # pragma: no cover
|
||||
pass # Not connected
|
||||
|
||||
def start(self):
|
||||
"""Sends email to recipients informing that the jail has started.
|
||||
"""
|
||||
self._sendMessage(
|
||||
"[Fail2Ban] %(jailname)s: started on %(hostname)s" %
|
||||
self.message_values,
|
||||
messages['start'] % self.message_values)
|
||||
|
||||
def stop(self):
|
||||
"""Sends email to recipients informing that the jail has stopped.
|
||||
"""
|
||||
self._sendMessage(
|
||||
"[Fail2Ban] %(jailname)s: stopped on %(hostname)s" %
|
||||
self.message_values,
|
||||
messages['stop'] % self.message_values)
|
||||
|
||||
def ban(self, aInfo):
|
||||
"""Sends email to recipients informing that ban has occurred.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
aInfo : dict
|
||||
Dictionary which includes information in relation to
|
||||
the ban.
|
||||
"""
|
||||
if aInfo.get('restored'):
|
||||
return
|
||||
aInfo.update(self.message_values)
|
||||
message = "".join([
|
||||
messages['ban']['head'],
|
||||
messages['ban'].get(self.matches, ""),
|
||||
messages['ban']['tail']
|
||||
])
|
||||
self._sendMessage(
|
||||
"[Fail2Ban] %(jailname)s: banned %(ip)s from %(hostname)s" %
|
||||
aInfo,
|
||||
message % aInfo)
|
||||
|
||||
Action = SMTPAction
|
||||
@@ -1,225 +0,0 @@
|
||||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban 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; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban 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 Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import socket
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formatdate, formataddr
|
||||
|
||||
from fail2ban.server.actions import ActionBase, CallingMap
|
||||
|
||||
messages = {}
|
||||
messages['start'] = \
|
||||
"""Hi,
|
||||
|
||||
The jail %(jailname)s has been started successfully.
|
||||
|
||||
Regards,
|
||||
Fail2Ban"""
|
||||
|
||||
messages['stop'] = \
|
||||
"""Hi,
|
||||
|
||||
The jail %(jailname)s has been stopped.
|
||||
|
||||
Regards,
|
||||
Fail2Ban"""
|
||||
|
||||
messages['ban'] = {}
|
||||
messages['ban']['head'] = \
|
||||
"""Hi,
|
||||
|
||||
The IP %(ip)s has just been banned for %(bantime)i seconds
|
||||
by Fail2Ban after %(failures)i attempts against %(jailname)s.
|
||||
"""
|
||||
messages['ban']['tail'] = \
|
||||
"""
|
||||
Regards,
|
||||
Fail2Ban"""
|
||||
messages['ban']['matches'] = \
|
||||
"""
|
||||
Matches for this ban:
|
||||
%(matches)s
|
||||
"""
|
||||
messages['ban']['ipmatches'] = \
|
||||
"""
|
||||
Matches for %(ip)s:
|
||||
%(ipmatches)s
|
||||
"""
|
||||
messages['ban']['ipjailmatches'] = \
|
||||
"""
|
||||
Matches for %(ip)s for jail %(jailname)s:
|
||||
%(ipjailmatches)s
|
||||
"""
|
||||
|
||||
|
||||
class SMTPAction(ActionBase):
|
||||
"""Fail2Ban action which sends emails to inform on jail starting,
|
||||
stopping and bans.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, jail, name, host="localhost", user=None, password=None,
|
||||
sendername="Fail2Ban", sender="fail2ban", dest="root", matches=None):
|
||||
"""Initialise action.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jail : Jail
|
||||
The jail which the action belongs to.
|
||||
name : str
|
||||
Named assigned to the action.
|
||||
host : str, optional
|
||||
SMTP host, of host:port format. Default host "localhost" and
|
||||
port "25"
|
||||
user : str, optional
|
||||
Username used for authentication with SMTP server.
|
||||
password : str, optional
|
||||
Password used for authentication with SMTP server.
|
||||
sendername : str, optional
|
||||
Name to use for from address in email. Default "Fail2Ban".
|
||||
sender : str, optional
|
||||
Email address to use for from address in email.
|
||||
Default "fail2ban".
|
||||
dest : str, optional
|
||||
Email addresses of intended recipient(s) in comma space ", "
|
||||
delimited format. Default "root".
|
||||
matches : str, optional
|
||||
Type of matches to be included from ban in email. Can be one
|
||||
of "matches", "ipmatches" or "ipjailmatches". Default None
|
||||
(see man jail.conf.5).
|
||||
"""
|
||||
|
||||
super(SMTPAction, self).__init__(jail, name)
|
||||
|
||||
self.host = host
|
||||
#TODO: self.ssl = ssl
|
||||
|
||||
self.user = user
|
||||
self.password =password
|
||||
|
||||
self.fromname = sendername
|
||||
self.fromaddr = sender
|
||||
self.toaddr = dest
|
||||
|
||||
self.matches = matches
|
||||
|
||||
self.message_values = CallingMap(
|
||||
jailname = self._jail.name,
|
||||
hostname = socket.gethostname,
|
||||
bantime = self._jail.actions.getBanTime,
|
||||
)
|
||||
|
||||
def _sendMessage(self, subject, text):
|
||||
"""Sends message based on arguments and instance's properties.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
subject : str
|
||||
Subject of the email.
|
||||
text : str
|
||||
Body of the email.
|
||||
|
||||
Raises
|
||||
------
|
||||
SMTPConnectionError
|
||||
Error on connecting to host.
|
||||
SMTPAuthenticationError
|
||||
Error authenticating with SMTP server.
|
||||
SMTPException
|
||||
See Python `smtplib` for full list of other possible
|
||||
exceptions.
|
||||
"""
|
||||
msg = MIMEText(text)
|
||||
msg['Subject'] = subject
|
||||
msg['From'] = formataddr((self.fromname, self.fromaddr))
|
||||
msg['To'] = self.toaddr
|
||||
msg['Date'] = formatdate()
|
||||
|
||||
smtp = smtplib.SMTP()
|
||||
try:
|
||||
self._logSys.debug("Connected to SMTP '%s', response: %i: %s",
|
||||
self.host, *smtp.connect(self.host))
|
||||
if self.user and self.password:
|
||||
smtp.login(self.user, self.password)
|
||||
failed_recipients = smtp.sendmail(
|
||||
self.fromaddr, self.toaddr.split(", "), msg.as_string())
|
||||
except smtplib.SMTPConnectError:
|
||||
self._logSys.error("Error connecting to host '%s'", self.host)
|
||||
raise
|
||||
except smtplib.SMTPAuthenticationError:
|
||||
self._logSys.error(
|
||||
"Failed to authenticate with host '%s' user '%s'",
|
||||
self.host, self.user)
|
||||
raise
|
||||
except smtplib.SMTPException:
|
||||
self._logSys.error(
|
||||
"Error sending mail to host '%s' from '%s' to '%s'",
|
||||
self.host, self.fromaddr, self.toaddr)
|
||||
raise
|
||||
else:
|
||||
if failed_recipients:
|
||||
self._logSys.warning(
|
||||
"Email to '%s' failed to following recipients: %r",
|
||||
self.toaddr, failed_recipients)
|
||||
self._logSys.debug("Email '%s' successfully sent", subject)
|
||||
finally:
|
||||
try:
|
||||
self._logSys.debug("Disconnected from '%s', response %i: %s",
|
||||
self.host, *smtp.quit())
|
||||
except smtplib.SMTPServerDisconnected:
|
||||
pass # Not connected
|
||||
|
||||
def start(self):
|
||||
"""Sends email to recipients informing that the jail has started.
|
||||
"""
|
||||
self._sendMessage(
|
||||
"[Fail2Ban] %(jailname)s: started on %(hostname)s" %
|
||||
self.message_values,
|
||||
messages['start'] % self.message_values)
|
||||
|
||||
def stop(self):
|
||||
"""Sends email to recipients informing that the jail has stopped.
|
||||
"""
|
||||
self._sendMessage(
|
||||
"[Fail2Ban] %(jailname)s: stopped on %(hostname)s" %
|
||||
self.message_values,
|
||||
messages['stop'] % self.message_values)
|
||||
|
||||
def ban(self, aInfo):
|
||||
"""Sends email to recipients informing that ban has occurred.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
aInfo : dict
|
||||
Dictionary which includes information in relation to
|
||||
the ban.
|
||||
"""
|
||||
aInfo.update(self.message_values)
|
||||
message = "".join([
|
||||
messages['ban']['head'],
|
||||
messages['ban'].get(self.matches, ""),
|
||||
messages['ban']['tail']
|
||||
])
|
||||
self._sendMessage(
|
||||
"[Fail2Ban] %(jailname)s: banned %(ip)s from %(hostname)s" %
|
||||
aInfo,
|
||||
message % aInfo)
|
||||
|
||||
Action = SMTPAction
|
||||
Reference in New Issue
Block a user