@@ -, +, @@ config/jail.conf | 9 ++- server/filterinotify.py | 157 +++++++++++++++++++++++++++++++++++++++++++++++ server/jail.py | 18 +++++- 3 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 server/filterinotify.py --- a/config/jail.conf +++ a/config/jail.conf @@ -26,13 +26,16 @@ findtime = 600 maxretry = 3 # "backend" specifies the backend used to get files modification. Available -# options are "gamin", "polling" and "auto". This option can be overridden in -# each jail too (use "gamin" for a jail and "polling" for another). +# options are "inotify", "gamin", "polling" and "auto". This option can be +# overridden in each jail too (use "gamin" for a jail and "polling" for +# another). # +# inotify: requires pyinotify and the a kernel supporting Inotify # gamin: requires Gamin (a file alteration monitor) to be installed. If Gamin # is not installed, Fail2ban will use polling. # polling: uses a polling algorithm which does not require external libraries. -# auto: will choose Gamin if available and polling otherwise. +# auto: will choose Inotify if pyinotify is present, if not then it will +# try Gamin and use that if available, and polling otherwise. backend = auto --- a/server/filterinotify.py +++ a/server/filterinotify.py @@ -0,0 +1,157 @@ +# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +# Author: Jonathan G. Underwood +# +# $Revision$ + +__author__ = "Jonathan G. Underwood" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2010 Jonathan G. Underwood" +__license__ = "GPL" + +from failmanager import FailManagerEmpty +from filter import FileFilter +from mytime import MyTime + +import time, logging + +import pyinotify +from pyinotify import ProcessEvent, WatchManager, Notifier + +# Gets the instance of the logger. +logSys = logging.getLogger("fail2ban.filter") + +## +# Log reader class. +# +# This class reads a log file and detects login failures or anything else +# that matches a given regular expression. This class is instanciated by +# a Jail object. + +class FilterInotify(ProcessEvent, FileFilter): + + ## + # Constructor. + # + # Initialize the filter object with default values. + # @param jail the jail object + + # Note that according to the pyinotify documentation we shouldn't + # define an __init__ function, but define a my_init function which is + # called by ProcessEvent.__init__. However, that approach appears not + # to work here and so we define __init__ and call + # ProcessEvent.__init__ from here. + def __init__(self, jail): + FileFilter.__init__(self, jail) + ProcessEvent.__init__(self) + self.__monitor = WatchManager() + self.__notifier = Notifier(self.__monitor, self) + self.__mask = pyinotify.IN_MODIFY | pyinotify.IN_CREATE + + ## + # Event handling functions used by pyinotify.ProcessEvent + # instance. These simply call the __handleMod method. + # @event an event object + + def process_IN_MODIFY(self, event): + logSys.debug("process_IN_MODIFY called") + self.__handleMod(event) + + def process_IN_CREATE(self, event): + logSys.debug("process_IN_CREATE called") + self.__handleMod(event) + + ## + # This method handles all modified file events + # @event an event object + + def __handleMod(self, event): + self.getFailures(event.path) + try: + while True: + ticket = self.failManager.toBan() + self.jail.putFailTicket(ticket) + except FailManagerEmpty: + self.failManager.cleanup(MyTime.time()) + self.dateDetector.sortTemplate() + + ## + # Add a log file path + # + # @param path log file path + + def addLogPath(self, path, tail = False): + if self.containsLogPath(path): + logSys.error(path + " already exists") + else: + wd = self.__monitor.add_watch(path, self.__mask) + if wd[path] > 0: + FileFilter.addLogPath(self, path, tail) + logSys.info("Added logfile = %s" % path) + else: + logSys.error("Failed to add an inotify watch for logfile = %s" % path) + + ## + # Delete a log path + # + # @param path the log file to delete + + def delLogPath(self, path): + if not self.containsLogPath(path): + logSys.error(path + " is not monitored") + else: + rd = self.__monitor.rm_watch(self.__monitor.get_wd(path)) + if rd[path]: + FileFilter.delLogPath(self, path) + logSys.info("Removed logfile = %s" % path) + else: + logSys.error("Failed to remove inotify watch for logfile = %s" % path) + + ## + # Main loop. + # + # This function is the main loop of the thread. It checks if the + # file has been modified and looks for failures. + # @return True when the thread exits nicely + + def run(self): + self.setActive(True) + while self._isActive(): + if not self.getIdle(): + # We cannot block here because we want to be able to + # exit. __notifier.check_events will block for + # timeout milliseconds. + if self.__notifier.check_events(timeout=10): + self.__notifier.read_events() + self.__notifier.process_events() + time.sleep(self.getSleepTime()) + else: + time.sleep(self.getSleepTime()) + + # Cleanup when shutting down + for wd in self.watchd.keys(): + self.__monitor.rm_watch(wd) + del self.__monitor + self.__notifier.stop() + del self.__notifier + + logSys.debug(self.jail.getName() + ": filter terminated") + return True + + + --- a/server/jail.py +++ a/server/jail.py @@ -40,17 +40,31 @@ class Jail: logSys.info("Creating new jail '%s'" % self.__name) if backend == "polling": self.__initPoller() + elif backend == "inotify": + self.__initInotify() + elif backend == "gamin": + self.__initGamin() else: try: - self.__initGamin() + self.__initInotify() except ImportError: - self.__initPoller() + try: + self.__initGamin() + except ImportError: + self.__initPoller() self.__action = Actions(self) def __initPoller(self): logSys.info("Jail '%s' uses poller" % self.__name) from filterpoll import FilterPoll self.__filter = FilterPoll(self) + + def __initInotify(self): + # Try to import pyinotify + import pyinotify + logSys.info("Jail '%s' uses Inotify" % self.__name) + from filterinotify import FilterInotify + self.__filter = FilterInotify(self) def __initGamin(self): # Try to import gamin