temp commit: reload now supported actions and action reloading (parameters, unban obsolete removed actions, etc.)

pull/1557/head
sebres 2016-09-08 23:56:32 +02:00
parent d1ef33cc45
commit 4fb511294e
8 changed files with 171 additions and 71 deletions

View File

@ -38,6 +38,7 @@ class ActionReader(DefinitionInitConfigReader):
_configOpts = { _configOpts = {
"actionstart": ["string", None], "actionstart": ["string", None],
"actionstop": ["string", None], "actionstop": ["string", None],
"actionreload": ["string", None],
"actioncheck": ["string", None], "actioncheck": ["string", None],
"actionrepair": ["string", None], "actionrepair": ["string", None],
"actionban": ["string", None], "actionban": ["string", None],

View File

@ -201,6 +201,7 @@ class CommandAction(ActionBase):
---------- ----------
actionban actionban
actioncheck actioncheck
actionreload
actionrepair actionrepair
actionstart actionstart
actionstop actionstop
@ -210,24 +211,35 @@ class CommandAction(ActionBase):
_escapedTags = set(('matches', 'ipmatches', 'ipjailmatches')) _escapedTags = set(('matches', 'ipmatches', 'ipjailmatches'))
timeout = 60 def clearAllParams(self):
## Command executed in order to initialize the system. """ Clear all lists/dicts parameters (used by reloading)
actionstart = '' """
## Command executed when an IP address gets banned. self.__init = 1
actionban = '' try:
## Command executed when an IP address gets removed. self.timeout = 60
actionunban = '' ## Command executed in order to initialize the system.
## Command executed in order to check requirements. self.actionstart = ''
actioncheck = '' ## Command executed when an IP address gets banned.
## Command executed in order to restore sane environment in error case. self.actionban = ''
actionrepair = '' ## Command executed when an IP address gets removed.
## Command executed in order to stop the system. self.actionunban = ''
actionstop = '' ## Command executed in order to check requirements.
self.actioncheck = ''
## Command executed in order to restore sane environment in error case.
self.actionrepair = ''
## Command executed in order to stop the system.
self.actionstop = ''
## Command executed in case of reloading action.
self.actionreload = ''
finally:
self.__init = 0
def __init__(self, jail, name): def __init__(self, jail, name):
super(CommandAction, self).__init__(jail, name) super(CommandAction, self).__init__(jail, name)
self.__init = 1
self.__properties = None self.__properties = None
self.__substCache = {} self.__substCache = {}
self.clearAllParams()
self._logSys.debug("Created %s" % self.__class__) self._logSys.debug("Created %s" % self.__class__)
@classmethod @classmethod
@ -235,7 +247,7 @@ class CommandAction(ActionBase):
return NotImplemented # Standard checks return NotImplemented # Standard checks
def __setattr__(self, name, value): def __setattr__(self, name, value):
if not name.startswith('_') and not callable(value): if not name.startswith('_') and not self.__init and not callable(value):
# special case for some pasrameters: # special case for some pasrameters:
if name in ('timeout', 'bantime'): if name in ('timeout', 'bantime'):
value = str(MyTime.str2seconds(value)) value = str(MyTime.str2seconds(value))
@ -268,8 +280,8 @@ class CommandAction(ActionBase):
def _substCache(self): def _substCache(self):
return self.__substCache return self.__substCache
def start(self): def _executeOperation(self, tag, operation):
"""Executes the "actionstart" command. """Executes the operation commands (like "actionstart", "actionstop", etc).
Replace the tags in the action command with actions properties Replace the tags in the action command with actions properties
and executes the resulting command. and executes the resulting command.
@ -278,20 +290,28 @@ class CommandAction(ActionBase):
res = True res = True
try: try:
# common (resp. ipv4): # common (resp. ipv4):
startCmd = self.replaceTag('<actionstart>', self._properties, startCmd = self.replaceTag(tag, self._properties,
conditional='family=inet4', cache=self.__substCache) conditional='family=inet4', cache=self.__substCache)
if startCmd: if startCmd:
res &= self.executeCmd(startCmd, self.timeout) res &= self.executeCmd(startCmd, self.timeout)
# start ipv6 actions if available: # start ipv6 actions if available:
if allowed_ipv6: if allowed_ipv6:
startCmd6 = self.replaceTag('<actionstart>', self._properties, startCmd6 = self.replaceTag(tag, self._properties,
conditional='family=inet6', cache=self.__substCache) conditional='family=inet6', cache=self.__substCache)
if startCmd6 and startCmd6 != startCmd: if startCmd6 and startCmd6 != startCmd:
res &= self.executeCmd(startCmd6, self.timeout) res &= self.executeCmd(startCmd6, self.timeout)
if not res: if not res:
raise RuntimeError("Error starting action %s/%s" % (self._jail, self._name,)) raise RuntimeError("Error %s action %s/%s" % (operation, self._jail, self._name,))
except ValueError as e: except ValueError as e:
raise RuntimeError("Error starting action %s/%s: %r" % (self._jail, self._name, e)) raise RuntimeError("Error %s action %s/%s: %r" % (operation, self._jail, self._name, e))
def start(self):
"""Executes the "actionstart" command.
Replace the tags in the action command with actions properties
and executes the resulting command.
"""
return self._executeOperation('<actionstart>', 'starting')
def ban(self, aInfo): def ban(self, aInfo):
"""Executes the "actionban" command. """Executes the "actionban" command.
@ -329,20 +349,20 @@ class CommandAction(ActionBase):
Replaces the tags in the action command with actions properties Replaces the tags in the action command with actions properties
and executes the resulting command. and executes the resulting command.
""" """
res = True return self._executeOperation('<actionstop>', 'stopping')
# common (resp. ipv4):
stopCmd = self.replaceTag('<actionstop>', self._properties, def reload(self, **kwargs):
conditional='family=inet4', cache=self.__substCache) """Executes the "actionreload" command.
if stopCmd:
res &= self.executeCmd(stopCmd, self.timeout) Parameters
# ipv6 actions if available: ----------
if allowed_ipv6: kwargs : dict
stopCmd6 = self.replaceTag('<actionstop>', self._properties, Currently unused, because CommandAction do not support initOpts
conditional='family=inet6', cache=self.__substCache)
if stopCmd6 and stopCmd6 != stopCmd: Replaces the tags in the action command with actions properties
res &= self.executeCmd(stopCmd6, self.timeout) and executes the resulting command.
if not res: """
raise RuntimeError("Error stopping action") return self._executeOperation('<actionreload>', 'reloading')
@classmethod @classmethod
def substituteRecursiveTags(cls, inptags, conditional=''): def substituteRecursiveTags(cls, inptags, conditional=''):

View File

@ -36,7 +36,7 @@ from collections import Mapping
try: try:
from collections import OrderedDict from collections import OrderedDict
except ImportError: except ImportError:
OrderedDict = None OrderedDict = dict
from .banmanager import BanManager from .banmanager import BanManager
from .jailthread import JailThread from .jailthread import JailThread
@ -81,14 +81,11 @@ class Actions(JailThread, Mapping):
JailThread.__init__(self) JailThread.__init__(self)
## The jail which contains this action. ## The jail which contains this action.
self._jail = jail self._jail = jail
if OrderedDict is not None: self._actions = OrderedDict()
self._actions = OrderedDict()
else:
self._actions = dict()
## The ban manager. ## The ban manager.
self.__banManager = BanManager() self.__banManager = BanManager()
def add(self, name, pythonModule=None, initOpts=None): def add(self, name, pythonModule=None, initOpts=None, reload=False):
"""Adds a new action. """Adds a new action.
Add a new action if not already present, defaulting to standard Add a new action if not already present, defaulting to standard
@ -116,7 +113,17 @@ class Actions(JailThread, Mapping):
""" """
# Check is action name already exists # Check is action name already exists
if name in self._actions: if name in self._actions:
raise ValueError("Action %s already exists" % name) if not reload:
raise ValueError("Action %s already exists" % name)
# don't create new action if reload supported:
action = self._actions[name]
if hasattr(action, 'reload'):
# don't execute reload right now, reload after all parameters are actualized
if hasattr(action, 'clearAllParams'):
action.clearAllParams()
self._reload_actions[name] = initOpts
return
## Create new action:
if pythonModule is None: if pythonModule is None:
action = CommandAction(self._jail, name) action = CommandAction(self._jail, name)
else: else:
@ -138,6 +145,27 @@ class Actions(JailThread, Mapping):
action = customActionModule.Action(self._jail, name, **initOpts) action = customActionModule.Action(self._jail, name, **initOpts)
self._actions[name] = action self._actions[name] = action
def reload(self, begin=True):
""" Begin or end of reloading resp. refreshing of all parameters
"""
if begin:
self._reload_actions = dict()
else:
if hasattr(self, '_reload_actions'):
# reload actions after all parameters set via stream:
for name, initOpts in self._reload_actions.iteritems():
if name in self._actions:
self._actions[name].reload(**initOpts if initOpts else {})
# remove obsolete actions (untouched by reload process):
delacts = OrderedDict((name, action) for name, action in self._actions.iteritems()
if name not in self._reload_actions)
if len(delacts):
# unban all tickets using remove action only:
self.__flushBan(db=False, actions=delacts)
# stop and remove it:
self.stopActions(actions=delacts)
delattr(self, '_reload_actions')
def __getitem__(self, name): def __getitem__(self, name):
try: try:
return self._actions[name] return self._actions[name]
@ -215,6 +243,24 @@ class Actions(JailThread, Mapping):
return 1 return 1
def stopActions(self, actions=None):
"""Stops the actions in reverse sequence (optionally filtered)
"""
if actions is None:
actions = self._actions
revactions = actions.items()
revactions.reverse()
for name, action in revactions:
try:
action.stop()
except Exception as e:
logSys.error("Failed to stop jail '%s' action '%s': %s",
self._jail.name, name, e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
del self._actions[name]
logSys.debug("%s: action %s terminated", self._jail.name, name)
def run(self): def run(self):
"""Main loop for Threading. """Main loop for Threading.
@ -239,18 +285,9 @@ class Actions(JailThread, Mapping):
continue continue
if not Utils.wait_for(self.__checkBan, self.sleeptime): if not Utils.wait_for(self.__checkBan, self.sleeptime):
self.__checkUnBan() self.__checkUnBan()
self.__flushBan() self.__flushBan()
self.stopActions()
actions = self._actions.items()
actions.reverse()
for name, action in actions:
try:
action.stop()
except Exception as e:
logSys.error("Failed to stop jail '%s' action '%s': %s",
self._jail.name, name, e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
logSys.debug(self._jail.name + ": action terminated")
return True return True
def __getBansMerged(self, mi, overalljails=False): def __getBansMerged(self, mi, overalljails=False):
@ -355,26 +392,33 @@ class Actions(JailThread, Mapping):
cnt, self.__banManager.size(), self._jail.name) cnt, self.__banManager.size(), self._jail.name)
return cnt return cnt
def __flushBan(self, db=False): def __flushBan(self, db=False, actions=None):
"""Flush the ban list. """Flush the ban list.
Unban all IP address which are still in the banning list. Unban all IP address which are still in the banning list.
If actions specified, don't flush list - just execute unban for
given actions (reload, obsolete resp. removed actions).
""" """
logSys.debug("Flush ban list") if actions is None:
lst = self.__banManager.flushBanList() logSys.debug("Flush ban list")
lst = self.__banManager.flushBanList()
else:
lst = iter(self.__banManager)
cnt = 0
for ticket in lst: for ticket in lst:
# delete ip from database also: # delete ip from database also:
if db and self._jail.database is not None: if db and self._jail.database is not None:
ip = str(ticket.getIP()) ip = str(ticket.getIP())
self._jail.database.delBan(self._jail, ip) self._jail.database.delBan(self._jail, ip)
# unban ip: # unban ip:
self.__unBan(ticket) self.__unBan(ticket, actions=actions)
cnt = len(lst) cnt += 1
logSys.debug("Unbanned %s, %s ticket(s) in %r", logSys.debug("Unbanned %s, %s ticket(s) in %r",
cnt, self.__banManager.size(), self._jail.name) cnt, self.__banManager.size(), self._jail.name)
return cnt return cnt
def __unBan(self, ticket): def __unBan(self, ticket, actions=None):
"""Unbans host corresponding to the ticket. """Unbans host corresponding to the ticket.
Executes the actions in order to unban the host given in the Executes the actions in order to unban the host given in the
@ -385,13 +429,15 @@ class Actions(JailThread, Mapping):
ticket : FailTicket ticket : FailTicket
Ticket of failures of which to unban Ticket of failures of which to unban
""" """
if actions is None:
actions = self._actions
aInfo = dict() aInfo = dict()
aInfo["ip"] = ticket.getIP() aInfo["ip"] = ticket.getIP()
aInfo["failures"] = ticket.getAttempt() aInfo["failures"] = ticket.getAttempt()
aInfo["time"] = ticket.getTime() aInfo["time"] = ticket.getTime()
aInfo["matches"] = "".join(ticket.getMatches()) aInfo["matches"] = "".join(ticket.getMatches())
logSys.notice("[%s] Unban %s" % (self._jail.name, aInfo["ip"])) logSys.notice("[%s] Unban %s" % (self._jail.name, aInfo["ip"]))
for name, action in self._actions.iteritems(): for name, action in actions.iteritems():
try: try:
action.unban(aInfo.copy()) action.unban(aInfo.copy())
except Exception as e: except Exception as e:

View File

@ -106,6 +106,15 @@ class BanManager:
with self.__lock: with self.__lock:
return self.__banList.keys() return self.__banList.keys()
##
# Returns a iterator to ban list (used in reload, so idle).
#
# @return ban list iterator
def __iter__(self):
with self.__lock:
return self.__banList.itervalues()
## ##
# Returns normalized value # Returns normalized value
# #

View File

@ -604,16 +604,14 @@ class Fail2BanDb(object):
results = list(self._getCurrentBans(jail=jail, ip=ip, forbantime=forbantime, fromtime=fromtime)) results = list(self._getCurrentBans(jail=jail, ip=ip, forbantime=forbantime, fromtime=fromtime))
if results: if results:
matches = []
failures = 0
for banip, timeofban, data in results: for banip, timeofban, data in results:
#TODO: Implement data parts once arbitrary match keys completed
ticket = FailTicket(banip, timeofban, matches)
ticket.setAttempt(failures)
matches = [] matches = []
failures = 0 failures = 0
matches.extend(data['matches']) if isinstance(data['matches'], list):
failures += data['failures'] matches.extend(data['matches'])
if data['failures']:
failures += data['failures']
ticket = FailTicket(banip, timeofban, matches)
ticket.setAttempt(failures) ticket.setAttempt(failures)
tickets.append(ticket) tickets.append(ticket)

View File

@ -117,14 +117,11 @@ class Filter(JailThread):
self._reload_logs = dict((k, 1) for k in self.getLogPaths()) self._reload_logs = dict((k, 1) for k in self.getLogPaths())
else: else:
if hasattr(self, '_reload_logs'): if hasattr(self, '_reload_logs'):
dellogs = dict()
# if it was not reloaded - remove obsolete log file: # if it was not reloaded - remove obsolete log file:
for path in self._reload_logs: for path in self._reload_logs:
self.delLogPath(path) self.delLogPath(path)
delattr(self, '_reload_logs') delattr(self, '_reload_logs')
## ##
# Add a regular expression which matches the failure. # Add a regular expression which matches the failure.
# #

View File

@ -267,6 +267,7 @@ class Server:
jail.idle = True jail.idle = True
self.__reload_state[jn] = jail self.__reload_state[jn] = jail
jail.filter.reload(begin=True) jail.filter.reload(begin=True)
jail.actions.reload(begin=True)
pass pass
else: else:
# end reload, all affected (or new) jails have already all new parameters (via stream) and (re)started: # end reload, all affected (or new) jails have already all new parameters (via stream) and (re)started:
@ -280,6 +281,7 @@ class Server:
else: else:
# commit (reload was finished): # commit (reload was finished):
jail.filter.reload(begin=False) jail.filter.reload(begin=False)
jail.actions.reload(begin=False)
for jn in deljails: for jn in deljails:
self.delJail(jn) self.delJail(jn)
self.__reload_state = {} self.__reload_state = {}
@ -416,7 +418,9 @@ class Server:
# Action # Action
def addAction(self, name, value, *args): def addAction(self, name, value, *args):
self.__jails[name].actions.add(value, *args) ## create (or reload) jail action:
self.__jails[name].actions.add(value, *args,
reload=name in self.__reload_state)
def getActions(self, name): def getActions(self, name):
return self.__jails[name].actions return self.__jails[name].actions

View File

@ -677,6 +677,20 @@ class Fail2banServerTest(Fail2banClientServerBase):
test2log = pjoin(tmp, "test2.log") test2log = pjoin(tmp, "test2.log")
test3log = pjoin(tmp, "test3.log") test3log = pjoin(tmp, "test3.log")
os.mkdir(pjoin(cfg, "action.d"))
def _write_action_cfg(actname="test-action1"):
fn = pjoin(cfg, "action.d", "%s.conf" % actname)
_write_file(fn, "w",
"[Definition]",
"actionstart = echo '[<name>] %s: ** start'" % actname,
"actionstop = echo '[<name>] %s: -- unban <ip>'" % actname,
"actionreload = echo '[<name>] %s: ** reload'" % actname,
"actionban = echo '[<name>] %s: ++ ban <ip>'" % actname,
"actionunban = echo '[<name>] %s: // stop'" % actname,
)
if DefLogSys.level < logging.DEBUG: # if HEAVYDEBUG
_out_file(fn)
def _write_jail_cfg(enabled=[1, 2]): def _write_jail_cfg(enabled=[1, 2]):
_write_file(pjoin(cfg, "jail.conf"), "w", _write_file(pjoin(cfg, "jail.conf"), "w",
"[INCLUDES]", "", "[INCLUDES]", "",
@ -686,7 +700,9 @@ class Fail2banServerTest(Fail2banClientServerBase):
"findtime = 10m", "findtime = 10m",
"failregex = ^\s*failure (401|403) from <HOST>", "failregex = ^\s*failure (401|403) from <HOST>",
"", "",
"[test-jail1]", "backend = polling", "filter =", "action =", "[test-jail1]", "backend = polling", "filter =",
"action = test-action1[name='%(__name__)s']",
" test-action2[name='%(__name__)s']",
"logpath = " + test1log, "logpath = " + test1log,
" " + test2log if 2 in enabled else "", " " + test2log if 2 in enabled else "",
" " + test3log if 2 in enabled else "", " " + test3log if 2 in enabled else "",
@ -701,6 +717,10 @@ class Fail2banServerTest(Fail2banClientServerBase):
if DefLogSys.level < logging.DEBUG: # if HEAVYDEBUG if DefLogSys.level < logging.DEBUG: # if HEAVYDEBUG
_out_file(pjoin(cfg, "jail.conf")) _out_file(pjoin(cfg, "jail.conf"))
# create default test actions:
_write_action_cfg(actname="test-action1")
_write_action_cfg(actname="test-action2")
_write_jail_cfg(enabled=[1]) _write_jail_cfg(enabled=[1])
_write_file(test1log, "w", *((str(int(MyTime.time())) + " failure 401 from 192.0.2.1: test 1",) * 3)) _write_file(test1log, "w", *((str(int(MyTime.time())) + " failure 401 from 192.0.2.1: test 1",) * 3))
_write_file(test2log, "w") _write_file(test2log, "w")
@ -737,6 +757,11 @@ class Fail2banServerTest(Fail2banClientServerBase):
self.assertLogged( self.assertLogged(
"Added logfile: %r" % test2log, "Added logfile: %r" % test2log,
"Added logfile: %r" % test3log, all=True) "Added logfile: %r" % test3log, all=True)
# test actions reloaded:
self.assertLogged(
"echo '[test-jail1] test-action1: ** reload' -- returned successfully",
"echo '[test-jail1] test-action2: ** reload' -- returned successfully", all=True)
# test 1 new jail: # test 1 new jail:
self.assertLogged( self.assertLogged(
"Creating new jail 'test-jail2'", "Creating new jail 'test-jail2'",