introduced new option `ignorecache` to improve performance of ignore failure check (using caching of `ignoreip`, `ignoreself` and `ignorecommand`)

pull/2176/head
sebres 2018-07-09 14:58:39 +02:00
parent 9b6d17d07e
commit f8f01d5ab7
9 changed files with 127 additions and 19 deletions

View File

@ -50,6 +50,8 @@ ver. 0.10.4-dev-1 (20??/??/??) - development edition
* systemd: fixed type error on option `journalflags`: an integer is required (gh-2125); * systemd: fixed type error on option `journalflags`: an integer is required (gh-2125);
### New Features ### New Features
* new option `ignorecache` to improve performance of ignore failure check (using caching of `ignoreip`,
`ignoreself` and `ignorecommand`), see `man jail.conf` for syntax-example;
* `ignorecommand` extended to use actions-similar replacement (capable to interpolate * `ignorecommand` extended to use actions-similar replacement (capable to interpolate
all possible tags like `<ip-host>`, `<family>`, `<fid>`, `F-USER` etc.) all possible tags like `<ip-host>`, `<family>`, `<fid>`, `F-USER` etc.)

View File

@ -100,6 +100,7 @@ class JailReader(ConfigReader):
["string", "ignorecommand", None], ["string", "ignorecommand", None],
["bool", "ignoreself", None], ["bool", "ignoreself", None],
["string", "ignoreip", None], ["string", "ignoreip", None],
["string", "ignorecache", None],
["string", "filter", ""], ["string", "filter", ""],
["string", "datepattern", None], ["string", "datepattern", None],
["string", "logtimezone", None], ["string", "logtimezone", None],

View File

@ -84,6 +84,8 @@ protocol = [
["set <JAIL> ignoreself true|false", "allows the ignoring of own IP addresses"], ["set <JAIL> ignoreself true|false", "allows the ignoring of own IP addresses"],
["set <JAIL> addignoreip <IP>", "adds <IP> to the ignore list of <JAIL>"], ["set <JAIL> addignoreip <IP>", "adds <IP> to the ignore list of <JAIL>"],
["set <JAIL> delignoreip <IP>", "removes <IP> from the ignore list of <JAIL>"], ["set <JAIL> delignoreip <IP>", "removes <IP> from the ignore list of <JAIL>"],
["set <JAIL> ignorecommand <VALUE>", "sets ignorecommand of <JAIL>"],
["set <JAIL> ignorecache <VALUE>", "sets ignorecache of <JAIL>"],
["set <JAIL> addlogpath <FILE> ['tail']", "adds <FILE> to the monitoring list of <JAIL>, optionally starting at the 'tail' of the file (default 'head')."], ["set <JAIL> addlogpath <FILE> ['tail']", "adds <FILE> to the monitoring list of <JAIL>, optionally starting at the 'tail' of the file (default 'head')."],
["set <JAIL> dellogpath <FILE>", "removes <FILE> from the monitoring list of <JAIL>"], ["set <JAIL> dellogpath <FILE>", "removes <FILE> from the monitoring list of <JAIL>"],
["set <JAIL> logencoding <ENCODING>", "sets the <ENCODING> of the log files for <JAIL>"], ["set <JAIL> logencoding <ENCODING>", "sets the <ENCODING> of the log files for <JAIL>"],
@ -91,7 +93,6 @@ protocol = [
["set <JAIL> deljournalmatch <MATCH>", "removes <MATCH> from the journal filter of <JAIL>"], ["set <JAIL> deljournalmatch <MATCH>", "removes <MATCH> from the journal filter of <JAIL>"],
["set <JAIL> addfailregex <REGEX>", "adds the regular expression <REGEX> which must match failures for <JAIL>"], ["set <JAIL> addfailregex <REGEX>", "adds the regular expression <REGEX> which must match failures for <JAIL>"],
["set <JAIL> delfailregex <INDEX>", "removes the regular expression at <INDEX> for failregex"], ["set <JAIL> delfailregex <INDEX>", "removes the regular expression at <INDEX> for failregex"],
["set <JAIL> ignorecommand <VALUE>", "sets ignorecommand of <JAIL>"],
["set <JAIL> addignoreregex <REGEX>", "adds the regular expression <REGEX> which should match pattern to exclude for <JAIL>"], ["set <JAIL> addignoreregex <REGEX>", "adds the regular expression <REGEX> which should match pattern to exclude for <JAIL>"],
["set <JAIL> delignoreregex <INDEX>", "removes the regular expression at <INDEX> for ignoreregex"], ["set <JAIL> delignoreregex <INDEX>", "removes the regular expression at <INDEX> for ignoreregex"],
["set <JAIL> findtime <TIME>", "sets the number of seconds <TIME> for which the filter will look back for <JAIL>"], ["set <JAIL> findtime <TIME>", "sets the number of seconds <TIME> for which the filter will look back for <JAIL>"],

View File

@ -81,6 +81,10 @@ class Filter(JailThread):
self.__ignoreSelf = True self.__ignoreSelf = True
## The ignore IP list. ## The ignore IP list.
self.__ignoreIpList = [] self.__ignoreIpList = []
## External command
self.__ignoreCommand = False
## Cache for ignoreip:
self.__ignoreCache = None
## Size of line buffer ## Size of line buffer
self.__lineBufferSize = 1 self.__lineBufferSize = 1
## Line buffer ## Line buffer
@ -90,8 +94,6 @@ class Filter(JailThread):
self.__lastDate = None self.__lastDate = None
## if set, treat log lines without explicit time zone to be in this time zone ## if set, treat log lines without explicit time zone to be in this time zone
self.__logtimezone = None self.__logtimezone = None
## External command
self.__ignoreCommand = False
## Default or preferred encoding (to decode bytes from file or journal): ## Default or preferred encoding (to decode bytes from file or journal):
self.__encoding = PREFER_ENC self.__encoding = PREFER_ENC
## Cache temporary holds failures info (used by multi-line for wrapping e. g. conn-id to host): ## Cache temporary holds failures info (used by multi-line for wrapping e. g. conn-id to host):
@ -397,19 +399,34 @@ class Filter(JailThread):
raise Exception("run() is abstract") raise Exception("run() is abstract")
## ##
# Set external command, for ignoredips # External command, for ignoredips
# #
def setIgnoreCommand(self, command): @property
def ignoreCommand(self):
return self.__ignoreCommand
@ignoreCommand.setter
def ignoreCommand(self, command):
self.__ignoreCommand = command self.__ignoreCommand = command
## ##
# Get external command, for ignoredips # Cache parameters for ignoredips
# #
def getIgnoreCommand(self): @property
return self.__ignoreCommand def ignoreCache(self):
return [self.__ignoreCache[0], self.__ignoreCache[1].maxCount, self.__ignoreCache[1].maxTime] \
if self.__ignoreCache else None
@ignoreCache.setter
def ignoreCache(self, command):
if command:
self.__ignoreCache = command['key'], Utils.Cache(
maxCount=int(command.get('max-count', 100)), maxTime=MyTime.str2seconds(command.get('max-time', 5*60))
)
else:
self.__ignoreCache = None
## ##
# Ban an IP - http://blogs.buanzo.com.ar/2009/04/fail2ban-patch-ban-ip-address-manually.html # Ban an IP - http://blogs.buanzo.com.ar/2009/04/fail2ban-patch-ban-ip-address-manually.html
# Arturo 'Buanzo' Busleiman <buanzo@buanzo.com.ar> # Arturo 'Buanzo' Busleiman <buanzo@buanzo.com.ar>
@ -502,29 +519,48 @@ class Filter(JailThread):
return self._inIgnoreIPList(ip, ticket, log_ignore) return self._inIgnoreIPList(ip, ticket, log_ignore)
def _inIgnoreIPList(self, ip, ticket, log_ignore=True): def _inIgnoreIPList(self, ip, ticket, log_ignore=True):
aInfo = None
# cached ?
if self.__ignoreCache:
key, c = self.__ignoreCache
if ticket:
aInfo = Actions.ActionInfo(ticket, self.jail)
key = CommandAction.replaceDynamicTags(key, aInfo)
else:
aInfo = { 'ip': ip }
key = CommandAction.replaceTag(key, aInfo)
v = c.get(key)
if v is not None:
return v
# check own IPs should be ignored and 'ip' is self IP: # check own IPs should be ignored and 'ip' is self IP:
if self.__ignoreSelf and ip in DNSUtils.getSelfIPs(): if self.__ignoreSelf and ip in DNSUtils.getSelfIPs():
self.logIgnoreIp(ip, log_ignore, ignore_source="ignoreself rule") self.logIgnoreIp(ip, log_ignore, ignore_source="ignoreself rule")
if self.__ignoreCache: c.set(key, True)
return True return True
for net in self.__ignoreIpList: for net in self.__ignoreIpList:
# check if the IP is covered by ignore IP # check if the IP is covered by ignore IP
if ip.isInNet(net): if ip.isInNet(net):
self.logIgnoreIp(ip, log_ignore, ignore_source=("ip" if net.isValid else "dns")) self.logIgnoreIp(ip, log_ignore, ignore_source=("ip" if net.isValid else "dns"))
if self.__ignoreCache: c.set(key, True)
return True return True
if self.__ignoreCommand: if self.__ignoreCommand:
if ticket: if ticket:
aInfo = Actions.ActionInfo(ticket, self.jail) if not aInfo: aInfo = Actions.ActionInfo(ticket, self.jail)
command = CommandAction.replaceDynamicTags(self.__ignoreCommand, aInfo) command = CommandAction.replaceDynamicTags(self.__ignoreCommand, aInfo)
else: else:
command = CommandAction.replaceTag(self.__ignoreCommand, { 'ip': ip }) if not aInfo: aInfo = { 'ip': ip }
command = CommandAction.replaceTag(self.__ignoreCommand, aInfo)
logSys.debug('ignore command: %s', command) logSys.debug('ignore command: %s', command)
ret, ret_ignore = CommandAction.executeCmd(command, success_codes=(0, 1)) ret, ret_ignore = CommandAction.executeCmd(command, success_codes=(0, 1))
ret_ignore = ret and ret_ignore == 0 ret_ignore = ret and ret_ignore == 0
self.logIgnoreIp(ip, log_ignore and ret_ignore, ignore_source="command") self.logIgnoreIp(ip, log_ignore and ret_ignore, ignore_source="command")
if self.__ignoreCache: c.set(key, ret_ignore)
return ret_ignore return ret_ignore
if self.__ignoreCache: c.set(key, False)
return False return False
def processLine(self, line, date=None): def processLine(self, line, date=None):

View File

@ -391,10 +391,17 @@ class Server:
return self.__jails[name].filter.getLogTimeZone() return self.__jails[name].filter.getLogTimeZone()
def setIgnoreCommand(self, name, value): def setIgnoreCommand(self, name, value):
self.__jails[name].filter.setIgnoreCommand(value) self.__jails[name].filter.ignoreCommand = value
def getIgnoreCommand(self, name): def getIgnoreCommand(self, name):
return self.__jails[name].filter.getIgnoreCommand() return self.__jails[name].filter.ignoreCommand
def setIgnoreCache(self, name, value):
value, options = extractOptions("cache["+value+"]")
self.__jails[name].filter.ignoreCache = options
def getIgnoreCache(self, name):
return self.__jails[name].filter.ignoreCache
def setPrefRegex(self, name, value): def setPrefRegex(self, name, value):
flt = self.__jails[name].filter flt = self.__jails[name].filter

View File

@ -200,6 +200,10 @@ class Transmitter:
value = command[2] value = command[2]
self.__server.setIgnoreCommand(name, value) self.__server.setIgnoreCommand(name, value)
return self.__server.getIgnoreCommand(name) return self.__server.getIgnoreCommand(name)
elif command[1] == "ignorecache":
value = command[2]
self.__server.setIgnoreCache(name, value)
return self.__server.getIgnoreCache(name)
elif command[1] == "addlogpath": elif command[1] == "addlogpath":
value = command[2] value = command[2]
tail = False tail = False
@ -358,6 +362,8 @@ class Transmitter:
return self.__server.getIgnoreIP(name) return self.__server.getIgnoreIP(name)
elif command[1] == "ignorecommand": elif command[1] == "ignorecommand":
return self.__server.getIgnoreCommand(name) return self.__server.getIgnoreCommand(name)
elif command[1] == "ignorecache":
return self.__server.getIgnoreCache(name)
elif command[1] == "prefregex": elif command[1] == "prefregex":
return self.__server.getPrefRegex(name) return self.__server.getPrefRegex(name)
elif command[1] == "failregex": elif command[1] == "failregex":

View File

@ -401,7 +401,7 @@ class IgnoreIP(LogCaptureTestCase):
self.assertLogged('Requested to manually ban an ignored IP 192.168.1.32. User knows best. Proceeding to ban it.') self.assertLogged('Requested to manually ban an ignored IP 192.168.1.32. User knows best. Proceeding to ban it.')
def testIgnoreCommand(self): def testIgnoreCommand(self):
self.filter.setIgnoreCommand(sys.executable + ' ' + os.path.join(TEST_FILES_DIR, "ignorecommand.py <ip>")) self.filter.ignoreCommand = sys.executable + ' ' + os.path.join(TEST_FILES_DIR, "ignorecommand.py <ip>")
self.assertTrue(self.filter.inIgnoreIPList("10.0.0.1")) self.assertTrue(self.filter.inIgnoreIPList("10.0.0.1"))
self.assertFalse(self.filter.inIgnoreIPList("10.0.0.0")) self.assertFalse(self.filter.inIgnoreIPList("10.0.0.0"))
self.assertLogged("returned successfully 0", "returned successfully 1", all=True) self.assertLogged("returned successfully 0", "returned successfully 1", all=True)
@ -411,7 +411,7 @@ class IgnoreIP(LogCaptureTestCase):
def testIgnoreCommandForTicket(self): def testIgnoreCommandForTicket(self):
# by host of IP (2001:db8::1 and 2001:db8::ffff map to "test-host" and "test-other" in the test-suite): # by host of IP (2001:db8::1 and 2001:db8::ffff map to "test-host" and "test-other" in the test-suite):
self.filter.setIgnoreCommand('if [ "<ip-host>" = "test-host" ]; then exit 0; fi; exit 1') self.filter.ignoreCommand = 'if [ "<ip-host>" = "test-host" ]; then exit 0; fi; exit 1'
self.pruneLog() self.pruneLog()
self.assertTrue(self.filter.inIgnoreIPList(FailTicket("2001:db8::1"))) self.assertTrue(self.filter.inIgnoreIPList(FailTicket("2001:db8::1")))
self.assertLogged("returned successfully 0") self.assertLogged("returned successfully 0")
@ -419,7 +419,7 @@ class IgnoreIP(LogCaptureTestCase):
self.assertFalse(self.filter.inIgnoreIPList(FailTicket("2001:db8::ffff"))) self.assertFalse(self.filter.inIgnoreIPList(FailTicket("2001:db8::ffff")))
self.assertLogged("returned successfully 1") self.assertLogged("returned successfully 1")
# by user-name (ignore tester): # by user-name (ignore tester):
self.filter.setIgnoreCommand('if [ "<F-USER>" = "tester" ]; then exit 0; fi; exit 1') self.filter.ignoreCommand = 'if [ "<F-USER>" = "tester" ]; then exit 0; fi; exit 1'
self.pruneLog() self.pruneLog()
self.assertTrue(self.filter.inIgnoreIPList(FailTicket("tester", data={'user': 'tester'}))) self.assertTrue(self.filter.inIgnoreIPList(FailTicket("tester", data={'user': 'tester'})))
self.assertLogged("returned successfully 0") self.assertLogged("returned successfully 0")
@ -427,6 +427,42 @@ class IgnoreIP(LogCaptureTestCase):
self.assertFalse(self.filter.inIgnoreIPList(FailTicket("root", data={'user': 'root'}))) self.assertFalse(self.filter.inIgnoreIPList(FailTicket("root", data={'user': 'root'})))
self.assertLogged("returned successfully 1", all=True) self.assertLogged("returned successfully 1", all=True)
def testIgnoreCache(self):
# like both test-cases above, just cached (so once per key)...
self.filter.ignoreCache = {"key":"<ip>"}
self.filter.ignoreCommand = 'if [ "<ip>" = "10.0.0.1" ]; then exit 0; fi; exit 1'
for i in xrange(5):
self.pruneLog()
self.assertTrue(self.filter.inIgnoreIPList("10.0.0.1"))
self.assertFalse(self.filter.inIgnoreIPList("10.0.0.0"))
if not i:
self.assertLogged("returned successfully 0", "returned successfully 1", all=True)
else:
self.assertNotLogged("returned successfully 0", "returned successfully 1", all=True)
# by host of IP:
self.filter.ignoreCache = {"key":"<ip-host>"}
self.filter.ignoreCommand = 'if [ "<ip-host>" = "test-host" ]; then exit 0; fi; exit 1'
for i in xrange(5):
self.pruneLog()
self.assertTrue(self.filter.inIgnoreIPList(FailTicket("2001:db8::1")))
self.assertFalse(self.filter.inIgnoreIPList(FailTicket("2001:db8::ffff")))
if not i:
self.assertLogged("returned successfully")
else:
self.assertNotLogged("returned successfully")
# by user-name:
self.filter.ignoreCache = {"key":"<F-USER>", "max-count":"10", "max-time":"1h"}
self.assertEqual(self.filter.ignoreCache, ["<F-USER>", 10, 60*60])
self.filter.ignoreCommand = 'if [ "<F-USER>" = "tester" ]; then exit 0; fi; exit 1'
for i in xrange(5):
self.pruneLog()
self.assertTrue(self.filter.inIgnoreIPList(FailTicket("tester", data={'user': 'tester'})))
self.assertFalse(self.filter.inIgnoreIPList(FailTicket("root", data={'user': 'root'})))
if not i:
self.assertLogged("returned successfully")
else:
self.assertNotLogged("returned successfully")
def testIgnoreCauseOK(self): def testIgnoreCauseOK(self):
ip = "93.184.216.34" ip = "93.184.216.34"
for ignore_source in ["dns", "ip", "command"]: for ignore_source in ["dns", "ip", "command"]:
@ -490,7 +526,7 @@ class IgnoreIPDNS(LogCaptureTestCase):
self.assertRaises(ValueError, lambda: mod.is_googlebot(mod.process_args([cmd]))) self.assertRaises(ValueError, lambda: mod.is_googlebot(mod.process_args([cmd])))
self.assertRaises(ValueError, lambda: mod.is_googlebot(mod.process_args([cmd, "192.0"]))) self.assertRaises(ValueError, lambda: mod.is_googlebot(mod.process_args([cmd, "192.0"])))
## via command: ## via command:
self.filter.setIgnoreCommand(cmd + " <ip>") self.filter.ignoreCommand = cmd + " <ip>"
for ip in bot_ips: for ip in bot_ips:
self.assertTrue(self.filter.inIgnoreIPList(str(ip)), "test of googlebot ip %s failed" % ip) self.assertTrue(self.filter.inIgnoreIPList(str(ip)), "test of googlebot ip %s failed" % ip)
self.assertLogged('-- returned successfully') self.assertLogged('-- returned successfully')
@ -498,7 +534,7 @@ class IgnoreIPDNS(LogCaptureTestCase):
self.assertFalse(self.filter.inIgnoreIPList("192.0")) self.assertFalse(self.filter.inIgnoreIPList("192.0"))
self.assertLogged('Argument must be a single valid IP.') self.assertLogged('Argument must be a single valid IP.')
self.pruneLog() self.pruneLog()
self.filter.setIgnoreCommand(cmd + " bad arguments <ip>") self.filter.ignoreCommand = cmd + " bad arguments <ip>"
self.assertFalse(self.filter.inIgnoreIPList("192.0")) self.assertFalse(self.filter.inIgnoreIPList("192.0"))
self.assertLogged('Please provide a single IP as an argument.') self.assertLogged('Please provide a single IP as an argument.')

View File

@ -463,7 +463,14 @@ class Transmitter(TransmitterBase):
(0, False)) (0, False))
def testJailIgnoreCommand(self): def testJailIgnoreCommand(self):
self.setGetTest("ignorecommand", "bin ", jail=self.jailName) self.setGetTest("ignorecommand", "bin/ignore-command <ip>", jail=self.jailName)
def testJailIgnoreCache(self):
self.setGetTest("ignorecache",
'key="<ip>",max-time=1d,max-count=9999',
["<ip>", 9999, 24*60*60],
jail=self.jailName)
self.setGetTest("ignorecache", '', None, jail=self.jailName)
def testJailRegex(self): def testJailRegex(self):
self.jailAddDelRegexTest("failregex", self.jailAddDelRegexTest("failregex",

View File

@ -233,7 +233,19 @@ list of IPs not to ban. They can include a DNS resp. CIDR mask too. The option a
command that is executed to determine if the current candidate IP for banning (or failure-ID for raw IDs) should not be banned. The option affects additionally to \fBignoreself\fR and \fBignoreip\fR and will be first executed if both don't hit. command that is executed to determine if the current candidate IP for banning (or failure-ID for raw IDs) should not be banned. The option affects additionally to \fBignoreself\fR and \fBignoreip\fR and will be first executed if both don't hit.
.br .br
IP will not be banned if command returns successfully (exit code 0). IP will not be banned if command returns successfully (exit code 0).
Like ACTION FILES, tags like <ip> are can be included in the ignorecommand value and will be substituted before execution. Currently only <ip> is supported however more will be added later. Like ACTION FILES, tags like <ip> are can be included in the ignorecommand value and will be substituted before execution.
.TP
.B ignorecache
provide cache parameters (default disabled) for ignore failure check (caching of the result from `ignoreip`, `ignoreself` and `ignorecommand`), syntax:
.RS
.nf
ignorecache = key="<F-USER>@<ip-host>", max-count=100, max-time=5m
ignorecommand = if [ "<F-USER>" = "technical" ] && [ "<ip-host>" = "my-host.example.com" ]; then exit 0; fi;
exit 1
.fi
This will cache the result of \fBignorecommand\fR (does not call it repeatedly) for 5 minutes (cache time) for maximal 100 entries (cache size), using values substituted like "user@host" as cache-keys. Set option \fBignorecache\fR to empty value disables the cache.
.RE
.TP .TP
.B bantime .B bantime
effective ban duration (in seconds or time abbreviation format). effective ban duration (in seconds or time abbreviation format).