mirror of https://github.com/fail2ban/fail2ban
use "maxEntries" (currently 50 as default) as range for max number of the last matches/failures, fail2ban will hold per IP in the list of failures in failmanager resp. in the database;
prevents out of memory situation if many IP's makes extremely many failures (or very large files since last fail2ban run); closes gh-1277 todo: parameter `maxentries` should be configurable (jail.conf resp. fail2ban.conf); todo: adjust ban-time-incr branch by merge (table "bips").pull/1346/head
parent
935d79eaae
commit
e065941ac5
|
@ -163,6 +163,7 @@ class Fail2BanDb(object):
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, filename, purgeAge=24*60*60):
|
def __init__(self, filename, purgeAge=24*60*60):
|
||||||
|
self.maxEntries = 50
|
||||||
try:
|
try:
|
||||||
self._lock = RLock()
|
self._lock = RLock()
|
||||||
self._db = sqlite3.connect(
|
self._db = sqlite3.connect(
|
||||||
|
@ -454,7 +455,7 @@ class Fail2BanDb(object):
|
||||||
if ip is not None:
|
if ip is not None:
|
||||||
query += " AND ip=?"
|
query += " AND ip=?"
|
||||||
queryArgs.append(ip)
|
queryArgs.append(ip)
|
||||||
query += " ORDER BY ip, timeofban"
|
query += " ORDER BY ip, timeofban desc"
|
||||||
|
|
||||||
return cur.execute(query, queryArgs)
|
return cur.execute(query, queryArgs)
|
||||||
|
|
||||||
|
@ -535,7 +536,14 @@ class Fail2BanDb(object):
|
||||||
matches = []
|
matches = []
|
||||||
failures = 0
|
failures = 0
|
||||||
tickdata = {}
|
tickdata = {}
|
||||||
matches.extend(data.get('matches', ()))
|
m = data.get('matches', [])
|
||||||
|
# pre-insert "maxadd" enries (because tickets are ordered desc by time)
|
||||||
|
maxadd = self.maxEntries - len(matches)
|
||||||
|
if maxadd > 0:
|
||||||
|
if len(m) <= maxadd:
|
||||||
|
matches = m + matches
|
||||||
|
else:
|
||||||
|
matches = m[-maxadd:] + matches
|
||||||
failures += data.get('failures', 1)
|
failures += data.get('failures', 1)
|
||||||
tickdata.update(data.get('data', {}))
|
tickdata.update(data.get('data', {}))
|
||||||
prev_timeofban = timeofban
|
prev_timeofban = timeofban
|
||||||
|
|
|
@ -43,6 +43,7 @@ class FailManager:
|
||||||
self.__maxRetry = 3
|
self.__maxRetry = 3
|
||||||
self.__maxTime = 600
|
self.__maxTime = 600
|
||||||
self.__failTotal = 0
|
self.__failTotal = 0
|
||||||
|
self.maxEntries = 50
|
||||||
self.__bgSvc = BgService()
|
self.__bgSvc = BgService()
|
||||||
|
|
||||||
def setFailTotal(self, value):
|
def setFailTotal(self, value):
|
||||||
|
@ -71,17 +72,26 @@ class FailManager:
|
||||||
ip = ticket.getIP()
|
ip = ticket.getIP()
|
||||||
try:
|
try:
|
||||||
fData = self.__failList[ip]
|
fData = self.__failList[ip]
|
||||||
# if the same object:
|
# if the same object - the same matches but +1 attempt:
|
||||||
if fData is ticket:
|
if fData is ticket:
|
||||||
matches = None
|
matches = None
|
||||||
|
attempt = 1
|
||||||
else:
|
else:
|
||||||
|
# will be incremented / extended (be sure we have at least +1 attempt):
|
||||||
matches = ticket.getMatches()
|
matches = ticket.getMatches()
|
||||||
|
attempt = ticket.getAttempt()
|
||||||
|
if attempt <= 0:
|
||||||
|
attempt += 1
|
||||||
unixTime = ticket.getTime()
|
unixTime = ticket.getTime()
|
||||||
|
fData.setLastTime(unixTime)
|
||||||
if fData.getLastReset() < unixTime - self.__maxTime:
|
if fData.getLastReset() < unixTime - self.__maxTime:
|
||||||
fData.setLastReset(unixTime)
|
fData.setLastReset(unixTime)
|
||||||
fData.setRetry(0)
|
fData.setRetry(0)
|
||||||
fData.inc(matches, 1, count)
|
fData.inc(matches, attempt, count)
|
||||||
fData.setLastTime(unixTime)
|
# truncate to maxEntries:
|
||||||
|
matches = fData.getMatches()
|
||||||
|
if len(matches) > self.maxEntries:
|
||||||
|
fData.setMatches(matches[-self.maxEntries:])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# if already FailTicket - add it direct, otherwise create (using copy all ticket data):
|
# if already FailTicket - add it direct, otherwise create (using copy all ticket data):
|
||||||
if isinstance(ticket, FailTicket):
|
if isinstance(ticket, FailTicket):
|
||||||
|
|
|
@ -251,6 +251,37 @@ class DatabaseTest(LogCaptureTestCase):
|
||||||
# be returned
|
# be returned
|
||||||
self.assertEqual(len(self.db.getBans(jail=self.jail,bantime=-1)), 2)
|
self.assertEqual(len(self.db.getBans(jail=self.jail,bantime=-1)), 2)
|
||||||
|
|
||||||
|
def testGetBansMerged_MaxEntries(self):
|
||||||
|
if Fail2BanDb is None: # pragma: no cover
|
||||||
|
return
|
||||||
|
self.testAddJail()
|
||||||
|
maxEntries = 2
|
||||||
|
failures = ["abc\n", "123\n", "ABC\n", "1234\n"]
|
||||||
|
# add failures sequential:
|
||||||
|
i = 80
|
||||||
|
for f in failures:
|
||||||
|
i -= 10
|
||||||
|
ticket = FailTicket("127.0.0.1", MyTime.time() - i, [f])
|
||||||
|
ticket.setAttempt(1)
|
||||||
|
self.db.addBan(self.jail, ticket)
|
||||||
|
# should retrieve 2 matches only, but count of all attempts:
|
||||||
|
self.db.maxEntries = maxEntries;
|
||||||
|
ticket = self.db.getBansMerged("127.0.0.1")
|
||||||
|
self.assertEqual(ticket.getIP(), "127.0.0.1")
|
||||||
|
self.assertEqual(ticket.getAttempt(), len(failures))
|
||||||
|
self.assertEqual(len(ticket.getMatches()), maxEntries)
|
||||||
|
self.assertEqual(ticket.getMatches(), failures[len(failures) - maxEntries:])
|
||||||
|
# add more failures at once:
|
||||||
|
ticket = FailTicket("127.0.0.1", MyTime.time() - 10, failures)
|
||||||
|
ticket.setAttempt(len(failures))
|
||||||
|
self.db.addBan(self.jail, ticket)
|
||||||
|
# should retrieve 2 matches only, but count of all attempts:
|
||||||
|
self.db.maxEntries = maxEntries;
|
||||||
|
ticket = self.db.getBansMerged("127.0.0.1")
|
||||||
|
self.assertEqual(ticket.getAttempt(), 2 * len(failures))
|
||||||
|
self.assertEqual(len(ticket.getMatches()), maxEntries)
|
||||||
|
self.assertEqual(ticket.getMatches(), failures[len(failures) - maxEntries:])
|
||||||
|
|
||||||
def testGetBansMerged(self):
|
def testGetBansMerged(self):
|
||||||
if Fail2BanDb is None: # pragma: no cover
|
if Fail2BanDb is None: # pragma: no cover
|
||||||
return
|
return
|
||||||
|
|
|
@ -35,6 +35,13 @@ class AddFailure(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Call before every test case."""
|
"""Call before every test case."""
|
||||||
|
self.__items = None
|
||||||
|
self.__failManager = FailManager()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Call after every test case."""
|
||||||
|
|
||||||
|
def _addDefItems(self):
|
||||||
self.__items = [[u'193.168.0.128', 1167605999.0],
|
self.__items = [[u'193.168.0.128', 1167605999.0],
|
||||||
[u'193.168.0.128', 1167605999.0],
|
[u'193.168.0.128', 1167605999.0],
|
||||||
[u'193.168.0.128', 1167605999.0],
|
[u'193.168.0.128', 1167605999.0],
|
||||||
|
@ -48,44 +55,87 @@ class AddFailure(unittest.TestCase):
|
||||||
['100.100.10.10', 1000001000.0],
|
['100.100.10.10', 1000001000.0],
|
||||||
['100.100.10.10', 1000001500.0],
|
['100.100.10.10', 1000001500.0],
|
||||||
['100.100.10.10', 1000002000.0]]
|
['100.100.10.10', 1000002000.0]]
|
||||||
|
|
||||||
self.__failManager = FailManager()
|
|
||||||
for i in self.__items:
|
for i in self.__items:
|
||||||
self.__failManager.addFailure(FailTicket(i[0], i[1]))
|
self.__failManager.addFailure(FailTicket(i[0], i[1]))
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
"""Call after every test case."""
|
|
||||||
|
|
||||||
def testFailManagerAdd(self):
|
def testFailManagerAdd(self):
|
||||||
|
self._addDefItems()
|
||||||
self.assertEqual(self.__failManager.size(), 3)
|
self.assertEqual(self.__failManager.size(), 3)
|
||||||
self.assertEqual(self.__failManager.getFailTotal(), 13)
|
self.assertEqual(self.__failManager.getFailTotal(), 13)
|
||||||
self.__failManager.setFailTotal(0)
|
self.__failManager.setFailTotal(0)
|
||||||
self.assertEqual(self.__failManager.getFailTotal(), 0)
|
self.assertEqual(self.__failManager.getFailTotal(), 0)
|
||||||
self.__failManager.setFailTotal(13)
|
self.__failManager.setFailTotal(13)
|
||||||
|
|
||||||
|
def testFailManagerAdd_MaxEntries(self):
|
||||||
|
maxEntries = 2
|
||||||
|
self.__failManager.maxEntries = maxEntries
|
||||||
|
failures = ["abc\n", "123\n", "ABC\n", "1234\n"]
|
||||||
|
# add failures sequential:
|
||||||
|
i = 80
|
||||||
|
for f in failures:
|
||||||
|
i -= 10
|
||||||
|
ticket = FailTicket("127.0.0.1", 1000002000 - i, [f])
|
||||||
|
ticket.setAttempt(1)
|
||||||
|
self.__failManager.addFailure(ticket)
|
||||||
|
#
|
||||||
|
manFailList = self.__failManager._FailManager__failList
|
||||||
|
self.assertEqual(len(manFailList), 1)
|
||||||
|
ticket = manFailList["127.0.0.1"]
|
||||||
|
# should retrieve 2 matches only, but count of all attempts (4):
|
||||||
|
self.assertEqual(ticket.getAttempt(), len(failures))
|
||||||
|
self.assertEqual(len(ticket.getMatches()), maxEntries)
|
||||||
|
self.assertEqual(ticket.getMatches(), failures[len(failures) - maxEntries:])
|
||||||
|
# add more failures at once:
|
||||||
|
ticket = FailTicket("127.0.0.1", 1000002000 - 10, failures)
|
||||||
|
ticket.setAttempt(len(failures))
|
||||||
|
self.__failManager.addFailure(ticket)
|
||||||
|
#
|
||||||
|
manFailList = self.__failManager._FailManager__failList
|
||||||
|
self.assertEqual(len(manFailList), 1)
|
||||||
|
ticket = manFailList["127.0.0.1"]
|
||||||
|
# should retrieve 2 matches only, but count of all attempts (8):
|
||||||
|
self.assertEqual(ticket.getAttempt(), 2 * len(failures))
|
||||||
|
self.assertEqual(len(ticket.getMatches()), maxEntries)
|
||||||
|
self.assertEqual(ticket.getMatches(), failures[len(failures) - maxEntries:])
|
||||||
|
# add self ticket again:
|
||||||
|
self.__failManager.addFailure(ticket)
|
||||||
|
#
|
||||||
|
manFailList = self.__failManager._FailManager__failList
|
||||||
|
self.assertEqual(len(manFailList), 1)
|
||||||
|
ticket = manFailList["127.0.0.1"]
|
||||||
|
# same matches, but +1 attempt (9)
|
||||||
|
self.assertEqual(ticket.getAttempt(), 2 * len(failures) + 1)
|
||||||
|
self.assertEqual(len(ticket.getMatches()), maxEntries)
|
||||||
|
self.assertEqual(ticket.getMatches(), failures[len(failures) - maxEntries:])
|
||||||
|
|
||||||
def testFailManagerMaxTime(self):
|
def testFailManagerMaxTime(self):
|
||||||
|
self._addDefItems()
|
||||||
self.assertEqual(self.__failManager.getMaxTime(), 600)
|
self.assertEqual(self.__failManager.getMaxTime(), 600)
|
||||||
self.__failManager.setMaxTime(13)
|
self.__failManager.setMaxTime(13)
|
||||||
self.assertEqual(self.__failManager.getMaxTime(), 13)
|
self.assertEqual(self.__failManager.getMaxTime(), 13)
|
||||||
self.__failManager.setMaxTime(600)
|
self.__failManager.setMaxTime(600)
|
||||||
|
|
||||||
def testDel(self):
|
def testDel(self):
|
||||||
|
self._addDefItems()
|
||||||
self.__failManager.delFailure('193.168.0.128')
|
self.__failManager.delFailure('193.168.0.128')
|
||||||
self.__failManager.delFailure('111.111.1.111')
|
self.__failManager.delFailure('111.111.1.111')
|
||||||
|
|
||||||
self.assertEqual(self.__failManager.size(), 2)
|
self.assertEqual(self.__failManager.size(), 2)
|
||||||
|
|
||||||
def testCleanupOK(self):
|
def testCleanupOK(self):
|
||||||
|
self._addDefItems()
|
||||||
timestamp = 1167606999.0
|
timestamp = 1167606999.0
|
||||||
self.__failManager.cleanup(timestamp)
|
self.__failManager.cleanup(timestamp)
|
||||||
self.assertEqual(self.__failManager.size(), 0)
|
self.assertEqual(self.__failManager.size(), 0)
|
||||||
|
|
||||||
def testCleanupNOK(self):
|
def testCleanupNOK(self):
|
||||||
|
self._addDefItems()
|
||||||
timestamp = 1167605990.0
|
timestamp = 1167605990.0
|
||||||
self.__failManager.cleanup(timestamp)
|
self.__failManager.cleanup(timestamp)
|
||||||
self.assertEqual(self.__failManager.size(), 2)
|
self.assertEqual(self.__failManager.size(), 2)
|
||||||
|
|
||||||
def testbanOK(self):
|
def testbanOK(self):
|
||||||
|
self._addDefItems()
|
||||||
self.__failManager.setMaxRetry(5)
|
self.__failManager.setMaxRetry(5)
|
||||||
#ticket = FailTicket('193.168.0.128', None)
|
#ticket = FailTicket('193.168.0.128', None)
|
||||||
ticket = self.__failManager.toBan()
|
ticket = self.__failManager.toBan()
|
||||||
|
@ -112,10 +162,12 @@ class AddFailure(unittest.TestCase):
|
||||||
'FailTicket: ip=193.168.0.128 time=1000002000.0 #attempts=5 matches=[]')
|
'FailTicket: ip=193.168.0.128 time=1000002000.0 #attempts=5 matches=[]')
|
||||||
|
|
||||||
def testbanNOK(self):
|
def testbanNOK(self):
|
||||||
|
self._addDefItems()
|
||||||
self.__failManager.setMaxRetry(10)
|
self.__failManager.setMaxRetry(10)
|
||||||
self.assertRaises(FailManagerEmpty, self.__failManager.toBan)
|
self.assertRaises(FailManagerEmpty, self.__failManager.toBan)
|
||||||
|
|
||||||
def testWindow(self):
|
def testWindow(self):
|
||||||
|
self._addDefItems()
|
||||||
ticket = self.__failManager.toBan()
|
ticket = self.__failManager.toBan()
|
||||||
self.assertNotEqual(ticket.getIP(), "100.100.10.10")
|
self.assertNotEqual(ticket.getIP(), "100.100.10.10")
|
||||||
ticket = self.__failManager.toBan()
|
ticket = self.__failManager.toBan()
|
||||||
|
|
Loading…
Reference in New Issue