filter.py

Go to the documentation of this file.
00001 # This file is part of Fail2Ban.
00002 #
00003 # Fail2Ban is free software; you can redistribute it and/or modify
00004 # it under the terms of the GNU General Public License as published by
00005 # the Free Software Foundation; either version 2 of the License, or
00006 # (at your option) any later version.
00007 #
00008 # Fail2Ban is distributed in the hope that it will be useful,
00009 # but WITHOUT ANY WARRANTY; without even the implied warranty of
00010 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00011 # GNU General Public License for more details.
00012 #
00013 # You should have received a copy of the GNU General Public License
00014 # along with Fail2Ban; if not, write to the Free Software
00015 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
00016 
00017 # Author: Cyril Jaquier
00018 # 
00019 # $Revision: 734 $
00020 
00021 __author__ = "Cyril Jaquier"
00022 __version__ = "$Revision: 734 $"
00023 __date__ = "$Date: 2009-08-29 00:26:17 +0200 (Sat, 29 Aug 2009) $"
00024 __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
00025 __license__ = "GPL"
00026 
00027 from failmanager import FailManager
00028 from ticket import FailTicket
00029 from jailthread import JailThread
00030 from mytime import MyTime
00031 from failregex import FailRegex, Regex, RegexException
00032 
00033 import logging, re, time
00034 
00035 # Gets the instance of the logger.
00036 logSys = logging.getLogger("fail2ban.filter")
00037 
00038 ##
00039 # Log reader class.
00040 #
00041 # This class reads a log file and detects login failures or anything else
00042 # that matches a given regular expression. This class is instanciated by
00043 # a Jail object.
00044 
00045 class Filter(JailThread):
00046 
00047     ##
00048     # Constructor.
00049     #
00050     # Initialize the filter object with default values.
00051     # @param jail the jail object
00052     
00053     def __init__(self, jail):
00054         JailThread.__init__(self)
00055         ## The jail which contains this filter.
00056         self.jail = jail
00057         ## The failures manager.
00058         self.failManager = FailManager()
00059         ## The regular expression list matching the failures.
00060         self.__failRegex = list()
00061         ## The regular expression list with expressions to ignore.
00062         self.__ignoreRegex = list()
00063         ## The amount of time to look back.
00064         self.__findTime = 6000
00065         ## The ignore IP list.
00066         self.__ignoreIpList = []
00067         logSys.debug("Created Filter")
00068 
00069 
00070     ##
00071     # Add a regular expression which matches the failure.
00072     #
00073     # The regular expression can also match any other pattern than failures
00074     # and thus can be used for many purporse.
00075     # @param value the regular expression
00076     
00077     def addFailRegex(self, value):
00078         try:
00079             regex = FailRegex(value)
00080             self.__failRegex.append(regex)
00081         except RegexException, e:
00082             logSys.error(e)
00083     
00084 
00085     def delFailRegex(self, index):
00086         try:
00087             del self.__failRegex[index]
00088         except IndexError:
00089             logSys.error("Cannot remove regular expression. Index %d is not "
00090                          "valid" % index)
00091     
00092     ##
00093     # Get the regular expression which matches the failure.
00094     #
00095     # @return the regular expression
00096     
00097     def getFailRegex(self):
00098         failRegex = list()
00099         for regex in self.__failRegex:
00100             failRegex.append(regex.getOriginalRegex())
00101         return failRegex
00102     
00103     ##
00104     # Add the regular expression which matches the failure.
00105     #
00106     # The regular expression can also match any other pattern than failures
00107     # and thus can be used for many purporse.
00108     # @param value the regular expression
00109     
00110     def addIgnoreRegex(self, value):
00111         try:
00112             regex = Regex(value)
00113             self.__ignoreRegex.append(regex)
00114         except RegexException, e:
00115             logSys.error(e)
00116     
00117     def delIgnoreRegex(self, index):
00118         try:
00119             del self.__ignoreRegex[index]
00120         except IndexError:
00121             logSys.error("Cannot remove regular expression. Index %d is not "
00122                          "valid" % index)
00123     
00124     ##
00125     # Get the regular expression which matches the failure.
00126     #
00127     # @return the regular expression
00128     
00129     def getIgnoreRegex(self):
00130         ignoreRegex = list()
00131         for regex in self.__ignoreRegex:
00132             ignoreRegex.append(regex.getRegex())
00133         return ignoreRegex
00134     
00135     ##
00136     # Set the time needed to find a failure.
00137     #
00138     # This value tells the filter how long it has to take failures into
00139     # account.
00140     # @param value the time
00141     
00142     def setFindTime(self, value):
00143         self.__findTime = value
00144         self.failManager.setMaxTime(value)
00145         logSys.info("Set findtime = %s" % value)
00146     
00147     ##
00148     # Get the time needed to find a failure.
00149     #
00150     # @return the time
00151     
00152     def getFindTime(self):
00153         return self.__findTime
00154     
00155     ##
00156     # Set the maximum retry value.
00157     #
00158     # @param value the retry value
00159     
00160     def setMaxRetry(self, value):
00161         self.failManager.setMaxRetry(value)
00162         logSys.info("Set maxRetry = %s" % value)
00163     
00164     ##
00165     # Get the maximum retry value.
00166     #
00167     # @return the retry value
00168     
00169     def getMaxRetry(self):
00170         return self.failManager.getMaxRetry()
00171     
00172     ##
00173     # Main loop.
00174     #
00175     # This function is the main loop of the thread. It checks if the
00176     # file has been modified and looks for failures.
00177     # @return True when the thread exits nicely
00178 
00179     def run(self):
00180         raise Exception("run() is abstract")
00181     
00182     ##
00183     # Ban an IP - http://blogs.buanzo.com.ar/2009/04/fail2ban-patch-ban-ip-address-manually.html
00184     # Arturo 'Buanzo' Busleiman <buanzo@buanzo.com.ar>
00185     #
00186     # to enable banip fail2ban-client BAN command
00187     
00188     def addBannedIP(self, ip):
00189         unixTime = time.time()
00190         self.failManager.addFailure(FailTicket(ip, unixTime))
00191         return ip
00192     
00193     ##
00194     # Add an IP/DNS to the ignore list.
00195     #
00196     # IP addresses in the ignore list are not taken into account
00197     # when finding failures. CIDR mask and DNS are also accepted.
00198     # @param ip IP address to ignore
00199     
00200     def addIgnoreIP(self, ip):
00201         logSys.debug("Add " + ip + " to ignore list")
00202         self.__ignoreIpList.append(ip)
00203         
00204     def delIgnoreIP(self, ip):
00205         logSys.debug("Remove " + ip + " from ignore list")
00206         self.__ignoreIpList.remove(ip)
00207         
00208     def getIgnoreIP(self):
00209         return self.__ignoreIpList
00210     
00211     ##
00212     # Check if IP address/DNS is in the ignore list.
00213     #
00214     # Check if the given IP address matches an IP address/DNS or a CIDR
00215     # mask in the ignore list.
00216     # @param ip IP address
00217     # @return True if IP address is in ignore list
00218     
00219     def inIgnoreIPList(self, ip):
00220         for i in self.__ignoreIpList:
00221             # An empty string is always false
00222             if i == "":
00223                 continue
00224             s = i.split('/', 1)
00225             # IP address without CIDR mask
00226             if len(s) == 1:
00227                 s.insert(1, '32')
00228             s[1] = long(s[1])
00229             try:
00230                 a = DNSUtils.cidr(s[0], s[1])
00231                 b = DNSUtils.cidr(ip, s[1])
00232             except Exception:
00233                 # Check if IP in DNS
00234                 ips = DNSUtils.dnsToIp(i)
00235                 if ip in ips:
00236                     return True
00237                 else:
00238                     continue
00239             if a == b:
00240                 return True
00241         return False
00242 
00243     def processLineAndAdd(self, line):
00244         try:
00245             # Decode line to UTF-8
00246             l = line.decode('utf-8')
00247         except UnicodeDecodeError:
00248             l = line
00249         for element in self.findFailure(l):
00250             ip = element[0]
00251             unixTime = element[1]
00252             if unixTime < MyTime.time() - self.getFindTime():
00253                 break
00254             if self.inIgnoreIPList(ip):
00255                 logSys.debug("Ignore %s" % ip)
00256                 continue
00257             logSys.debug("Found %s" % ip)
00258             self.failManager.addFailure(FailTicket(ip, unixTime))
00259 
00260     ##
00261     # Returns true if the line should be ignored.
00262     #
00263     # Uses ignoreregex.
00264     # @param line: the line
00265     # @return: a boolean
00266 
00267     def ignoreLine(self, line):
00268         for ignoreRegex in self.__ignoreRegex:
00269             ignoreRegex.process()
00270             if ignoreRegex.match(line):
00271                 return True
00272         return False
00273 
00274     ##
00275     # Finds the failure in a line given split into time and log parts.
00276     #
00277     # Uses the failregex pattern to find it and timeregex in order
00278     # to find the logging time.
00279     # @return a dict with IP and timestamp.
00280 
00281     def findFailure(self, line):
00282         failList = list()
00283         # Checks if we must ignore this line.
00284         if self.ignoreLine(line):
00285             # The ignoreregex matched. Return.
00286             return failList
00287         # Iterates over all the regular expressions.
00288         for failRegex in self.__failRegex:
00289             failRegex.search(line)
00290             if failRegex.hasMatched():
00291                 # The failregex matched.
00292                 try:
00293                     host = failRegex.getHost()
00294                     date = failRegex.getTime()
00295                     if not date:
00296                         logSys.warning("Unable to get a valid date: %s" % line)
00297                         break
00298                     # Use Unix timestamp.
00299                     dateUnix = time.mktime(date)
00300                     ipMatch = DNSUtils.textToIp(host)
00301                     if ipMatch:
00302                         for ip in ipMatch:
00303                             failList.append([ip, dateUnix])
00304                         # We matched a regex, it is enough to stop.
00305                         break
00306                 except RegexException, e:
00307                     logSys.error(e)
00308         return failList
00309     
00310 
00311     ##
00312     # Get the status of the filter.
00313     #
00314     # Get some informations about the filter state such as the total
00315     # number of failures.
00316     # @return a list with tuple
00317     
00318     def status(self):
00319         ret = [("Currently failed", self.failManager.size()),
00320                ("Total failed", self.failManager.getFailTotal())]
00321         return ret
00322 
00323 
00324 class FileFilter(Filter):
00325     
00326     def __init__(self, jail):
00327         Filter.__init__(self, jail)
00328         ## The log file path.
00329         self.__logPath = []
00330     
00331     ##
00332     # Add a log file path
00333     #
00334     # @param path log file path
00335 
00336     def addLogPath(self, path, tail = True):
00337         container = FileContainer(path, tail)
00338         self.__logPath.append(container)
00339     
00340     ##
00341     # Delete a log path
00342     #
00343     # @param path the log file to delete
00344     
00345     def delLogPath(self, path):
00346         for log in self.__logPath:
00347             if log.getFileName() == path:
00348                 self.__logPath.remove(log)
00349                 return
00350 
00351     ##
00352     # Get the log file path
00353     #
00354     # @return log file path
00355         
00356     def getLogPath(self):
00357         return self.__logPath
00358     
00359     ##
00360     # Check whether path is already monitored.
00361     #
00362     # @param path The path
00363     # @return True if the path is already monitored else False
00364     
00365     def containsLogPath(self, path):
00366         for log in self.__logPath:
00367             if log.getFileName() == path:
00368                 return True
00369         return False
00370     
00371     def getFileContainer(self, path):
00372         for log in self.__logPath:
00373             if log.getFileName() == path:
00374                 return log
00375         return None
00376     
00377     ##
00378     # Gets all the failure in the log file.
00379     #
00380     # Gets all the failure in the log file which are newer than
00381     # MyTime.time()-self.findTime. When a failure is detected, a FailTicket
00382     # is created and is added to the FailManager.
00383     
00384     def getFailures(self, filename):
00385         container = self.getFileContainer(filename)
00386         if container == None:
00387             logSys.error("Unable to get failures in " + filename)
00388             return False
00389         # Try to open log file.
00390         try:
00391             container.open()
00392         except Exception, e:
00393             logSys.error("Unable to open %s" % filename)
00394             logSys.exception(e)
00395             return False
00396         
00397         line = container.readline()
00398         while not line == "":
00399             if not self._isActive():
00400                 # The jail has been stopped
00401                 break
00402             self.processLineAndAdd(line)
00403             # Read a new line.
00404             line = container.readline()
00405         container.close()
00406         return True
00407     
00408     def status(self):
00409         ret = Filter.status(self)
00410         path = [m.getFileName() for m in self.getLogPath()]
00411         ret.append(("File list", path))
00412         return ret
00413 
00414 ##
00415 # FileContainer class.
00416 #
00417 # This class manages a file handler and takes care of log rotation detection.
00418 # In order to detect log rotation, the hash (MD5) of the first line of the file
00419 # is computed and compared to the previous hash of this line.
00420 
00421 import md5
00422 
00423 class FileContainer:
00424     
00425     def __init__(self, filename, tail = True):
00426         self.__filename = filename
00427         self.__tail = tail
00428         self.__handler = None
00429         # Try to open the file. Raises an exception if an error occured.
00430         handler = open(filename)
00431         try:
00432             firstLine = handler.readline()
00433             # Computes the MD5 of the first line.
00434             self.__hash = md5.new(firstLine).digest()
00435             # Start at the beginning of file if tail mode is off.
00436             if tail:
00437                 handler.seek(0, 2)
00438                 self.__pos = handler.tell()
00439             else:
00440                 self.__pos = 0
00441         finally:
00442             handler.close()
00443     
00444     def getFileName(self):
00445         return self.__filename
00446     
00447     def open(self):
00448         self.__handler = open(self.__filename)
00449         firstLine = self.__handler.readline()
00450         # Computes the MD5 of the first line.
00451         myHash = md5.new(firstLine).digest()
00452         # Compare hash.
00453         if not self.__hash == myHash:
00454             logSys.info("Log rotation detected for %s" % self.__filename)
00455             self.__hash = myHash
00456             self.__pos = 0
00457         # Sets the file pointer to the last position.
00458         self.__handler.seek(self.__pos)
00459     
00460     def readline(self):
00461         if self.__handler == None:
00462             return ""
00463         return self.__handler.readline()
00464     
00465     def close(self):
00466         if not self.__handler == None:
00467             # Saves the last position.
00468             self.__pos = self.__handler.tell()
00469             # Closes the file.
00470             self.__handler.close()
00471             self.__handler = None
00472 
00473 
00474 
00475 ##
00476 # Utils class for DNS and IP handling.
00477 #
00478 # This class contains only static methods used to handle DNS and IP
00479 # addresses.
00480 
00481 import socket, struct
00482 
00483 class DNSUtils:
00484     
00485     IP_CRE = re.compile("(?:\d{1,3}\.){3}\d{1,3}")
00486     
00487     #@staticmethod
00488     def dnsToIp(dns):
00489         """ Convert a DNS into an IP address using the Python socket module.
00490             Thanks to Kevin Drapel.
00491         """
00492         try:
00493             return socket.gethostbyname_ex(dns)[2]
00494         except socket.gaierror:
00495             logSys.warn("Unable to find a corresponding IP address for %s"
00496                         % dns)
00497             return list()
00498     dnsToIp = staticmethod(dnsToIp)
00499     
00500     #@staticmethod
00501     def searchIP(text):
00502         """ Search if an IP address if directly available and return
00503             it.
00504         """
00505         match = DNSUtils.IP_CRE.match(text)
00506         if match:
00507             return match
00508         else:
00509             return None
00510     searchIP = staticmethod(searchIP)
00511     
00512     #@staticmethod
00513     def isValidIP(string):
00514         """ Return true if str is a valid IP
00515         """
00516         s = string.split('/', 1)
00517         try:
00518             socket.inet_aton(s[0])
00519             return True
00520         except socket.error:
00521             return False
00522     isValidIP = staticmethod(isValidIP)
00523     
00524     #@staticmethod
00525     def textToIp(text):
00526         """ Return the IP of DNS found in a given text.
00527         """
00528         ipList = list()
00529         # Search for plain IP
00530         plainIP = DNSUtils.searchIP(text)
00531         if not plainIP == None:
00532             plainIPStr = plainIP.group(0)
00533             if DNSUtils.isValidIP(plainIPStr):
00534                 ipList.append(plainIPStr)
00535         if not ipList:
00536             # Try to get IP from possible DNS
00537             ip = DNSUtils.dnsToIp(text)
00538             for e in ip:
00539                 ipList.append(e)
00540         return ipList
00541     textToIp = staticmethod(textToIp)
00542     
00543     #@staticmethod
00544     def cidr(i, n):
00545         """ Convert an IP address string with a CIDR mask into a 32-bit
00546             integer.
00547         """
00548         # 32-bit IPv4 address mask
00549         MASK = 0xFFFFFFFFL
00550         return ~(MASK >> n) & MASK & DNSUtils.addr2bin(i)
00551     cidr = staticmethod(cidr)
00552     
00553     #@staticmethod
00554     def addr2bin(string):
00555         """ Convert a string IPv4 address into an unsigned integer.
00556         """
00557         return struct.unpack("!L", socket.inet_aton(string))[0]
00558     addr2bin = staticmethod(addr2bin)
00559     
00560     #@staticmethod
00561     def bin2addr(addr):
00562         """ Convert a numeric IPv4 address into string n.n.n.n form.
00563         """
00564         return socket.inet_ntoa(struct.pack("!L", addr))
00565     bin2addr = staticmethod(bin2addr)
Generated on Thu Jun 20 03:01:40 2013 for Fail2Ban by  doxygen 1.6.3