diff --git a/ChangeLog b/ChangeLog index 2d15a42c..24e50715 100644 --- a/ChangeLog +++ b/ChangeLog @@ -98,6 +98,8 @@ filter = flt[logtype=short] * `filter.d/traefik-auth.conf`: used to ban hosts, that were failed through traefik ### Enhancements +* introduced new options: `dbmaxmatches` (fail2ban.conf) and `maxmatches` (jail.conf) to contol + how many matches per ticket fail2ban can hold in memory and store in database (gh-2402, gh-2118); * fail2ban.conf: introduced new section `[Thread]` and option `stacksize` to configure default size of the stack for threads running in fail2ban (gh-2356), it could be set in `fail2ban.local` to avoid runtime error "can't start new thread" (see gh-969); diff --git a/MANIFEST b/MANIFEST index d528ea7f..ed441bac 100644 --- a/MANIFEST +++ b/MANIFEST @@ -145,6 +145,7 @@ config/filter.d/sshd.conf config/filter.d/stunnel.conf config/filter.d/suhosin.conf config/filter.d/tine20.conf +config/filter.d/traefik-auth.conf config/filter.d/uwimap-auth.conf config/filter.d/vsftpd.conf config/filter.d/webmin-auth.conf @@ -335,9 +336,11 @@ fail2ban/tests/files/logs/solid-pop3d fail2ban/tests/files/logs/squid fail2ban/tests/files/logs/squirrelmail fail2ban/tests/files/logs/sshd +fail2ban/tests/files/logs/sshd-journal fail2ban/tests/files/logs/stunnel fail2ban/tests/files/logs/suhosin fail2ban/tests/files/logs/tine20 +fail2ban/tests/files/logs/traefik-auth fail2ban/tests/files/logs/uwimap-auth fail2ban/tests/files/logs/vsftpd fail2ban/tests/files/logs/webmin-auth diff --git a/config/fail2ban.conf b/config/fail2ban.conf index 08e8fcf3..ba0e9204 100644 --- a/config/fail2ban.conf +++ b/config/fail2ban.conf @@ -5,11 +5,11 @@ # Changes: in most of the cases you should not modify this # file, but provide customizations in fail2ban.local file, e.g.: # -# [Definition] +# [DEFAULT] # loglevel = DEBUG # -[Definition] +[DEFAULT] # Option: loglevel # Notes.: Set the log level output. @@ -68,6 +68,15 @@ dbfile = /var/lib/fail2ban/fail2ban.sqlite3 # Values: [ SECONDS ] Default: 86400 (24hours) dbpurgeage = 1d +# Options: dbmaxmatches +# Notes.: Number of matches stored in database per ticket (resolvable via +# tags / in actions) +# Values: [ INT ] Default: 10 +dbmaxmatches = 10 + +[Definition] + + [Thread] # Options: stacksize diff --git a/config/jail.conf b/config/jail.conf index 2ac552e3..569a3ae3 100644 --- a/config/jail.conf +++ b/config/jail.conf @@ -107,6 +107,9 @@ findtime = 10m # "maxretry" is the number of failures before a host get banned. maxretry = 5 +# "maxmatches" is the number of matches stored in ticket (resolvable via tag in actions). +maxmatches = %(maxretry)s + # "backend" specifies the backend used to get files modification. # Available options are "pyinotify", "gamin", "polling", "systemd" and "auto". # This option can be overridden in each jail as well. diff --git a/fail2ban/client/fail2banreader.py b/fail2ban/client/fail2banreader.py index 5760e0ba..3270b767 100644 --- a/fail2ban/client/fail2banreader.py +++ b/fail2ban/client/fail2banreader.py @@ -54,6 +54,7 @@ class Fail2banReader(ConfigReader): ["string", "logtarget", "STDERR"], ["string", "syslogsocket", "auto"], ["string", "dbfile", "/var/lib/fail2ban/fail2ban.sqlite3"], + ["int", "dbmaxmatches", None], ["string", "dbpurgeage", "1d"]] self.__opts = ConfigReader.getOptions(self, "Definition", opts) if updateMainOpt: @@ -73,7 +74,7 @@ class Fail2banReader(ConfigReader): # Also dbfile should be set before all other database options. # So adding order indices into items, to be stripped after sorting, upon return order = {"thread":0, "syslogsocket":11, "loglevel":12, "logtarget":13, - "dbfile":50, "dbpurgeage":51} + "dbfile":50, "dbmaxmatches":51, "dbpurgeage":51} stream = list() for opt in self.__opts: if opt in order: diff --git a/fail2ban/client/jailreader.py b/fail2ban/client/jailreader.py index 75ad1a53..ab3921bd 100644 --- a/fail2ban/client/jailreader.py +++ b/fail2ban/client/jailreader.py @@ -93,6 +93,7 @@ class JailReader(ConfigReader): opts = [["bool", "enabled", False], ["string", "backend", "auto"], ["int", "maxretry", None], + ["int", "maxmatches", None], ["string", "findtime", None], ["string", "bantime", None], ["bool", "bantime.increment", None], diff --git a/fail2ban/protocol.py b/fail2ban/protocol.py index 3f7024a0..6b33d30a 100644 --- a/fail2ban/protocol.py +++ b/fail2ban/protocol.py @@ -72,6 +72,8 @@ protocol = [ ['', "DATABASE", ""], ["set dbfile ", "set the location of fail2ban persistent datastore. Set to \"None\" to disable"], ["get dbfile", "get the location of fail2ban persistent datastore"], +["set dbmaxmatches ", "sets the max number of matches stored in database per ticket"], +["get dbmaxmatches", "gets the max number of matches stored in database per ticket"], ["set dbpurgeage ", "sets the max age in that history of bans will be kept"], ["get dbpurgeage", "gets the max age in seconds that history of bans will be kept"], ['', "JAIL CONTROL", ""], @@ -103,6 +105,7 @@ protocol = [ ["set banip ... ", "manually Ban for "], ["set unbanip [--report-absent] ... ", "manually Unban in "], ["set maxretry ", "sets the number of failures before banning the host for "], +["set maxmatches ", "sets the max number of matches stored in memory per ticket in "], ["set maxlines ", "sets the number of to buffer for regex search for "], ["set addaction [ ]", "adds a new action named for . Optionally for a Python based action, a and can be specified, else will be a Command Action"], ["set delaction ", "removes the action from "], @@ -131,6 +134,7 @@ protocol = [ ["get usedns", "gets the usedns setting for "], ["get banip [|--with-time]", "gets the list of of banned IP addresses for . Optionally the separator character ('', default is space) or the option '--with-time' (printing the times of ban) may be specified. The IPs are ordered by end of ban."], ["get maxretry", "gets the number of failures allowed for "], +["get maxmatches", "gets the max number of matches stored in memory per ticket in "], ["get maxlines", "gets the number of lines to buffer for "], ["get actions", "gets a list of actions for "], ["", "COMMAND ACTION INFORMATION",""], diff --git a/fail2ban/server/database.py b/fail2ban/server/database.py index ac01382e..0dd9acb6 100644 --- a/fail2ban/server/database.py +++ b/fail2ban/server/database.py @@ -191,7 +191,7 @@ class Fail2BanDb(object): def __init__(self, filename, purgeAge=24*60*60, outDatedFactor=3): - self.maxEntries = 50 + self.maxMatches = 10 self._lock = RLock() self._dbFilename = filename self._purgeAge = purgeAge @@ -575,8 +575,13 @@ class Fail2BanDb(object): #TODO: Implement data parts once arbitrary match keys completed data = ticket.getData() matches = data.get('matches') - if matches and len(matches) > self.maxEntries: - data['matches'] = matches[-self.maxEntries:] + if self.maxMatches: + if matches and len(matches) > self.maxMatches: + data = data.copy() + data['matches'] = matches[-self.maxMatches:] + elif matches: + data = data.copy() + del data['matches'] cur.execute( "INSERT INTO bans(jail, ip, timeofban, bantime, bancount, data) VALUES(?, ?, ?, ?, ?, ?)", (jail.name, ip, int(round(ticket.getTime())), ticket.getBanTime(jail.actions.getBanTime()), ticket.getBanCount(), @@ -710,7 +715,7 @@ class Fail2BanDb(object): tickdata = {} m = data.get('matches', []) # pre-insert "maxadd" enries (because tickets are ordered desc by time) - maxadd = self.maxEntries - len(matches) + maxadd = self.maxMatches - len(matches) if maxadd > 0: if len(m) <= maxadd: matches = m + matches @@ -768,12 +773,14 @@ class Fail2BanDb(object): queryArgs.append(fromtime - forbantime) if ip is None: query += " GROUP BY ip ORDER BY ip, timeofban DESC" + else: + query += " ORDER BY timeofban DESC LIMIT 1" cur = self._db.cursor() return cur.execute(query, queryArgs) @commitandrollback def getCurrentBans(self, cur, jail=None, ip=None, forbantime=None, fromtime=None, - correctBanTime=True + correctBanTime=True, maxmatches=None ): """Reads tickets (with merged info) currently affected from ban from the database. @@ -821,12 +828,22 @@ class Fail2BanDb(object): continue # logSys.debug('restore ticket %r, %r, %r', banip, timeofban, data) ticket = FailTicket(banip, timeofban, data=data) + # filter matches if expected (current count > as maxmatches specified): + if maxmatches is None: + maxmatches = self.maxMatches + if maxmatches: + matches = ticket.getMatches() + if matches and len(matches) > maxmatches: + ticket.setMatches(matches[-maxmatches:]) + else: + ticket.setMatches(None) # logSys.debug('restored ticket: %r', ticket) ticket.setBanTime(bantime) ticket.setBanCount(bancount) + if ip is not None: return ticket tickets.append(ticket) - return tickets if ip is None else ticket + return tickets def _cleanjails(self, cur): """Remove empty jails jails and log files from database. diff --git a/fail2ban/server/failmanager.py b/fail2ban/server/failmanager.py index 17dd8b11..8182fc33 100644 --- a/fail2ban/server/failmanager.py +++ b/fail2ban/server/failmanager.py @@ -43,7 +43,7 @@ class FailManager: self.__maxRetry = 3 self.__maxTime = 600 self.__failTotal = 0 - self.maxEntries = 50 + self.maxMatches = 50 self.__bgSvc = BgService() def setFailTotal(self, value): @@ -87,7 +87,7 @@ class FailManager: attempt = 1 else: # will be incremented / extended (be sure we have at least +1 attempt): - matches = ticket.getMatches() + matches = ticket.getMatches() if self.maxMatches else None attempt = ticket.getAttempt() if attempt <= 0: attempt += 1 @@ -97,10 +97,13 @@ class FailManager: fData.setLastReset(unixTime) fData.setRetry(0) fData.inc(matches, attempt, count) - # truncate to maxEntries: - matches = fData.getMatches() - if len(matches) > self.maxEntries: - fData.setMatches(matches[-self.maxEntries:]) + # truncate to maxMatches: + if self.maxMatches: + matches = fData.getMatches() + if len(matches) > self.maxMatches: + fData.setMatches(matches[-self.maxMatches:]) + else: + fData.setMatches(None) except KeyError: # not found - already banned - prevent to add failure if comes from observer: if observed or isinstance(ticket, BanTicket): diff --git a/fail2ban/server/filtersystemd.py b/fail2ban/server/filtersystemd.py index bc791061..f3ffc827 100644 --- a/fail2ban/server/filtersystemd.py +++ b/fail2ban/server/filtersystemd.py @@ -208,7 +208,14 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover if not v: v = logentry.get('_PID') if v: - logelements[-1] += ("[%i]" % v) + try: # [integer] (if already numeric): + v = "[%i]" % v + except TypeError: + try: # as [integer] (try to convert to int): + v = "[%i]" % int(v, 0) + except (TypeError, ValueError): # fallback - [string] as it is + v = "[%s]" % v + logelements[-1] += v logelements[-1] += ":" if logelements[-1] == "kernel:": if '_SOURCE_MONOTONIC_TIMESTAMP' in logentry: diff --git a/fail2ban/server/jail.py b/fail2ban/server/jail.py index c6d61856..9453e205 100644 --- a/fail2ban/server/jail.py +++ b/fail2ban/server/jail.py @@ -281,7 +281,7 @@ class Jail(object): # use ban time as search time if we have not enabled a increasing: forbantime = self.actions.getBanTime() for ticket in self.database.getCurrentBans(jail=self, forbantime=forbantime, - correctBanTime=correctBanTime + correctBanTime=correctBanTime, maxmatches=self.filter.failManager.maxMatches ): try: #logSys.debug('restored ticket: %s', ticket) diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py index 39e797dd..853eb75c 100644 --- a/fail2ban/server/server.py +++ b/fail2ban/server/server.py @@ -459,6 +459,12 @@ class Server: def getUseDns(self, name): return self.__jails[name].filter.getUseDns() + def setMaxMatches(self, name, value): + self.__jails[name].filter.failManager.maxMatches = value + + def getMaxMatches(self, name): + return self.__jails[name].filter.failManager.maxMatches + def setMaxRetry(self, name, value): self.__jails[name].filter.setMaxRetry(value) diff --git a/fail2ban/server/ticket.py b/fail2ban/server/ticket.py index 5debc79d..09e19cf0 100644 --- a/fail2ban/server/ticket.py +++ b/fail2ban/server/ticket.py @@ -144,7 +144,13 @@ class Ticket(object): return self._data['failures'] def setMatches(self, matches): - self._data['matches'] = matches or [] + if matches: + self._data['matches'] = matches + else: + try: + del self._data['matches'] + except KeyError: + pass def getMatches(self): return [(line if not isinstance(line, (list, tuple)) else "".join(line)) \ diff --git a/fail2ban/server/transmitter.py b/fail2ban/server/transmitter.py index d46c8e76..f83e9d5f 100644 --- a/fail2ban/server/transmitter.py +++ b/fail2ban/server/transmitter.py @@ -183,10 +183,19 @@ class Transmitter: else: if self.__quiet: return return db.filename + elif name == "dbmaxmatches": + db = self.__server.getDatabase() + if db is None: + logSys.log(logging.MSG, "dbmaxmatches setting was not in effect since no db yet") + return None + else: + db.maxMatches = int(command[1]) + if self.__quiet: return + return db.maxMatches elif name == "dbpurgeage": db = self.__server.getDatabase() if db is None: - logSys.warning("dbpurgeage setting was not in effect since no db yet") + logSys.log(logging.MSG, "dbpurgeage setting was not in effect since no db yet") return None else: db.purgeage = command[1] @@ -310,6 +319,11 @@ class Transmitter: self.__server.setLogTimeZone(name, value) if self.__quiet: return return self.__server.getLogTimeZone(name) + elif command[1] == "maxmatches": + value = command[2] + self.__server.setMaxMatches(name, int(value)) + if self.__quiet: return + return self.__server.getMaxMatches(name) elif command[1] == "maxretry": value = command[2] self.__server.setMaxRetry(name, int(value)) @@ -404,6 +418,12 @@ class Transmitter: return None else: return db.filename + elif name == "dbmaxmatches": + db = self.__server.getDatabase() + if db is None: + return None + else: + return db.maxMatches elif name == "dbpurgeage": db = self.__server.getDatabase() if db is None: @@ -439,6 +459,8 @@ class Transmitter: return self.__server.getDatePattern(name) elif command[1] == "logtimezone": return self.__server.getLogTimeZone(name) + elif command[1] == "maxmatches": + return self.__server.getMaxMatches(name) elif command[1] == "maxretry": return self.__server.getMaxRetry(name) elif command[1] == "maxlines": diff --git a/fail2ban/tests/clientreadertestcase.py b/fail2ban/tests/clientreadertestcase.py index 29629a53..8320370b 100644 --- a/fail2ban/tests/clientreadertestcase.py +++ b/fail2ban/tests/clientreadertestcase.py @@ -881,18 +881,20 @@ class JailsReaderTest(LogCaptureTestCase): self.assertTrue( find_set('syslogsocket') < find_set('loglevel') < find_set('logtarget') ) - # then dbfile should be before dbpurgeage + # then dbfile should be before dbmaxmatches and dbpurgeage self.assertTrue(find_set('dbpurgeage') > find_set('dbfile')) + self.assertTrue(find_set('dbmaxmatches') > find_set('dbfile')) # and there is logging information left to be passed into the # server - self.assertSortedEqual(commands, - [['set', 'dbfile', - '/var/lib/fail2ban/fail2ban.sqlite3'], - ['set', 'dbpurgeage', '1d'], - ['set', 'loglevel', "INFO"], - ['set', 'logtarget', '/var/log/fail2ban.log'], - ['set', 'syslogsocket', 'auto']]) + self.assertSortedEqual(commands,[ + ['set', 'syslogsocket', 'auto'], + ['set', 'loglevel', "INFO"], + ['set', 'logtarget', '/var/log/fail2ban.log'], + ['set', 'dbfile', '/var/lib/fail2ban/fail2ban.sqlite3'], + ['set', 'dbmaxmatches', 10], + ['set', 'dbpurgeage', '1d'], + ]) # and if we force change configurator's fail2ban's baseDir # there should be an error message (test visually ;) -- diff --git a/fail2ban/tests/databasetestcase.py b/fail2ban/tests/databasetestcase.py index 248ea9fd..a1df2993 100644 --- a/fail2ban/tests/databasetestcase.py +++ b/fail2ban/tests/databasetestcase.py @@ -368,9 +368,9 @@ class DatabaseTest(LogCaptureTestCase): # be returned self.assertEqual(len(self.db.getBans(jail=self.jail,bantime=-1)), 2) - def testGetBansMerged_MaxEntries(self): + def testGetBansMerged_MaxMatches(self): self.testAddJail() - maxEntries = 2 + maxMatches = 2 failures = [ {"matches": ["abc\n"], "user": set(['test'])}, {"matches": ["123\n"], "user": set(['test'])}, @@ -386,29 +386,44 @@ class DatabaseTest(LogCaptureTestCase): ticket.setAttempt(1) self.db.addBan(self.jail, ticket) # should retrieve 2 matches only, but count of all attempts: - self.db.maxEntries = maxEntries; + self.db.maxMatches = maxMatches; 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(), matches2find[-maxEntries:]) + self.assertEqual(len(ticket.getMatches()), maxMatches) + self.assertEqual(ticket.getMatches(), matches2find[-maxMatches:]) # add more failures at once: ticket = FailTicket("127.0.0.1", MyTime.time() - 10, matches2find, data={"user": set(['test', 'root'])}) 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(), matches2find[-maxEntries:]) + self.assertEqual(len(ticket.getMatches()), maxMatches) + self.assertEqual(ticket.getMatches(), matches2find[-maxMatches:]) # also using getCurrentBans: ticket = self.db.getCurrentBans(self.jail, "127.0.0.1", fromtime=MyTime.time()-100) self.assertTrue(ticket is not None) self.assertEqual(ticket.getAttempt(), len(failures)) - self.assertEqual(len(ticket.getMatches()), maxEntries) - self.assertEqual(ticket.getMatches(), matches2find[-maxEntries:]) + self.assertEqual(len(ticket.getMatches()), maxMatches) + self.assertEqual(ticket.getMatches(), matches2find[-maxMatches:]) + # maxmatches of jail < dbmaxmatches (so read 1 match and 0 matches): + ticket = self.db.getCurrentBans(self.jail, "127.0.0.1", fromtime=MyTime.time()-100, + maxmatches=1) + self.assertEqual(len(ticket.getMatches()), 1) + self.assertEqual(ticket.getMatches(), failures[3]['matches']) + ticket = self.db.getCurrentBans(self.jail, "127.0.0.1", fromtime=MyTime.time()-100, + maxmatches=0) + self.assertEqual(len(ticket.getMatches()), 0) + # dbmaxmatches = 0, should retrieve 0 matches by last ban: + ticket.setMatches(["1","2","3"]) + self.db.maxMatches = 0; + self.db.addBan(self.jail, ticket) + ticket = self.db.getCurrentBans(self.jail, "127.0.0.1", fromtime=MyTime.time()-100) + self.assertTrue(ticket is not None) + self.assertEqual(ticket.getAttempt(), len(failures)) + self.assertEqual(len(ticket.getMatches()), 0) def testGetBansMerged(self): self.testAddJail() diff --git a/fail2ban/tests/fail2banclienttestcase.py b/fail2ban/tests/fail2banclienttestcase.py index dc2df024..6b67fe14 100644 --- a/fail2ban/tests/fail2banclienttestcase.py +++ b/fail2ban/tests/fail2banclienttestcase.py @@ -208,6 +208,7 @@ def _start_params(tmp, use_stock=False, use_stock_cfg=None, "pidfile = " + pjoin(tmp, "f2b.pid"), "backend = polling", "dbfile = " + db, + "dbmaxmatches = 100", "dbpurgeage = 1d", "", ) diff --git a/fail2ban/tests/failmanagertestcase.py b/fail2ban/tests/failmanagertestcase.py index ad89ec76..a5425286 100644 --- a/fail2ban/tests/failmanagertestcase.py +++ b/fail2ban/tests/failmanagertestcase.py @@ -69,9 +69,9 @@ class AddFailure(unittest.TestCase): self.assertEqual(self.__failManager.getFailTotal(), 0) self.__failManager.setFailTotal(13) - def testFailManagerAdd_MaxEntries(self): - maxEntries = 2 - self.__failManager.maxEntries = maxEntries + def testFailManagerAdd_MaxMatches(self): + maxMatches = 2 + self.__failManager.maxMatches = maxMatches failures = ["abc\n", "123\n", "ABC\n", "1234\n"] # add failures sequential: i = 80 @@ -86,8 +86,8 @@ class AddFailure(unittest.TestCase): 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:]) + self.assertEqual(len(ticket.getMatches()), maxMatches) + self.assertEqual(ticket.getMatches(), failures[len(failures) - maxMatches:]) # add more failures at once: ticket = FailTicket("127.0.0.1", 1000002000 - 10, failures) ticket.setAttempt(len(failures)) @@ -98,8 +98,8 @@ class AddFailure(unittest.TestCase): 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:]) + self.assertEqual(len(ticket.getMatches()), maxMatches) + self.assertEqual(ticket.getMatches(), failures[len(failures) - maxMatches:]) # add self ticket again: self.__failManager.addFailure(ticket) # @@ -108,8 +108,16 @@ class AddFailure(unittest.TestCase): 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:]) + self.assertEqual(len(ticket.getMatches()), maxMatches) + self.assertEqual(ticket.getMatches(), failures[len(failures) - maxMatches:]) + # no matches by maxMatches == 0 : + self.__failManager.maxMatches = 0 + self.__failManager.addFailure(ticket) + manFailList = self.__failManager._FailManager__failList + ticket = manFailList["127.0.0.1"] + self.assertEqual(len(ticket.getMatches()), 0) + # test set matches None to None: + ticket.setMatches(None) def testFailManagerMaxTime(self): self._addDefItems() diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index dd5cc08f..a3239c34 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -197,6 +197,8 @@ class Transmitter(TransmitterBase): self.setGetTest("dbfile", tmpFilename) # the same file name (again no jails / not changed): self.setGetTest("dbfile", tmpFilename) + self.setGetTest("dbmaxmatches", "100", 100) + self.setGetTestNOK("dbmaxmatches", "LIZARD") self.setGetTest("dbpurgeage", "600", 600) self.setGetTestNOK("dbpurgeage", "LIZARD") # the same file name (again with jails / not changed): @@ -211,6 +213,12 @@ class Transmitter(TransmitterBase): self.assertEqual(self.transm.proceed( ["get", "dbfile"]), (0, None)) + self.assertEqual(self.transm.proceed( + ["set", "dbmaxmatches", "100"]), + (0, None)) + self.assertEqual(self.transm.proceed( + ["get", "dbmaxmatches"]), + (0, None)) self.assertEqual(self.transm.proceed( ["set", "dbpurgeage", "500"]), (0, None)) @@ -418,6 +426,12 @@ class Transmitter(TransmitterBase): _getBanListTest(jail, unbanip="192.168.0.1", outList=[]) + def testJailMaxMatches(self): + self.setGetTest("maxmatches", "5", 5, jail=self.jailName) + self.setGetTest("maxmatches", "2", 2, jail=self.jailName) + self.setGetTest("maxmatches", "-2", -2, jail=self.jailName) + self.setGetTestNOK("maxmatches", "Duck", jail=self.jailName) + def testJailMaxRetry(self): self.setGetTest("maxretry", "5", 5, jail=self.jailName) self.setGetTest("maxretry", "2", 2, jail=self.jailName) diff --git a/man/fail2ban-client.1 b/man/fail2ban-client.1 index abd49d00..89a1cb51 100644 --- a/man/fail2ban-client.1 +++ b/man/fail2ban-client.1 @@ -168,6 +168,14 @@ persistent datastore. Set to get the location of fail2ban persistent datastore .TP +\fBset dbmaxmatches \fR +sets the max number of matches +stored in database per ticket +.TP +\fBget dbmaxmatches\fR +gets the max number of matches +stored in database per ticket +.TP \fBset dbpurgeage \fR sets the max age in that history of bans will be kept @@ -286,6 +294,11 @@ sets the number of failures before banning the host for .TP +\fBset maxmatches \fR +sets the max number of matches +stored in memory per ticket in + +.TP \fBset maxlines \fR sets the number of to buffer for regex search for @@ -402,6 +415,11 @@ are ordered by end of ban. gets the number of failures allowed for .TP +\fBget maxmatches\fR +gets the max number of matches +stored in memory per ticket in + +.TP \fBget maxlines\fR gets the number of lines to buffer for diff --git a/man/jail.conf.5 b/man/jail.conf.5 index 0f3a4aa7..4f5c48e0 100644 --- a/man/jail.conf.5 +++ b/man/jail.conf.5 @@ -156,6 +156,11 @@ Database filename. Default: /var/lib/fail2ban/fail2ban.sqlite3 .br This defines where the persistent data for fail2ban is stored. This persistent data allows bans to be reinstated and continue reading log files from the last read position when fail2ban is restarted. A value of \fINone\fR disables this feature. .TP +.B dbmaxmatches +Max number of matches stored in database per ticket. Default: 10 +.br +This option sets the max number of matched log-lines could be stored per ticket in the database. This also affects values resolvable via tags \fB\fR and \fB\fR in actions. +.TP .B dbpurgeage Database purge age in seconds. Default: 86400 (24hours) .br @@ -276,6 +281,9 @@ regex (Python \fBreg\fRular \fBex\fRpression) to be added to the filter's failre .TP .B ignoreregex regex which, if the log line matches, would cause Fail2Ban not consider that line. This line will be ignored even if it matches a failregex of the jail or any of its filters. +.TP +.B maxmatches +max number of matched log-lines the jail would hold in memory per ticket. By default it is the same value as \fBmaxretry\fR of jail (or default). This option also affects values resolvable via tag \fB\fR in actions. .SS Backends Available options are listed below.