"magic" formula for auto increasing of retry count for known (bad) ip, corresponding banCount of it

(one try will count than 2, 3, 5, 9 ...)
pull/716/head
sebres 2014-05-14 11:21:31 +02:00
parent 0121e09907
commit d22ab320e2
8 changed files with 239 additions and 53 deletions

View File

@ -324,9 +324,13 @@ class Actions(JailThread, Mapping):
if banCount > 0: if banCount > 0:
banTime = be['evformula'](self.BanTimeIncr(banTime, banCount)) banTime = be['evformula'](self.BanTimeIncr(banTime, banCount))
bTicket.setBanTime(banTime); bTicket.setBanTime(banTime);
# check current ticket time to prevent increasing for twice read tickets (restored from log file besides database after restart)
if bTicket.getTime() > timeOfBan:
logSys.info('[%s] %s was already banned: %s # at last %s - increase time %s to %s' % (self._jail.name, ip, banCount, logSys.info('[%s] %s was already banned: %s # at last %s - increase time %s to %s' % (self._jail.name, ip, banCount,
datetime.datetime.fromtimestamp(timeOfBan).strftime("%Y-%m-%d %H:%M:%S"), datetime.datetime.fromtimestamp(timeOfBan).strftime("%Y-%m-%d %H:%M:%S"),
datetime.timedelta(seconds=int(orgBanTime)), datetime.timedelta(seconds=int(banTime)))); datetime.timedelta(seconds=int(orgBanTime)), datetime.timedelta(seconds=int(banTime))));
else:
bTicket.setRestored(True)
break break
except Exception as e: except Exception as e:
logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG) logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
@ -372,23 +376,27 @@ class Actions(JailThread, Mapping):
self._jail.database.getBansMerged( self._jail.database.getBansMerged(
ip=ip, jail=self._jail).getAttempt()) ip=ip, jail=self._jail).getAttempt())
try: try:
# if ban time was not set: # if not permanent, not restored and ban time was not set:
if not ticket.getRestored() and bTicket.getBanTime() is None: if btime != -1 and not ticket.getRestored() and bTicket.getBanTime() is None:
btime = self.incrBanTime(bTicket) btime = self.incrBanTime(bTicket)
bTicket.setBanTime(btime); bTicket.setBanTime(btime);
except Exception as e: except Exception as e:
logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG) logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
#logSys.error('%s', e, exc_info=True) #logSys.error('%s', e, exc_info=True)
if btime != -1:
logtime = (datetime.timedelta(seconds=int(btime)),
datetime.datetime.fromtimestamp(aInfo["time"] + btime).strftime("%Y-%m-%d %H:%M:%S"))
else:
logtime = ('permanent', 'infinite')
if self.__banManager.addBanTicket(bTicket): if self.__banManager.addBanTicket(bTicket):
if self._jail.database is not None: if self._jail.database is not None:
# add to database always only after ban time was calculated an not yet already banned: # add to database always only after ban time was calculated an not yet already banned:
# if ticked was not restored from database - put it into database: # if ticked was not restored from database - put it into database:
if not ticket.getRestored(): if not ticket.getRestored() and not bTicket.getRestored():
self._jail.database.addBan(self._jail, bTicket) self._jail.database.addBan(self._jail, bTicket)
logSys.notice("[%s] %sBan %s (%d # %s -> %s)" % (self._jail.name, ('Resore ' if ticket.getRestored() else ''), logSys.notice("[%s] %sBan %s (%d # %s -> %s)" % ((self._jail.name, ('Resore ' if ticket.getRestored() else ''),
aInfo["ip"], bTicket.getBanCount(), datetime.timedelta(seconds=int(btime)), aInfo["ip"], bTicket.getBanCount()) + logtime))
datetime.datetime.fromtimestamp(aInfo["time"] + btime).strftime("%Y-%m-%d %H:%M:%S")))
for name, action in self._actions.iteritems(): for name, action in self._actions.iteritems():
try: try:
action.ban(aInfo) action.ban(aInfo)
@ -399,8 +407,8 @@ class Actions(JailThread, Mapping):
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG) exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
return True return True
else: else:
logSys.notice("[%s] %s already banned" % (self._jail.name, logSys.notice("[%s] %s already banned (%d # %s -> %s)" % ((self._jail.name,
aInfo["ip"])) aInfo["ip"], bTicket.getBanCount()) + logtime))
return False return False
def __checkUnBan(self): def __checkUnBan(self):

View File

@ -130,10 +130,11 @@ class BanManager:
def createBanTicket(ticket): def createBanTicket(ticket):
ip = ticket.getIP() ip = ticket.getIP()
# if ticked was restored from database - set time of original restored ticket: # if ticked was restored from database - set time of original restored ticket:
if ticket.getRestored(): # 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)
lastTime = ticket.getTime() lastTime = ticket.getTime()
else: # if not ticket.getRestored():
lastTime = MyTime.time() # lastTime = MyTime.time()
banTicket = BanTicket(ip, lastTime, ticket.getMatches()) banTicket = BanTicket(ip, lastTime, ticket.getMatches())
banTicket.setAttempt(ticket.getAttempt()) banTicket.setAttempt(ticket.getAttempt())
return banTicket return banTicket
@ -149,11 +150,25 @@ class BanManager:
def addBanTicket(self, ticket): def addBanTicket(self, ticket):
try: try:
self.__lock.acquire() self.__lock.acquire()
if not self._inBanList(ticket): # check already banned
for i in self.__banList:
if ticket.getIP() == i.getIP():
# if already permanent
btorg, torg = i.getBanTime(self.__banTime), i.getTime()
if btorg == -1:
return False
# if given time is less than already banned time
btnew, tnew = ticket.getBanTime(self.__banTime), ticket.getTime()
if btnew != -1 and tnew + btnew <= torg + btorg:
return False
# we have longest ban - set new (increment) ban time
i.setTime(tnew)
i.setBanTime(btnew)
return False
# not yet banned - add new
self.__banList.append(ticket) self.__banList.append(ticket)
self.__banTotal += 1 self.__banTotal += 1
return True return True
return False
finally: finally:
self.__lock.release() self.__lock.release()
@ -199,8 +214,7 @@ class BanManager:
return list() return list()
# Gets the list of ticket to remove. # Gets the list of ticket to remove.
unBanList = [ticket for ticket in self.__banList unBanList = [ticket for ticket in self.__banList if ticket.isTimedOut(time, self.__banTime)]
if ticket.getTime() < time - ticket.getBanTime(self.__banTime)]
# Removes tickets. # Removes tickets.
self.__banList = [ticket for ticket in self.__banList self.__banList = [ticket for ticket in self.__banList

View File

@ -87,7 +87,7 @@ class Fail2BanDb(object):
filename filename
purgeage purgeage
""" """
__version__ = 3 __version__ = 4
# Note all _TABLE_* strings must end in ';' for py26 compatibility # Note all _TABLE_* strings must end in ';' for py26 compatibility
_TABLE_fail2banDb = "CREATE TABLE fail2banDb(version INTEGER);" _TABLE_fail2banDb = "CREATE TABLE fail2banDb(version INTEGER);"
_TABLE_jails = "CREATE TABLE jails(" \ _TABLE_jails = "CREATE TABLE jails(" \
@ -123,10 +123,20 @@ class Fail2BanDb(object):
"CREATE INDEX bans_jail_ip ON bans(jail, ip);" \ "CREATE INDEX bans_jail_ip ON bans(jail, ip);" \
"CREATE INDEX bans_ip ON bans(ip);" \ "CREATE INDEX bans_ip ON bans(ip);" \
# todo: for performance reasons create a table with currently banned unique jails-ips only (with last ban of time and bantime). _TABLE_bips = "CREATE TABLE bips(" \
# check possible view performance instead of new table; "ip TEXT NOT NULL, " \
"jail TEXT NOT NULL, " \
"timeofban INTEGER NOT NULL, " \
"bantime INTEGER NOT NULL, " \
"bancount INTEGER NOT NULL default 1, " \
"data JSON, " \
"PRIMARY KEY(ip, jail), " \
"FOREIGN KEY(jail) REFERENCES jails(name) " \
");" \
"CREATE INDEX bips_timeofban ON bips(timeofban);" \
"CREATE INDEX bips_ip ON bips(ip);" \
def __init__(self, filename, purgeAge=24*60*60): def __init__(self, filename, purgeAge=24*60*60, outDatedFactor=3):
try: try:
self._lock = Lock() self._lock = Lock()
self._db = sqlite3.connect( self._db = sqlite3.connect(
@ -134,6 +144,7 @@ class Fail2BanDb(object):
detect_types=sqlite3.PARSE_DECLTYPES) detect_types=sqlite3.PARSE_DECLTYPES)
self._dbFilename = filename self._dbFilename = filename
self._purgeAge = purgeAge self._purgeAge = purgeAge
self._outDatedFactor = outDatedFactor;
self._bansMergedCache = {} self._bansMergedCache = {}
@ -198,6 +209,8 @@ class Fail2BanDb(object):
cur.executescript(Fail2BanDb._TABLE_logs) cur.executescript(Fail2BanDb._TABLE_logs)
# Bans # Bans
cur.executescript(Fail2BanDb._TABLE_bans) cur.executescript(Fail2BanDb._TABLE_bans)
# BIPs (bad ips)
cur.executescript(Fail2BanDb._TABLE_bips)
cur.execute("SELECT version FROM fail2banDb LIMIT 1") cur.execute("SELECT version FROM fail2banDb LIMIT 1")
return cur.fetchone()[0] return cur.fetchone()[0]
@ -232,8 +245,12 @@ class Fail2BanDb(object):
"%s;" "%s;"
"INSERT INTO bans SELECT * from bans_temp;" "INSERT INTO bans SELECT * from bans_temp;"
"DROP TABLE bans_temp;" "DROP TABLE bans_temp;"
"UPDATE fail2banDb SET version = 3;"
"COMMIT;" % Fail2BanDb._TABLE_bans) "COMMIT;" % Fail2BanDb._TABLE_bans)
if version < 4:
cur.executescript("BEGIN TRANSACTION;"
"%s;"
"UPDATE fail2banDb SET version = 4;"
"COMMIT;" % Fail2BanDb._TABLE_bips)
cur.execute("SELECT version FROM fail2banDb LIMIT 1") cur.execute("SELECT version FROM fail2banDb LIMIT 1")
return cur.fetchone()[0] return cur.fetchone()[0]
@ -386,6 +403,11 @@ class Fail2BanDb(object):
(jail.name, ticket.getIP(), ticket.getTime(), ticket.getBanTime(jail.actions.getBanTime()), ticket.getBanCount() + 1, (jail.name, ticket.getIP(), ticket.getTime(), ticket.getBanTime(jail.actions.getBanTime()), ticket.getBanCount() + 1,
{"matches": ticket.getMatches(), {"matches": ticket.getMatches(),
"failures": ticket.getAttempt()})) "failures": ticket.getAttempt()}))
cur.execute(
"INSERT OR REPLACE INTO bips(ip, jail, timeofban, bantime, bancount, data) VALUES(?, ?, ?, ?, ?, ?)",
(ticket.getIP(), jail.name, ticket.getTime(), ticket.getBanTime(jail.actions.getBanTime()), ticket.getBanCount() + 1,
{"matches": ticket.getMatches(),
"failures": ticket.getAttempt()}))
@commitandrollback @commitandrollback
def _getBans(self, cur, jail=None, bantime=None, ip=None): def _getBans(self, cur, jail=None, bantime=None, ip=None):
@ -487,41 +509,46 @@ class Fail2BanDb(object):
self._bansMergedCache[cacheKey] = tickets if ip is None else ticket self._bansMergedCache[cacheKey] = tickets if ip is None else ticket
return tickets if ip is None else ticket return tickets if ip is None else ticket
def getBan(self, ip, jail=None, forbantime=None, overalljails=None): def getBan(self, ip, jail=None, forbantime=None, overalljails=None, fromtime=None):
#query = "SELECT count(ip), max(timeofban) FROM bans WHERE ip = ?" if not overalljails:
if overalljails is None or not overalljails: query = "SELECT bancount, timeofban, bantime FROM bips"
query = "SELECT bancount, max(timeofban), bantime FROM bans"
else: else:
query = "SELECT max(bancount), max(timeofban), bantime FROM bans" query = "SELECT max(bancount), max(timeofban), max(bantime) FROM bips"
query += " WHERE ip = ?" query += " WHERE ip = ?"
queryArgs = [ip] queryArgs = [ip]
if (overalljails is None or not overalljails) and jail is not None: if not overalljails and jail is not None:
query += " AND jail=?" query += " AND jail=?"
queryArgs.append(jail.name) queryArgs.append(jail.name)
if forbantime is not None: if forbantime is not None:
query += " AND timeofban > ?" query += " AND timeofban > ?"
queryArgs.append(MyTime.time() - forbantime) queryArgs.append(MyTime.time() - forbantime)
if fromtime is not None:
query += " AND timeofban > ?"
queryArgs.append(fromtime)
if overalljails or jail is None:
query += " GROUP BY ip ORDER BY timeofban DESC LIMIT 1" query += " GROUP BY ip ORDER BY timeofban DESC LIMIT 1"
cur = self._db.cursor() cur = self._db.cursor()
#logSys.debug((query, queryArgs));
return cur.execute(query, queryArgs) return cur.execute(query, queryArgs)
def _getCurrentBans(self, jail = None, ip = None, forbantime=None, fromtime=None): def _getCurrentBans(self, jail = None, ip = None, forbantime=None, fromtime=None):
if fromtime is None: if fromtime is None:
fromtime = MyTime.time() fromtime = MyTime.time()
#query = "SELECT count(ip), max(timeofban) FROM bans WHERE ip = ?"
query = "SELECT ip, max(timeofban), bantime, bancount, data FROM bans WHERE 1"
queryArgs = [] queryArgs = []
if jail is not None: if jail is not None:
query += " AND jail=?" query = "SELECT ip, timeofban, bantime, bancount, data FROM bips WHERE jail=?"
queryArgs.append(jail.name) queryArgs.append(jail.name)
else:
query = "SELECT ip, max(timeofban), bantime, bancount, data FROM bips WHERE 1"
if ip is not None: if ip is not None:
query += " AND ip=?" query += " AND ip=?"
queryArgs.append(ip) queryArgs.append(ip)
query += " AND timeofban + bantime > ?" query += " AND (timeofban + bantime > ? OR bantime = -1)"
queryArgs.append(fromtime) queryArgs.append(fromtime)
if forbantime is not None: if forbantime is not None:
query += " AND timeofban > ?" query += " AND timeofban > ?"
queryArgs.append(fromtime - forbantime) queryArgs.append(fromtime - forbantime)
if ip is None:
query += " GROUP BY ip ORDER BY ip, timeofban DESC" query += " GROUP BY ip ORDER BY ip, timeofban DESC"
cur = self._db.cursor() cur = self._db.cursor()
#logSys.debug((query, queryArgs)); #logSys.debug((query, queryArgs));
@ -558,15 +585,35 @@ class Fail2BanDb(object):
self._bansMergedCache[cacheKey] = tickets if ip is None else ticket self._bansMergedCache[cacheKey] = tickets if ip is None else ticket
return tickets if ip is None else ticket return tickets if ip is None else ticket
@commitandrollback def _cleanjails(self, cur):
def purge(self, cur): """Remove empty jails jails and log files from database.
"""
cur.execute(
"DELETE FROM jails WHERE enabled = 0 "
"AND NOT EXISTS(SELECT * FROM bans WHERE jail = jails.name) "
"AND NOT EXISTS(SELECT * FROM bips WHERE jail = jails.name)")
def _purge_bips(self, cur):
"""Purge old bad ips (jails and log files from database).
Currently it is timed out IP, whose time since last ban is several times out-dated (outDatedFactor is default 3).
Permanent banned ips will be never removed.
"""
cur.execute(
"DELETE FROM bips WHERE timeofban < ? and bantime != -1 and (timeofban + (bantime * ?)) < ?",
(int(MyTime.time()) - self._purgeAge, self._outDatedFactor, int(MyTime.time()) - self._purgeAge))
#@commitandrollback
def purge(self):
"""Purge old bans, jails and log files from database. """Purge old bans, jails and log files from database.
""" """
cur = self._db.cursor()
self._bansMergedCache = {} self._bansMergedCache = {}
cur.execute( cur.execute(
"DELETE FROM bans WHERE timeofban < ?", "DELETE FROM bans WHERE timeofban < ?",
(MyTime.time() - self._purgeAge, )) (MyTime.time() - self._purgeAge, ))
cur.execute( affected = cur.rowcount
"DELETE FROM jails WHERE enabled = 0 " self._purge_bips(cur)
"AND NOT EXISTS(SELECT * FROM bans WHERE jail = jails.name)") affected += cur.rowcount
if affected:
self._cleanjails(cur)

View File

@ -52,8 +52,8 @@ class FailData:
def getMatches(self): def getMatches(self):
return self.__matches return self.__matches
def inc(self, matches=None): def inc(self, matches=None, count=1):
self.__retry += 1 self.__retry += count
self.__matches += matches or [] self.__matches += matches or []
def setLastTime(self, value): def setLastTime(self, value):

View File

@ -84,7 +84,7 @@ class FailManager:
finally: finally:
self.__lock.release() self.__lock.release()
def addFailure(self, ticket): def addFailure(self, ticket, count=1):
try: try:
self.__lock.acquire() self.__lock.acquire()
ip = ticket.getIP() ip = ticket.getIP()
@ -95,11 +95,11 @@ class FailManager:
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) fData.inc(matches, count)
fData.setLastTime(unixTime) fData.setLastTime(unixTime)
else: else:
fData = FailData() fData = FailData()
fData.inc(matches) fData.inc(matches, count)
fData.setLastReset(unixTime) fData.setLastReset(unixTime)
fData.setLastTime(unixTime) fData.setLastTime(unixTime)
self.__failList[ip] = fData self.__failList[ip] = fData

View File

@ -420,9 +420,26 @@ class Filter(JailThread):
if self.inIgnoreIPList(ip): if self.inIgnoreIPList(ip):
logSys.info("[%s] Ignore %s" % (self.jail.name, ip)) logSys.info("[%s] Ignore %s" % (self.jail.name, ip))
continue continue
# increase retry count for known (bad) ip, corresponding banCount of it (one try will count than 2, 3, 5, 9 ...) :
banCount = 0
retryCount = 1
db = self.jail.database
if db is not None:
try:
for banCount, timeOfBan, lastBanTime in db.getBan(ip, self.jail):
retryCount = ((1 << (banCount if banCount < 20 else 20))/2 + 1)
# if lastBanTime == -1 or timeOfBan + lastBanTime * 2 > MyTime.time():
# retryCount = self.failManager.getMaxRetry()
break
retryCount = min(retryCount, self.failManager.getMaxRetry())
except Exception as e:
#logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
logSys.error('%s', e, exc_info=True)
if banCount == 1 and retryCount == 1:
logSys.info("[%s] Found %s" % (self.jail.name, ip)) logSys.info("[%s] Found %s" % (self.jail.name, ip))
## print "D: Adding a ticket for %s" % ((ip, unixTime, [line]),) else:
self.failManager.addFailure(FailTicket(ip, unixTime, lines)) logSys.info("[%s] Found %s, %s # -> %s" % (self.jail.name, ip, banCount, retryCount))
self.failManager.addFailure(FailTicket(ip, unixTime, lines), retryCount)
## ##
# Returns true if the line should be ignored. # Returns true if the line should be ignored.

View File

@ -91,6 +91,14 @@ class Ticket:
def getBanCount(self): def getBanCount(self):
return self.__banCount; return self.__banCount;
def isTimedOut(self, time, defaultBT = None):
bantime = (self.__banTime if not self.__banTime is None else defaultBT);
# permanent
if bantime == -1:
return False
# timed out
return (time > self.__time + bantime)
def setAttempt(self, value): def setAttempt(self, value):
self.__attempt = value self.__attempt = value

View File

@ -407,3 +407,95 @@ class BanTimeIncr(unittest.TestCase):
str(restored_tickets[2]), str(restored_tickets[2]),
'FailTicket: ip=%s time=%s bantime=%s bancount=1 #attempts=0 matches=[]' % (ip+'2', stime-24*60*60, 12*60*60) 'FailTicket: ip=%s time=%s bantime=%s bancount=1 #attempts=0 matches=[]' % (ip+'2', stime-24*60*60, 12*60*60)
) )
# should be still banned
self.assertFalse(restored_tickets[1].isTimedOut(stime))
self.assertFalse(restored_tickets[1].isTimedOut(stime))
# the last should be timed out now
self.assertTrue(restored_tickets[2].isTimedOut(stime))
self.assertFalse(restored_tickets[2].isTimedOut(stime-18*60*60))
# test permanent, create timed out:
ticket=FailTicket(ip+'3', stime-36*60*60, [])
self.assertTrue(ticket.isTimedOut(stime, 600))
# not timed out - permanent jail:
self.assertFalse(ticket.isTimedOut(stime, -1))
# not timed out - permanent ticket:
ticket.setBanTime(-1)
self.assertFalse(ticket.isTimedOut(stime, 600))
self.assertFalse(ticket.isTimedOut(stime, -1))
# timed out - permanent jail but ticket time (not really used behavior)
ticket.setBanTime(600)
self.assertTrue(ticket.isTimedOut(stime, -1))
# get currently banned pis with permanent one:
ticket.setBanTime(-1)
self.db.addBan(jail, ticket)
restored_tickets = self.db.getCurrentBans(fromtime=stime)
self.assertEqual(len(restored_tickets), 3)
self.assertEqual(
str(restored_tickets[2]),
'FailTicket: ip=%s time=%s bantime=%s bancount=1 #attempts=0 matches=[]' % (ip+'3', stime-36*60*60, -1)
)
# purge (nothing should be changed):
self.db.purge()
restored_tickets = self.db.getCurrentBans(fromtime=stime)
self.assertEqual(len(restored_tickets), 3)
# set short time and purge again:
ticket.setBanTime(600)
self.db.addBan(jail, ticket)
self.db.purge()
# this old ticket should be removed now:
restored_tickets = self.db.getCurrentBans(fromtime=stime)
self.assertEqual(len(restored_tickets), 2)
self.assertEqual(restored_tickets[0].getIP(), ip)
# purge remove 1st ip
self.db._purgeAge = -48*60*60
self.db.purge()
restored_tickets = self.db.getCurrentBans(fromtime=stime)
self.assertEqual(len(restored_tickets), 1)
self.assertEqual(restored_tickets[0].getIP(), ip+'1')
# this should purge all bans, bips and logs - nothing should be found now
self.db._purgeAge = -240*60*60
self.db.purge()
restored_tickets = self.db.getCurrentBans(fromtime=stime)
self.assertEqual(restored_tickets, [])
# two separate jails :
jail1 = DummyJail()
jail1.database = self.db
self.db.addJail(jail1)
jail2 = DummyJail()
jail2.database = self.db
self.db.addJail(jail2)
ticket1 = FailTicket(ip, stime, [])
ticket1.setBanTime(6000)
self.db.addBan(jail1, ticket1)
ticket2 = FailTicket(ip, stime-6000, [])
ticket2.setBanTime(12000)
ticket2.setBanCount(1)
self.db.addBan(jail2, ticket2)
restored_tickets = self.db.getCurrentBans(jail=jail1, fromtime=stime)
self.assertEqual(len(restored_tickets), 1)
self.assertEqual(
str(restored_tickets[0]),
'FailTicket: ip=%s time=%s bantime=%s bancount=1 #attempts=0 matches=[]' % (ip, stime, 6000)
)
restored_tickets = self.db.getCurrentBans(jail=jail2, fromtime=stime)
self.assertEqual(len(restored_tickets), 1)
self.assertEqual(
str(restored_tickets[0]),
'FailTicket: ip=%s time=%s bantime=%s bancount=2 #attempts=0 matches=[]' % (ip, stime-6000, 12000)
)
# get last ban values for this ip separately for each jail:
for row in self.db.getBan(ip, jail1):
self.assertEqual(row, (1, stime, 6000))
break
for row in self.db.getBan(ip, jail2):
self.assertEqual(row, (2, stime-6000, 12000))
break
# get max values for this ip (over all jails):
for row in self.db.getBan(ip, overalljails=True):
self.assertEqual(row, (2, stime, 12000))
break