diff --git a/ChangeLog b/ChangeLog index 2c202c07..37d74955 100644 --- a/ChangeLog +++ b/ChangeLog @@ -165,6 +165,31 @@ fail2ban-client set loglevel INFO - faster match and fewer searching of appropriate templates (DateDetector.matchTime calls rarer DateTemplate.matchDate now); - several standard filters extended with exact prefixed or anchored date templates; +* Added possibility to recognize restored state of the tickets (see gh-1669). + New option `norestored` introduced, to ignore restored tickets (after restart). + To avoid execution of ban/unban for the restored tickets, `norestored = true` + could be added in definition section of action. + For conditional usage in the shell-based actions an interpolation `` + could be used also. E. g. it is enough to add following script-piece at begin + of `actionban` (or `actionunban`) to prevent execution: + `if [ '' = '1' ]; then exit 0; fi;` + Several actions extended now using `norestored` option: + - complain.conf + - dshield.conf + - mail-buffered.conf + - mail-whois-lines.conf + - mail-whois.conf + - mail.conf + - sendmail-buffered.conf + - sendmail-geoip-lines.conf + - sendmail-whois-ipjailmatches.conf + - sendmail-whois-ipmatches.conf + - sendmail-whois-lines.conf + - sendmail-whois-matches.conf + - sendmail-whois.conf + - sendmail.conf + - smtp.py + - xarf-login-attack.conf * fail2ban-testcases: - `assertLogged` extended with parameter wait (to wait up to specified timeout, before we throw assert exception) + test cases rewritten using that diff --git a/config/action.d/complain.conf b/config/action.d/complain.conf index e4ceb35f..84dbaf39 100644 --- a/config/action.d/complain.conf +++ b/config/action.d/complain.conf @@ -34,6 +34,9 @@ before = helpers-common.conf [Definition] +# bypass ban/unban for restored tickets +norestored = 1 + # Option: actionstart # Notes.: command executed once at the start of Fail2Ban. # Values: CMD diff --git a/config/action.d/dshield.conf b/config/action.d/dshield.conf index a0041986..4f2e09ca 100644 --- a/config/action.d/dshield.conf +++ b/config/action.d/dshield.conf @@ -28,6 +28,9 @@ [Definition] +# bypass ban/unban for restored tickets +norestored = 1 + # Option: actionstart # Notes.: command executed once at the start of Fail2Ban. # Values: CMD diff --git a/config/action.d/helpers-common.conf b/config/action.d/helpers-common.conf index 7fa8e9e4..b04c7f7a 100644 --- a/config/action.d/helpers-common.conf +++ b/config/action.d/helpers-common.conf @@ -7,6 +7,9 @@ _grep_logs = logpath=""; grep -E %(_grep_logs_args)s $logpath | _grep_logs_args = '(^|[^0-9])([^0-9]|$)' +# Used for actions, that should not by executed if ticket was restored: +_bypass_if_restored = if [ '' = '1' ]; then exit 0; fi; + [Init] greplimit = tail -n grepmax = 1000 diff --git a/config/action.d/mail-buffered.conf b/config/action.d/mail-buffered.conf index 914d4a5a..e74db9cc 100644 --- a/config/action.d/mail-buffered.conf +++ b/config/action.d/mail-buffered.conf @@ -6,6 +6,9 @@ [Definition] +# bypass ban/unban for restored tickets +norestored = 1 + # Option: actionstart # Notes.: command executed once at the start of Fail2Ban. # Values: CMD diff --git a/config/action.d/mail-whois-lines.conf b/config/action.d/mail-whois-lines.conf index cbd970c9..0852ba8f 100644 --- a/config/action.d/mail-whois-lines.conf +++ b/config/action.d/mail-whois-lines.conf @@ -11,6 +11,9 @@ before = mail-whois-common.conf [Definition] +# bypass ban/unban for restored tickets +norestored = 1 + # Option: actionstart # Notes.: command executed once at the start of Fail2Ban. # Values: CMD @@ -52,6 +55,7 @@ _ban_mail_content = ( printf %%b "Hi,\n printf %%b "\n Regards,\n Fail2Ban" ) + actionban = %(_ban_mail_content)s | "[Fail2Ban] : banned from `uname -n`" # Option: actionunban diff --git a/config/action.d/mail-whois.conf b/config/action.d/mail-whois.conf index 018c327d..553bfb69 100644 --- a/config/action.d/mail-whois.conf +++ b/config/action.d/mail-whois.conf @@ -10,6 +10,9 @@ before = mail-whois-common.conf [Definition] +# bypass ban/unban for restored tickets +norestored = 1 + # Option: actionstart # Notes.: command executed once at the start of Fail2Ban. # Values: CMD diff --git a/config/action.d/mail.conf b/config/action.d/mail.conf index 7bf51a1d..4715ecc5 100644 --- a/config/action.d/mail.conf +++ b/config/action.d/mail.conf @@ -6,6 +6,9 @@ [Definition] +# bypass ban/unban for restored tickets +norestored = 1 + # Option: actionstart # Notes.: command executed once at the start of Fail2Ban. # Values: CMD diff --git a/config/action.d/sendmail-buffered.conf b/config/action.d/sendmail-buffered.conf index 80eb20a3..a91a6957 100644 --- a/config/action.d/sendmail-buffered.conf +++ b/config/action.d/sendmail-buffered.conf @@ -10,6 +10,9 @@ before = sendmail-common.conf [Definition] +# bypass ban/unban for restored tickets +norestored = 1 + # Option: actionstart # Notes.: command executed once at the start of Fail2Ban. # Values: CMD diff --git a/config/action.d/sendmail-geoip-lines.conf b/config/action.d/sendmail-geoip-lines.conf index a5616e9f..34c3aedd 100644 --- a/config/action.d/sendmail-geoip-lines.conf +++ b/config/action.d/sendmail-geoip-lines.conf @@ -11,6 +11,9 @@ before = sendmail-common.conf [Definition] +# bypass ban/unban for restored tickets +norestored = 1 + # Option: actionban # Notes.: Command executed when banning an IP. Take care that the # command is executed with Fail2Ban user rights. diff --git a/config/action.d/sendmail-whois-ipjailmatches.conf b/config/action.d/sendmail-whois-ipjailmatches.conf index 689ffe45..5bcefe89 100644 --- a/config/action.d/sendmail-whois-ipjailmatches.conf +++ b/config/action.d/sendmail-whois-ipjailmatches.conf @@ -10,6 +10,9 @@ before = sendmail-common.conf [Definition] +# bypass ban/unban for restored tickets +norestored = 1 + # Option: actionban # Notes.: command executed when banning an IP. Take care that the # command is executed with Fail2Ban user rights. diff --git a/config/action.d/sendmail-whois-ipmatches.conf b/config/action.d/sendmail-whois-ipmatches.conf index b06e6db6..4a8edcb7 100644 --- a/config/action.d/sendmail-whois-ipmatches.conf +++ b/config/action.d/sendmail-whois-ipmatches.conf @@ -10,6 +10,9 @@ before = sendmail-common.conf [Definition] +# bypass ban/unban for restored tickets +norestored = 1 + # Option: actionban # Notes.: command executed when banning an IP. Take care that the # command is executed with Fail2Ban user rights. diff --git a/config/action.d/sendmail-whois-lines.conf b/config/action.d/sendmail-whois-lines.conf index e1c85928..e3a1c974 100644 --- a/config/action.d/sendmail-whois-lines.conf +++ b/config/action.d/sendmail-whois-lines.conf @@ -11,6 +11,9 @@ before = sendmail-common.conf [Definition] +# bypass ban/unban for restored tickets +norestored = 1 + # Option: actionban # Notes.: command executed when banning an IP. Take care that the # command is executed with Fail2Ban user rights. diff --git a/config/action.d/sendmail-whois-matches.conf b/config/action.d/sendmail-whois-matches.conf index 8bca5937..fc4ba061 100644 --- a/config/action.d/sendmail-whois-matches.conf +++ b/config/action.d/sendmail-whois-matches.conf @@ -10,6 +10,9 @@ before = sendmail-common.conf [Definition] +# bypass ban/unban for restored tickets +norestored = 1 + # Option: actionban # Notes.: command executed when banning an IP. Take care that the # command is executed with Fail2Ban user rights. diff --git a/config/action.d/sendmail-whois.conf b/config/action.d/sendmail-whois.conf index 55b80bc5..b8d99423 100644 --- a/config/action.d/sendmail-whois.conf +++ b/config/action.d/sendmail-whois.conf @@ -10,6 +10,9 @@ before = sendmail-common.conf [Definition] +# bypass ban/unban for restored tickets +norestored = 1 + # Option: actionban # Notes.: command executed when banning an IP. Take care that the # command is executed with Fail2Ban user rights. diff --git a/config/action.d/sendmail.conf b/config/action.d/sendmail.conf index 5f5670c3..62c94439 100644 --- a/config/action.d/sendmail.conf +++ b/config/action.d/sendmail.conf @@ -10,6 +10,9 @@ before = sendmail-common.conf [Definition] +# bypass ban/unban for restored tickets +norestored = 1 + # Option: actionban # Notes.: command executed when banning an IP. Take care that the # command is executed with Fail2Ban user rights. diff --git a/config/action.d/smtp.py b/config/action.d/smtp.py index 2429cf48..aa85b259 100644 --- a/config/action.d/smtp.py +++ b/config/action.d/smtp.py @@ -126,6 +126,9 @@ class SMTPAction(ActionBase): bantime = 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. @@ -211,6 +214,8 @@ class SMTPAction(ActionBase): Dictionary which includes information in relation to the ban. """ + if aInfo.get('restored'): + return aInfo.update(self.message_values) message = "".join([ messages['ban']['head'], diff --git a/config/action.d/xarf-login-attack.conf b/config/action.d/xarf-login-attack.conf index 3ab73817..5274cdaf 100644 --- a/config/action.d/xarf-login-attack.conf +++ b/config/action.d/xarf-login-attack.conf @@ -32,6 +32,9 @@ [Definition] +# bypass ban/unban for restored tickets +norestored = 1 + actionstart = actionstop = diff --git a/fail2ban/client/actionreader.py b/fail2ban/client/actionreader.py index e5025fa3..55ceda93 100644 --- a/fail2ban/client/actionreader.py +++ b/fail2ban/client/actionreader.py @@ -43,10 +43,15 @@ class ActionReader(DefinitionInitConfigReader): "actionrepair": ["string", None], "actionban": ["string", None], "actionunban": ["string", None], + "norestored": ["string", None], } def __init__(self, file_, jailName, initOpts, **kwargs): - self._name = initOpts.get("actname", file_) + actname = initOpts.get("actname") + if actname is None: + actname = file_ + initOpts["actname"] = actname + self._name = actname DefinitionInitConfigReader.__init__( self, file_, jailName, initOpts, **kwargs) @@ -64,16 +69,22 @@ class ActionReader(DefinitionInitConfigReader): return self._name def convert(self): + opts = self.getCombined(ignore=('timeout', 'bantime')) + # type-convert only after combined (otherwise boolean converting prevents substitution): + if opts.get('norestored'): + opts['norestored'] = self._convert_to_boolean(opts['norestored']) + # stream-convert: head = ["set", self._jailName] stream = list() stream.append(head + ["addaction", self._name]) multi = [] - for opt, optval in self._opts.iteritems(): + for opt, optval in opts.iteritems(): if opt in self._configOpts: multi.append([opt, optval]) if self._initOpts: for opt, optval in self._initOpts.iteritems(): - multi.append([opt, optval]) + if opt not in self._configOpts: + multi.append([opt, optval]) if len(multi) > 1: stream.append(["multi-set", self._jailName, "action", self._name, multi]) elif len(multi): diff --git a/fail2ban/client/configreader.py b/fail2ban/client/configreader.py index a72ca1e9..7840cd12 100644 --- a/fail2ban/client/configreader.py +++ b/fail2ban/client/configreader.py @@ -30,6 +30,7 @@ from ConfigParser import NoOptionError, NoSectionError from .configparserinc import sys, SafeConfigParserWithIncludes, logLevel from ..helpers import getLogger +from ..server.action import CommandAction # Gets the instance of the logger. logSys = getLogger(__name__) @@ -319,6 +320,28 @@ class DefinitionInitConfigReader(ConfigReader): self._initOpts['known/'+opt] = v if not opt in self._initOpts: self._initOpts[opt] = v + + def _convert_to_boolean(self, value): + return value.lower() in ("1", "yes", "true", "on") + + def getCombined(self, ignore=()): + combinedopts = self._opts + ignore = set(ignore).copy() + if self._initOpts: + combinedopts = _merge_dicts(self._opts, self._initOpts) + if not len(combinedopts): + return {} + # ignore conditional options: + for n in combinedopts: + cond = SafeConfigParserWithIncludes.CONDITIONAL_RE.match(n) + if cond: + n, cond = cond.groups() + ignore.add(n) + # substiture options already specified direct: + opts = CommandAction.substituteRecursiveTags(combinedopts, ignore=ignore) + if not opts: + raise ValueError('recursive tag definitions unable to be resolved') + return opts def convert(self): raise NotImplementedError diff --git a/fail2ban/client/filterreader.py b/fail2ban/client/filterreader.py index cdf0d8af..d89ef5ad 100644 --- a/fail2ban/client/filterreader.py +++ b/fail2ban/client/filterreader.py @@ -27,8 +27,7 @@ __license__ = "GPL" import os import shlex -from .configreader import DefinitionInitConfigReader, _merge_dicts -from ..server.action import CommandAction +from .configreader import DefinitionInitConfigReader from ..helpers import getLogger # Gets the instance of the logger. @@ -52,17 +51,6 @@ class FilterReader(DefinitionInitConfigReader): def getFile(self): return self.__file - def getCombined(self): - combinedopts = self._opts - if self._initOpts: - combinedopts = _merge_dicts(self._opts, self._initOpts) - if not len(combinedopts): - return {} - opts = CommandAction.substituteRecursiveTags(combinedopts) - if not opts: - raise ValueError('recursive tag definitions unable to be resolved') - return opts - def convert(self): stream = list() opts = self.getCombined() @@ -70,6 +58,7 @@ class FilterReader(DefinitionInitConfigReader): return stream for opt, value in opts.iteritems(): if opt in ("failregex", "ignoreregex"): + if value is None: continue multi = [] for regex in value.split('\n'): # Do not send a command if the rule is empty. @@ -87,6 +76,7 @@ class FilterReader(DefinitionInitConfigReader): stream.append(["set", self._jailName, "datepattern", value]) # Do not send a command if the match is empty. elif opt == 'journalmatch': + if value is None: continue for match in value.split("\n"): if match == '': continue stream.append( diff --git a/fail2ban/client/jailreader.py b/fail2ban/client/jailreader.py index b223532e..f4adadbf 100644 --- a/fail2ban/client/jailreader.py +++ b/fail2ban/client/jailreader.py @@ -136,7 +136,8 @@ class JailReader(ConfigReader): if not filterName: raise JailDefError("Invalid filter definition %r" % flt) self.__filter = FilterReader( - filterName, self.__name, filterOpt, share_config=self.share_config, basedir=self.getBaseDir()) + filterName, self.__name, filterOpt, + share_config=self.share_config, basedir=self.getBaseDir()) ret = self.__filter.read() # merge options from filter as 'known/...': self.__filter.getOptions(self.__opts) diff --git a/fail2ban/server/action.py b/fail2ban/server/action.py index 62aa51dc..46a19cd1 100644 --- a/fail2ban/server/action.py +++ b/fail2ban/server/action.py @@ -219,9 +219,9 @@ class CommandAction(ActionBase): self.timeout = 60 ## Command executed in order to initialize the system. self.actionstart = '' - ## Command executed when an IP address gets banned. + ## Command executed when ticket gets banned. self.actionban = '' - ## Command executed when an IP address gets removed. + ## Command executed when ticket gets removed. self.actionunban = '' ## Command executed in order to check requirements. self.actioncheck = '' @@ -365,7 +365,7 @@ class CommandAction(ActionBase): return self._executeOperation('', 'reloading') @classmethod - def substituteRecursiveTags(cls, inptags, conditional=''): + def substituteRecursiveTags(cls, inptags, conditional='', ignore=()): """Sort out tag definitions within other tags. Since v.0.9.2 supports embedded interpolation (see test cases for examples). @@ -387,21 +387,26 @@ class CommandAction(ActionBase): # copy return tags dict to prevent modifying of inptags: tags = inptags.copy() t = TAG_CRE + ignore = set(ignore) + done = cls._escapedTags.copy() | ignore # repeat substitution while embedded-recursive (repFlag is True) - done = cls._escapedTags.copy() while True: repFlag = False # substitute each value: for tag in tags.iterkeys(): - # ignore escaped or already done: + # ignore escaped or already done (or in ignore list): if tag in done: continue - value = str(tags[tag]) + value = orgval = str(tags[tag]) # search and replace all tags within value, that can be interpolated using other tags: m = t.search(value) refCounts = {} #logSys.log(5, 'TAG: %s, value: %s' % (tag, value)) while m: found_tag = m.group(1) + # don't replace tags that should be currently ignored (pre-replacement): + if found_tag in ignore: + m = t.search(value, m.end()) + continue #logSys.log(5, 'found: %s' % found_tag) if found_tag == tag or refCounts.get(found_tag, 1) > MAX_TAG_REPLACE_COUNT: # recursive definitions are bad @@ -429,7 +434,7 @@ class CommandAction(ActionBase): m = t.search(value, m.start()) #logSys.log(5, 'TAG: %s, newvalue: %s' % (tag, value)) # was substituted? - if tags[tag] != value: + if orgval != value: # check still contains any tag - should be repeated (possible embedded-recursive substitution): if t.search(value): repFlag = True diff --git a/fail2ban/server/actions.py b/fail2ban/server/actions.py index dd4c97c2..5bd00c85 100644 --- a/fail2ban/server/actions.py +++ b/fail2ban/server/actions.py @@ -348,6 +348,8 @@ class Actions(JailThread, Mapping): aInfo["failures"] = bTicket.getAttempt() aInfo["time"] = bTicket.getTime() aInfo["matches"] = "\n".join(bTicket.getMatches()) + # to bypass actions, that should not be executed for restored tickets + aInfo["restored"] = 1 if ticket.restored else 0 if self._jail.database is not None: mi4ip = lambda overalljails=False, self=self, \ mi={'ip':ip, 'ticket':bTicket}: self.__getBansMerged(mi, overalljails) @@ -361,6 +363,8 @@ class Actions(JailThread, Mapping): logSys.notice("[%s] %sBan %s", self._jail.name, ('' if not bTicket.restored else 'Restore '), ip) for name, action in self._actions.iteritems(): try: + if ticket.restored and getattr(action, 'norestored', False): + continue action.ban(aInfo.copy()) except Exception as e: logSys.error( @@ -449,10 +453,14 @@ class Actions(JailThread, Mapping): aInfo["failures"] = ticket.getAttempt() aInfo["time"] = ticket.getTime() aInfo["matches"] = "".join(ticket.getMatches()) + # to bypass actions, that should not be executed for restored tickets + aInfo["restored"] = 1 if ticket.restored else 0 if actions is None: logSys.notice("[%s] Unban %s", self._jail.name, aInfo["ip"]) for name, action in unbactions.iteritems(): try: + if ticket.restored and getattr(action, 'norestored', False): + continue logSys.debug("[%s] action %r: unban %s", self._jail.name, name, aInfo["ip"]) action.unban(aInfo.copy()) except Exception as e: diff --git a/fail2ban/tests/action_d/test_smtp.py b/fail2ban/tests/action_d/test_smtp.py index d9ad0f3a..1dba454c 100644 --- a/fail2ban/tests/action_d/test_smtp.py +++ b/fail2ban/tests/action_d/test_smtp.py @@ -94,16 +94,21 @@ class SMTPActionTest(unittest.TestCase): "Subject: [Fail2Ban] %s: stopped" % self.jail.name in self.smtpd.data) - def testBan(self): + def _testBan(self, restored=False): aInfo = { 'ip': "127.0.0.2", 'failures': 3, 'matches': "Test fail 1\n", 'ipjailmatches': "Test fail 1\nTest Fail2\n", 'ipmatches': "Test fail 1\nTest Fail2\nTest Fail3\n", - } + } + if restored: + aInfo['restored'] = 1 self.action.ban(aInfo) + if restored: # no mail, should raises attribute error: + self.assertRaises(AttributeError, lambda: self.smtpd.mailfrom) + return self.assertEqual(self.smtpd.mailfrom, "fail2ban") self.assertEqual(self.smtpd.rcpttos, ["root"]) subject = "Subject: [Fail2Ban] %s: banned %s" % ( @@ -123,6 +128,12 @@ class SMTPActionTest(unittest.TestCase): self.action.matches = "ipmatches" self.action.ban(aInfo) self.assertIn(aInfo['ipmatches'], self.smtpd.data) + + def testBan(self): + self._testBan() + + def testNOPByRestored(self): + self._testBan(restored=True) def testOptions(self): self.action.start() diff --git a/fail2ban/tests/clientreadertestcase.py b/fail2ban/tests/clientreadertestcase.py index 39622cd0..4a692598 100644 --- a/fail2ban/tests/clientreadertestcase.py +++ b/fail2ban/tests/clientreadertestcase.py @@ -347,7 +347,7 @@ class FilterReaderTest(unittest.TestCase): ['set', 'testcase01', 'addjournalmatch', "FIELD= with spaces ", "+", "AFIELD= with + char and spaces"], ['set', 'testcase01', 'datepattern', "%Y %m %d %H:%M:%S"], - ['set', 'testcase01', 'maxlines', "1"], # Last for overide test + ['set', 'testcase01', 'maxlines', 1], # Last for overide test ] filterReader = FilterReader("testcase01", "testcase01", {}) filterReader.setBaseDir(TEST_FILES_DIR) @@ -517,12 +517,10 @@ class JailsReaderTest(LogCaptureTestCase): ['add', 'brokenaction', 'auto'], ['set', 'brokenaction', 'addfailregex', ''], ['set', 'brokenaction', 'addaction', 'brokenaction'], - ['set', - 'brokenaction', - 'action', - 'brokenaction', - 'actionban', - 'hit with big stick '], + ['multi-set', 'brokenaction', 'action', 'brokenaction', [ + ['actionban', 'hit with big stick '], + ['actname', 'brokenaction'] + ]], ['add', 'parse_to_end_of_jail.conf', 'auto'], ['set', 'parse_to_end_of_jail.conf', 'addfailregex', ''], ['start', 'emptyaction'], @@ -548,7 +546,10 @@ class JailsReaderTest(LogCaptureTestCase): actionName = os.path.basename(actionConfig).replace('.conf', '') actionReader = ActionReader(actionName, "TEST", {}, basedir=CONFIG_DIR) self.assertTrue(actionReader.read()) - actionReader.getOptions({}) # populate _opts + try: + actionReader.getOptions({}) # populate _opts + except Exception as e: # pragma: no cover + self.fail("action %r\n%s: %s" % (actionName, type(e).__name__, e)) if not actionName.endswith('-common'): self.assertIn('Definition', actionReader.sections(), msg="Action file %r is lacking [Definition] section" % actionConfig) diff --git a/fail2ban/tests/fail2banclienttestcase.py b/fail2ban/tests/fail2banclienttestcase.py index e6cc46cb..b8417be5 100644 --- a/fail2ban/tests/fail2banclienttestcase.py +++ b/fail2ban/tests/fail2banclienttestcase.py @@ -755,12 +755,17 @@ class Fail2banServerTest(Fail2banClientServerBase): os.remove(fn) return _write_file(fn, "w", + "[DEFAULT]", + "_exec_once = 0", + "", "[Definition]", - "actionstart = echo '[] %s: ** start'" % actname, start, - "actionreload = echo '[] %s: .. reload'" % actname, reload, - "actionban = echo '[] %s: ++ ban '" % actname, ban, - "actionunban = echo '[] %s: -- unban '" % actname, unban, - "actionstop = echo '[] %s: __ stop'" % actname, stop, + "norestored = %(_exec_once)s", + "restore = ", + "actionstart = echo '[%(name)s] %(actname)s: ** start'", start, + "actionreload = echo '[%(name)s] %(actname)s: .. reload'", reload, + "actionban = echo '[%(name)s] %(actname)s: ++ ban %(restore)s'", ban, + "actionunban = echo '[%(name)s] %(actname)s: -- unban '", unban, + "actionstop = echo '[%(name)s] %(actname)s: __ stop'", stop, ) if unittest.F2B.log_level <= logging.DEBUG: # pragma: no cover _out_file(fn) @@ -777,17 +782,26 @@ class Fail2banServerTest(Fail2banClientServerBase): "", "[test-jail1]", "backend = " + backend, "filter =", "action = ", - " test-action1[name='%(__name__)s']" if 1 in actions else "", - " test-action2[name='%(__name__)s']" if 2 in actions else "", + " test-action1[name='%(__name__)s']" \ + if 1 in actions else "", + " test-action2[name='%(__name__)s', restore='restored: ']" \ + if 2 in actions else "", + " test-action2[name='%(__name__)s', actname=test-action3, _exec_once=1, restore='restored: ']" \ + if 3 in actions else "", "logpath = " + test1log, " " + test2log if 2 in enabled else "", " " + test3log if 2 in enabled else "", "failregex = ^\s*failure (401|403) from ", - " ^\s*error (401|403) from " if 2 in enabled else "", + " ^\s*error (401|403) from " \ + if 2 in enabled else "", "enabled = true" if 1 in enabled else "", "", "[test-jail2]", "backend = " + backend, "filter =", - "action =", + "action = ", + " test-action2[name='%(__name__)s', restore='restored: ']" \ + if 2 in actions else "", + " test-action2[name='%(__name__)s', actname=test-action3, _exec_once=1, restore='restored: ']" \ + if 3 in actions else "", "logpath = " + test2log, "enabled = true" if 2 in enabled else "", ) @@ -798,7 +812,7 @@ class Fail2banServerTest(Fail2banClientServerBase): _write_action_cfg(actname="test-action1") _write_action_cfg(actname="test-action2") - _write_jail_cfg(enabled=[1], actions=[1,2]) + _write_jail_cfg(enabled=[1], actions=[1,2,3]) # append one wrong configured jail: _write_file(pjoin(cfg, "jail.conf"), "a", "", "[broken-jail]", "", "filter = broken-jail-filter", "enabled = true") @@ -821,6 +835,11 @@ class Fail2banServerTest(Fail2banClientServerBase): self.assertLogged( "stdout: '[test-jail1] test-action1: ** start'", "stdout: '[test-jail1] test-action2: ** start'", all=True) + # test restored is 0 (both actions available): + self.assertLogged( + "stdout: '[test-jail1] test-action2: ++ ban 192.0.2.1 restored: 0'", + "stdout: '[test-jail1] test-action3: ++ ban 192.0.2.1 restored: 0'", + all=True, wait=MID_WAITTIME) # broken jail was logged (in client and server log): self.assertLogged( @@ -882,10 +901,10 @@ class Fail2banServerTest(Fail2banClientServerBase): self.assertNotLogged( "stdout: '[test-jail1] test-action1: -- unban 192.0.2.1'") - # don't need both actions anymore: + # don't need action1 anymore: _write_action_cfg(actname="test-action1", allow=False) - _write_action_cfg(actname="test-action2", allow=False) - _write_jail_cfg(actions=[]) + # leave action2 just to test restored interpolation: + _write_jail_cfg(actions=[2,3]) # write new failures: self.pruneLog("[test-phase 2b]") @@ -913,7 +932,8 @@ class Fail2banServerTest(Fail2banClientServerBase): "[test-jail2] Found 192.0.2.2", "[test-jail2] Ban 192.0.2.2", "[test-jail2] Found 192.0.2.3", - "[test-jail2] Ban 192.0.2.3", all=True) + "[test-jail2] Ban 192.0.2.3", + all=True) # rotate logs: _write_file(test1log, "w+") @@ -936,6 +956,20 @@ class Fail2banServerTest(Fail2banClientServerBase): "[test-jail2] Restore Ban 192.0.2.4", "[test-jail2] Restore Ban 192.0.2.8", all=True ) + # test restored is 1 (only test-action2): + self.assertLogged( + "stdout: '[test-jail2] test-action2: ++ ban 192.0.2.4 restored: 1'", + "stdout: '[test-jail2] test-action2: ++ ban 192.0.2.8 restored: 1'", + all=True, wait=MID_WAITTIME) + # test test-action3 not executed at all (norestored check): + self.assertNotLogged( + "stdout: '[test-jail2] test-action3: ++ ban 192.0.2.4 restored: 1'", + "stdout: '[test-jail2] test-action3: ++ ban 192.0.2.8 restored: 1'", + all=True) + + # don't need actions anymore: + _write_action_cfg(actname="test-action2", allow=False) + _write_jail_cfg(actions=[]) # restart jail with unban all: self.pruneLog("[test-phase 2d]")