diff --git a/ChangeLog b/ChangeLog index 1ff60891..03adcabc 100644 --- a/ChangeLog +++ b/ChangeLog @@ -69,9 +69,14 @@ TODO: implementing of options resp. other tasks from PR #1346 - `` - failure identifier (if raw resp. failures without IP address) - `` - PTR reversed representation of IP address - `` - host name of the IP address + - `` - ban count of this offender if known as bad (started by 1 for unknown) + - `` - current ban-time of the ticket (prolongation can be retarded up to 10 sec.) - `` - interpolates to the corresponding filter group capture `...` - `` - fully-qualified name of host (the same as `$(hostname -f)`) - `` - short hostname (the same as `$(uname -n)`) +* Introduced new action command `actionprolong` to prolong ban-time (e. g. set new timeout if expected); + Several actions (like ipset, etc.) rewritten using net logic with `actionprolong`. + Note: because ban-time is dynamic, it was removed from jail.conf as timeout argument (check jail.local). * Allow to use filter options by `fail2ban-regex`, example: fail2ban-regex text.log "sshd[mode=aggressive]" * Samples test case factory extended with filter options - dict in JSON to control diff --git a/config/action.d/firewallcmd-ipset.conf b/config/action.d/firewallcmd-ipset.conf index 69447627..ecbb3bef 100644 --- a/config/action.d/firewallcmd-ipset.conf +++ b/config/action.d/firewallcmd-ipset.conf @@ -18,7 +18,7 @@ before = firewallcmd-common.conf [Definition] -actionstart = ipset create hash:ip timeout +actionstart = ipset create hash:ip firewall-cmd --direct --add-rule filter 0 -p -m multiport --dports -m set --match-set src -j actionstop = firewall-cmd --direct --remove-rule filter 0 -p -m multiport --dports -m set --match-set src -j @@ -27,6 +27,8 @@ actionstop = firewall-cmd --direct --remove-rule filter 0 -p

timeout -exist +actionprolong = %(actionban)s + actionunban = ipset del -exist [Init] @@ -38,12 +40,6 @@ actionunban = ipset del -exist # chain = INPUT_direct -# Option: bantime -# Notes: specifies the bantime in seconds (handled internally rather than by fail2ban) -# Values: [ NUM ] Default: 600 - -bantime = 600 - ipmset = f2b- [Init?family=inet6] diff --git a/config/action.d/iptables-ipset-proto6-allports.conf b/config/action.d/iptables-ipset-proto6-allports.conf index b761ad8c..a0ede56e 100644 --- a/config/action.d/iptables-ipset-proto6-allports.conf +++ b/config/action.d/iptables-ipset-proto6-allports.conf @@ -26,7 +26,7 @@ before = iptables-common.conf # Notes.: command executed once at the start of Fail2Ban. # Values: CMD # -actionstart = ipset create hash:ip timeout +actionstart = ipset create hash:ip -I -m set --match-set src -j # Option: actionflush @@ -51,6 +51,8 @@ actionstop = -D -m set --match-set src -j timeout -exist +actionprolong = %(actionban)s + # Option: actionunban # Notes.: command executed when unbanning an IP. Take care that the # command is executed with Fail2Ban user rights. @@ -61,12 +63,6 @@ actionunban = ipset del -exist [Init] -# Option: bantime -# Notes: specifies the bantime in seconds (handled internally rather than by fail2ban) -# Values: [ NUM ] Default: 600 -# -bantime = 600 - ipmset = f2b- familyopt = diff --git a/config/action.d/iptables-ipset-proto6.conf b/config/action.d/iptables-ipset-proto6.conf index e337eedf..b13eb711 100644 --- a/config/action.d/iptables-ipset-proto6.conf +++ b/config/action.d/iptables-ipset-proto6.conf @@ -26,7 +26,7 @@ before = iptables-common.conf # Notes.: command executed once at the start of Fail2Ban. # Values: CMD # -actionstart = ipset create hash:ip timeout +actionstart = ipset create hash:ip -I -p -m multiport --dports -m set --match-set src -j # Option: actionflush @@ -51,6 +51,8 @@ actionstop = -D -p -m multiport --dports -m # actionban = ipset add timeout -exist +actionprolong = %(actionban)s + # Option: actionunban # Notes.: command executed when unbanning an IP. Take care that the # command is executed with Fail2Ban user rights. @@ -61,12 +63,6 @@ actionunban = ipset del -exist [Init] -# Option: bantime -# Notes: specifies the bantime in seconds (handled internally rather than by fail2ban) -# Values: [ NUM ] Default: 600 -# -bantime = 600 - ipmset = f2b- familyopt = diff --git a/config/action.d/osx-afctl.conf b/config/action.d/osx-afctl.conf index a319fc6b..a75e5723 100644 --- a/config/action.d/osx-afctl.conf +++ b/config/action.d/osx-afctl.conf @@ -12,5 +12,5 @@ actioncheck = actionban = /usr/libexec/afctl -a -t actionunban = /usr/libexec/afctl -r -[Init] -bantime = 2880 +actionprolong = %(actionunban)s && %(actionban)s + diff --git a/config/action.d/shorewall-ipset-proto6.conf b/config/action.d/shorewall-ipset-proto6.conf index 1ebcfb01..8d80460f 100644 --- a/config/action.d/shorewall-ipset-proto6.conf +++ b/config/action.d/shorewall-ipset-proto6.conf @@ -51,7 +51,7 @@ # Values: CMD # actionstart = if ! ipset -quiet -name list f2b- >/dev/null; - then ipset -quiet -exist create f2b- hash:ip timeout ; + then ipset -quiet -exist create f2b- hash:ip; fi # Option: actionstop @@ -68,6 +68,8 @@ actionstop = ipset flush f2b- # actionban = ipset add f2b- timeout -exist +actionprolong = %(actionban)s + # Option: actionunban # Notes.: command executed when unbanning an IP. Take care that the # command is executed with Fail2Ban user rights. @@ -76,10 +78,3 @@ actionban = ipset add f2b- timeout -exist # actionunban = ipset del f2b- -exist -[Init] - -# Option: bantime -# Notes: specifies the bantime in seconds (handled internally rather than by fail2ban) -# Values: [ NUM ] Default: 600 -# -bantime = 600 diff --git a/config/jail.conf b/config/jail.conf index df354bb3..4daacf63 100644 --- a/config/jail.conf +++ b/config/jail.conf @@ -202,22 +202,22 @@ banaction = iptables-multiport banaction_allports = iptables-allports # The simplest action to take: ban only -action_ = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] +action_ = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] # ban & send an e-mail with whois report to the destemail. -action_mw = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] +action_mw = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] %(mta)s-whois[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s"] # ban & send an e-mail with whois report and relevant log lines # to the destemail. -action_mwl = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] +action_mwl = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] %(mta)s-whois-lines[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"] # See the IMPORTANT note in action.d/xarf-login-attack for when to use this action # # ban & send a xarf e-mail to abuse contact of IP address and include relevant log lines # to the destemail. -action_xarf = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] +action_xarf = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] xarf-login-attack[service=%(__name__)s, sender="%(sender)s", logpath=%(logpath)s, port="%(port)s"] # ban IP on CloudFlare & send an e-mail with whois report and relevant log lines diff --git a/fail2ban/client/actionreader.py b/fail2ban/client/actionreader.py index ace0b898..7ba54d31 100644 --- a/fail2ban/client/actionreader.py +++ b/fail2ban/client/actionreader.py @@ -45,6 +45,7 @@ class ActionReader(DefinitionInitConfigReader): "actioncheck": ["string", None], "actionrepair": ["string", None], "actionban": ["string", None], + "actionprolong": ["string", None], "actionunban": ["string", None], "norestored": ["string", None], } diff --git a/fail2ban/server/action.py b/fail2ban/server/action.py index d00458ba..f1983d1e 100644 --- a/fail2ban/server/action.py +++ b/fail2ban/server/action.py @@ -149,7 +149,7 @@ class CallingMap(MutableMapping, object): def __len__(self): return len(self.data) - def copy(self): # pargma: no cover + def copy(self): # pragma: no cover return self.__class__(_merge_copy_dicts(self.data, self.storage)) @@ -224,6 +224,10 @@ class ActionBase(object): """ pass + @property + def _prolongable(self): # pragma: no cover - abstract + return False + def unban(self, aInfo): # pragma: no cover - abstract """Executed when a ban expires. @@ -236,6 +240,11 @@ class ActionBase(object): pass +WRAP_CMD_PARAMS = { + 'timeout': 'str2seconds', + 'bantime': 'ignore', +} + class CommandAction(ActionBase): """A action which executes OS shell commands. @@ -306,7 +315,10 @@ class CommandAction(ActionBase): def __setattr__(self, name, value): if not name.startswith('_') and not self.__init and not callable(value): # special case for some pasrameters: - if name in ('timeout', 'bantime'): + wrp = WRAP_CMD_PARAMS.get(name) + if wrp == 'ignore': # ignore (filter) dynamic parameters + return + elif wrp == 'str2seconds': value = str(MyTime.str2seconds(value)) # parameters changed - clear properties and substitution cache: self.__properties = None @@ -434,6 +446,26 @@ class CommandAction(ActionBase): if not self._processCmd('', aInfo): raise RuntimeError("Error banning %(ip)s" % aInfo) + @property + def _prolongable(self): + return (hasattr(self, 'actionprolong') and self.actionprolong + and not str(self.actionprolong).isspace()) + + def prolong(self, aInfo): + """Executes the "actionprolong" command. + + Replaces the tags in the action command with actions properties + and ban information, and executes the resulting command. + + Parameters + ---------- + aInfo : dict + Dictionary which includes information in relation to + the ban. + """ + if not self._processCmd('', aInfo): + raise RuntimeError("Error prolonging %(ip)s" % aInfo) + def unban(self, aInfo): """Executes the "actionunban" command. @@ -498,8 +530,10 @@ class CommandAction(ActionBase): """ return self._executeOperation('', 'reloading') - @staticmethod - def escapeTag(value): + ESCAPE_CRE = re.compile(r"""[\\#&;`|*?~<>^()\[\]{}$'"\n\r]""") + + @classmethod + def escapeTag(cls, value): """Escape characters which may be used for command injection. Parameters @@ -516,12 +550,15 @@ class CommandAction(ActionBase): ----- The following characters are escaped:: - \\#&;`|*?~<>^()[]{}$'" + \\#&;`|*?~<>^()[]{}$'"\n\r """ - for c in '\\#&;`|*?~<>^()[]{}$\'"': - if c in value: - value = value.replace(c, '\\' + c) + _map2c = {'\n': 'n', '\r': 'r'} + def substChar(m): + c = m.group() + return '\\' + _map2c.get(c, c) + + value = cls.ESCAPE_CRE.sub(substChar, value) return value @classmethod @@ -780,7 +817,8 @@ class CommandAction(ActionBase): RuntimeError If command execution times out. """ - logSys.debug(realCmd) + if logSys.getEffectiveLevel() < logging.DEBUG: # pragma: no cover + logSys.log(9, realCmd) if not realCmd: logSys.debug("Nothing to do") return True diff --git a/fail2ban/server/actions.py b/fail2ban/server/actions.py index f415c8ca..f940bb45 100644 --- a/fail2ban/server/actions.py +++ b/fail2ban/server/actions.py @@ -34,7 +34,7 @@ try: except ImportError: OrderedDict = dict -from .banmanager import BanManager +from .banmanager import BanManager, BanTicket from .ipdns import DNSUtils from .jailthread import JailThread from .action import ActionBase, CommandAction, CallingMap @@ -298,6 +298,8 @@ class Actions(JailThread, Mapping): "fid": lambda self: self.__ticket.getID(), "failures": lambda self: self.__ticket.getAttempt(), "time": lambda self: self.__ticket.getTime(), + "bantime": lambda self: self._getBanTime(), + "bancount": lambda self: self.__ticket.getBanCount(), "matches": lambda self: "\n".join(self.__ticket.getMatches()), # to bypass actions, that should not be executed for restored tickets "restored": lambda self: (1 if self.__ticket.restored else 0), @@ -322,9 +324,14 @@ class Actions(JailThread, Mapping): self.immutable = immutable self.data = data - def copy(self): # pargma: no cover + def copy(self): # pragma: no cover return self.__class__(self.__ticket, self.__jail, self.immutable, self.data.copy()) + def _getBanTime(self): + btime = self.__ticket.getBanTime() + if btime is None: btime = self.__jail.actions.getBanTime() + return btime + def _mi4ip(self, overalljails=False): """Gets bans merged once, a helper for lambda(s), prevents stop of executing action by any exception inside. @@ -390,15 +397,9 @@ class Actions(JailThread, Mapping): ticket = self._jail.getFailTicket() if not ticket: break - bTicket = BanManager.createBanTicket(ticket) - btime = ticket.getBanTime() - if btime is not None: - bTicket.setBanTime(btime) - bTicket.setBanCount(ticket.getBanCount()) - else: - btime = self.__banManager.getBanTime() - if ticket.restored: - bTicket.restored = True + + bTicket = BanTicket.wrap(ticket) + btime = ticket.getBanTime(self.__banManager.getBanTime()) ip = bTicket.getIP() aInfo = self.__getActionInfo(bTicket) reason = {} @@ -445,6 +446,29 @@ class Actions(JailThread, Mapping): self.__banManager.getBanTotal(), self.__banManager.size(), self._jail.name) return cnt + def _prolongBan(self, ticket): + # prevent to prolong ticket that was removed in-between, + # if it in ban list - ban time already prolonged (and it stays there): + if not self.__banManager._inBanList(ticket): return + # do actions : + aInfo = None + for name, action in self._actions.iteritems(): + try: + if ticket.restored and getattr(action, 'norestored', False): + continue + if not action._prolongable: + continue + if aInfo is None: + aInfo = self.__getActionInfo(ticket) + if not aInfo.immutable: aInfo.reset() + action.prolong(aInfo) + except Exception as e: + logSys.error( + "Failed to execute ban jail '%s' action '%s' " + "info '%r': %s", + self._jail.name, name, aInfo, e, + exc_info=logSys.getEffectiveLevel()<=logging.DEBUG) + def __checkUnBan(self): """Check for IP address to unban. diff --git a/fail2ban/server/banmanager.py b/fail2ban/server/banmanager.py index 1d14ae84..0425db51 100644 --- a/fail2ban/server/banmanager.py +++ b/fail2ban/server/banmanager.py @@ -243,21 +243,6 @@ class BanManager: logSys.exception(e) return [] - ## - # Create a ban ticket. - # - # Create a BanTicket from a FailTicket. The timestamp of the BanTicket - # is the current time. This is a static method. - # @param ticket the FailTicket - # @return a BanTicket - - @staticmethod - def createBanTicket(ticket): - # we should always use correct time to calculate correct end time (ban time is variable now, - # + possible double banning by restore from database and from log file) - # so use as lastTime always time from ticket. - return BanTicket(ticket=ticket) - ## # Add a ban ticket. # @@ -291,6 +276,7 @@ class BanManager: # not yet banned - add new one: self.__banList[fid] = ticket self.__banTotal += 1 + ticket.incrBanCount() # correct next unban time: if self.__nextUnbanTime > eob: self.__nextUnbanTime = eob diff --git a/fail2ban/server/failmanager.py b/fail2ban/server/failmanager.py index 1255cf21..6ce9b74e 100644 --- a/fail2ban/server/failmanager.py +++ b/fail2ban/server/failmanager.py @@ -27,7 +27,7 @@ __license__ = "GPL" from threading import Lock import logging -from .ticket import FailTicket +from .ticket import FailTicket, BanTicket from ..helpers import getLogger, BgService # Gets the instance of the logger. @@ -103,13 +103,13 @@ class FailManager: fData.setMatches(matches[-self.maxEntries:]) except KeyError: # not found - already banned - prevent to add failure if comes from observer: - if observed: + if observed or isinstance(ticket, BanTicket): return # if already FailTicket - add it direct, otherwise create (using copy all ticket data): if isinstance(ticket, FailTicket): fData = ticket; else: - fData = FailTicket(ticket=ticket) + fData = FailTicket.wrap(ticket) if count > ticket.getAttempt(): fData.setRetry(count) self.__failList[fid] = fData diff --git a/fail2ban/server/jail.py b/fail2ban/server/jail.py index 33f6db0b..0bb9f6fb 100644 --- a/fail2ban/server/jail.py +++ b/fail2ban/server/jail.py @@ -226,7 +226,7 @@ class Jail(object): if opt == 'increment': if isinstance(value, str): be[opt] = value.lower() in ("yes", "true", "ok", "1") - if be[opt] and self.database is None: + if be.get(opt) and self.database is None: logSys.warning("ban time increment is not available as long jail database is not set") if opt in ['maxtime', 'rndtime']: if not value is None: diff --git a/fail2ban/server/observer.py b/fail2ban/server/observer.py index 14233c55..92ff8bc6 100644 --- a/fail2ban/server/observer.py +++ b/fail2ban/server/observer.py @@ -121,9 +121,28 @@ class ObserverThread(JailThread): def add_timer(self, starttime, *event): """Add a timer event to queue will start (and wake) in 'starttime' seconds """ + # in testing we should wait (looping) for the possible time drifts: + if MyTime.myTime is not None and starttime: + # test time after short sleep: + t = threading.Timer(Utils.DEFAULT_SLEEP_INTERVAL, self._delayedEvent, + (MyTime.time() + starttime, time.time() + starttime, event) + ) + t.start() + return + # add timer event: t = threading.Timer(starttime, self.add, event) t.start() + def _delayedEvent(self, endMyTime, endTime, event): + if MyTime.time() >= endMyTime or time.time() >= endTime: + self.add_timer(0, *event) + return + # repeat after short sleep: + t = threading.Timer(Utils.DEFAULT_SLEEP_INTERVAL, self._delayedEvent, + (endMyTime, endTime, event) + ) + t.start() + def pulse_notify(self): """Notify wakeup (sets /and resets/ notify event) """ @@ -164,8 +183,6 @@ class ObserverThread(JailThread): self.add_named_timer('DB_PURGE', self.__db_purge_interval, 'db_purge') ## Mapping of all possible event types of observer: __meth = { - 'failureFound': self.failureFound, - 'banFound': self.banFound, # universal lambda: 'call': self.call_lambda, # system and service events: @@ -196,7 +213,8 @@ class ObserverThread(JailThread): if ev is None: break ## retrieve method by name - meth = __meth[ev[0]] + meth = ev[0] + if not callable(ev[0]): meth = __meth.get(meth) or getattr(self, meth) ## execute it with rest of event as variable arguments meth(*ev[1:]) except Exception as e: @@ -359,6 +377,7 @@ class ObserverThread(JailThread): db = jail.database if db is not None: for banCount, timeOfBan, lastBanTime in db.getBan(ip, jail): + banCount = max(banCount, ticket.getBanCount()) retryCount = ((1 << (banCount if banCount < 20 else 20))/2 + 1) # if lastBanTime == -1 or timeOfBan + lastBanTime * 2 > MyTime.time(): # retryCount = maxRetry @@ -378,8 +397,8 @@ class ObserverThread(JailThread): (', Ban' if retryCount >= maxRetry else '')) # retryCount-1, because a ticket was already once incremented by filter self retryCount = failManager.addFailure(ticket, retryCount - 1, True) - - # after observe we have increased count >= maxretry ... + ticket.setBanCount(banCount) + # after observe we have increased attempt count, compare it >= maxretry ... if retryCount >= maxRetry: # perform the banning of the IP now (again) # [todo]: this code part will be used multiple times - optimize it later. @@ -424,12 +443,14 @@ class ObserverThread(JailThread): for banCount, timeOfBan, lastBanTime in \ jail.database.getBan(ip, jail, overalljails=be.get('overalljails', False)) \ : + # increment count in ticket (if still not increased from banmanager, test-cases?): + if banCount >= ticket.getBanCount(): + ticket.setBanCount(banCount+1) logSys.debug('IP %s was already banned: %s #, %s', ip, banCount, timeOfBan); - ticket.setBanCount(banCount); # calculate new ban time if banCount > 0: banTime = be['evformula'](self.BanTimeIncr(banTime, banCount)) - ticket.setBanTime(banTime); + ticket.setBanTime(banTime) # check current ticket time to prevent increasing for twice read tickets (restored from log file besides database after restart) if ticket.getTime() > timeOfBan: logSys.info('[%s] IP %s is bad: %s # last %s - incr %s to %s' % (jail.name, ip, banCount, @@ -448,12 +469,14 @@ class ObserverThread(JailThread): Observer will check ip was known (bad) and possibly increase/prolong a ban time Secondary we will actualize the bans and bips (bad ip) in database """ - oldbtime = btime - ip = ticket.getIP() - logSys.debug("[%s] Observer: ban found %s, %s", jail.name, ip, btime) + if ticket.restored: # pragma: no cover (normally not resored tickets only) + return try: - # if not permanent, not restored and ban time was not set - check time should be increased: - if btime != -1 and not ticket.restored and ticket.getBanTime() is None: + oldbtime = btime + ip = ticket.getIP() + logSys.debug("[%s] Observer: ban found %s, %s", jail.name, ip, btime) + # if not permanent and ban time was not set - check time should be increased: + if btime != -1 and ticket.getBanTime() is None: btime = self.incrBanTime(jail, btime, ticket) # if we should prolong ban time: if btime == -1 or btime > oldbtime: @@ -469,12 +492,13 @@ class ObserverThread(JailThread): return False else: logtime = ('permanent', 'infinite') - # increment count: - ticket.incrBanCount() # if ban time was prolonged - log again with new ban time: if btime != oldbtime: logSys.notice("[%s] Increase Ban %s (%d # %s -> %s)", jail.name, ip, ticket.getBanCount(), *logtime) + # delayed prolonging ticket via actions that expected this (not later than 10 sec): + logSys.log(5, "[%s] Observer: prolong %s in %s", jail.name, ip, (btime, oldbtime)) + self.add_timer(min(10, max(0, btime - oldbtime - 5)), self.prolongBan, ticket, jail) # add ticket to database, but only if was not restored (not already read from database): if jail.database is not None and not ticket.restored: # add to database always only after ban time was calculated an not yet already banned: @@ -482,6 +506,21 @@ class ObserverThread(JailThread): except Exception as e: logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG) + def prolongBan(self, ticket, jail): + """ Notify observer a ban occured for ip + + Observer will check ip was known (bad) and possibly increase/prolong a ban time + Secondary we will actualize the bans and bips (bad ip) in database + """ + try: + btime = ticket.getBanTime() + ip = ticket.getIP() + logSys.debug("[%s] Observer: prolong %s, %s", jail.name, ip, btime) + # prolong ticket via actions that expected this: + jail.actions._prolongBan(ticket) + except Exception as e: + logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG) + # Global observer initial created in server (could be later rewriten via singleton) class _Observers: def __init__(self): diff --git a/fail2ban/server/ticket.py b/fail2ban/server/ticket.py index f154ab10..c1a14cb2 100644 --- a/fail2ban/server/ticket.py +++ b/fail2ban/server/ticket.py @@ -33,6 +33,7 @@ logSys = getLogger(__name__) class Ticket(object): + __slots__ = ('_ip', '_flags', '_banCount', '_banTime', '_time', '_data', '_retry', '_lastReset') MAX_TIME = 0X7FFFFFFFFFFF ;# 4461763-th year @@ -59,36 +60,44 @@ class Ticket(object): self._data[k] = v if ticket: # ticket available - copy whole information from ticket: - self.__dict__.update(i for i in ticket.__dict__.iteritems() if i[0] in self.__dict__) + self.update(ticket) + #self.__dict__.update(i for i in ticket.__dict__.iteritems() if i[0] in self.__dict__) def __str__(self): return "%s: ip=%s time=%s bantime=%s bancount=%s #attempts=%d matches=%r" % \ - (self.__class__.__name__.split('.')[-1], self.__ip, self._time, - self._banTime, self._banCount, - self._data['failures'], self._data.get('matches', [])) + (self.__class__.__name__.split('.')[-1], self._ip, self._time, + self._banTime, self._banCount, + self._data['failures'], self._data.get('matches', [])) def __repr__(self): return str(self) def __eq__(self, other): try: - return self.__ip == other.__ip and \ + return self._ip == other._ip and \ round(self._time, 2) == round(other._time, 2) and \ self._data == other._data except AttributeError: return False + def update(self, ticket): + for n in ticket.__slots__: + v = getattr(ticket, n, None) + if v is not None: + setattr(self, n, v) + + def setIP(self, value): # guarantee using IPAddr instead of unicode, str for the IP if isinstance(value, basestring): value = IPAddr(value) - self.__ip = value + self._ip = value def getID(self): - return self._data.get('fid', self.__ip) + return self._data.get('fid', self._ip) def getIP(self): - return self.__ip + return self._ip def setTime(self, value): self._time = value @@ -97,16 +106,17 @@ class Ticket(object): return self._time def setBanTime(self, value): - self._banTime = value; + self._banTime = value def getBanTime(self, defaultBT=None): return (self._banTime if self._banTime is not None else defaultBT) - def setBanCount(self, value): - self._banCount = value; + def setBanCount(self, value, always=False): + if always or value > self._banCount: + self._banCount = value - def incrBanCount(self, value = 1): - self._banCount += value; + def incrBanCount(self, value=1): + self._banCount += value def getBanCount(self): return self._banCount; @@ -204,21 +214,21 @@ class FailTicket(Ticket): def __init__(self, ip=None, time=None, matches=None, data={}, ticket=None): # this class variables: - self.__retry = 0 - self.__lastReset = None + self._retry = 0 + self._lastReset = None # create/copy using default ticket constructor: Ticket.__init__(self, ip, time, matches, data, ticket) # init: if ticket is None: - self.__lastReset = time if time is not None else self.getTime() - if not self.__retry: - self.__retry = self._data['failures']; + self._lastReset = time if time is not None else self.getTime() + if not self._retry: + self._retry = self._data['failures']; def setRetry(self, value): """ Set artificial retry count, normally equal failures / attempt, used in incremental features (BanTimeIncr) to increase retry count for bad IPs """ - self.__retry = value + self._retry = value if not self._data['failures']: self._data['failures'] = 1 if not value: @@ -229,10 +239,10 @@ class FailTicket(Ticket): """ Returns failures / attempt count or artificial retry count increased for bad IPs """ - return max(self.__retry, self._data['failures']) + return max(self._retry, self._data['failures']) def inc(self, matches=None, attempt=1, count=1): - self.__retry += count + self._retry += count self._data['failures'] += attempt if matches: # we should duplicate "matches", because possibly referenced to multiple tickets: @@ -249,15 +259,24 @@ class FailTicket(Ticket): return self._time def getLastReset(self): - return self.__lastReset + return self._lastReset def setLastReset(self, value): - self.__lastReset = value + self._lastReset = value + + @staticmethod + def wrap(o): + o.__class__ = FailTicket + return o ## # Ban Ticket. # # This class extends the Ticket class. It is mainly used by the BanManager. -class BanTicket(Ticket): - pass +class BanTicket(FailTicket): + + @staticmethod + def wrap(o): + o.__class__ = BanTicket + return o diff --git a/fail2ban/server/utils.py b/fail2ban/server/utils.py index 58363ff0..613f623d 100644 --- a/fail2ban/server/utils.py +++ b/fail2ban/server/utils.py @@ -102,7 +102,7 @@ class Utils(): def unset(self, k): try: del self._cache[k] - except KeyError: # pragme: no cover + except KeyError: # pragma: no cover pass @@ -330,7 +330,7 @@ class Utils(): return e.errno == errno.EPERM else: return True - else: # pragma : no cover (no windows currently supported) + else: # pragma: no cover (no windows currently supported) @staticmethod def pid_exists(pid): import ctypes diff --git a/fail2ban/tests/actiontestcase.py b/fail2ban/tests/actiontestcase.py index cbd0aaca..47b266fd 100644 --- a/fail2ban/tests/actiontestcase.py +++ b/fail2ban/tests/actiontestcase.py @@ -206,15 +206,15 @@ class CommandActionTest(LogCaptureTestCase): self.assertEqual( self.__action.replaceTag("", {'matches': "some >char< should \< be[ escap}ed&\n"}), - "some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\n") + "some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\\n") self.assertEqual( self.__action.replaceTag("", {'ipmatches': "some >char< should \< be[ escap}ed&\n"}), - "some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\n") + "some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\\n") self.assertEqual( self.__action.replaceTag("", - {'ipjailmatches': "some >char< should \< be[ escap}ed&\n"}), - "some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\n") + {'ipjailmatches': "some >char< should \< be[ escap}ed&\r\n"}), + "some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\\r\\n") # Recursive aInfo["ABC"] = "" diff --git a/fail2ban/tests/banmanagertestcase.py b/fail2ban/tests/banmanagertestcase.py index 2436babf..1abad7d6 100644 --- a/fail2ban/tests/banmanagertestcase.py +++ b/fail2ban/tests/banmanagertestcase.py @@ -100,21 +100,22 @@ class AddFailure(unittest.TestCase): def testBanTimeIncr(self): ticket = BanTicket(self.__ticket.getIP(), self.__ticket.getTime()) - ## increase twice and at end permanent: + ## increase twice and at end permanent, check time/count increase: + c = 0 for i in (1000, 2000, -1): - self.__banManager.addBanTicket(self.__ticket) + self.__banManager.addBanTicket(self.__ticket); c += 1 ticket.setBanTime(i) - self.assertFalse(self.__banManager.addBanTicket(ticket)) + self.assertFalse(self.__banManager.addBanTicket(ticket)); # no incr of c (already banned) self.assertEqual(str(self.__banManager.getTicketByID(ticket.getIP())), - "BanTicket: ip=%s time=%s bantime=%s bancount=0 #attempts=0 matches=[]" % (ticket.getIP(), ticket.getTime(), i)) + "BanTicket: ip=%s time=%s bantime=%s bancount=%s #attempts=0 matches=[]" % (ticket.getIP(), ticket.getTime(), i, c)) ## after permanent, it should remain permanent ban time (-1): - self.__banManager.addBanTicket(self.__ticket) + self.__banManager.addBanTicket(self.__ticket); c += 1 ticket.setBanTime(-1) - self.assertFalse(self.__banManager.addBanTicket(ticket)) + self.assertFalse(self.__banManager.addBanTicket(ticket)); # no incr of c (already banned) ticket.setBanTime(1000) - self.assertFalse(self.__banManager.addBanTicket(ticket)) + self.assertFalse(self.__banManager.addBanTicket(ticket)); # no incr of c (already banned) self.assertEqual(str(self.__banManager.getTicketByID(ticket.getIP())), - "BanTicket: ip=%s time=%s bantime=%s bancount=0 #attempts=0 matches=[]" % (ticket.getIP(), ticket.getTime(), -1)) + "BanTicket: ip=%s time=%s bantime=%s bancount=%s #attempts=0 matches=[]" % (ticket.getIP(), ticket.getTime(), -1, c)) def testUnban(self): btime = self.__banManager.getBanTime() diff --git a/fail2ban/tests/clientreadertestcase.py b/fail2ban/tests/clientreadertestcase.py index bfa68e03..b974a6f5 100644 --- a/fail2ban/tests/clientreadertestcase.py +++ b/fail2ban/tests/clientreadertestcase.py @@ -566,8 +566,6 @@ class JailsReaderTest(LogCaptureTestCase): # all must have some actionban defined self.assertTrue(actionReader._opts.get('actionban', '').strip(), msg="Action file %r is lacking actionban" % actionConfig) - self.assertIn('Init', actionReader.sections(), - msg="Action file %r is lacking [Init] section" % actionConfig) def testReadStockJailConf(self): jails = JailsReader(basedir=CONFIG_DIR, share_config=CONFIG_DIR_SHARE_CFG) # we are running tests from root project dir atm diff --git a/fail2ban/tests/fail2banclienttestcase.py b/fail2ban/tests/fail2banclienttestcase.py index 10ba0572..e1832ad8 100644 --- a/fail2ban/tests/fail2banclienttestcase.py +++ b/fail2ban/tests/fail2banclienttestcase.py @@ -43,7 +43,8 @@ from .. import protocol from ..server import server from ..server.mytime import MyTime from ..server.utils import Utils -from .utils import LogCaptureTestCase, logSys as DefLogSys, with_tmpdir, shutil, logging +from .utils import LogCaptureTestCase, logSys as DefLogSys, with_tmpdir, shutil, logging, \ + TEST_NOW, tearDownMyTime from ..helpers import getLogger @@ -80,6 +81,11 @@ fail2banclient.output = \ fail2banserver.output = \ protocol.output = _test_output +def _time_shift(shift): + # jump to the future (+shift minutes): + logSys.debug("===>>> time shift + %s min", shift) + MyTime.setTime(MyTime.time() + shift*60) + Observers = server.Observers @@ -89,6 +95,22 @@ def _observer_wait_idle(): Observers.Main.wait_empty(MID_WAITTIME) Observers.Main.wait_idle(MID_WAITTIME / 5) +def _observer_wait_before_incrban(cond, timeout=MID_WAITTIME): + """Helper to block observer before increase bantime until some condition gets true""" + if Observers.Main is not None: + # switch ban handler: + _obs_banFound = Observers.Main.banFound + def _banFound(*args, **kwargs): + # restore original handler: + Observers.Main.banFound = _obs_banFound + # wait for: + logSys.debug(' [Observer::banFound] *** observer blocked for test') + Utils.wait_for(cond, timeout) + logSys.debug(' [Observer::banFound] +++ observer runs again') + # original banFound: + _obs_banFound(*args, **kwargs) + Observers.Main.banFound = _banFound + # # Mocking .exit so we could test its correct operation. # Two custom exceptions will be assessed to be raised in the tests @@ -317,6 +339,7 @@ def with_foreground_server_thread(startextra={}): # so don't kill (same process) - if success, just wait for end of worker: if phase.get('end', None): th.join() + tearDownMyTime() return wrapper return _deco_wrapper @@ -343,6 +366,7 @@ class Fail2banClientServerBase(LogCaptureTestCase): server.DEF_LOGTARGET = SRV_DEF_LOGTARGET server.DEF_LOGLEVEL = SRV_DEF_LOGLEVEL LogCaptureTestCase.tearDown(self) + tearDownMyTime() @staticmethod def _test_exit(code=0): @@ -1158,3 +1182,106 @@ class Fail2banServerTest(Fail2banClientServerBase): self.assertLogged( "Jail 'test-jail1' stopped", "Jail 'test-jail1' started", all=True) + + @with_foreground_server_thread() + def testServerObserver(self, tmp, startparams): + cfg = pjoin(tmp, "config") + test1log = pjoin(tmp, "test1.log") + + os.mkdir(pjoin(cfg, "action.d")) + def _write_action_cfg(actname="test-action1", prolong=True): + fn = pjoin(cfg, "action.d", "%s.conf" % actname) + _write_file(fn, "w", + "[DEFAULT]", + "", + "[Definition]", + "actionban = printf %%s \"[%(name)s] %(actname)s: ++ ban -c -t : \"", \ + "actionprolong = printf %%s \"[%(name)s] %(actname)s: ++ prolong -c -t : \"" \ + if prolong else "", + "actionunban = printf %%b '[%(name)s] %(actname)s: -- unban '", + ) + if unittest.F2B.log_level <= logging.DEBUG: # pragma: no cover + _out_file(fn) + + def _write_jail_cfg(backend="polling"): + _write_file(pjoin(cfg, "jail.conf"), "w", + "[INCLUDES]", "", + "[DEFAULT]", "", + "usedns = no", + "maxretry = 3", + "findtime = 1m", + "bantime = 5m", + "bantime.increment = true", + "datepattern = {^LN-BEG}EPOCH", + "", + "[test-jail1]", "backend = " + backend, "filter =", + "action = test-action1[name='%(__name__)s']", + " test-action2[name='%(__name__)s']", + "logpath = " + test1log, + "failregex = ^\s*failure 401|403 from :\s*.*$", + "enabled = true", + "", + ) + if unittest.F2B.log_level <= logging.DEBUG: # pragma: no cover + _out_file(pjoin(cfg, "jail.conf")) + + # create test config: + _write_action_cfg(actname="test-action1", prolong=False) + _write_action_cfg(actname="test-action2", prolong=True) + _write_jail_cfg() + + _write_file(test1log, "w") + # initial start: + self.pruneLog("[test-phase 0) time-0]") + self.execSuccess(startparams, "reload") + # generate bad ip: + _write_file(test1log, "w+", *( + (str(int(MyTime.time())) + " failure 401 from 192.0.2.11: I'm bad \"hacker\" `` $(echo test)",) * 3 + )) + # wait for ban: + _observer_wait_idle() + self.assertLogged( + "stdout: '[test-jail1] test-action1: ++ ban 192.0.2.11 -c 1 -t 300 : ", + "stdout: '[test-jail1] test-action2: ++ ban 192.0.2.11 -c 1 -t 300 : ", + all=True, wait=MID_WAITTIME) + # wait for observer idle (write all tickets to db): + _observer_wait_idle() + + self.pruneLog("[test-phase 1) time+10m]") + # jump to the future (+10 minutes): + _time_shift(10) + _observer_wait_idle() + self.assertLogged( + "stdout: '[test-jail1] test-action1: -- unban 192.0.2.11", + "stdout: '[test-jail1] test-action2: -- unban 192.0.2.11", + "0 ticket(s) in 'test-jail1'", + all=True, wait=MID_WAITTIME) + _observer_wait_idle() + + self.pruneLog("[test-phase 2) time+10m]") + # following tests are time-related - observer can prolong ticket (increase ban-time) + # before banning, so block it here before banFound called, prolong case later: + wakeObs = False + _observer_wait_before_incrban(lambda: wakeObs) + # write again (IP already bad): + _write_file(test1log, "w+", *( + (str(int(MyTime.time())) + " failure 401 from 192.0.2.11: I'm very bad \"hacker\" `` $(echo test)",) * 2 + )) + # wait for ban: + self.assertLogged( + "stdout: '[test-jail1] test-action1: ++ ban 192.0.2.11 -c 2 -t 300 : ", + "stdout: '[test-jail1] test-action2: ++ ban 192.0.2.11 -c 2 -t 300 : ", + all=True, wait=MID_WAITTIME) + # unblock observer here and wait it is done: + wakeObs = True + _observer_wait_idle() + + self.pruneLog("[test-phase 2) time+11m]") + # jump to the future (+1 minute): + _time_shift(1) + # wait for observer idle (write all tickets to db): + _observer_wait_idle() + # wait for prolong: + self.assertLogged( + "stdout: '[test-jail1] test-action2: ++ prolong 192.0.2.11 -c 2 -t 600 : ", + all=True, wait=MID_WAITTIME) diff --git a/fail2ban/tests/observertestcase.py b/fail2ban/tests/observertestcase.py index 40148b1f..80e2e2b7 100644 --- a/fail2ban/tests/observertestcase.py +++ b/fail2ban/tests/observertestcase.py @@ -31,9 +31,8 @@ import tempfile import time from ..server.mytime import MyTime -from ..server.ticket import FailTicket +from ..server.ticket import FailTicket, BanTicket from ..server.failmanager import FailManager -from ..server.banmanager import BanManager from ..server.observer import Observers, ObserverThread from ..server.utils import Utils from .utils import LogCaptureTestCase @@ -246,7 +245,6 @@ class BanTimeIncrDB(unittest.TestCase): # incr time and ban a ticket again : ticket.setTime(stime + 15) self.assertEqual(self.incrBanTime(ticket, 10), 20) - ticket.incrBanCount() self.db.addBan(jail, ticket) # get a ticket already banned in this jail: self.assertEqual( @@ -292,7 +290,6 @@ class BanTimeIncrDB(unittest.TestCase): ticket.setTime(stime + lastBanTime + 5) banTime = self.incrBanTime(ticket, 10) self.assertEqual(banTime, lastBanTime * 2) - ticket.incrBanCount() self.db.addBan(jail, ticket) lastBanTime = banTime # increase again, but the last multiplier reached (time not increased): @@ -300,7 +297,6 @@ class BanTimeIncrDB(unittest.TestCase): banTime = self.incrBanTime(ticket, 10) self.assertNotEqual(banTime, lastBanTime * 2) self.assertEqual(banTime, lastBanTime) - ticket.incrBanCount() self.db.addBan(jail, ticket) lastBanTime = banTime # add two tickets from yesterday: one unbanned (bantime already out-dated): @@ -500,7 +496,7 @@ class BanTimeIncrDB(unittest.TestCase): # wrap FailTicket to BanTicket: failticket2 = ticket2 - ticket2 = BanManager.createBanTicket(failticket2) + ticket2 = BanTicket.wrap(failticket2) self.assertEqual(ticket2, failticket2) # add this ticket to ban (use observer only without ban manager): obs.add('banFound', ticket2, jail, 10) diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index c98d6ee2..becf939c 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -1065,8 +1065,20 @@ class ServerConfigReaderTests(LogCaptureTestCase): logSys.debug(l) return True + def _testActionInfos(self): + if not hasattr(self, '__aInfos'): + dmyjail = DummyJail() + self.__aInfos = {} + for t, ip in (('ipv4', '192.0.2.1'), ('ipv6', '2001:DB8::')): + ticket = BanTicket(ip) + ticket.setBanTime(600) + self.__aInfos[t] = _actions.Actions.ActionInfo(ticket, dmyjail) + return self.__aInfos + def _testExecActions(self, server): jails = server._Server__jails + + aInfos = self._testActionInfos() for jail in jails: # print(jail, jails[jail]) for a in jails[jail].actions: @@ -1083,16 +1095,16 @@ class ServerConfigReaderTests(LogCaptureTestCase): action.start() # test ban ip4 : logSys.debug('# === ban-ipv4 ==='); self.pruneLog() - action.ban({'ip': IPAddr('192.0.2.1'), 'family': 'inet4'}) + action.ban(aInfos['ipv4']) # test unban ip4 : logSys.debug('# === unban ipv4 ==='); self.pruneLog() - action.unban({'ip': IPAddr('192.0.2.1'), 'family': 'inet4'}) + action.unban(aInfos['ipv4']) # test ban ip6 : logSys.debug('# === ban ipv6 ==='); self.pruneLog() - action.ban({'ip': IPAddr('2001:DB8::'), 'family': 'inet6'}) + action.ban(aInfos['ipv6']) # test unban ip6 : logSys.debug('# === unban ipv6 ==='); self.pruneLog() - action.unban({'ip': IPAddr('2001:DB8::'), 'family': 'inet6'}) + action.unban(aInfos['ipv6']) # test stop : logSys.debug('# === stop ==='); self.pruneLog() action.stop() @@ -1310,11 +1322,11 @@ class ServerConfigReaderTests(LogCaptureTestCase): ('j-w-iptables-ipset', 'iptables-ipset-proto6[name=%(__name__)s, bantime="10m", port="http", protocol="tcp", chain="INPUT"]', { 'ip4': (' f2b-j-w-iptables-ipset ',), 'ip6': (' f2b-j-w-iptables-ipset6 ',), 'ip4-start': ( - "`ipset create f2b-j-w-iptables-ipset hash:ip timeout 600`", + "`ipset create f2b-j-w-iptables-ipset hash:ip`", "`iptables -w -I INPUT -p tcp -m multiport --dports http -m set --match-set f2b-j-w-iptables-ipset src -j REJECT --reject-with icmp-port-unreachable`", ), 'ip6-start': ( - "`ipset create f2b-j-w-iptables-ipset6 hash:ip timeout 600 family inet6`", + "`ipset create f2b-j-w-iptables-ipset6 hash:ip family inet6`", "`ip6tables -w -I INPUT -p tcp -m multiport --dports http -m set --match-set f2b-j-w-iptables-ipset6 src -j REJECT --reject-with icmp6-port-unreachable`", ), 'flush': ( @@ -1348,11 +1360,11 @@ class ServerConfigReaderTests(LogCaptureTestCase): ('j-w-iptables-ipset-ap', 'iptables-ipset-proto6-allports[name=%(__name__)s, bantime="10m", chain="INPUT"]', { 'ip4': (' f2b-j-w-iptables-ipset-ap ',), 'ip6': (' f2b-j-w-iptables-ipset-ap6 ',), 'ip4-start': ( - "`ipset create f2b-j-w-iptables-ipset-ap hash:ip timeout 600`", + "`ipset create f2b-j-w-iptables-ipset-ap hash:ip`", "`iptables -w -I INPUT -m set --match-set f2b-j-w-iptables-ipset-ap src -j REJECT --reject-with icmp-port-unreachable`", ), 'ip6-start': ( - "`ipset create f2b-j-w-iptables-ipset-ap6 hash:ip timeout 600 family inet6`", + "`ipset create f2b-j-w-iptables-ipset-ap6 hash:ip family inet6`", "`ip6tables -w -I INPUT -m set --match-set f2b-j-w-iptables-ipset-ap6 src -j REJECT --reject-with icmp6-port-unreachable`", ), 'flush': ( @@ -1646,11 +1658,11 @@ class ServerConfigReaderTests(LogCaptureTestCase): ('j-w-fwcmd-ipset', 'firewallcmd-ipset[name=%(__name__)s, bantime="10m", port="http", protocol="tcp", chain="INPUT"]', { 'ip4': (' f2b-j-w-fwcmd-ipset ',), 'ip6': (' f2b-j-w-fwcmd-ipset6 ',), 'ip4-start': ( - "`ipset create f2b-j-w-fwcmd-ipset hash:ip timeout 600`", + "`ipset create f2b-j-w-fwcmd-ipset hash:ip`", "`firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -p tcp -m multiport --dports http -m set --match-set f2b-j-w-fwcmd-ipset src -j REJECT --reject-with icmp-port-unreachable`", ), 'ip6-start': ( - "`ipset create f2b-j-w-fwcmd-ipset6 hash:ip timeout 600`", + "`ipset create f2b-j-w-fwcmd-ipset6 hash:ip`", "`firewall-cmd --direct --add-rule ipv6 filter INPUT 0 -p tcp -m multiport --dports http -m set --match-set f2b-j-w-fwcmd-ipset6 src -j REJECT --reject-with icmp6-port-unreachable`", ), 'stop': ( @@ -1695,10 +1707,7 @@ class ServerConfigReaderTests(LogCaptureTestCase): jails = server._Server__jails - tickets = { - 'ip4': BanTicket('192.0.2.1'), - 'ip6': BanTicket('2001:DB8::'), - } + aInfos = self._testActionInfos() for jail, act, tests in testJailsActions: # print(jail, jails[jail]) for a in jails[jail].actions: @@ -1716,32 +1725,28 @@ class ServerConfigReaderTests(LogCaptureTestCase): self.assertLogged(*tests['start'], all=True) else: self.assertNotLogged(*tests['ip4-start']+tests['ip6-start'], all=True) - ainfo = { - 'ip4': _actions.Actions.ActionInfo(tickets['ip4'], jails[jail]), - 'ip6': _actions.Actions.ActionInfo(tickets['ip6'], jails[jail]), - } # test ban ip4 : self.pruneLog('# === ban-ipv4 ===') - action.ban(ainfo['ip4']) + action.ban(aInfos['ipv4']) if tests.get('ip4-start'): self.assertLogged(*tests['ip4-start'], all=True) if tests.get('ip6-start'): self.assertNotLogged(*tests['ip6-start'], all=True) self.assertLogged(*tests['ip4-check']+tests['ip4-ban'], all=True) self.assertNotLogged(*tests['ip6'], all=True) # test unban ip4 : self.pruneLog('# === unban ipv4 ===') - action.unban(ainfo['ip4']) + action.unban(aInfos['ipv4']) self.assertLogged(*tests['ip4-check']+tests['ip4-unban'], all=True) self.assertNotLogged(*tests['ip6'], all=True) # test ban ip6 : self.pruneLog('# === ban ipv6 ===') - action.ban(ainfo['ip6']) + action.ban(aInfos['ipv6']) if tests.get('ip6-start'): self.assertLogged(*tests['ip6-start'], all=True) if tests.get('ip4-start'): self.assertNotLogged(*tests['ip4-start'], all=True) self.assertLogged(*tests['ip6-check']+tests['ip6-ban'], all=True) self.assertNotLogged(*tests['ip4'], all=True) # test unban ip6 : self.pruneLog('# === unban ipv6 ===') - action.unban(ainfo['ip6']) + action.unban(aInfos['ipv6']) self.assertLogged(*tests['ip6-check']+tests['ip6-unban'], all=True) self.assertNotLogged(*tests['ip4'], all=True) # test flush for actions should supported this: