mirror of https://github.com/fail2ban/fail2ban
failmanager, ticket: avoid reset of retry count by pause between attempts near to findTime - adjust time of ticket will now change current attempts considering findTime as an estimation from rate by previous known interval (if it exceeds the findTime);
this should avoid some false positives as well as provide more safe handling around `maxretry/findtime` relation especially on busy circumstances.pull/2651/head
parent
4766547e1f
commit
6281dc3633
|
@ -92,10 +92,7 @@ class FailManager:
|
||||||
if attempt <= 0:
|
if attempt <= 0:
|
||||||
attempt += 1
|
attempt += 1
|
||||||
unixTime = ticket.getTime()
|
unixTime = ticket.getTime()
|
||||||
fData.setLastTime(unixTime)
|
fData.adjustTime(unixTime, self.__maxTime)
|
||||||
if fData.getLastReset() < unixTime - self.__maxTime:
|
|
||||||
fData.setLastReset(unixTime)
|
|
||||||
fData.setRetry(0)
|
|
||||||
fData.inc(matches, attempt, count)
|
fData.inc(matches, attempt, count)
|
||||||
# truncate to maxMatches:
|
# truncate to maxMatches:
|
||||||
if self.maxMatches:
|
if self.maxMatches:
|
||||||
|
@ -136,7 +133,7 @@ class FailManager:
|
||||||
def cleanup(self, time):
|
def cleanup(self, time):
|
||||||
with self.__lock:
|
with self.__lock:
|
||||||
todelete = [fid for fid,item in self.__failList.iteritems() \
|
todelete = [fid for fid,item in self.__failList.iteritems() \
|
||||||
if item.getLastTime() + self.__maxTime <= time]
|
if item.getTime() + self.__maxTime <= time]
|
||||||
if len(todelete) == len(self.__failList):
|
if len(todelete) == len(self.__failList):
|
||||||
# remove all:
|
# remove all:
|
||||||
self.__failList = dict()
|
self.__failList = dict()
|
||||||
|
@ -150,7 +147,7 @@ class FailManager:
|
||||||
else:
|
else:
|
||||||
# create new dictionary without items to be deleted:
|
# create new dictionary without items to be deleted:
|
||||||
self.__failList = dict((fid,item) for fid,item in self.__failList.iteritems() \
|
self.__failList = dict((fid,item) for fid,item in self.__failList.iteritems() \
|
||||||
if item.getLastTime() + self.__maxTime > time)
|
if item.getTime() + self.__maxTime > time)
|
||||||
self.__bgSvc.service()
|
self.__bgSvc.service()
|
||||||
|
|
||||||
def delFailure(self, fid):
|
def delFailure(self, fid):
|
||||||
|
|
|
@ -218,21 +218,20 @@ class FailTicket(Ticket):
|
||||||
|
|
||||||
def __init__(self, ip=None, time=None, matches=None, data={}, ticket=None):
|
def __init__(self, ip=None, time=None, matches=None, data={}, ticket=None):
|
||||||
# this class variables:
|
# this class variables:
|
||||||
self.__retry = 0
|
self._firstTime = None
|
||||||
self.__lastReset = None
|
self._retry = 1
|
||||||
# create/copy using default ticket constructor:
|
# create/copy using default ticket constructor:
|
||||||
Ticket.__init__(self, ip, time, matches, data, ticket)
|
Ticket.__init__(self, ip, time, matches, data, ticket)
|
||||||
# init:
|
# init:
|
||||||
if ticket is None:
|
if not isinstance(ticket, FailTicket):
|
||||||
self.__lastReset = time if time is not None else self.getTime()
|
self._firstTime = time if time is not None else self.getTime()
|
||||||
if not self.__retry:
|
self._retry = self._data.get('failures', 1)
|
||||||
self.__retry = self._data['failures'];
|
|
||||||
|
|
||||||
def setRetry(self, value):
|
def setRetry(self, value):
|
||||||
""" Set artificial retry count, normally equal failures / attempt,
|
""" Set artificial retry count, normally equal failures / attempt,
|
||||||
used in incremental features (BanTimeIncr) to increase retry count for bad IPs
|
used in incremental features (BanTimeIncr) to increase retry count for bad IPs
|
||||||
"""
|
"""
|
||||||
self.__retry = value
|
self._retry = value
|
||||||
if not self._data['failures']:
|
if not self._data['failures']:
|
||||||
self._data['failures'] = 1
|
self._data['failures'] = 1
|
||||||
if not value:
|
if not value:
|
||||||
|
@ -243,10 +242,23 @@ class FailTicket(Ticket):
|
||||||
""" Returns failures / attempt count or
|
""" Returns failures / attempt count or
|
||||||
artificial retry count increased for bad IPs
|
artificial retry count increased for bad IPs
|
||||||
"""
|
"""
|
||||||
return max(self.__retry, self._data['failures'])
|
return self._retry
|
||||||
|
|
||||||
|
def adjustTime(self, time, maxTime):
|
||||||
|
""" Adjust time of ticket and current attempts count considering given maxTime
|
||||||
|
as estimation from rate by previous known interval (if it exceeds the findTime)
|
||||||
|
"""
|
||||||
|
if time > self._time:
|
||||||
|
# expand current interval and attemps count (considering maxTime):
|
||||||
|
if self._firstTime < time - maxTime:
|
||||||
|
# adjust retry calculated as estimation from rate by previous known interval:
|
||||||
|
self._retry = int(round(self._retry / float(time - self._firstTime) * maxTime))
|
||||||
|
self._firstTime = time - maxTime
|
||||||
|
# last time of failure:
|
||||||
|
self._time = time
|
||||||
|
|
||||||
def inc(self, matches=None, attempt=1, count=1):
|
def inc(self, matches=None, attempt=1, count=1):
|
||||||
self.__retry += count
|
self._retry += count
|
||||||
self._data['failures'] += attempt
|
self._data['failures'] += attempt
|
||||||
if matches:
|
if matches:
|
||||||
# we should duplicate "matches", because possibly referenced to multiple tickets:
|
# we should duplicate "matches", because possibly referenced to multiple tickets:
|
||||||
|
@ -255,19 +267,6 @@ class FailTicket(Ticket):
|
||||||
else:
|
else:
|
||||||
self._data['matches'] = matches
|
self._data['matches'] = matches
|
||||||
|
|
||||||
def setLastTime(self, value):
|
|
||||||
if value > self._time:
|
|
||||||
self._time = value
|
|
||||||
|
|
||||||
def getLastTime(self):
|
|
||||||
return self._time
|
|
||||||
|
|
||||||
def getLastReset(self):
|
|
||||||
return self.__lastReset
|
|
||||||
|
|
||||||
def setLastReset(self, value):
|
|
||||||
self.__lastReset = value
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Ban Ticket.
|
# Ban Ticket.
|
||||||
#
|
#
|
||||||
|
|
|
@ -69,10 +69,10 @@ class TicketTests(unittest.TestCase):
|
||||||
self.assertEqual(ft.getTime(), tm)
|
self.assertEqual(ft.getTime(), tm)
|
||||||
self.assertEqual(ft.getMatches(), matches2)
|
self.assertEqual(ft.getMatches(), matches2)
|
||||||
ft.setAttempt(2)
|
ft.setAttempt(2)
|
||||||
self.assertEqual(ft.getAttempt(), 2)
|
|
||||||
# retry is max of set retry and failures:
|
|
||||||
self.assertEqual(ft.getRetry(), 2)
|
|
||||||
ft.setRetry(1)
|
ft.setRetry(1)
|
||||||
|
self.assertEqual(ft.getAttempt(), 2)
|
||||||
|
self.assertEqual(ft.getRetry(), 1)
|
||||||
|
ft.setRetry(2)
|
||||||
self.assertEqual(ft.getRetry(), 2)
|
self.assertEqual(ft.getRetry(), 2)
|
||||||
ft.setRetry(3)
|
ft.setRetry(3)
|
||||||
self.assertEqual(ft.getRetry(), 3)
|
self.assertEqual(ft.getRetry(), 3)
|
||||||
|
@ -86,13 +86,21 @@ class TicketTests(unittest.TestCase):
|
||||||
self.assertEqual(ft.getRetry(), 14)
|
self.assertEqual(ft.getRetry(), 14)
|
||||||
self.assertEqual(ft.getMatches(), matches3)
|
self.assertEqual(ft.getMatches(), matches3)
|
||||||
# last time (ignore if smaller as time):
|
# last time (ignore if smaller as time):
|
||||||
self.assertEqual(ft.getLastTime(), tm)
|
|
||||||
ft.setLastTime(tm-60)
|
|
||||||
self.assertEqual(ft.getTime(), tm)
|
self.assertEqual(ft.getTime(), tm)
|
||||||
self.assertEqual(ft.getLastTime(), tm)
|
ft.adjustTime(tm-60, 3600)
|
||||||
ft.setLastTime(tm+60)
|
self.assertEqual(ft.getTime(), tm)
|
||||||
|
self.assertEqual(ft.getRetry(), 14)
|
||||||
|
ft.adjustTime(tm+60, 3600)
|
||||||
self.assertEqual(ft.getTime(), tm+60)
|
self.assertEqual(ft.getTime(), tm+60)
|
||||||
self.assertEqual(ft.getLastTime(), tm+60)
|
self.assertEqual(ft.getRetry(), 14)
|
||||||
|
ft.adjustTime(tm+3600, 3600)
|
||||||
|
self.assertEqual(ft.getTime(), tm+3600)
|
||||||
|
self.assertEqual(ft.getRetry(), 14)
|
||||||
|
# adjust time so interval is larger than find time (3600), so reset retry count:
|
||||||
|
ft.adjustTime(tm+7200, 3600)
|
||||||
|
self.assertEqual(ft.getTime(), tm+7200)
|
||||||
|
self.assertEqual(ft.getRetry(), 7); # estimated attempts count
|
||||||
|
self.assertEqual(ft.getAttempt(), 4); # real known failure count
|
||||||
ft.setData('country', 'DE')
|
ft.setData('country', 'DE')
|
||||||
self.assertEqual(ft.getData(),
|
self.assertEqual(ft.getData(),
|
||||||
{'matches': ['first', 'second', 'third'], 'failures': 4, 'country': 'DE'})
|
{'matches': ['first', 'second', 'third'], 'failures': 4, 'country': 'DE'})
|
||||||
|
@ -102,10 +110,10 @@ class TicketTests(unittest.TestCase):
|
||||||
self.assertEqual(ft, ft2)
|
self.assertEqual(ft, ft2)
|
||||||
self.assertEqual(ft.getData(), ft2.getData())
|
self.assertEqual(ft.getData(), ft2.getData())
|
||||||
self.assertEqual(ft2.getAttempt(), 4)
|
self.assertEqual(ft2.getAttempt(), 4)
|
||||||
self.assertEqual(ft2.getRetry(), 14)
|
self.assertEqual(ft2.getRetry(), 7)
|
||||||
self.assertEqual(ft2.getMatches(), matches3)
|
self.assertEqual(ft2.getMatches(), matches3)
|
||||||
self.assertEqual(ft2.getTime(), ft.getTime())
|
self.assertEqual(ft2.getTime(), ft.getTime())
|
||||||
self.assertEqual(ft2.getLastTime(), ft.getLastTime())
|
self.assertEqual(ft2.getTime(), ft.getTime())
|
||||||
self.assertEqual(ft2.getBanTime(), ft.getBanTime())
|
self.assertEqual(ft2.getBanTime(), ft.getBanTime())
|
||||||
|
|
||||||
def testTicketFlags(self):
|
def testTicketFlags(self):
|
||||||
|
|
Loading…
Reference in New Issue