diff --git a/fail2ban/server/actions.py b/fail2ban/server/actions.py index bae22215..1241c7f5 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 @@ -299,6 +299,7 @@ class Actions(JailThread, Mapping): "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), @@ -396,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 = {} diff --git a/fail2ban/server/banmanager.py b/fail2ban/server/banmanager.py index ee9ecae8..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.wrap(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/observer.py b/fail2ban/server/observer.py index 1a5a79df..92ff8bc6 100644 --- a/fail2ban/server/observer.py +++ b/fail2ban/server/observer.py @@ -377,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 @@ -396,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. @@ -442,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, @@ -466,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 """ + if ticket.restored: # pragma: no cover (normally not resored tickets only) + return try: oldbtime = btime ip = ticket.getIP() logSys.debug("[%s] Observer: ban found %s, %s", jail.name, ip, btime) - # 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: + # 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: @@ -487,15 +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: + # 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, btime - oldbtime - 5), self.prolongBan, ticket, jail) + 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: diff --git a/fail2ban/server/ticket.py b/fail2ban/server/ticket.py index 3dc7aba3..c1a14cb2 100644 --- a/fail2ban/server/ticket.py +++ b/fail2ban/server/ticket.py @@ -106,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; 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/fail2banclienttestcase.py b/fail2ban/tests/fail2banclienttestcase.py index a46c911d..e1832ad8 100644 --- a/fail2ban/tests/fail2banclienttestcase.py +++ b/fail2ban/tests/fail2banclienttestcase.py @@ -1195,8 +1195,8 @@ class Fail2banServerTest(Fail2banClientServerBase): "[DEFAULT]", "", "[Definition]", - "actionban = printf %%s \"[%(name)s] %(actname)s: ++ ban -t : \"", \ - "actionprolong = printf %%s \"[%(name)s] %(actname)s: ++ prolong -t : \"" \ + "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 '", ) @@ -1241,8 +1241,8 @@ class Fail2banServerTest(Fail2banClientServerBase): # wait for ban: _observer_wait_idle() self.assertLogged( - "stdout: '[test-jail1] test-action1: ++ ban 192.0.2.11 -t 300 : ", - "stdout: '[test-jail1] test-action2: ++ ban 192.0.2.11 -t 300 : ", + "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() @@ -1269,8 +1269,8 @@ class Fail2banServerTest(Fail2banClientServerBase): )) # wait for ban: self.assertLogged( - "stdout: '[test-jail1] test-action1: ++ ban 192.0.2.11 -t 300 : ", - "stdout: '[test-jail1] test-action2: ++ ban 192.0.2.11 -t 300 : ", + "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 @@ -1283,5 +1283,5 @@ class Fail2banServerTest(Fail2banClientServerBase): _observer_wait_idle() # wait for prolong: self.assertLogged( - "stdout: '[test-jail1] test-action2: ++ prolong 192.0.2.11 -t 600 : ", + "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)