From 03e7d722de6f7ea72b6dd9280f8762c96e4b678e Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Wed, 12 Oct 2005 05:11:16 +0000 Subject: [PATCH] uff - nasty refactoring to make fail2ban loop if there is some error occur --- config/fail2ban.conf.default | 16 ++- debian/changelog | 5 +- fail2ban | 37 +++++- fail2ban.py | 213 +++++++++++++++++++---------------- firewall/firewall.py | 18 ++- utils/process.py | 4 + 6 files changed, 181 insertions(+), 112 deletions(-) diff --git a/config/fail2ban.conf.default b/config/fail2ban.conf.default index 6035941b..0675165b 100644 --- a/config/fail2ban.conf.default +++ b/config/fail2ban.conf.default @@ -185,9 +185,15 @@ fwstart = iptables -N fail2ban-http # Values: CMD Default: # fwend = iptables -D INPUT -p tcp --dport http -j fail2ban-http - iptables -D fail2ban-http -j RETURN + iptables -F fail2ban-http iptables -X fail2ban-http +# Option: fwcheck +# Notes.: command executed once before each fwban command +# Values: CMD Default: +# +fwcheck = iptables -L INPUT | grep -q fail2ban-http + # Option: fwban # Notes.: command executed when banning an IP. Take care that the # command is executed with Fail2Ban user rights. @@ -257,9 +263,15 @@ fwstart = iptables -N fail2ban-ssh # Values: CMD Default: # fwend = iptables -D INPUT -p tcp --dport ssh -j fail2ban-ssh - iptables -D fail2ban-ssh -j RETURN + iptables -F fail2ban-ssh iptables -X fail2ban-ssh +# Option: fwcheck +# Notes.: command executed once before each fwban command +# Values: CMD Default: +# +fwcheck = iptables -L INPUT | grep -q fail2ban-ssh + # Option: fwbanrule # Notes.: command executed when banning an IP. Take care that the # command is executed with Fail2Ban user rights. diff --git a/debian/changelog b/debian/changelog index 7c9d1776..70545aee 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -fail2ban (0.5.4-5.6) unstable; urgency=low +fail2ban (0.5.4-5.7) unstable; urgency=low * Added a notification regarding the importance of 0.5.4-5 change of failregex in the config file @@ -11,6 +11,9 @@ fail2ban (0.5.4-5.6) unstable; urgency=low SSH: Illegal -> Invalid. Should match both now * Fixed a problem of raise AttributeError exception reported as a side effect of crash during parsing of the config file + * Introduced fwcheck option to verify consistency of the + chains. Implemented automatic restart of fail2ban main function in + case if check of fwban failed. Should close few bugs -- Yaroslav Halchenko Mon, 3 Oct 2005 22:26:28 -1000 diff --git a/fail2ban b/fail2ban index 47707a99..bcc8781e 100755 --- a/fail2ban +++ b/fail2ban @@ -26,7 +26,7 @@ __date__ = "$Date: 2005/08/04 20:51:14 $" __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" -import sys, traceback, logging +import sys, traceback, logging, time # Appends our own modules path. sys.path.append("/usr/share/fail2ban") @@ -34,6 +34,16 @@ sys.path.append("/usr/share/fail2ban") # Now we can import our modules. import fail2ban from utils.pidlock import PIDLock +from utils.process import ExternalError + +# Start the application. Handle all the unhandled exceptions +# yoh: I don't think that this parameters need to be configured +# and probably maxRestarts should be removed +legitRestartTime = 10 # legitimate minimal restart time +maxRestarts = 100 # max number of times to perform restart + +lastRestartTime = time.time() +restarts = 0 # Get the instance of the logger. logSys = logging.getLogger("fail2ban") @@ -41,9 +51,21 @@ logSys = logging.getLogger("fail2ban") # Get PID lock file instance pidLock = PIDLock() -# Start the application. Handle all the unhandled exceptions try: - fail2ban.main() + while True: + restarts += 1 + try: + fail2ban.main(restarts>1) + except ExternalError, e: + # There went something wrong while dealing with Iptables. May be chain got + # removed? + logSys.error("Fail2Ban got a problem: " + e.__str__()) + if (time.time() - lastRestartTime > legitRestartTime) and (restarts < maxRestarts): + logSys.error("Restarting for the %d time "%restarts) + lastRestartTime = time.time() + else: + logSys.error("Exiting: restarts follow too often, or too many restart attempts") + sys.exit(0) except SystemExit: # We called sys.exit(). Nothing wrong so just pass pass @@ -55,6 +77,9 @@ except Exception, e: logSys.error("Type: " + `type.__name__` + "\n" + "Value: " + `e.args` + "\n" + "TB: " + `tbStack`) - # Remove the PID lock file. Should close #1239562 - pidLock.remove() - logging.shutdown() + +# Remove the PID lock file. Should close #1239562 +pidLock.remove() +logSys.info("Exiting...") +logging.shutdown() + diff --git a/fail2ban.py b/fail2ban.py index 6db4f2ba..0958ccf8 100755 --- a/fail2ban.py +++ b/fail2ban.py @@ -92,9 +92,8 @@ def sigTERMhandler(signum, frame): logSys.debug("Signal handler called with sig "+`signum`) killApp() -def killApp(): - """ Flush the ban list, remove the PID lock file and exit - nicely. +def restoreFwRules(): + """ Flush the ban list """ logSys.warn("Restoring firewall rules...") for element in logFwList: @@ -103,12 +102,15 @@ def killApp(): for element in logFwList: l = element[4] executeCmd(l["fwend"], conf["debug"]) - # Execute global start command + # Execute global end command executeCmd(conf["cmdend"], conf["debug"]) - # Remove the PID lock - pidLock.remove() - logSys.info("Exiting...") - logging.shutdown() + +def killApp(): + """ Flush the ban list, remove and exit + nicely. + """ + # Restore Fw rules + restoreFwRules() sys.exit(0) def getCmdLineOptions(optList): @@ -136,9 +138,12 @@ def getCmdLineOptions(optList): if opt[0] == "-k": conf["kill"] = True -def main(): +def main(secondaryStart): """ Fail2Ban main function """ + # (re)Initialize global variables + logFwList.__init__() + conf.clear() # Add the default logging handler stdout = logging.StreamHandler(sys.stdout) @@ -213,106 +218,110 @@ def main(): except KeyError: pass - # 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) + # If it is not a hot restart + # fork, setup logging, create pid, check for root + if not secondaryStart: + # Start Fail2Ban in daemon mode + if conf["background"]: + logSys.debug("Daemonizing") + retCode = createDaemon() + signal.signal(signal.SIGTERM, sigTERMhandler) + if not retCode: + logSys.error("Unable to start daemon") + sys.exit(-1) - # 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] + # 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 + # 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: - facility = conf["syslog-facility"] + # 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) - 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) + # Process some options - # Process some options + # 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) - # Verbose level - if conf["verbose"]: - logSys.warn("Verbose level is "+`conf["verbose"]`) - if conf["verbose"] == 1: - logSys.setLevel(logging.INFO) - elif conf["verbose"] > 1: + # 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") - # 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") + # 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) # 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"]`) @@ -350,7 +359,8 @@ def main(): ["str", "fwstart", ""], ["str", "fwend", ""], ["str", "fwban", ""], - ["str", "fwunban", ""]) + ["str", "fwunban", ""], + ["str", "fwcheck", ""]) logSys.info("Fail2Ban v" + version + " is running") @@ -364,7 +374,7 @@ def main(): lObj = LogReader(l["logfile"], l["timeregex"], l["timepattern"], l["failregex"], l["maxfailures"], l["findtime"]) # Creates a firewall object - fObj = Firewall(l["fwban"], l["fwunban"], l["bantime"]) + fObj = Firewall(l["fwban"], l["fwunban"], l["fwcheck"], l["bantime"]) # Links them into a list. I'm not really happy # with this :/ logFwList.append([t, lObj, fObj, dict(), l]) @@ -449,7 +459,10 @@ def main(): mail.sendmail(mailConf["subject"], mailConf["message"], aInfo) del element[3][attempt] - + except ExternalError: + # restore as much as possible before restart + restoreFwRules() + raise except KeyboardInterrupt: # When the user press + we exit nicely. killApp() diff --git a/firewall/firewall.py b/firewall/firewall.py index 567c8ce3..5ee9bd79 100644 --- a/firewall/firewall.py +++ b/firewall/firewall.py @@ -28,6 +28,7 @@ import time, os, logging, re from utils.process import executeCmd from utils.strings import replaceTag +from utils.process import ExternalError # Gets the instance of the logger. logSys = logging.getLogger("fail2ban") @@ -37,9 +38,10 @@ class Firewall: the IP. """ - def __init__(self, banRule, unBanRule, banTime): + def __init__(self, banRule, unBanRule, checkRule, banTime): self.banRule = banRule self.unBanRule = unBanRule + self.checkRule = checkRule self.banTime = banTime self.banList = dict() @@ -52,7 +54,10 @@ class Firewall: logSys.warn("Ban " + ip) self.banList[ip] = crtTime aInfo["bantime"] = crtTime - executeCmd(self.banIP(aInfo), debug) + self.runCheck("pre-fwban", debug) + cmd = self.banIP(aInfo) + if executeCmd(cmd, debug): + raise ExternalError("Firewall: execution of fwban command '%s' failed"%cmd) else: logSys.error(ip+" already in ban list") @@ -63,6 +68,7 @@ class Firewall: if self.inBanList(ip): logSys.warn("Unban "+ip) del self.banList[ip] + self.runCheck("pre-fwunban", debug) executeCmd(self.unBanIP(aInfo), debug) else: logSys.error(ip+" not in ban list") @@ -71,7 +77,13 @@ class Firewall: """ Checks if IP is in ban list. """ return self.banList.has_key(ip) - + + def runCheck(self, location, debug): + """ Runs fwcheck command and throws an exception if it returns non-0 result """ + if executeCmd(self.checkRule, debug): + raise ExternalError("Firewall: %s fwcheck command '%s' failed" + %(location,self.checkRule)) + def checkForUnBan(self, debug): """ Check for IP to remove from ban list. """ diff --git a/utils/process.py b/utils/process.py index 7f1060cc..b36f5098 100644 --- a/utils/process.py +++ b/utils/process.py @@ -29,6 +29,10 @@ import os, logging, signal # Gets the instance of the logger. logSys = logging.getLogger("fail2ban") +class ExternalError(UserWarning): + """ Exception to warn about failed fwcheck or fwban command """ + pass + def createDaemon(): """ Detach a process from the controlling terminal and run it in the background as a daemon.