NF: Add systemd journal backend

pull/224/head
Steven Hiscocks 2013-05-10 00:15:07 +01:00
parent 7a86d30c6d
commit f7d328195f
18 changed files with 596 additions and 8 deletions

View File

@ -25,6 +25,12 @@ __license__ = "GPL"
import getopt, sys, time, logging, os, locale import getopt, sys, time, logging, os, locale
from ConfigParser import NoOptionError, NoSectionError, MissingSectionHeaderError from ConfigParser import NoOptionError, NoSectionError, MissingSectionHeaderError
try:
from fail2ban.server.filtersystemd import FilterSystemd
from systemd import journal
except:
journal = None
from fail2ban.version import version from fail2ban.version import version
from fail2ban.client.configparserinc import SafeConfigParserWithIncludes from fail2ban.client.configparserinc import SafeConfigParserWithIncludes
from fail2ban.server.filter import Filter from fail2ban.server.filter import Filter
@ -69,6 +75,7 @@ class Fail2banRegex:
self.__filter = Filter(None) self.__filter = Filter(None)
self.__ignoreregex = list() self.__ignoreregex = list()
self.__failregex = list() self.__failregex = list()
self.__journalmatch = ""
self.__verbose = False self.__verbose = False
self.__maxlines_set = False # so we allow to override maxlines in cmdline self.__maxlines_set = False # so we allow to override maxlines in cmdline
self.encoding = locale.getpreferredencoding() self.encoding = locale.getpreferredencoding()
@ -111,10 +118,14 @@ class Fail2banRegex:
print " -V, --version print the version" print " -V, --version print the version"
print " -v, --verbose verbose output" print " -v, --verbose verbose output"
print " -l INT, --maxlines=INT set maxlines for multi-line regex default: 1" print " -l INT, --maxlines=INT set maxlines for multi-line regex default: 1"
print " -m MATCHES, --matches=MATCHES"
print " journalctl style matches, overriding filter file."
print " Special value \"ALL\" searches entire journal"
print print
print "Log:" print "Log:"
print " string a string representing a log line" print " string a string representing a log line"
print " filename path to a log file (/var/log/auth.log)" print " filename path to a log file (/var/log/auth.log)"
print " \"systemd-journal\" search systemd journal (systemd python required)"
print print
print "Regex:" print "Regex:"
print " string a string representing a 'failregex'" print " string a string representing a 'failregex'"
@ -223,6 +234,10 @@ class Fail2banRegex:
print "ERROR: Invalid value for maxlines (%(maxlines)r) " \ print "ERROR: Invalid value for maxlines (%(maxlines)r) " \
"read from %(value)s" % locals() "read from %(value)s" % locals()
return False return False
try:
self.__journalmatch = reader.get("Init", "journalmatch")
except (NoSectionError, NoOptionError):
pass
else: else:
if len(value) > 53: if len(value) > 53:
stripReg = value[0:50] + "..." stripReg = value[0:50] + "..."
@ -342,13 +357,16 @@ class Fail2banRegex:
print "information." print "information."
return True return True
def getJournalMatch(self):
return self.__journalmatch
if __name__ == "__main__": if __name__ == "__main__":
fail2banRegex = Fail2banRegex() fail2banRegex = Fail2banRegex()
# Reads the command line options. # Reads the command line options.
try: try:
cmdOpts = 'hVcvl:e:' cmdOpts = 'hVcvl:e:m:'
cmdLongOpts = ['help', 'version', 'verbose', 'maxlines=', 'encoding='] cmdLongOpts = ['help', 'version', 'verbose', 'maxlines=', 'encoding=',
'matches=']
optList, args = getopt.getopt(sys.argv[1:], cmdOpts, cmdLongOpts) optList, args = getopt.getopt(sys.argv[1:], cmdOpts, cmdLongOpts)
except getopt.GetoptError: except getopt.GetoptError:
fail2banRegex.dispUsage() fail2banRegex.dispUsage()
@ -391,6 +409,42 @@ if __name__ == "__main__":
print e print e
print print
sys.exit(-1) sys.exit(-1)
elif cmd_log == "systemd-journal":
if journal is None:
print "Error: systemd library not found. Exiting..."
sys.exit(-1)
myjournal = journal.Reader()
journalmatch = ""
# Parse journal matches from command line
for opt in optList:
if opt[0] in ["-m", "--matches"]:
journalmatch = opt[1]
# If no command line option, take journal match from filter
if not journalmatch:
journalmatch = fail2banRegex.getJournalMatch()
try:
if journalmatch != "ALL":
for element in journalmatch.split():
if element == "+":
myjournal.add_disjunction()
else:
myjournal.add_match(element)
except ValueError:
print "Error: Invalid journal match: %s" % journalmatch
print "Exiting..."
sys.exit(-1)
print "Use systemd journal match: %s" % (journalmatch or "ALL")
while True:
try:
entry = myjournal.get_next()
except OSError:
continue
else:
if not entry:
break
line = FilterSystemd.formatJournalEntry(entry)
fail2banRegex.testIgnoreRegex(line)
fail2banRegex.testRegex(line)
else: else:
if len(sys.argv[1]) > 53: if len(sys.argv[1]) > 53:
stripLog = cmd_log[0:50] + "..." stripLog = cmd_log[0:50] + "..."

View File

@ -21,3 +21,11 @@ failregex = .*(?:pop3-login|imap-login):.*(?:Authentication failure|Aborted logi
# Values: TEXT # Values: TEXT
# #
ignoreregex = ignoreregex =
[Init]
# Option: journalmatch
# Notes.: systemd journalctl style match filter for journal based backends
# Values: TEXT
#
journalmatch = _SYSTEMD_UNIT=dovecot.service

View File

@ -21,3 +21,11 @@ failregex = reject: RCPT from (.*)\[<HOST>\]: 554
# Values: TEXT # Values: TEXT
# #
ignoreregex = ignoreregex =
[Init]
# Option: journalmatch
# Notes.: systemd journalctl style match filter for journal based backends
# Values: TEXT
#
journalmatch = _SYSTEMD_UNIT=postfix.service

View File

@ -36,3 +36,11 @@ failregex = fail2ban.actions:\s+WARNING\s+\[(?:.*)\]\s+Ban\s+<HOST>
# #
# Ignore our own bans, to keep our counts exact. # Ignore our own bans, to keep our counts exact.
ignoreregex = fail2ban.actions:\s+WARNING\s+\[%(_jailname)s\]\s+Ban\s+<HOST> ignoreregex = fail2ban.actions:\s+WARNING\s+\[%(_jailname)s\]\s+Ban\s+<HOST>
[Init]
# Option: journalmatch
# Notes.: systemd journalctl style match filter for journal based backends
# Values: TEXT
#
journalmatch = _SYSTEMD_UNIT=fail2ban.service

View File

@ -34,3 +34,11 @@ failregex = ^%(__prefix_line)sDid not receive identification string from <HOST>\
# Values: TEXT # Values: TEXT
# #
ignoreregex = ignoreregex =
[Init]
# Option: journalmatch
# Notes.: systemd journalctl style match filter for journal based backend
# Values: TEXT
#
journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd

View File

@ -39,3 +39,11 @@ failregex = ^%(__prefix_line)s(?:error: PAM: )?[aA]uthentication (?:failure|erro
# Values: TEXT # Values: TEXT
# #
ignoreregex = ignoreregex =
[Init]
# Option: journalmatch
# Notes.: systemd journalctl style match filter for journal based backend
# Values: TEXT
#
journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd

View File

@ -42,7 +42,7 @@ findtime = 600
maxretry = 5 maxretry = 5
# "backend" specifies the backend used to get files modification. # "backend" specifies the backend used to get files modification.
# Available options are "pyinotify", "gamin", "polling" and "auto". # Available options are "pyinotify", "gamin", "polling", "systemd" and "auto".
# This option can be overridden in each jail as well. # This option can be overridden in each jail as well.
# #
# pyinotify: requires pyinotify (a file alteration monitor) to be installed. # pyinotify: requires pyinotify (a file alteration monitor) to be installed.
@ -50,6 +50,9 @@ maxretry = 5
# gamin: requires Gamin (a file alteration monitor) to be installed. # gamin: requires Gamin (a file alteration monitor) to be installed.
# If Gamin is not installed, Fail2ban will use auto. # If Gamin is not installed, Fail2ban will use auto.
# polling: uses a polling algorithm which does not require external libraries. # polling: uses a polling algorithm which does not require external libraries.
# systemd: uses systemd python library to access the systemd journal.
# Specifying "logpath" is not valid for this backend.
# See "journalmatch" in the jails associated filter config
# auto: will try to use the following backends, in order: # auto: will try to use the following backends, in order:
# pyinotify, gamin, polling. # pyinotify, gamin, polling.
backend = auto backend = auto

View File

@ -113,6 +113,12 @@ class Beautifier:
elif inC[2] == "logencoding": elif inC[2] == "logencoding":
msg = "Current log encoding is set to:\n" msg = "Current log encoding is set to:\n"
msg = msg + response msg = msg + response
elif inC[2] in ("journalmatch", "addjournalmatch", "deljournalmatch"):
if len(response) == 0:
msg = "No journal match filter set"
else:
msg = "Current match filter:\n"
msg += ' + '.join(response)
elif inC[2] in ("ignoreip", "addignoreip", "delignoreip"): elif inC[2] in ("ignoreip", "addignoreip", "delignoreip"):
if len(response) == 0: if len(response) == 0:
msg = "No IP address/network is ignored" msg = "No IP address/network is ignored"

View File

@ -56,5 +56,9 @@ class FilterReader(DefinitionInitConfigReader):
if self._initOpts: if self._initOpts:
if 'maxlines' in self._initOpts: if 'maxlines' in self._initOpts:
stream.append(["set", self._jailName, "maxlines", self._initOpts["maxlines"]]) stream.append(["set", self._jailName, "maxlines", self._initOpts["maxlines"]])
# Do not send a command if the match is empty.
if self._initOpts.get("journalmatch", '') != '':
for match in self._initOpts["journalmatch"].split("\n"):
stream.append(["set", self._jailName, "addjournalmatch", match])
return stream return stream

View File

@ -55,6 +55,8 @@ protocol = [
["set <JAIL> addlogpath <FILE>", "adds <FILE> to the monitoring list of <JAIL>"], ["set <JAIL> addlogpath <FILE>", "adds <FILE> to the monitoring list of <JAIL>"],
["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>"],
["set <JAIL> addjournalmatch <MATCH>", "adds <MATCH> to 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> 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>"],
@ -79,6 +81,7 @@ protocol = [
['', "JAIL INFORMATION", ""], ['', "JAIL INFORMATION", ""],
["get <JAIL> logpath", "gets the list of the monitored files for <JAIL>"], ["get <JAIL> logpath", "gets the list of the monitored files for <JAIL>"],
["get <JAIL> logencoding <ENCODING>", "gets the <ENCODING> of the log files for <JAIL>"], ["get <JAIL> logencoding <ENCODING>", "gets the <ENCODING> of the log files for <JAIL>"],
["get <JAIL> journalmatch", "gets the journal filter match for <JAIL>"],
["get <JAIL> ignoreip", "gets the list of ignored IP addresses for <JAIL>"], ["get <JAIL> ignoreip", "gets the list of ignored IP addresses for <JAIL>"],
["get <JAIL> failregex", "gets the list of regular expressions which matches the failures for <JAIL>"], ["get <JAIL> failregex", "gets the list of regular expressions which matches the failures for <JAIL>"],
["get <JAIL> ignoreregex", "gets the list of regular expressions which matches patterns to ignore for <JAIL>"], ["get <JAIL> ignoreregex", "gets the list of regular expressions which matches patterns to ignore for <JAIL>"],

View File

@ -643,6 +643,21 @@ class FileContainer:
self.__handler = None self.__handler = None
##
# JournalFilter class.
#
# Base interface class for systemd journal filters
class JournalFilter(Filter):
def addJournalMatch(self, match):
pass
def delJournalMatch(self, match):
pass
def getJournalMatch(self, match):
return []
## ##
# Utils class for DNS and IP handling. # Utils class for DNS and IP handling.

View File

@ -0,0 +1,237 @@
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
# vi: set ft=python sts=4 ts=4 sw=4 noet :
# This file is part of Fail2Ban.
#
# Fail2Ban is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Fail2Ban is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Fail2Ban; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# Original author: Cyril Jaquier
__author__ = "Cyril Jaquier, Lee Clemens, Yaroslav Halchenko, Steven Hiscocks"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Lee Clemens, 2012 Yaroslav Halchenko, 2013 Steven Hiscocks"
__license__ = "GPL"
import logging, datetime
from distutils.version import LooseVersion
from systemd import journal
if LooseVersion(getattr(journal, '__version__', "0")) < '204':
raise ImportError("Fail2Ban requires systemd >= 204")
from failmanager import FailManagerEmpty
from filter import JournalFilter
from mytime import MyTime
# Gets the instance of the logger.
logSys = logging.getLogger("fail2ban.filter")
##
# Journal reader class.
#
# This class reads from systemd journal and detects login failures or anything
# else that matches a given regular expression. This class is instantiated by
# a Jail object.
class FilterSystemd(JournalFilter):
##
# Constructor.
#
# Initialize the filter object with default values.
# @param jail the jail object
def __init__(self, jail, **kwargs):
JournalFilter.__init__(self, jail, **kwargs)
self.__modified = False
# Initialise systemd-journal connection
self.__journal = journal.Reader(converters={'__CURSOR': lambda x: x})
self.__matches = []
logSys.debug("Created FilterSystemd")
##
# Add a journal match filter
#
# @param match journalctl syntax matches
def addJournalMatch(self, match):
if self.__matches:
self.__journal.add_disjunction() # Add OR
try:
for match_element in match.split():
if match_element == "+":
self.__journal.add_disjunction()
else:
self.__journal.add_match(match_element)
except:
logSys.error("Error adding journal match for: %s", match)
self.resetJournalMatches()
else:
for match_element in match.split('+'):
self.__matches.append(match_element.strip())
logSys.debug("Adding journal match for: %s", match)
##
# Reset a journal match filter called on removal or failure
#
# @return None
def resetJournalMatches(self):
self.__journal.flush_matches()
logSys.debug("Flushed all journal matches")
match_copy = self.__matches[:]
self.__matches = []
for match in match_copy:
self.addJournalMatch(match)
##
# Delete a journal match filter
#
# @param match journalctl syntax matches
def delJournalMatch(self, match):
if match in self.__matches:
del self.__matches[self.__matches.index(match)]
self.resetJournalMatches()
##
# Get current journal match filter
#
# @return journalctl syntax matches
def getJournalMatch(self):
return self.__matches
##
# Join group of log elements which may be a mix of bytes and strings
#
# @param elements list of strings and bytes
# @return elements joined as string
@staticmethod
def _joinStrAndBytes(elements):
strElements = []
for element in elements:
if isinstance(element, str):
strElements.append(element)
else:
strElements.append(str(element, errors='ignore'))
return " ".join(strElements)
##
# Format journal log entry into syslog style
#
# @param entry systemd journal entry dict
# @return format log line
@staticmethod
def formatJournalEntry(logentry):
logelements = [logentry.get('_SOURCE_REALTIME_TIMESTAMP',
logentry.get('__REALTIME_TIMESTAMP')).strftime("%b %d %H:%M:%S %Y")]
if logentry.get('_HOSTNAME'):
logelements.append(logentry['_HOSTNAME'])
if logentry.get('SYSLOG_IDENTIFIER'):
logelements.append(logentry['SYSLOG_IDENTIFIER'])
if logentry.get('_PID'):
logelements[-1] += ("[%i]" % logentry['_PID'])
logelements[-1] += ":"
elif logentry.get('_COMM'):
logelements.append(logentry['_COMM'])
if logentry.get('_PID'):
logelements[-1] += ("[%i]" % logentry['_PID'])
logelements[-1] += ":"
if logelements[-1] == "kernel:":
if '_SOURCE_MONOTONIC_TIMESTAMP' in logentry:
monotonic = logentry.get('_SOURCE_MONOTONIC_TIMESTAMP')
else:
monotonic = logentry.get('__MONOTONIC_TIMESTAMP')[0]
logelements.append("[%12.6f]" % monotonic.total_seconds())
if isinstance(logentry.get('MESSAGE',''), list):
logelements.append(" ".join(logentry['MESSAGE']))
else:
logelements.append(logentry.get('MESSAGE', ''))
try:
logline = u" ".join(logelements) + u"\n"
except UnicodeDecodeError:
# Python 2, so treat as string
logline = " ".join([str(logline) for logline in logelements]) + "\n"
except TypeError:
# Python 3, one or more elements bytes
logSys.warning("Error decoding log elements from journal: %s" %
repr(logelements))
logline = self._joinStrAndBytes(logelements) + "\n"
logSys.debug("Read systemd journal entry: %s" % repr(logline))
return logline
##
# Main loop.
#
# Peridocily check for new journal entries matching the filter and
# handover to FailManager
def run(self):
self.setActive(True)
# Seek to now - findtime in journal
start_time = datetime.datetime.now() - \
datetime.timedelta(seconds=int(self.getFindTime()))
self.__journal.seek_realtime(start_time)
# Move back one entry to ensure do not end up in dead space
# if start time beyond end of journal
try:
self.__journal.get_previous()
except OSError:
pass # Reading failure, so safe to ignore
while self._isActive():
if not self.getIdle():
while self._isActive():
try:
logentry = self.__journal.get_next()
except OSError:
logSys.warning(
"Error reading line from systemd journal")
continue
if logentry:
self.processLineAndAdd(
self.formatJournalEntry(logentry))
self.__modified = True
else:
break
if self.__modified:
try:
while True:
ticket = self.failManager.toBan()
self.jail.putFailTicket(ticket)
except FailManagerEmpty:
self.failManager.cleanup(MyTime.time())
self.dateDetector.sortTemplate()
self.__modified = False
self.__journal.wait(self.getSleepTime())
logSys.debug((self.jail is not None and self.jail.getName()
or "jailless") +" filter terminated")
return True
##
# Get the status of the filter.
#
# Get some informations about the filter state such as the total
# number of failures.
# @return a list with tuple
def status(self):
ret = JournalFilter.status(self)
ret.append(("Journal matches", [" + ".join(self.__matches)]))
return ret

View File

@ -35,7 +35,7 @@ class Jail:
#Known backends. Each backend should have corresponding __initBackend method #Known backends. Each backend should have corresponding __initBackend method
# yoh: stored in a list instead of a tuple since only # yoh: stored in a list instead of a tuple since only
# list had .index until 2.6 # list had .index until 2.6
_BACKENDS = ['pyinotify', 'gamin', 'polling'] _BACKENDS = ['pyinotify', 'gamin', 'polling', 'systemd']
def __init__(self, name, backend = "auto"): def __init__(self, name, backend = "auto"):
self.__name = name self.__name = name
@ -101,6 +101,13 @@ class Jail:
from filterpyinotify import FilterPyinotify from filterpyinotify import FilterPyinotify
self.__filter = FilterPyinotify(self) self.__filter = FilterPyinotify(self)
def _initSystemd(self):
# Try to import systemd
import systemd
logSys.info("Jail '%s' uses systemd" % self.__name)
from filtersystemd import FilterSystemd
self.__filter = FilterSystemd(self)
def setName(self, name): def setName(self, name):
self.__name = name self.__name = name

View File

@ -26,6 +26,7 @@ __license__ = "GPL"
from threading import Lock, RLock from threading import Lock, RLock
from jails import Jails from jails import Jails
from filter import FileFilter, JournalFilter
from transmitter import Transmitter from transmitter import Transmitter
from asyncserver import AsyncServer from asyncserver import AsyncServer
from asyncserver import AsyncServerException from asyncserver import AsyncServerException
@ -169,14 +170,41 @@ class Server:
return self.__jails.getFilter(name).getIgnoreIP() return self.__jails.getFilter(name).getIgnoreIP()
def addLogPath(self, name, fileName): def addLogPath(self, name, fileName):
self.__jails.getFilter(name).addLogPath(fileName) filter_ = self.__jails.getFilter(name)
if isinstance(filter_, FileFilter):
filter_.addLogPath(fileName)
def delLogPath(self, name, fileName): def delLogPath(self, name, fileName):
self.__jails.getFilter(name).delLogPath(fileName) filter_ = self.__jails.getFilter(name)
if isinstance(filter_, FileFilter):
self.__jails.getFilter(name).delLogPath(fileName)
def getLogPath(self, name): def getLogPath(self, name):
return [m.getFileName() filter_ = self.__jails.getFilter(name)
for m in self.__jails.getFilter(name).getLogPath()] if isinstance(filter_, FileFilter):
return [m.getFileName()
for m in filter_.getLogPath()]
else:
logSys.info("Jail %s is not a FileFilter instance" % name)
return []
def addJournalMatch(self, name, match):
filter_ = self.__jails.getFilter(name)
if isinstance(filter_, JournalFilter):
filter_.addJournalMatch(match)
def delJournalMatch(self, name, match):
filter_ = self.__jails.getFilter(name)
if isinstance(filter_, JournalFilter):
filter_.delJournalMatch(match)
def getJournalMatch(self, name):
filter_ = self.__jails.getFilter(name)
if isinstance(filter_, JournalFilter):
return filter_.getJournalMatch()
else:
logSys.info("Jail %s is not a JournalFilter instance" % name)
return []
def setLogEncoding(self, name, encoding): def setLogEncoding(self, name, encoding):
return self.__jails.getFilter(name).setLogEncoding(encoding) return self.__jails.getFilter(name).setLogEncoding(encoding)

View File

@ -144,6 +144,14 @@ class Transmitter:
value = command[2] value = command[2]
self.__server.setLogEncoding(name, value) self.__server.setLogEncoding(name, value)
return self.__server.getLogEncoding(name) return self.__server.getLogEncoding(name)
elif command[1] == "addjournalmatch":
value = ' '.join(command[2:])
self.__server.addJournalMatch(name, value)
return self.__server.getJournalMatch(name)
elif command[1] == "deljournalmatch":
value = ' '.join(command[2:])
self.__server.delJournalMatch(name, value)
return self.__server.getJournalMatch(name)
elif command[1] == "addfailregex": elif command[1] == "addfailregex":
value = command[2] value = command[2]
self.__server.addFailRegex(name, value) self.__server.addFailRegex(name, value)
@ -250,6 +258,8 @@ class Transmitter:
return self.__server.getLogPath(name) return self.__server.getLogPath(name)
elif command[1] == "logencoding": elif command[1] == "logencoding":
return self.__server.getLogEncoding(name) return self.__server.getLogEncoding(name)
elif command[1] == "journalmatch":
return self.__server.getJournalMatch(name)
elif command[1] == "ignoreip": elif command[1] == "ignoreip":
return self.__server.getIgnoreIP(name) return self.__server.getIgnoreIP(name)
elif command[1] == "failregex": elif command[1] == "failregex":

View File

@ -0,0 +1,19 @@
error: PAM: Authentication failure for kevin from 193.168.0.128
error: PAM: Authentication failure for kevin from 193.168.0.128
error: PAM: Authentication failure for kevin from 193.168.0.128
error: PAM: Authentication failure for kevin from failed.dns.ch
error: PAM: Authentication failure for kevin from failed.dns.ch
error: PAM: Authentication failure for kevin from failed.dns.ch
error: PAM: Authentication failure for kevin from 193.168.0.128
error: PAM: Authentication failure for kevin from 193.168.0.128
error: PAM: Authentication failure for kevin from 193.168.0.128
error: PAM: Authentication failure for kevin from 193.168.0.128
error: PAM: Authentication failure for kevin from 193.168.0.128
error: PAM: Authentication failure for kevin from 193.168.0.128
error: PAM: Authentication failure for kevin from 193.168.0.128
error: PAM: Authentication failure for kevin from 193.168.0.128
error: PAM: Authentication failure for kevin from 193.168.0.128
error: PAM: Authentication failure for kevin from 87.142.124.10
error: PAM: Authentication failure for kevin from 87.142.124.10
error: PAM: Authentication failure for kevin from 87.142.124.10
error: PAM: Authentication failure for kevin from 87.142.124.10

View File

@ -29,6 +29,11 @@ import sys
import time import time
import tempfile import tempfile
try:
from systemd import journal
except ImportError:
journal = None
from fail2ban.server.jail import Jail from fail2ban.server.jail import Jail
from fail2ban.server.filterpoll import FilterPoll from fail2ban.server.filterpoll import FilterPoll
from fail2ban.server.filter import FileFilter, DNSUtils from fail2ban.server.filter import FileFilter, DNSUtils
@ -160,6 +165,34 @@ def _copy_lines_between_files(in_, fout, n=None, skip=0, mode='a', terminal_line
time.sleep(0.1) time.sleep(0.1)
return fout return fout
def _copy_lines_to_journal(in_, fields={},n=None, skip=0, terminal_line=""):
"""Copy lines from one file to systemd journal
Returns None
"""
if isinstance(in_, str): # pragma: no branch - only used with str in test cases
fin = open(in_, 'r')
else:
fin = in_
# Required for filtering
fields.update({"SYSLOG_IDENTIFIER": "fail2ban-testcases",
"PRIORITY": "7",
})
# Skip
for i in xrange(skip):
_ = fin.readline()
# Read/Write
i = 0
while n is None or i < n:
l = fin.readline()
if terminal_line is not None and l == terminal_line:
break
journal.send(MESSAGE=l.strip(), **fields)
i += 1
if isinstance(in_, str): # pragma: no branch - only used with str in test cases
# Opened earlier, therefore must close it
fin.close()
# #
# Actual tests # Actual tests
# #
@ -574,6 +607,129 @@ def get_monitor_failures_testcase(Filter_):
% (Filter_.__name__, testclass_name) # 'tempfile') % (Filter_.__name__, testclass_name) # 'tempfile')
return MonitorFailures return MonitorFailures
def get_monitor_failures_journal_testcase(Filter_):
"""Generator of TestCase's for journal based filters/backends
"""
class MonitorJournalFailures(unittest.TestCase):
def setUp(self):
"""Call before every test case."""
self.test_file = os.path.join(TEST_FILES_DIR, "testcase-journal.log")
self.jail = DummyJail()
self.filter = Filter_(self.jail)
# UUID used to ensure that only meeages generated
# as part of this test are picked up by the filter
import uuid
self.test_uuid = str(uuid.uuid4())
self.name = "monitorjournalfailures-%s" % self.test_uuid
self.filter.addJournalMatch(
"SYSLOG_IDENTIFIER=fail2ban-testcases "
"TEST_FIELD=1 "
"TEST_UUID=%s" % str(self.test_uuid))
self.filter.addJournalMatch(
"SYSLOG_IDENTIFIER=fail2ban-testcases "
"TEST_FIELD=2 "
"TEST_UUID=%s" % self.test_uuid)
self.journal_fields = {
'TEST_FIELD': "1", 'TEST_UUID': self.test_uuid}
self.filter.setActive(True)
self.filter.addFailRegex("(?:(?:Authentication failure|Failed [-/\w+]+) for(?: [iI](?:llegal|nvalid) user)?|[Ii](?:llegal|nvalid) user|ROOT LOGIN REFUSED) .*(?: from|FROM) <HOST>")
self.filter.start()
def tearDown(self):
self.filter.stop()
self.filter.join() # wait for the thread to terminate
pass
def __str__(self):
return "MonitorJournalFailures%s(%s)" \
% (Filter_, hasattr(self, 'name') and self.name or 'tempfile')
def isFilled(self, delay=2.):
"""Wait up to `delay` sec to assure that it was modified or not
"""
time0 = time.time()
while time.time() < time0 + delay:
if len(self.jail):
return True
time.sleep(0.1)
return False
def isEmpty(self, delay=0.4):
# shorter wait time for not modified status
return not self.isFilled(delay)
def assert_correct_ban(self, test_ip, test_attempts):
self.assertTrue(self.isFilled(10)) # give Filter a chance to react
ticket = self.jail.getFailTicket()
attempts = ticket.getAttempt()
ip = ticket.getIP()
matches = ticket.getMatches()
self.assertEqual(ip, test_ip)
self.assertEqual(attempts, test_attempts)
def test_grow_file(self):
self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan)
# Now let's feed it with entries from the file
_copy_lines_to_journal(
self.test_file, self.journal_fields, n=2)
self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan)
# and our dummy jail is empty as well
self.assertFalse(len(self.jail))
# since it should have not been enough
_copy_lines_to_journal(
self.test_file, self.journal_fields, skip=2, n=3)
self.assertTrue(self.isFilled(6))
# so we sleep for up to 6 sec for it not to become empty,
# and meanwhile pass to other thread(s) and filter should
# have gathered new failures and passed them into the
# DummyJail
self.assertEqual(len(self.jail), 1)
# and there should be no "stuck" ticket in failManager
self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan)
self.assert_correct_ban("193.168.0.128", 3)
self.assertEqual(len(self.jail), 0)
# Lets read some more to check it bans again
_copy_lines_to_journal(
self.test_file, self.journal_fields, skip=5, n=4)
self.assert_correct_ban("193.168.0.128", 3)
def test_delJournalMatch(self):
# Smoke test for removing of match
# basic full test
_copy_lines_to_journal(
self.test_file, self.journal_fields, n=5)
self.assert_correct_ban("193.168.0.128", 3)
# and now remove the JournalMatch
self.filter.delJournalMatch(
"SYSLOG_IDENTIFIER=fail2ban-testcases "
"TEST_FIELD=1 "
"TEST_UUID=%s" % str(self.test_uuid))
_copy_lines_to_journal(
self.test_file, self.journal_fields, n=5, skip=5)
# so we should get no more failures detected
self.assertTrue(self.isEmpty(2))
# but then if we add it back again
self.filter.addJournalMatch(
"SYSLOG_IDENTIFIER=fail2ban-testcases "
"TEST_FIELD=1 "
"TEST_UUID=%s" % str(self.test_uuid))
self.assert_correct_ban("193.168.0.128", 4)
_copy_lines_to_journal(
self.test_file, self.journal_fields, n=6, skip=10)
# we should detect the failures
self.assertTrue(self.isFilled(6))
return MonitorJournalFailures
class GetFailures(unittest.TestCase): class GetFailures(unittest.TestCase):

View File

@ -201,6 +201,12 @@ def gatherTests(regexps=None, no_network=False):
for Filter_ in filters: for Filter_ in filters:
tests.addTest(unittest.makeSuite( tests.addTest(unittest.makeSuite(
filtertestcase.get_monitor_failures_testcase(Filter_))) filtertestcase.get_monitor_failures_testcase(Filter_)))
try:
from fail2ban.server.filtersystemd import FilterSystemd
tests.addTest(unittest.makeSuite(filtertestcase.get_monitor_failures_journal_testcase(FilterSystemd)))
except Exception, e: # pragma: no cover
logSys.warning("I: Skipping systemd backend testing. Got exception '%s'" % e)
# Server test for logging elements which break logging used to support # Server test for logging elements which break logging used to support
# testcases analysis # testcases analysis