From f9b78ba92798da40509c5743c1d15e13d6a7f3ec Mon Sep 17 00:00:00 2001 From: Michael Gebetsroither Date: Thu, 3 Jan 2013 18:46:31 +0100 Subject: [PATCH 01/26] add support for blocking through blackhole routes --- config/action.d/route.conf | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 config/action.d/route.conf diff --git a/config/action.d/route.conf b/config/action.d/route.conf new file mode 100644 index 00000000..2d11c700 --- /dev/null +++ b/config/action.d/route.conf @@ -0,0 +1,19 @@ +# Fail2Ban configuration file +# +# Author: Michael Gebetsroither +# +# This is for blocking whole hosts through blackhole routes. +# +# PRO: +# - Works on all kernel versions and as no compatibility problems (back to debian lenny and WAY further). +# - It's FAST for very large numbers of blocked ips. +# - It's FAST because it Blocks traffic before it enters common iptables chains used for filtering. +# - It's per host, ideal as action against ssh password bruteforcing to block further attack attempts. +# - No additional software required beside iproute/iproute2 +# +# CON: +# - Blocking is per IP and NOT per service, but ideal as action against ssh password bruteforcing hosts + +[Definition] +actionban = ip route add blackhole +actionunban = ip route del blackhole From 03433f79cd52a87c3fd7309e7fe9967076bbb866 Mon Sep 17 00:00:00 2001 From: Michael Gebetsroither Date: Fri, 4 Jan 2013 16:09:04 +0100 Subject: [PATCH 02/26] add example jail.conf for blocking through blackhole routes for ssh --- config/jail.conf | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/config/jail.conf b/config/jail.conf index 3f2425b4..fb9f9ca0 100644 --- a/config/jail.conf +++ b/config/jail.conf @@ -101,6 +101,17 @@ action = hostsdeny ignoreregex = for myuser from logpath = /var/log/sshd.log +# Here we use blackhole routes for not requiring any additional kernel support +# to store large volumes of banned IPs + +[ssh-route] + +enabled = false +filter = sshd +action = route +logpath = /var/log/sshd.log +maxretry = 5 + # Here we use a combination of Netfilter/Iptables and IPsets # for storing large volumes of banned IPs # From 6004fe7a9449caf2aeb135cb3b7e76efa52b2ad7 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Mon, 11 Feb 2013 16:17:52 -0500 Subject: [PATCH 03/26] just trailing spaces in setup.py --- setup.py | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/setup.py b/setup.py index 53b03f0f..37086227 100755 --- a/setup.py +++ b/setup.py @@ -36,33 +36,33 @@ to reject the IP address or executes user defined commands.''' setup( - name = "fail2ban", - version = version, - description = "Ban IPs that make too many password failure", - long_description = longdesc, - author = "Cyril Jaquier", - author_email = "cyril.jaquier@fail2ban.org", - url = "http://www.fail2ban.org", - license = "GPL", - platforms = "Posix", + name = "fail2ban", + version = version, + description = "Ban IPs that make too many password failure", + long_description = longdesc, + author = "Cyril Jaquier", + author_email = "cyril.jaquier@fail2ban.org", + url = "http://www.fail2ban.org", + license = "GPL", + platforms = "Posix", scripts = [ - 'fail2ban-client', - 'fail2ban-server', + 'fail2ban-client', + 'fail2ban-server', 'fail2ban-regex' - ], + ], packages = [ - 'common', - 'client', + 'common', + 'client', 'server' - ], + ], data_files = [ - ('/etc/fail2ban', + ('/etc/fail2ban', glob("config/*.conf") - ), - ('/etc/fail2ban/filter.d', + ), + ('/etc/fail2ban/filter.d', glob("config/filter.d/*.conf") - ), - ('/etc/fail2ban/action.d', + ), + ('/etc/fail2ban/action.d', glob("config/action.d/*.conf") ), ('/var/run/fail2ban', @@ -78,20 +78,20 @@ elements = { "/etc/": [ "fail2ban.conf" - ], + ], "/usr/bin/": [ "fail2ban.py" - ], + ], "/usr/lib/fail2ban/firewall/": [ - "iptables.py", - "ipfwadm.py", + "iptables.py", + "ipfwadm.py", "ipfw.py" ], "/usr/lib/fail2ban/": [ - "version.py", + "version.py", "protocol.py" ] } From 8cf006827e6f7def17aef82f7a792f66747be8ca Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Tue, 12 Feb 2013 08:48:05 -0500 Subject: [PATCH 04/26] BF: remove path from grep call in sendmail-whois-lines.conf Closes: gh-118 --- config/action.d/sendmail-whois-lines.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/action.d/sendmail-whois-lines.conf b/config/action.d/sendmail-whois-lines.conf index d1e6e40f..b84440d0 100644 --- a/config/action.d/sendmail-whois-lines.conf +++ b/config/action.d/sendmail-whois-lines.conf @@ -57,7 +57,7 @@ actionban = printf %%b "Subject: [Fail2Ban] : banned Here are more information about :\n `/usr/bin/whois `\n\n Lines containing IP: in \n - `/bin/grep '\<\>' `\n\n + `grep '\<\>' `\n\n Regards,\n Fail2Ban" | /usr/sbin/sendmail -f From 47b1ee39d8e5e918afaea40f361b3932a2b8616e Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Sun, 17 Feb 2013 12:37:34 +1100 Subject: [PATCH 05/26] add blocking type --- config/action.d/route.conf | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/config/action.d/route.conf b/config/action.d/route.conf index 2d11c700..ec940b74 100644 --- a/config/action.d/route.conf +++ b/config/action.d/route.conf @@ -15,5 +15,11 @@ # - Blocking is per IP and NOT per service, but ideal as action against ssh password bruteforcing hosts [Definition] -actionban = ip route add blackhole -actionunban = ip route del blackhole +actionban = ip route add +actionunban = ip route del + +# Type of blocking +# +# Type can be blackhole, unreachable and prohibit. Unreachable and prohibit correspond to the ICMP reject messages. + +type = blackhole From ce3ab34dd8bc0181fbb68c7fc8b9ed18e35cf5f3 Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Sun, 17 Feb 2013 22:14:01 +0000 Subject: [PATCH 06/26] Added ability to specify PID file --- client/fail2banreader.py | 3 ++- config/fail2ban.conf | 7 +++++++ fail2ban-client | 18 ++++++++++++++---- fail2ban-server | 10 ++++++++-- server/server.py | 12 +++++------- 5 files changed, 36 insertions(+), 14 deletions(-) diff --git a/client/fail2banreader.py b/client/fail2banreader.py index ee097bd6..6954b3bf 100644 --- a/client/fail2banreader.py +++ b/client/fail2banreader.py @@ -42,7 +42,8 @@ class Fail2banReader(ConfigReader): ConfigReader.read(self, "fail2ban") def getEarlyOptions(self): - opts = [["string", "socket", "/tmp/fail2ban.sock"]] + opts = [["string", "socket", "/tmp/fail2ban.sock"], + ["string", "pidfile", "/var/run/fail2ban/fail2ban.pid"]] return ConfigReader.getOptions(self, "Definition", opts) def getOptions(self): diff --git a/config/fail2ban.conf b/config/fail2ban.conf index f2f1b215..e759513b 100644 --- a/config/fail2ban.conf +++ b/config/fail2ban.conf @@ -40,3 +40,10 @@ logtarget = /var/log/fail2ban.log # socket = /var/run/fail2ban/fail2ban.sock +# Option: pidfile +# Notes.: Set the PID file. This is used to store the process ID of the +# fail2ban server. +# Values: FILE Default: /var/run/fail2ban/fail2ban.sock +# +pidfile = /var/run/fail2ban/fail2ban.pid + diff --git a/fail2ban-client b/fail2ban-client index 13d018e6..595144ef 100755 --- a/fail2ban-client +++ b/fail2ban-client @@ -62,6 +62,7 @@ class Fail2banClient: self.__conf["verbose"] = 1 self.__conf["interactive"] = False self.__conf["socket"] = None + self.__conf["pidfile"] = None def dispVersion(self): print "Fail2Ban v" + version @@ -84,6 +85,7 @@ class Fail2banClient: print "Options:" print " -c configuration directory" print " -s socket path" + print " -p pidfile path" print " -d dump configuration. For debugging" print " -i interactive mode" print " -v increase verbosity" @@ -119,6 +121,8 @@ class Fail2banClient: self.__conf["conf"] = opt[1] elif opt[0] == "-s": self.__conf["socket"] = opt[1] + elif opt[0] == "-p": + self.__conf["pidfile"] = opt[1] elif opt[0] == "-d": self.__conf["dump"] = True elif opt[0] == "-v": @@ -183,6 +187,7 @@ class Fail2banClient: return False # Start the server self.__startServerAsync(self.__conf["socket"], + self.__conf["pidfile"], self.__conf["force"]) try: # Wait for the server to start @@ -231,7 +236,7 @@ class Fail2banClient: # # Start the Fail2ban server in daemon mode. - def __startServerAsync(self, socket, force = False): + def __startServerAsync(self, socket, pidfile, force = False): # Forks the current process. pid = os.fork() if pid == 0: @@ -242,6 +247,9 @@ class Fail2banClient: # Set the socket path. args.append("-s") args.append(socket) + # Set the pidfile + args.append("-p") + args.append(pidfile) # Force the execution if needed. if force: args.append("-x") @@ -297,7 +305,7 @@ class Fail2banClient: # Reads the command line options. try: - cmdOpts = 'hc:s:xdviqV' + cmdOpts = 'hc:s:p:xdviqV' cmdLongOpts = ['help', 'version'] optList, args = getopt.getopt(self.__argv[1:], cmdOpts, cmdLongOpts) except getopt.GetoptError: @@ -328,9 +336,11 @@ class Fail2banClient: # Set socket path self.__configurator.readEarly() - socket = self.__configurator.getEarlyOptions() + conf = self.__configurator.getEarlyOptions() if self.__conf["socket"] == None: - self.__conf["socket"] = socket["socket"] + self.__conf["socket"] = conf["socket"] + if self.__conf["pidfile"] == None: + self.__conf["pidfile"] = conf["pidfile"] logSys.info("Using socket file " + self.__conf["socket"]) if self.__conf["dump"]: diff --git a/fail2ban-server b/fail2ban-server index 0f3410c9..81db58bd 100755 --- a/fail2ban-server +++ b/fail2ban-server @@ -54,6 +54,7 @@ class Fail2banServer: self.__conf["background"] = True self.__conf["force"] = False self.__conf["socket"] = "/var/run/fail2ban/fail2ban.sock" + self.__conf["pidfile"] = "/var/run/fail2ban/fail2ban.pid" def dispVersion(self): print "Fail2Ban v" + version @@ -81,6 +82,7 @@ class Fail2banServer: print " -b start in background" print " -f start in foreground" print " -s socket path" + print " -p pidfile path" print " -x force execution of the server (remove socket file)" print " -h, --help display this help message" print " -V, --version print the version" @@ -97,6 +99,8 @@ class Fail2banServer: self.__conf["background"] = False if opt[0] == "-s": self.__conf["socket"] = opt[1] + if opt[0] == "-p": + self.__conf["pidfile"] = opt[1] if opt[0] == "-x": self.__conf["force"] = True if opt[0] in ["-h", "--help"]: @@ -112,7 +116,7 @@ class Fail2banServer: # Reads the command line options. try: - cmdOpts = 'bfs:xhV' + cmdOpts = 'bfs:p:xhV' cmdLongOpts = ['help', 'version'] optList, args = getopt.getopt(self.__argv[1:], cmdOpts, cmdLongOpts) except getopt.GetoptError: @@ -123,7 +127,9 @@ class Fail2banServer: try: self.__server = Server(self.__conf["background"]) - self.__server.start(self.__conf["socket"], self.__conf["force"]) + self.__server.start(self.__conf["socket"], + self.__conf["pidfile"], + self.__conf["force"]) return True except Exception, e: logSys.exception(e) diff --git a/server/server.py b/server/server.py index d9532be2..3889c491 100644 --- a/server/server.py +++ b/server/server.py @@ -40,8 +40,6 @@ logSys = logging.getLogger("fail2ban.server") class Server: - PID_FILE = "/var/run/fail2ban/fail2ban.pid" - def __init__(self, daemon = False): self.__loggingLock = Lock() self.__lock = RLock() @@ -59,7 +57,7 @@ class Server: logSys.debug("Caught signal %d. Exiting" % signum) self.quit() - def start(self, sock, force = False): + def start(self, sock, pidfile, force = False): logSys.info("Starting Fail2ban v" + version.version) # Install signal handlers @@ -79,8 +77,8 @@ class Server: # Creates a PID file. try: - logSys.debug("Creating PID file %s" % Server.PID_FILE) - pidFile = open(Server.PID_FILE, 'w') + logSys.debug("Creating PID file %s" % pidfile) + pidFile = open(pidfile, 'w') pidFile.write("%s\n" % os.getpid()) pidFile.close() except IOError, e: @@ -94,8 +92,8 @@ class Server: logSys.error("Could not start server: %s", e) # Removes the PID file. try: - logSys.debug("Remove PID file %s" % Server.PID_FILE) - os.remove(Server.PID_FILE) + logSys.debug("Remove PID file %s" % pidfile) + os.remove(pidfile) except OSError, e: logSys.error("Unable to remove PID file: %s" % e) logSys.info("Exiting Fail2ban") From 40c5a2d996ac16f0690c2a8b8f0b95979277d4ee Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Mon, 18 Feb 2013 23:08:44 -0500 Subject: [PATCH 07/26] ENH: adding more of diagnostic messages into -client while starting the daemon --- fail2ban-client | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/fail2ban-client b/fail2ban-client index 595144ef..7ee4a47c 100755 --- a/fail2ban-client +++ b/fail2ban-client @@ -134,11 +134,11 @@ class Fail2banClient: elif opt[0] == "-i": self.__conf["interactive"] = True elif opt[0] in ["-h", "--help"]: - self.dispUsage() - sys.exit(0) - elif opt[0] in ["-V", "--version"]: - self.dispVersion() - sys.exit(0) + self.dispUsage() + sys.exit(0) + elif opt[0] in ["-V", "--version"]: + self.dispVersion() + sys.exit(0) def __ping(self): return self.__processCmd([["ping"]], False) @@ -185,6 +185,18 @@ class Fail2banClient: # Do not continue if configuration is not 100% valid if not ret: return False + # verify that directory for the socket file exists + socket_dir = os.path.dirname(self.__conf["socket"]) + if not os.path.exists(socket_dir): + logSys.error( + "There is no directory %s to contain the socket file %s." + % (socket_dir, self.__conf["socket"])) + return False + if not os.access(socket_dir, os.W_OK | os.X_OK): + logSys.error( + "Directory %s exists but not accessible for writing" + % (socket_dir,)) + return False # Start the server self.__startServerAsync(self.__conf["socket"], self.__conf["pidfile"], @@ -196,10 +208,10 @@ class Fail2banClient: self.__processCmd(self.__stream, False) return True except ServerExecutionException: - logSys.error("Could not start server. Maybe an old " + - "socket file is still present. Try to " + - "remove " + self.__conf["socket"] + ". If " + - "you used fail2ban-client to start the " + + logSys.error("Could not start server. Maybe an old " + "socket file is still present. Try to " + "remove " + self.__conf["socket"] + ". If " + "you used fail2ban-client to start the " "server, adding the -x option will do it") return False elif len(cmd) == 1 and cmd[0] == "reload": @@ -256,16 +268,17 @@ class Fail2banClient: try: # Use the current directory. exe = os.path.abspath(os.path.join(sys.path[0], self.SERVER)) + logSys.debug("Starting %r with args %r" % (exe, args)) os.execv(exe, args) except OSError: try: # Use the PATH env. + logSys.warning("Initial start attempt failed. Starting %r with the same args" % (self.SERVER,)) os.execvp(self.SERVER, args) except OSError: - print "Could not find %s" % self.SERVER + logSys.error("Could not start %s" % self.SERVER) os.exit(-1) - - + def __waitOnServer(self): # Wait for the server to start cnt = 0 From 088e40c48192caed3a0c415e09211cd08c7c85d0 Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Wed, 20 Feb 2013 23:14:42 +0000 Subject: [PATCH 08/26] Rewrite and enable server testcase for Transmitter --- fail2ban-testcases | 2 +- testcases/servertestcase.py | 449 ++++++++++++++++++++++++++++++------ 2 files changed, 381 insertions(+), 70 deletions(-) diff --git a/fail2ban-testcases b/fail2ban-testcases index 99fefd57..d2bbfed1 100755 --- a/fail2ban-testcases +++ b/fail2ban-testcases @@ -124,7 +124,7 @@ else: # Server #tests.addTest(unittest.makeSuite(servertestcase.StartStop)) -#tests.addTest(unittest.makeSuite(servertestcase.Transmitter)) +tests.addTest(unittest.makeSuite(servertestcase.Transmitter)) tests.addTest(unittest.makeSuite(actiontestcase.ExecuteAction)) # FailManager tests.addTest(unittest.makeSuite(failmanagertestcase.AddFailure)) diff --git a/testcases/servertestcase.py b/testcases/servertestcase.py index 54eac444..47d4ef0b 100644 --- a/testcases/servertestcase.py +++ b/testcases/servertestcase.py @@ -27,7 +27,7 @@ __date__ = "$Date$" __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" -import unittest, socket, time +import unittest, socket, time, tempfile, os from server.server import Server class StartStop(unittest.TestCase): @@ -55,76 +55,387 @@ class Transmitter(unittest.TestCase): def setUp(self): """Call before every test case.""" self.__server = Server() + self.__transm = self.__server._Server__transm + self.__server.setLogTarget("/dev/null") self.__server.setLogLevel(0) - self.__server.start(False) + sock_fd, sock_name = tempfile.mkstemp('fail2ban.sock', 'transmitter') + os.close(sock_fd) + pidfile_fd, pidfile_name = tempfile.mkstemp( + 'fail2ban.pid', 'transmitter') + os.close(pidfile_fd) + self.__server.start(sock_name, pidfile_name, force=False) + self.jailName = "TestJail1" + self.__server.addJail(self.jailName, "auto") def tearDown(self): """Call after every test case.""" self.__server.quit() - - def testSetActionOK(self): - name = "TestCase" - cmdList = [["add", name], - ["set", name, "actionstart", "Action Start"], - ["set", name, "actionstop", "Action Stop"], - ["set", name, "actioncheck", "Action Check"], - ["set", name, "actionban", "Action Ban"], - ["set", name, "actionunban", "Action Unban"], - ["quit"]] - - outList = [(0, name), - (0, 'Action Start'), - (0, 'Action Stop'), - (0, 'Action Check'), - (0, 'Action Ban'), - (0, 'Action Unban'), - (0, None)] - - cnt = 0 - for cmd in cmdList: - self.assertEqual(self.__server.transm.proceed(cmd), outList[cnt]) - cnt += 1 - - def testSetActionNOK(self): - name = "TestCase" - cmdList = [["addd", name], - ["set", name, "test"], - ["prout prout", "Stop"], - ["fail2ban", "sucks"], - ["set"], - ["_/&%", "@*+%&"], - [" quit"]] - - outList = [1, - 1, - 1, - 1, - 1, - 1, - 1] - - cnt = 0 - for cmd in cmdList: - msg = self.__server.transm.proceed(cmd) - self.assertEqual(msg[0], outList[cnt]) - cnt += 1 - - def testJail(self): - name = "TestCase" - cmdList = [["add", name], - ["set", name, "logpath", "testcases/files/testcase01.log"], - ["set", name, "timeregex", "\S{3}\s{1,2}\d{1,2} \d{2}:\d{2}:\d{2}"], - ["set", name, "timepattern", "%b %d %H:%M:%S"], - ["set", name, "failregex", "Authentication failure"], - ["start", name], - ["stop", name], - ["quit"]] - - for cmd in cmdList: - self.__server.transm.proceed(cmd) - if cmd == ["start", name]: - time.sleep(2) - jail = self.__server.jails[name] - self.assertEqual(jail.getFilter().failManager.size(), 0) - self.assertEqual(jail.getAction().banManager.size(), 2) - + + def setGetTest(self, cmd, inValue, outValue=None, jail=None): + setCmd = ["set", cmd, inValue] + getCmd = ["get", cmd] + if jail is not None: + setCmd.insert(1, jail) + getCmd.insert(1, jail) + if outValue is None: + outValue = inValue + + self.assertEqual(self.__transm.proceed(setCmd), (0, outValue)) + self.assertEqual(self.__transm.proceed(getCmd), (0, outValue)) + + def setGetTestNOK(self, cmd, inValue, jail=None): + setCmd = ["set", cmd, inValue] + getCmd = ["get", cmd] + if jail is not None: + setCmd.insert(1, jail) + getCmd.insert(1, jail) + + # Get initial value before trying invalid value + initValue = self.__transm.proceed(getCmd)[1] + self.assertEqual(self.__transm.proceed(setCmd)[0], 1) + # Check after failed set that value is same as previous + self.assertEqual(self.__transm.proceed(getCmd), (0, initValue)) + + def jailAddDelTest(self, cmd, values, jail): + cmdAdd = "add" + cmd + cmdDel = "del" + cmd + + self.assertEqual( + self.__transm.proceed(["get", jail, cmd]), (0, [])) + for n, value in enumerate(values): + self.assertEqual( + self.__transm.proceed(["set", jail, cmdAdd, value]), + (0, values[:n+1])) + self.assertEqual( + self.__transm.proceed(["get", jail, cmd]), + (0, values[:n+1])) + for n, value in enumerate(values): + self.assertEqual( + self.__transm.proceed(["set", jail, cmdDel, value]), + (0, values[n+1:])) + self.assertEqual( + self.__transm.proceed(["get", jail, cmd]), + (0, values[n+1:])) + + def jailAddDelRegexTest(self, cmd, inValues, outValues, jail): + cmdAdd = "add" + cmd + cmdDel = "del" + cmd + + if outValues is None: + outValues = inValues + + self.assertEqual( + self.__transm.proceed(["get", jail, cmd]), (0, [])) + for n, value in enumerate(inValues): + self.assertEqual( + self.__transm.proceed(["set", jail, cmdAdd, value]), + (0, outValues[:n+1])) + self.assertEqual( + self.__transm.proceed(["get", jail, cmd]), + (0, outValues[:n+1])) + for n, value in enumerate(inValues): + self.assertEqual( + self.__transm.proceed(["set", jail, cmdDel, 0]), # First item + (0, outValues[n+1:])) + self.assertEqual( + self.__transm.proceed(["get", jail, cmd]), + (0, outValues[n+1:])) + + def testPing(self): + self.assertEqual(self.__transm.proceed(["ping"]), (0, "pong")) + + def testSleep(self): + t0 = time.time() + self.assertEqual(self.__transm.proceed(["sleep", "1"]), (0, None)) + t1 = time.time() + # Approx 1 second delay + self.assertAlmostEqual(t1 - t0, 1, places=2) + + def testLogTarget(self): + logTargets = [] + for _ in xrange(3): + tmpFile = tempfile.mkstemp("fail2ban", "transmitter") + logTargets.append(tmpFile[1]) + os.close(tmpFile[0]) + for logTarget in logTargets: + self.setGetTest("logtarget", logTarget) + + # If path is invalid, do not change logtarget + value = "/this/path/should/not/exist" + self.assertEqual( + self.__transm.proceed(["set", "logtarget", value]), + (0, logTarget)) #NOTE: Shouldn't this return 1 + self.assertEqual( + self.__transm.proceed(["get", "logtarget"]), (0, logTargets[-1])) + + self.__transm.proceed(["set", "/dev/null"]) + for logTarget in logTargets: + os.remove(logTarget) + + def testLogLevel(self): + self.setGetTest("loglevel", "4", 4) + self.setGetTest("loglevel", "2", 2) + self.setGetTest("loglevel", "-1", -1) + self.setGetTestNOK("loglevel", "Bird") + + def testAddJail(self): + jail2 = "TestJail2" + jail3 = "TestJail3" + jail4 = "TestJail4" + self.assertEqual( + self.__transm.proceed(["add", jail2, "polling"]), (0, jail2)) + self.assertEqual(self.__transm.proceed(["add", jail3]), (0, jail3)) + self.assertEqual( + self.__transm.proceed(["add", jail4, "invalid backend"])[0], 1) + self.assertEqual( + self.__transm.proceed(["add", jail4, "auto"]), (0, jail4)) + # Duplicate Jail + self.assertEqual( + self.__transm.proceed(["add", self.jailName, "polling"])[0], 1) + # All name is reserved + self.assertEqual( + self.__transm.proceed(["add", "all", "polling"])[0], 1) + + def testJailIdle(self): + self.assertEqual( + self.__transm.proceed(["set", self.jailName, "idle", "on"]), + (0, True)) + self.assertEqual( + self.__transm.proceed(["set", self.jailName, "idle", "off"]), + (0, False)) + self.assertEqual( + self.__transm.proceed(["set", self.jailName, "idle", "CAT"])[0], + 0) #NOTE: Should this return 1 + + def testJailFindTime(self): + self.setGetTest("findtime", "120", 120, jail=self.jailName) + self.setGetTest("findtime", "60", 60, jail=self.jailName) + self.setGetTest("findtime", "-60", -60, jail=self.jailName) + self.setGetTestNOK("findtime", "Dog", jail=self.jailName) + + def testJailBanTime(self): + self.setGetTest("bantime", "600", 600, jail=self.jailName) + self.setGetTest("bantime", "50", 50, jail=self.jailName) + self.setGetTest("bantime", "-50", -50, jail=self.jailName) + self.setGetTestNOK("bantime", "Cat", jail=self.jailName) + + def testJailUseDNS(self): + self.setGetTest("usedns", "yes", jail=self.jailName) + self.setGetTest("usedns", "warn", jail=self.jailName) + self.setGetTest("usedns", "no", jail=self.jailName) + + # Safe default should be "no" + value = "Fish" + self.assertEqual( + self.__transm.proceed(["set", self.jailName, "usedns", value]), + (0, "no")) + + def testJailBanIP(self): + self.__server.startJail(self.jailName) # Jail must be started + + self.assertEqual( + self.__transm.proceed(["set", self.jailName, "banip", "127.0.0.1"]), + (0, "127.0.0.1")) + time.sleep(1) # Give chance to ban + self.assertEqual( + self.__transm.proceed(["set", self.jailName, "banip", "Badger"]), + (0, "Badger")) #NOTE: Is IP address validated? Is DNS Lookup done? + time.sleep(1) # Give chance to ban + # Unban IP + self.assertEqual( + self.__transm.proceed( + ["set", self.jailName, "unbanip", "127.0.0.1"]), + (0, "127.0.0.1")) + # Unban IP which isn't banned + self.assertEqual( + self.__transm.proceed( + ["set", self.jailName, "unbanip", "192.168.1.1"]), + (0, "None")) #NOTE: Should this return 1? + + def testJailMaxRetry(self): + self.setGetTest("maxretry", "5", 5, jail=self.jailName) + self.setGetTest("maxretry", "2", 2, jail=self.jailName) + self.setGetTest("maxretry", "-2", -2, jail=self.jailName) + self.setGetTestNOK("maxretry", "Duck", jail=self.jailName) + + def testJailLogPath(self): + self.jailAddDelTest( + "logpath", + [ + "testcases/files/testcase01.log", + "testcases/files/testcase02.log", + "testcases/files/testcase03.log", + ], + self.jailName + ) + # Try duplicates + value = "testcases/files/testcase04.log" + self.assertEqual( + self.__transm.proceed(["set", self.jailName, "addlogpath", value]), + (0, [value])) + # Will silently ignore duplicate + self.assertEqual( + self.__transm.proceed(["set", self.jailName, "addlogpath", value]), + (0, [value])) + self.assertEqual( + self.__transm.proceed(["get", self.jailName, "logpath"]), + (0, [value])) + self.assertEqual( + self.__transm.proceed(["set", self.jailName, "dellogpath", value]), + (0, [])) + + # Invalid file + value = "this_file_shouldn't_exist" + result = self.__transm.proceed( + ["set", self.jailName, "addlogpath", value]) + self.assertTrue(isinstance(result[1], IOError)) + + def testJailIgnoreIP(self): + self.jailAddDelTest( + "ignoreip", + [ + "127.0.0.1", + "192.168.1.1", + "8.8.8.8", + ], + self.jailName + ) + + # Try duplicates + value = "127.0.0.1" + self.assertEqual( + self.__transm.proceed(["set", self.jailName, "addignoreip", value]), + (0, [value])) + # Will allow duplicate + #NOTE: Should duplicates be allowed, or silent ignore like logpath? + self.assertEqual( + self.__transm.proceed(["set", self.jailName, "addignoreip", value]), + (0, [value, value])) + self.assertEqual( + self.__transm.proceed(["get", self.jailName, "ignoreip"]), + (0, [value, value])) + self.assertEqual( + self.__transm.proceed(["set", self.jailName, "delignoreip", value]), + (0, [value])) + + def testJailRegex(self): + self.jailAddDelRegexTest("failregex", + [ + "user john at ", + "Admin user login from ", + "failed attempt from again", + ], + [ + "user john at (?:::f{4,6}:)?(?P[\w\-.^_]+)", + "Admin user login from (?:::f{4,6}:)?(?P[\w\-.^_]+)", + "failed attempt from (?:::f{4,6}:)?(?P[\w\-.^_]+) again", + ], + self.jailName + ) + + self.assertEqual( + self.__transm.proceed( + ["set", self.jailName, "addfailregex", "No host regex"]), + (0, [])) #NOTE: Shouldn't this return 1? + self.assertEqual( + self.__transm.proceed( + ["set", self.jailName, "addfailregex", 654])[0], + 1) + + def testJailIgnoreRegex(self): + self.jailAddDelRegexTest("ignoreregex", + [ + "user john", + "Admin user login from ", + "Dont match me!", + ], + [ + "user john", + "Admin user login from (?:::f{4,6}:)?(?P[\w\-.^_]+)", + "Dont match me!", + ], + self.jailName + ) + + self.assertEqual( + self.__transm.proceed( + ["set", self.jailName, "addignoreregex", 50])[0], + 1) + + def testStatus(self): + jails = [self.jailName] + self.assertEqual(self.__transm.proceed(["status"]), + (0, [('Number of jail', len(jails)), ('Jail list', ", ".join(jails))])) + self.__server.addJail("TestJail2", "auto") + jails.append("TestJail2") + self.assertEqual(self.__transm.proceed(["status"]), + (0, [('Number of jail', len(jails)), ('Jail list', ", ".join(jails))])) + + def testJailStatus(self): + self.assertEqual(self.__transm.proceed(["status", self.jailName]), + (0, + [ + ('filter', [ + ('Currently failed', 0), + ('Total failed', 0), + ('File list', [])] + ), + ('action', [ + ('Currently banned', 0), + ('Total banned', 0), + ('IP list', [])] + ) + ] + ) + ) + + def testAction(self): + action = "TestCaseAction" + cmdList = [ + "actionstart", + "actionstop", + "actioncheck", + "actionban", + "actionunban", + ] + cmdValueList = [ + "Action Start", + "Action Stop", + "Action Check", + "Action Ban", + "Action Unban", + ] + + self.assertEqual( + self.__transm.proceed(["set", self.jailName, "addaction", action]), + (0, action)) + for cmd, value in zip(cmdList, cmdValueList): + self.assertEqual( + self.__transm.proceed( + ["set", self.jailName, cmd, action, value]), + (0, value)) + for cmd, value in zip(cmdList, cmdValueList): + self.assertEqual( + self.__transm.proceed(["get", self.jailName, cmd, action]), + (0, value)) + self.assertEqual( + self.__transm.proceed( + ["set", self.jailName, "setcinfo", action, "KEY", "VALUE"]), + (0, "VALUE")) + #TODO: Implement below in server.transmitter? + """ + self.assertEqual( + self.__transm.proceed( + ["get", self.jailName, "cinfo", action, "KEY"]), + (0, "VALUE")) + """ + self.assertEqual( + self.__transm.proceed( + ["set", self.jailName, "delcinfo", action, "KEY"]), + (0, None)) + #FIXME: This is broken in server.transmitter (value not defined) + """ + self.assertEqual( + self.__transm.proceed(["set", self.jailName, "delaction", action]), + (0, None)) + """ From b6a68f5138c12e5f1374d76ac76f8f43bc814bfc Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Wed, 20 Feb 2013 23:24:46 +0000 Subject: [PATCH 09/26] Fix for missing value in transmitter delaction --- server/transmitter.py | 1 + testcases/servertestcase.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/server/transmitter.py b/server/transmitter.py index 23b609a1..3bd7fb14 100644 --- a/server/transmitter.py +++ b/server/transmitter.py @@ -183,6 +183,7 @@ class Transmitter: self.__server.addAction(name, value) return self.__server.getLastAction(name).getName() elif command[1] == "delaction": + value = command[2] self.__server.delAction(name, value) return None elif command[1] == "setcinfo": diff --git a/testcases/servertestcase.py b/testcases/servertestcase.py index 47d4ef0b..0b33a96f 100644 --- a/testcases/servertestcase.py +++ b/testcases/servertestcase.py @@ -433,9 +433,10 @@ class Transmitter(unittest.TestCase): self.__transm.proceed( ["set", self.jailName, "delcinfo", action, "KEY"]), (0, None)) - #FIXME: This is broken in server.transmitter (value not defined) - """ self.assertEqual( self.__transm.proceed(["set", self.jailName, "delaction", action]), (0, None)) - """ + self.assertEqual( + self.__transm.proceed( + ["set", self.jailName, "delaction", "Doesn't exist"]), + (0, None)) #NOTE: Should this return 1? From b36835f6f061efded70c747539a1da2d66b9d270 Mon Sep 17 00:00:00 2001 From: Steven Hiscocks Date: Wed, 20 Feb 2013 23:33:39 +0000 Subject: [PATCH 10/26] Added transmitter get cinfo option for action --- common/protocol.py | 1 + server/transmitter.py | 4 ++++ testcases/servertestcase.py | 7 ++++--- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/common/protocol.py b/common/protocol.py index 2f8ffa6c..99a2fe09 100644 --- a/common/protocol.py +++ b/common/protocol.py @@ -90,6 +90,7 @@ protocol = [ ["get actioncheck ", "gets the check command for the action for "], ["get actionban ", "gets the ban command for the action for "], ["get actionunban ", "gets the unban command for the action for "], +["get cinfo ", "gets the value for for the action for "], ] ## diff --git a/server/transmitter.py b/server/transmitter.py index 3bd7fb14..a02b94a2 100644 --- a/server/transmitter.py +++ b/server/transmitter.py @@ -266,6 +266,10 @@ class Transmitter: elif command[1] == "actionunban": act = command[2] return self.__server.getActionUnban(name, act) + elif command[1] == "cinfo": + act = command[2] + key = command[3] + return self.__server.getCInfo(name, act, key) raise Exception("Invalid command (no get action or not yet implemented)") def status(self, command): diff --git a/testcases/servertestcase.py b/testcases/servertestcase.py index 0b33a96f..00f56b81 100644 --- a/testcases/servertestcase.py +++ b/testcases/servertestcase.py @@ -422,13 +422,14 @@ class Transmitter(unittest.TestCase): self.__transm.proceed( ["set", self.jailName, "setcinfo", action, "KEY", "VALUE"]), (0, "VALUE")) - #TODO: Implement below in server.transmitter? - """ self.assertEqual( self.__transm.proceed( ["get", self.jailName, "cinfo", action, "KEY"]), (0, "VALUE")) - """ + self.assertEqual( + self.__transm.proceed( + ["get", self.jailName, "cinfo", action, "InvalidKey"])[0], + 1) self.assertEqual( self.__transm.proceed( ["set", self.jailName, "delcinfo", action, "KEY"]), From 012264dce169b8ae9a18a599371bcb9685ec3570 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Thu, 21 Feb 2013 20:58:27 -0500 Subject: [PATCH 11/26] BF: safeguard closing of log handlers + close in reverse order otherwise there might be "stuck" handler in the queue. and closing exceptions can occur -- even stock logging guards in recent versions --- server/server.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/server/server.py b/server/server.py index 3889c491..dea303db 100644 --- a/server/server.py +++ b/server/server.py @@ -367,11 +367,20 @@ class Server: logSys.error("Unable to log to " + target) logSys.info("Logging to previous target " + self.__logTarget) return False - # Removes previous handlers - for handler in logging.getLogger("fail2ban").handlers: - # Closes the handler. + # Removes previous handlers -- in reverse order since removeHandler + # alter the list in-place and that can confuses the iterable + for handler in logging.getLogger("fail2ban").handlers[::-1]: + # Remove the handler. logging.getLogger("fail2ban").removeHandler(handler) - handler.close() + # And try to close -- it might be closed already + try: + handler.flush() + handler.close() + except ValueError: + if sys.version_info >= (2,6): + raise + # is known to be thrown after logging was shutdown once + # with older Pythons -- seems to be safe to ignore there # tell the handler to use this format hdlr.setFormatter(formatter) logging.getLogger("fail2ban").addHandler(hdlr) From 154aa38e3f8174baee6593c3022c53f25b8aa364 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Thu, 21 Feb 2013 20:59:46 -0500 Subject: [PATCH 12/26] BF: do not shutdown logging until all jails stop -- so move into Server.quit() Together with previous commit it should resolve failures with the server tests on python < 2.6 --- server/server.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/server/server.py b/server/server.py index dea303db..0e1b6c97 100644 --- a/server/server.py +++ b/server/server.py @@ -97,12 +97,6 @@ class Server: except OSError, e: logSys.error("Unable to remove PID file: %s" % e) logSys.info("Exiting Fail2ban") - # Shutdowns the logging. - try: - self.__loggingLock.acquire() - logging.shutdown() - finally: - self.__loggingLock.release() def quit(self): # Stop communication first because if jail's unban action @@ -112,8 +106,17 @@ class Server: # are exiting) # See https://github.com/fail2ban/fail2ban/issues/7 self.__asyncServer.stop() + # Now stop all the jails self.stopAllJail() + + # Only now shutdown the logging. + try: + self.__loggingLock.acquire() + logging.shutdown() + finally: + self.__loggingLock.release() + def addJail(self, name, backend): self.__jails.add(name, backend) From 59c35bc44a175a672e084bc30511dfa3436ff052 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Fri, 1 Mar 2013 19:57:56 -0500 Subject: [PATCH 13/26] Downgrade log rotation detection message to DEBUG level from INFO. Closes: gh-129 This message useful only when debugging problems so it is more reasonable to have it suppressed otherwise --- server/filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/filter.py b/server/filter.py index fb79fcbe..5b2b85e0 100644 --- a/server/filter.py +++ b/server/filter.py @@ -550,7 +550,7 @@ class FileContainer: stats = os.fstat(self.__handler.fileno()) # Compare hash and inode if self.__hash != myHash or self.__ino != stats.st_ino: - logSys.info("Log rotation detected for %s" % self.__filename) + logSys.debug("Log rotation detected for %s" % self.__filename) self.__hash = myHash self.__ino = stats.st_ino self.__pos = 0 From 6e774275161712e80fa99708395958239cd1bbd6 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Thu, 7 Mar 2013 13:03:49 -0500 Subject: [PATCH 14/26] refresh generated manpages (since 0.8.2 state) --- man/fail2ban-client.1 | 39 +++++++++++++++++++++++++-------------- man/fail2ban-regex.1 | 13 ++++++++----- man/fail2ban-server.1 | 13 ++++++++----- 3 files changed, 41 insertions(+), 24 deletions(-) diff --git a/man/fail2ban-client.1 b/man/fail2ban-client.1 index 1bbddf09..431e690f 100644 --- a/man/fail2ban-client.1 +++ b/man/fail2ban-client.1 @@ -1,12 +1,12 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.36. -.TH FAIL2BAN-CLIENT "1" "March 2008" "fail2ban-client v0.8.2" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.40.10. +.TH FAIL2BAN-CLIENT "1" "March 2013" "fail2ban-client v0.8.8" "User Commands" .SH NAME fail2ban-client \- configure and control the server .SH SYNOPSIS .B fail2ban-client [\fIOPTIONS\fR] \fI\fR .SH DESCRIPTION -Fail2Ban v0.8.2 reads log file that contains password failure report +Fail2Ban v0.8.8 reads log file that contains password failure report and bans the corresponding IP addresses using firewall rules. .SH OPTIONS .TP @@ -16,6 +16,9 @@ configuration directory \fB\-s\fR socket path .TP +\fB\-p\fR +pidfile path +.TP \fB\-d\fR dump configuration. For debugging .TP @@ -110,7 +113,7 @@ adds to the monitoring list of .TP \fBset dellogpath \fR -removes to the monitoring +removes from the monitoring list of .TP \fBset addfailregex \fR @@ -140,6 +143,15 @@ back for sets the number of seconds