Logo Search packages:      
Sourcecode: fail2ban version File versions  Download package

fail2ban.py

# 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
# Modified by: Yaroslav Halchenko (SYSLOG, findtime)
# 
# $Revision: 1.21 $

__author__ = "Cyril Jaquier"
__version__ = "$Revision: 1.21 $"
__date__ = "$Date: 2005/11/20 17:07:47 $"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"

import time, sys, getopt, os, string, signal, logging, logging.handlers, copy
from ConfigParser import *

from version import version
from firewall.firewall import Firewall
from logreader.logreader import LogReader
from confreader.configreader import ConfigReader
from utils.mail import Mail
from utils.pidlock import PIDLock
from utils.dns import *
from utils.process import *

# Get the instance of the logger.
logSys = logging.getLogger("fail2ban")

# Get PID lock file instance
pidLock = PIDLock()

# Global variables
logFwList = list()
conf = dict()

def dispUsage():
      """ Prints Fail2Ban command line options and exits
      """
      print "Usage: "+sys.argv[0]+" [OPTIONS]"
      print
      print "Fail2Ban v"+version+" reads log file that contains password failure report"
      print "and bans the corresponding IP addresses using firewall rules."
      print
      print "  -b         start in background"
      print "  -d         start in debug mode"
      print "  -c <FILE>  read configuration file FILE"
      print "  -p <FILE>  create PID lock in FILE"
      print "  -h         display this help message"
      print "  -i <IP(s)> IP(s) to ignore"
      print "  -k         kill a currently running instance"
      print "  -r <VALUE> allow a max of VALUE password failure [maxfailures]"
      print "  -t <TIME>  ban IP for TIME seconds [bantime]"
      print "  -f <TIME>  lifetime in seconds of failed entry [findtime]"
      print "  -v         verbose. Use twice for greater effect"
      print "  -V         print software version"
      print
      print "Report bugs to <lostcontrol@users.sourceforge.net>"
      sys.exit(0)

def dispVersion():
      """ Prints Fail2Ban version and exits
      """
      print sys.argv[0]+" "+version
      sys.exit(0)

def checkForRoot():
      """ Check for root user.
      """
      uid = `os.getuid()`
      if uid == '0':
            return True
      else:
            return False

def sigTERMhandler(signum, frame):
      """ Handles the TERM signal when in daemon mode in order to
            exit properly.
      """
      logSys.debug("Signal handler called with sig "+`signum`)
      killApp()   

def setFwMustCheck(value):
      """ Set the mustCheck value of the firewalls (True/False)
      """
      for element in logFwList:
            element[2].setMustCheck(value)

def initializeFwRules():
      """ Initializes firewalls by running cmdstart and then
          fwstart for each section
      """
      # Execute global start command
      executeCmd(conf["cmdstart"], conf["debug"])
      # Execute start command of each section
      for element in logFwList:
            element[2].initialize(conf["debug"])

def reBan():
      """ For each section asks the Firewall to reban known IPs
      """
      for element in logFwList:
            element[2].reBan(conf["debug"])

def restoreFwRules():
      """ Flush the ban list
      """
      logSys.warn("Restoring firewall rules...")
      try:
            for element in logFwList:
                  # Execute end command of each section
                  element[2].restore(conf["debug"])
            # Execute global end command
            executeCmd(conf["cmdend"], conf["debug"])
      except ExternalError:
            # nothing bad really - we can survive :-)
            pass

def killApp():
      """ Flush the ban list, remove the PID lock file and exit
            nicely.
      """
      # Restore Fw rules
      restoreFwRules()
      # Remove the PID lock
      pidLock.remove()
      logSys.info("Exiting...")
      logging.shutdown()
      sys.exit(0)

def getCmdLineOptions(optList):
      """ Gets the command line options
      """
      for opt in optList:
            if opt[0] == "-v":
                  conf["verbose"] = conf["verbose"] + 1
            if opt[0] == "-b":
                  conf["background"] = True
            if opt[0] == "-d":
                  conf["debug"] = True
            if opt[0] == "-t":
                  try:
                        conf["bantime"] = int(opt[1])
                  except ValueError:
                        logSys.warn("banTime must be an integer")
                        logSys.warn("Using default value")
            if opt[0] == "-f":
                  try:
                        conf["findtime"] = int(opt[1])
                  except ValueError:
                        logSys.warn("findTime must be an integer")
                        logSys.warn("Using default value")
            if opt[0] == "-i":
                  conf["ignoreip"] = opt[1]
            if opt[0] == "-r":
                  conf["maxfailures"] = int(opt[1])
            if opt[0] == "-p":
                  conf["pidlock"] = opt[1]
            if opt[0] == "-k":
                  conf["kill"] = True

def main():
      """ Fail2Ban main function
      """
      
      # Add the default logging handler
      stdout = logging.StreamHandler(sys.stdout)
      logSys.addHandler(stdout)
      
      # Default formatter
      formatterstring='%(levelname)s: %(message)s'
      formatter = logging.Formatter('%(asctime)s ' + formatterstring)
      stdout.setFormatter(formatter)
      
      conf["kill"] = False
      conf["verbose"] = 0
      conf["conffile"] = "/etc/fail2ban.conf"
      
      # Reads the command line options.
      try:
            cmdOpts = 'hvVbdkc:t:i:r:p:'
            cmdLongOpts = ['help','version']
            optList, args = getopt.getopt(sys.argv[1:], cmdOpts, cmdLongOpts)
      except getopt.GetoptError:
            dispUsage()
      
      # Pre-parsing of command line options for the -c option
      for opt in optList:
            if opt[0] == "-c":
                  conf["conffile"] = opt[1]
            if opt[0] in ["-h", "--help"]:
                  dispUsage()
            if opt[0] in ["-V", "--version"]:
                  dispVersion()
      
      # Reads the config file and create a LogReader instance for
      # each log file to check.
      confReader = ConfigReader(conf["conffile"]);
      confReader.openConf()
      
      # Options
      optionValues = (["bool", "background", False],
                              ["str", "logtargets", "/var/log/fail2ban.log"],
                              ["str", "syslog-target", "/dev/log"],
                              ["int", "syslog-facility", 1],
                              ["bool", "debug", False],
                              ["str", "pidlock", "/var/run/fail2ban.pid"],
                              ["int", "maxfailures", 5],
                              ["int", "bantime", 600],
                              ["int", "findtime", 600],
                              ["str", "ignoreip", ""],
                              ["int", "polltime", 1],
                              ["str", "cmdstart", ""],
                              ["str", "cmdend", ""],
                              ["int", "reinittime", 100],
                              ["int", "maxreinits", 100])
      
      # Gets global configuration options
      conf.update(confReader.getLogOptions("DEFAULT", optionValues))
      
      # Gets command line options
      getCmdLineOptions(optList)
      
      # PID lock
      pidLock.setPath(conf["pidlock"])
      
      # Now we can kill properly a running instance if needed
      if conf["kill"]:
            pid = pidLock.exists()
            if pid:
                  killPID(int(pid))
                  logSys.warn("Killed Fail2Ban with PID "+pid)
                  sys.exit(0)
            else:
                  logSys.error("No running Fail2Ban found")
                  sys.exit(-1)

      # Start Fail2Ban in daemon mode
      if conf["background"]:
            retCode = createDaemon()
            signal.signal(signal.SIGTERM, sigTERMhandler)
            if not retCode:
                  logSys.error("Unable to start daemon")
                  sys.exit(-1)

      # Process some options
      # First setup Log targets
      # Bug fix for #1234699
      os.umask(0077)
      for target in conf["logtargets"].split():
            # target formatter 
            # By default global formatter is taken. Is different for SYSLOG
            tformatter = formatter
            if target == "STDERR":
                  hdlr = logging.StreamHandler(sys.stderr)
            elif target == "SYSLOG":
                  # SYSLOG target can be either
                  # a socket (file, so it starts with /)
                  # or hostname
                  # or hostname:port
                  syslogtargets = re.findall("(/[\w/]*)|([^/ ][^: ]*)(:(\d+)){,1}",
                                                         conf["syslog-target"])
                  # we are waiting for a single match
                  syslogtargets = syslogtargets[0]

                  # assign facility if it was defined
                  if conf["syslog-facility"] < 0:
                        facility = handlers.SysLogHandler.LOG_USER
                  else:
                        facility = conf["syslog-facility"]

                  if len(syslogtargets) == 0: # everything default
                        hdlr = logging.handlers.SysLogHandler()
                  else:
                        if not ( syslogtargets[0] == "" ): # got socket
                              syslogtarget = syslogtargets[0]
                        else:       # got hostname and maybe a port
                              if syslogtargets[3] == "": # no port specified
                                    port = 514
                              else:
                                    port = int(syslogtargets[3])
                              syslogtarget = (syslogtargets[1], port)
                        hdlr = logging.handlers.SysLogHandler(syslogtarget, facility)
                  tformatter = logging.Formatter("fail2ban[%(process)d]: " +
                                                               formatterstring);
            else:
                  # Target should be a file
                  try:
                        open(target, "a")
                        hdlr = logging.FileHandler(target)
                  except IOError:
                        logSys.error("Unable to log to " + target)
                        continue
            # Set formatter and add handler to logger
            hdlr.setFormatter(tformatter)
            logSys.addHandler(hdlr)
      
      # Verbose level
      if conf["verbose"]:
            logSys.warn("Verbose level is "+`conf["verbose"]`)
            if conf["verbose"] == 1:
                  logSys.setLevel(logging.INFO)
            elif conf["verbose"] > 1:
                  logSys.setLevel(logging.DEBUG)
            
      # Set debug log level
      if conf["debug"]:
            logSys.setLevel(logging.DEBUG)
            formatterstring = ('%(levelname)s: [%(filename)s (%(lineno)d)] ' +
                                       '%(message)s')
            formatter = logging.Formatter("%(asctime)s " + formatterstring)
            stdout.setFormatter(formatter)
            logSys.warn("DEBUG MODE: FIREWALL COMMANDS ARE _NOT_ EXECUTED BUT " +
                              "ONLY DISPLAYED IN THE LOG MESSAGES")
      
      # Ignores IP list
      ignoreIPList = conf["ignoreip"].split(' ')

      # Checks for root user. This is necessary because log files
      # are owned by root and firewall needs root access.
      if not checkForRoot():
            logSys.error("You must be root")
            if not conf["debug"]:
                  sys.exit(-1)
                  
      # Checks that no instance of Fail2Ban is currently running.
      pid = pidLock.exists()
      if pid:
            logSys.error("Fail2Ban already running with PID "+pid)
            sys.exit(-1)
      else:
            ret = pidLock.create()
            if not ret:
                  # Unable to create PID lock. Exit
                  sys.exit(-1)
      
      logSys.debug("ConfFile is " + conf["conffile"])
      logSys.debug("BanTime is " + `conf["bantime"]`)
      logSys.debug("FindTime is " + `conf["findtime"]`)
      logSys.debug("MaxFailure is " + `conf["maxfailures"]`)
      
      # Options
      optionValues = (["bool", "enabled", False],
                              ["str", "host", "localhost"],
                              ["int", "port", "25"],
                              ["str", "from", "root"],
                              ["str", "to", "root"],
                              ["bool", "localtime", False],
                              ["str", "subject", "[Fail2Ban] Banned <ip>"],
                              ["str", "message", "Fail2Ban notification"])
      
      # Gets global configuration options
      mailConf = confReader.getLogOptions("MAIL", optionValues)
      
      # Create mailer if enabled
      if mailConf["enabled"]:
            logSys.debug("Mail enabled")
            mail = Mail(mailConf["host"], mailConf["port"])
            mail.setFromAddr(mailConf["from"])
            mail.setToAddr(mailConf["to"])
            mail.setLocalTimeFlag(mailConf["localtime"])
            logSys.debug("to: " + mailConf["to"] + " from: " + mailConf["from"])
      
      # Options
      optionValues = (["bool", "enabled", False],
                              ["str", "logfile", "/dev/null"],
                              ["int", "maxfailures", conf["maxfailures"]],
                              ["int", "bantime", conf["bantime"]],
                              ["int", "findtime", conf["findtime"]],
                              ["str", "timeregex", ""],
                              ["str", "timepattern", ""],
                              ["str", "failregex", ""],
                              ["str", "fwstart", ""],
                              ["str", "fwend", ""],
                              ["str", "fwban", ""],
                              ["str", "fwunban", ""],
                              ["str", "fwcheck", ""])
      
      logSys.info("Fail2Ban v" + version + " is running")
      
      # Gets the options of each sections
      for t in confReader.getSections():
            l = confReader.getLogOptions(t, optionValues)
            if l["enabled"]:
                  # Creates a logreader object
                  lObj = LogReader(l["logfile"], l["timeregex"], l["timepattern"],
                                           l["failregex"], l["maxfailures"], l["findtime"])
                  # Creates a firewall object
                  fObj = Firewall(l["fwstart"], l["fwend"], l["fwban"], l["fwunban"],
                                          l["fwcheck"], l["bantime"])
                  # "Name" the firewall
                  fObj.setSection(t)
                  # Links them into a list. I'm not really happy
                  # with this :/
                  logFwList.append([t, lObj, fObj, dict(), l])
      
      # We add 127.0.0.1 to the ignore list has we do not want
      # to be ban ourself.
      for element in logFwList:
            element[1].addIgnoreIP("127.0.0.1")
      while len(ignoreIPList) > 0:
            ip = ignoreIPList.pop()
            # Bug fix for #1239557
            if isValidIP(ip):
                  for element in logFwList:
                        element[1].addIgnoreIP(ip)
            else:
                  logSys.warn(ip + " is not a valid IP address")
      
      initializeFwRules()
      # try to reinit once if it fails immediately
      lastReinitTime = time.time() - conf["reinittime"] - 1
      reinits = 0
      # Main loop
      while True:
            try:
                  sys.stdout.flush()
                  sys.stderr.flush()
                  
                  # Checks if some IP have to be remove from ban
                  # list.
                  for element in logFwList:
                        element[2].checkForUnBan(conf["debug"])

                  # If the log file has not been modified since the
                  # last time, we sleep for 1 second. This is active
                  # polling so not very effective.
                  modList = list()
                  for element in logFwList:
                        if element[1].isModified():
                              modList.append(element)
                  
                  if len(modList) == 0:
                        time.sleep(conf["polltime"])
                        continue
                  
                  # Gets the failure list from the log file. For a given IP,
                  # takes only the service which has the most password failures.
                  for element in modList:
                        e = element[1].getFailures()
                        for key in e.iterkeys():
                              if element[3].has_key(key):
                                    element[3][key] = (element[3][key][0] + e[key][0],
                                                                  e[key][1])
                              else:
                                    element[3][key] = (e[key][0], e[key][1])
                  
                  # Remove the oldest failure attempts from the global list.
                  # We iterate the failure list and ban IP that make
                  # *retryAllowed* login failures.
                  unixTime = time.time()
                  for element in logFwList:
                        fails = element[3].copy()
                        findTime = element[1].getFindTime()
                        for attempt in fails:
                              failTime = fails[attempt][1]
                              if failTime < unixTime - findTime:
                                    del element[3][attempt]
                              elif fails[attempt][0] >= element[1].getMaxRetry():
                                    aInfo = {"section": element[0],
                                                 "ip": attempt,
                                                 "failures": element[3][attempt][0],
                                                 "failtime": failTime}
                                    logSys.info(element[0] + ": " + aInfo["ip"] +
                                                      " has " + `aInfo["failures"]` +
                                                      " login failure(s). Banned.")
                                    element[2].addBanIP(aInfo, conf["debug"])
                                    # Send a mail notification
                                    if 'mail' in locals():
                                          mail.sendmail(mailConf["subject"],
                                                              mailConf["message"], aInfo)
                                    del element[3][attempt]
            except ExternalError, e:
                  # Something wrong while dealing with Iptables.
                  # May be chain got removed?
                  reinits += 1
                  logSys.error(e)
                  if ((unixTime - lastReinitTime > conf["reinittime"]) and
                        ((conf["maxreinits"] < 0) or (reinits < conf["maxreinits"]))):
                        logSys.warn("#%d reinitialization of firewalls"%reinits)
                        lastReinitTime = unixTime
                  else:
                        logSys.error("Exiting: reinits follow too often, or too many " +
                                           "reinit attempts")
                        killApp()
                  # We already failed runCheck so disable it until
                  # restoring a safe state
                  setFwMustCheck(False)
                  # save firewalls to keep a list of IPs for rebanning
                  logFwListCopy = copy.deepcopy(logFwList)
                  try:
                        # restore as much as possible
                        restoreFwRules()
                        # reinitialize all the chains
                        initializeFwRules()
                        # restore the lists of baned IPs
                        logFwList.__init__(logFwListCopy)
                        # reBan known IPs
                        reBan()
                        # Now we can enable the runCheck test again
                        setFwMustCheck(True)
                  except ExternalError:
                        raise ExternalError("Big Oops happened: situation is out of " +
                                                      "control. Something is wrong with your " +
                                                      "setup. Please check your settings")
            except KeyboardInterrupt:
                  # When the user press <ctrl>+<c> we exit nicely.
                  killApp()

Generated by  Doxygen 1.6.0   Back to index