From e442503133c86f5ef78dcc63cf1b90014213a22f Mon Sep 17 00:00:00 2001 From: Lee Clemens Date: Fri, 30 Dec 2011 00:18:52 -0500 Subject: [PATCH] Added pyinotify backend --- MANIFEST | 1 + config/jail.conf | 17 +++-- server/filterpyinotify.py | 148 ++++++++++++++++++++++++++++++++++++++ server/jail.py | 39 ++++++++-- 4 files changed, 194 insertions(+), 11 deletions(-) create mode 100644 server/filterpyinotify.py diff --git a/MANIFEST b/MANIFEST index 4c60f8e4..eef145b6 100644 --- a/MANIFEST +++ b/MANIFEST @@ -20,6 +20,7 @@ client/configurator.py client/csocket.py server/asyncserver.py server/filter.py +server/filterpyinotify.py server/filtergamin.py server/filterpoll.py server/iso8601.py diff --git a/config/jail.conf b/config/jail.conf index fec6b1bd..cdef1cb3 100644 --- a/config/jail.conf +++ b/config/jail.conf @@ -25,14 +25,17 @@ findtime = 600 # "maxretry" is the number of failures before a host get banned. 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). +# "backend" specifies the backend used to get files modification. +# Available options are "pyinotify", "gamin", "polling" and "auto". +# This option can be overridden in each jail as well. # -# 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. +# pyinotify: requires pyinotify (a file alteration monitor) to be installed. +# If pyinotify is not installed, Fail2ban will use auto. +# gamin: requires Gamin (a file alteration monitor) to be installed. +# If Gamin is not installed, Fail2ban will use auto. +# polling: uses a polling algorithm which does not require external libraries. +# auto: will try to use the following backends, in order: +# pyinotify, gamin, polling. backend = auto diff --git a/server/filterpyinotify.py b/server/filterpyinotify.py new file mode 100644 index 00000000..c5e70e22 --- /dev/null +++ b/server/filterpyinotify.py @@ -0,0 +1,148 @@ +# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +# Author: Cyril Jaquier +# +# $Revision$ + +__author__ = "Cyril Jaquier" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2004 Cyril Jaquier" +__license__ = "GPL" + +from failmanager import FailManagerEmpty +from filter import FileFilter +from mytime import MyTime + +import time, logging, pyinotify + + + +# 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 instantiated by +# a Jail object. + +class ProcessPyinotify(pyinotify.ProcessEvent): + def __init__(self, FileFilter, **kargs): + super(ProcessPyinotify, self).__init__(**kargs) + self.__FileFilter = FileFilter + pass + + # just need default, since using mask on watch to limit events + def process_default(self, event): + logSys.debug("PYINOTIFY: Callback for Event: %s" % event) + self.__FileFilter.callback(event.pathname) + + +class FilterPyinotify(FileFilter): + # Constructor. + # + # Initialize the filter object with default values. + # @param jail the jail object + def __init__(self, jail): + FileFilter.__init__(self, jail) + self.__modified = False + self.monitor = pyinotify.WatchManager() + self.watches = dict() + + + def callback(self, path): + self.getFailures(path) + try: + while True: + ticket = self.failManager.toBan() + self.jail.putFailTicket(ticket) + except FailManagerEmpty: + self.failManager.cleanup(MyTime.time()) + self.dateDetector.sortTemplate() + self.__modified = False + + ## + # 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, pyinotify.IN_MODIFY) + self.watches[path] = wd[path] + FileFilter.addLogPath(self, path, tail) + logSys.info("Added 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: + self.monitor.rm_watch(self.watches[path]) + FileFilter.delLogPath(self, path) + logSys.info("Removed 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) + self.notifier = pyinotify.ThreadedNotifier(self.monitor, + ProcessPyinotify(self)) + self.notifier.start() + while self._isActive(): + if not self.getIdle(): + self.notifier.process_events() + # Convert sleep seconds to millis + if self.notifier.check_events(): + self.notifier.read_events() + else: + time.sleep(self.getSleepTime()) + # Cleanup pyinotify + self.__cleanup() + logSys.debug(self.jail.getName() + ": filter terminated") + return True + + ## + # Call super.stop() and then stop the 'Notifier' + + def stop(self): + # Call super to set __isRunning + super(FilterPyinotify, self).stop() + # Now stop the Notifier, otherwise we're deadlocked + self.notifier.stop() + + ## + # Deallocates the resources used by pyinotify. + + def __cleanup(self): + del self.notifier + del self.monitor diff --git a/server/jail.py b/server/jail.py index eefe69e5..16bdd088 100644 --- a/server/jail.py +++ b/server/jail.py @@ -41,13 +41,37 @@ class Jail: self.__queue = Queue.Queue() self.__filter = None logSys.info("Creating new jail '%s'" % self.__name) - if backend == "polling": - self.__initPoller() - else: + self.__setBackend = False + if backend == "auto": + # Quick-escape for auto (default/fall-back condition) + self.__setBackend = False + elif backend == "pyinotify": + try: + self.__initPyinotify() + self.__setBackend = True + except ImportError: + self.__setBackend = False + elif backend == "gamin": try: self.__initGamin() + self.__setBackend = True except ImportError: - self.__initPoller() + self.__setBackend = False + elif backend == "polling": + self.__initPoller() + self.__setBackend = True + + if not self.__setBackend: + # If auto, or unrecognized, or failed using an explicit value + try: + self.__initPyinotify() + except ImportError: + try: + self.__initGamin() + except ImportError: + self.__initPoller() + self.__setBackend = True + self.__action = Actions(self) def __initPoller(self): @@ -62,6 +86,13 @@ class Jail: from filtergamin import FilterGamin self.__filter = FilterGamin(self) + def __initPyinotify(self): + # Try to import pyinotify + import pyinotify + logSys.info("Jail '%s' uses pyinotify" % self.__name) + from filterpyinotify import FilterPyinotify + self.__filter = FilterPyinotify(self) + def setName(self, name): self.__name = name