# 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$ __author__ = "Cyril Jaquier" __version__ = "$Revision$" __date__ = "$Date$" __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()