From 4fb511294e01d33e934c32b7d458088e19d42343 Mon Sep 17 00:00:00 2001 From: sebres <serg.brester@sebres.de> Date: Thu, 8 Sep 2016 23:56:32 +0200 Subject: [PATCH] temp commit: reload now supported actions and action reloading (parameters, unban obsolete removed actions, etc.) --- fail2ban/client/actionreader.py | 1 + fail2ban/server/action.py | 88 +++++++++++++--------- fail2ban/server/actions.py | 96 ++++++++++++++++++------ fail2ban/server/banmanager.py | 9 +++ fail2ban/server/database.py | 12 ++- fail2ban/server/filter.py | 3 - fail2ban/server/server.py | 6 +- fail2ban/tests/fail2banclienttestcase.py | 27 ++++++- 8 files changed, 171 insertions(+), 71 deletions(-) diff --git a/fail2ban/client/actionreader.py b/fail2ban/client/actionreader.py index 2e02c4aa..e5025fa3 100644 --- a/fail2ban/client/actionreader.py +++ b/fail2ban/client/actionreader.py @@ -38,6 +38,7 @@ class ActionReader(DefinitionInitConfigReader): _configOpts = { "actionstart": ["string", None], "actionstop": ["string", None], + "actionreload": ["string", None], "actioncheck": ["string", None], "actionrepair": ["string", None], "actionban": ["string", None], diff --git a/fail2ban/server/action.py b/fail2ban/server/action.py index 9f539e2c..976adb20 100644 --- a/fail2ban/server/action.py +++ b/fail2ban/server/action.py @@ -201,6 +201,7 @@ class CommandAction(ActionBase): ---------- actionban actioncheck + actionreload actionrepair actionstart actionstop @@ -210,24 +211,35 @@ class CommandAction(ActionBase): _escapedTags = set(('matches', 'ipmatches', 'ipjailmatches')) - timeout = 60 - ## Command executed in order to initialize the system. - actionstart = '' - ## Command executed when an IP address gets banned. - actionban = '' - ## Command executed when an IP address gets removed. - actionunban = '' - ## Command executed in order to check requirements. - actioncheck = '' - ## Command executed in order to restore sane environment in error case. - actionrepair = '' - ## Command executed in order to stop the system. - actionstop = '' + def clearAllParams(self): + """ Clear all lists/dicts parameters (used by reloading) + """ + self.__init = 1 + try: + self.timeout = 60 + ## Command executed in order to initialize the system. + self.actionstart = '' + ## Command executed when an IP address gets banned. + self.actionban = '' + ## Command executed when an IP address gets removed. + self.actionunban = '' + ## 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): super(CommandAction, self).__init__(jail, name) + self.__init = 1 self.__properties = None self.__substCache = {} + self.clearAllParams() self._logSys.debug("Created %s" % self.__class__) @classmethod @@ -235,7 +247,7 @@ class CommandAction(ActionBase): return NotImplemented # Standard checks 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: if name in ('timeout', 'bantime'): value = str(MyTime.str2seconds(value)) @@ -268,8 +280,8 @@ class CommandAction(ActionBase): def _substCache(self): return self.__substCache - def start(self): - """Executes the "actionstart" command. + def _executeOperation(self, tag, operation): + """Executes the operation commands (like "actionstart", "actionstop", etc). Replace the tags in the action command with actions properties and executes the resulting command. @@ -278,20 +290,28 @@ class CommandAction(ActionBase): res = True try: # common (resp. ipv4): - startCmd = self.replaceTag('<actionstart>', self._properties, + startCmd = self.replaceTag(tag, self._properties, conditional='family=inet4', cache=self.__substCache) if startCmd: res &= self.executeCmd(startCmd, self.timeout) # start ipv6 actions if available: if allowed_ipv6: - startCmd6 = self.replaceTag('<actionstart>', self._properties, + startCmd6 = self.replaceTag(tag, self._properties, conditional='family=inet6', cache=self.__substCache) if startCmd6 and startCmd6 != startCmd: res &= self.executeCmd(startCmd6, self.timeout) 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: - 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): """Executes the "actionban" command. @@ -329,20 +349,20 @@ class CommandAction(ActionBase): Replaces the tags in the action command with actions properties and executes the resulting command. """ - res = True - # common (resp. ipv4): - stopCmd = self.replaceTag('<actionstop>', self._properties, - conditional='family=inet4', cache=self.__substCache) - if stopCmd: - res &= self.executeCmd(stopCmd, self.timeout) - # ipv6 actions if available: - if allowed_ipv6: - stopCmd6 = self.replaceTag('<actionstop>', self._properties, - conditional='family=inet6', cache=self.__substCache) - if stopCmd6 and stopCmd6 != stopCmd: - res &= self.executeCmd(stopCmd6, self.timeout) - if not res: - raise RuntimeError("Error stopping action") + return self._executeOperation('<actionstop>', 'stopping') + + def reload(self, **kwargs): + """Executes the "actionreload" command. + + Parameters + ---------- + kwargs : dict + Currently unused, because CommandAction do not support initOpts + + Replaces the tags in the action command with actions properties + and executes the resulting command. + """ + return self._executeOperation('<actionreload>', 'reloading') @classmethod def substituteRecursiveTags(cls, inptags, conditional=''): diff --git a/fail2ban/server/actions.py b/fail2ban/server/actions.py index 244d1e68..09b6823d 100644 --- a/fail2ban/server/actions.py +++ b/fail2ban/server/actions.py @@ -36,7 +36,7 @@ from collections import Mapping try: from collections import OrderedDict except ImportError: - OrderedDict = None + OrderedDict = dict from .banmanager import BanManager from .jailthread import JailThread @@ -81,14 +81,11 @@ class Actions(JailThread, Mapping): JailThread.__init__(self) ## The jail which contains this action. self._jail = jail - if OrderedDict is not None: - self._actions = OrderedDict() - else: - self._actions = dict() + self._actions = OrderedDict() ## The ban manager. 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. 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 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: action = CommandAction(self._jail, name) else: @@ -138,6 +145,27 @@ class Actions(JailThread, Mapping): action = customActionModule.Action(self._jail, name, **initOpts) 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): try: return self._actions[name] @@ -215,6 +243,24 @@ class Actions(JailThread, Mapping): 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): """Main loop for Threading. @@ -239,18 +285,9 @@ class Actions(JailThread, Mapping): continue if not Utils.wait_for(self.__checkBan, self.sleeptime): self.__checkUnBan() + self.__flushBan() - - 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") + self.stopActions() return True def __getBansMerged(self, mi, overalljails=False): @@ -355,26 +392,33 @@ class Actions(JailThread, Mapping): cnt, self.__banManager.size(), self._jail.name) return cnt - def __flushBan(self, db=False): + def __flushBan(self, db=False, actions=None): """Flush the ban 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") - lst = self.__banManager.flushBanList() + if actions is None: + logSys.debug("Flush ban list") + lst = self.__banManager.flushBanList() + else: + lst = iter(self.__banManager) + cnt = 0 for ticket in lst: # delete ip from database also: if db and self._jail.database is not None: ip = str(ticket.getIP()) self._jail.database.delBan(self._jail, ip) # unban ip: - self.__unBan(ticket) - cnt = len(lst) + self.__unBan(ticket, actions=actions) + cnt += 1 logSys.debug("Unbanned %s, %s ticket(s) in %r", cnt, self.__banManager.size(), self._jail.name) return cnt - def __unBan(self, ticket): + def __unBan(self, ticket, actions=None): """Unbans host corresponding to the ticket. Executes the actions in order to unban the host given in the @@ -385,13 +429,15 @@ class Actions(JailThread, Mapping): ticket : FailTicket Ticket of failures of which to unban """ + if actions is None: + actions = self._actions aInfo = dict() aInfo["ip"] = ticket.getIP() aInfo["failures"] = ticket.getAttempt() aInfo["time"] = ticket.getTime() aInfo["matches"] = "".join(ticket.getMatches()) logSys.notice("[%s] Unban %s" % (self._jail.name, aInfo["ip"])) - for name, action in self._actions.iteritems(): + for name, action in actions.iteritems(): try: action.unban(aInfo.copy()) except Exception as e: diff --git a/fail2ban/server/banmanager.py b/fail2ban/server/banmanager.py index e0a9e5ca..41e202ef 100644 --- a/fail2ban/server/banmanager.py +++ b/fail2ban/server/banmanager.py @@ -106,6 +106,15 @@ class BanManager: with self.__lock: 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 # diff --git a/fail2ban/server/database.py b/fail2ban/server/database.py index fc544be4..53db84a9 100644 --- a/fail2ban/server/database.py +++ b/fail2ban/server/database.py @@ -604,16 +604,14 @@ class Fail2BanDb(object): results = list(self._getCurrentBans(jail=jail, ip=ip, forbantime=forbantime, fromtime=fromtime)) if results: - matches = [] - failures = 0 for banip, timeofban, data in results: - #TODO: Implement data parts once arbitrary match keys completed - ticket = FailTicket(banip, timeofban, matches) - ticket.setAttempt(failures) matches = [] failures = 0 - matches.extend(data['matches']) - failures += data['failures'] + if isinstance(data['matches'], list): + matches.extend(data['matches']) + if data['failures']: + failures += data['failures'] + ticket = FailTicket(banip, timeofban, matches) ticket.setAttempt(failures) tickets.append(ticket) diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index 1c466c01..6b0599f5 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -117,14 +117,11 @@ class Filter(JailThread): self._reload_logs = dict((k, 1) for k in self.getLogPaths()) else: if hasattr(self, '_reload_logs'): - dellogs = dict() # if it was not reloaded - remove obsolete log file: for path in self._reload_logs: self.delLogPath(path) delattr(self, '_reload_logs') - - ## # Add a regular expression which matches the failure. # diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py index 041b0983..fc91ca73 100644 --- a/fail2ban/server/server.py +++ b/fail2ban/server/server.py @@ -267,6 +267,7 @@ class Server: jail.idle = True self.__reload_state[jn] = jail jail.filter.reload(begin=True) + jail.actions.reload(begin=True) pass else: # end reload, all affected (or new) jails have already all new parameters (via stream) and (re)started: @@ -280,6 +281,7 @@ class Server: else: # commit (reload was finished): jail.filter.reload(begin=False) + jail.actions.reload(begin=False) for jn in deljails: self.delJail(jn) self.__reload_state = {} @@ -416,7 +418,9 @@ class Server: # Action 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): return self.__jails[name].actions diff --git a/fail2ban/tests/fail2banclienttestcase.py b/fail2ban/tests/fail2banclienttestcase.py index 394a000f..d818e252 100644 --- a/fail2ban/tests/fail2banclienttestcase.py +++ b/fail2ban/tests/fail2banclienttestcase.py @@ -677,6 +677,20 @@ class Fail2banServerTest(Fail2banClientServerBase): test2log = pjoin(tmp, "test2.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]): _write_file(pjoin(cfg, "jail.conf"), "w", "[INCLUDES]", "", @@ -686,7 +700,9 @@ class Fail2banServerTest(Fail2banClientServerBase): "findtime = 10m", "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, " " + test2log 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 _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_file(test1log, "w", *((str(int(MyTime.time())) + " failure 401 from 192.0.2.1: test 1",) * 3)) _write_file(test2log, "w") @@ -737,6 +757,11 @@ class Fail2banServerTest(Fail2banClientServerBase): self.assertLogged( "Added logfile: %r" % test2log, "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: self.assertLogged( "Creating new jail 'test-jail2'",