diff --git a/.travis.yml b/.travis.yml index 41eeca27..9a92a7f6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ python: - "2.7" - "3.2" - "3.3" + - "3.4" - "pypy" before_install: - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then sudo apt-get update -qq; fi diff --git a/ChangeLog b/ChangeLog index bbe32054..69cbe909 100644 --- a/ChangeLog +++ b/ChangeLog @@ -22,13 +22,21 @@ ver. 0.9.1 (2014/xx/xx) - better, faster, stronger * Nginx filter to support missing server_name. Closes gh-676 * fail2ban-regex assertion error caused by miscount missed lines with multiline regex + * Fix actions failing to execute for Python 3.4.0. Workaround for + http://bugs.python.org/issue21207 + * Database now returns persistent bans on restart (bantime < 0) + * Recursive action tags now fully processed. Fixes issue with bsd-ipfw + action + * Correct times for non-timezone date times formats - Thanks sebres - New features: + - Added monit filter thanks Jason H Martin. - Enhancements * Fail2ban-regex - add print-all-matched option. Closes gh-652 * Suppress fail2ban-client warnings for non-critical config options + * Match non "Bye Bye" disconnect messages for sshd locked account regex ver. 0.9.0 (2014/03/14) - beta ---------- diff --git a/THANKS b/THANKS index 2c084dee..2c5b65bf 100644 --- a/THANKS +++ b/THANKS @@ -48,6 +48,7 @@ Ivo Truxa John Thoe Jacques Lav!gnotte Ioan Indreias +Jason H Martin Jonathan Kamens Jonathan Lanning Jonathan Underwood @@ -85,6 +86,7 @@ Rolf Fokkens Roman Gelfand Russell Odom Sebastian Arcus +sebres Sireyessire silviogarbes Stefan Tatschner diff --git a/bin/fail2ban-client b/bin/fail2ban-client index 8737c49d..289d7b39 100755 --- a/bin/fail2ban-client +++ b/bin/fail2ban-client @@ -51,6 +51,7 @@ class Fail2banClient: self.__conf["conf"] = "/etc/fail2ban" self.__conf["dump"] = False self.__conf["force"] = False + self.__conf["background"] = True self.__conf["verbose"] = 1 self.__conf["interactive"] = False self.__conf["socket"] = None @@ -83,6 +84,8 @@ class Fail2banClient: print " -v increase verbosity" print " -q decrease verbosity" print " -x force execution of the server (remove socket file)" + print " -b start server in background (default)" + print " -f start server in foreground (note that the client forks once itself)" print " -h, --help display this help message" print " -V, --version print the version" print @@ -125,6 +128,10 @@ class Fail2banClient: self.__conf["force"] = True elif opt[0] == "-i": self.__conf["interactive"] = True + elif opt[0] == "-b": + self.__conf["background"] = True + elif opt[0] == "-f": + self.__conf["background"] = False elif opt[0] in ["-h", "--help"]: self.dispUsage() sys.exit(0) @@ -194,7 +201,8 @@ class Fail2banClient: # Start the server self.__startServerAsync(self.__conf["socket"], self.__conf["pidfile"], - self.__conf["force"]) + self.__conf["force"], + self.__conf["background"]) try: # Wait for the server to start self.__waitOnServer() @@ -242,14 +250,12 @@ class Fail2banClient: # # Start the Fail2ban server in daemon mode. - def __startServerAsync(self, socket, pidfile, force = False): + def __startServerAsync(self, socket, pidfile, force = False, background = True): # Forks the current process. pid = os.fork() if pid == 0: args = list() args.append(self.SERVER) - # Start in background mode. - args.append("-b") # Set the socket path. args.append("-s") args.append(socket) @@ -259,6 +265,12 @@ class Fail2banClient: # Force the execution if needed. if force: args.append("-x") + # Start in foreground mode if requested. + if background: + args.append("-b") + else: + args.append("-f") + try: # Use the current directory. exe = os.path.abspath(os.path.join(sys.path[0], self.SERVER)) @@ -312,7 +324,7 @@ class Fail2banClient: # Reads the command line options. try: - cmdOpts = 'hc:s:p:xdviqV' + cmdOpts = 'hc:s:p:xfbdviqV' cmdLongOpts = ['help', 'version'] optList, args = getopt.getopt(self.__argv[1:], cmdOpts, cmdLongOpts) except getopt.GetoptError: diff --git a/bin/fail2ban-regex b/bin/fail2ban-regex index 974566bc..3a887867 100755 --- a/bin/fail2ban-regex +++ b/bin/fail2ban-regex @@ -45,7 +45,7 @@ from fail2ban.client.filterreader import FilterReader from fail2ban.server.filter import Filter from fail2ban.server.failregex import RegexException -from fail2ban.tests.utils import FormatterWithTraceBack +from fail2ban.helpers import FormatterWithTraceBack # Gets the instance of the logger. logSys = logging.getLogger("fail2ban") diff --git a/bin/fail2ban-testcases b/bin/fail2ban-testcases index b3bddf1c..0e2fdb4b 100755 --- a/bin/fail2ban-testcases +++ b/bin/fail2ban-testcases @@ -34,7 +34,8 @@ if os.path.exists("fail2ban/__init__.py"): sys.path.insert(0, ".") from fail2ban.version import version -from fail2ban.tests.utils import FormatterWithTraceBack, gatherTests +from fail2ban.tests.utils import gatherTests +from fail2ban.helpers import FormatterWithTraceBack from fail2ban.server.mytime import MyTime from optparse import OptionParser, Option diff --git a/config/filter.d/monit.conf b/config/filter.d/monit.conf new file mode 100644 index 00000000..1fcd980b --- /dev/null +++ b/config/filter.d/monit.conf @@ -0,0 +1,9 @@ +# Fail2Ban filter for monit.conf, looks for failed access attempts +# +# + +[Definition] + +failregex = ^\[[A-Z]+\s+\]\s*error\s*:\s*Warning:\s+Client '' supplied unknown user '\w+' accessing monit httpd$ + ^\[[A-Z]+\s+\]\s*error\s*:\s*Warning:\s+Client '' supplied wrong password for user '\w+' accessing monit httpd$ + diff --git a/config/filter.d/sshd.conf b/config/filter.d/sshd.conf index 059052fc..195744f2 100644 --- a/config/filter.d/sshd.conf +++ b/config/filter.d/sshd.conf @@ -30,7 +30,7 @@ failregex = ^%(__prefix_line)s(?:error: PAM: )?[aA]uthentication (?:failure|erro ^%(__prefix_line)sReceived disconnect from : 3: \S+: Auth fail$ ^%(__prefix_line)sUser .+ from not allowed because a group is listed in DenyGroups\s*$ ^%(__prefix_line)sUser .+ from not allowed because none of user's groups are listed in AllowGroups\s*$ - ^(?P<__prefix>%(__prefix_line)s)User .+ not allowed because account is locked(?P=__prefix)(?:error: )?Received disconnect from : 11: Bye Bye \[preauth\]$ + ^(?P<__prefix>%(__prefix_line)s)User .+ not allowed because account is locked(?P=__prefix)(?:error: )?Received disconnect from : 11: .+ \[preauth\]$ ^(?P<__prefix>%(__prefix_line)s)Disconnecting: Too many authentication failures for .+? \[preauth\](?P=__prefix)(?:error: )?Connection closed by \[preauth\]$ ^(?P<__prefix>%(__prefix_line)s)Connection from port \d+(?P=__prefix)Disconnecting: Too many authentication failures for .+? \[preauth\]$ diff --git a/config/jail.conf b/config/jail.conf index 361ea758..275d72c6 100644 --- a/config/jail.conf +++ b/config/jail.conf @@ -409,6 +409,12 @@ maxretry = 5 port = http,https logpath = /var/log/tomcat*/catalina.out +[monit] +#Ban clients brute-forcing the monit gui login +filter = monit +port = 2812 +logpath = /var/log/monit + [webmin-auth] diff --git a/fail2ban/helpers.py b/fail2ban/helpers.py index 74ea7a7a..2579381d 100644 --- a/fail2ban/helpers.py +++ b/fail2ban/helpers.py @@ -20,9 +20,90 @@ __author__ = "Cyril Jaquier, Arturo 'Buanzo' Busleiman, Yaroslav Halchenko" __license__ = "GPL" +import sys +import os +import traceback +import re +import logging def formatExceptionInfo(): """ Consistently format exception information """ - import sys cla, exc = sys.exc_info()[:2] return (cla.__name__, str(exc)) + +# +# Following "traceback" functions are adopted from PyMVPA distributed +# under MIT/Expat and copyright by PyMVPA developers (i.e. me and +# Michael). Hereby I re-license derivative work on these pieces under GPL +# to stay in line with the main Fail2Ban license +# +def mbasename(s): + """Custom function to include directory name if filename is too common + + Also strip .py at the end + """ + base = os.path.basename(s) + if base.endswith('.py'): + base = base[:-3] + if base in set(['base', '__init__']): + base = os.path.basename(os.path.dirname(s)) + '.' + base + return base + +class TraceBack(object): + """Customized traceback to be included in debug messages + """ + + def __init__(self, compress=False): + """Initialize TrackBack metric + + Parameters + ---------- + compress : bool + if True then prefix common with previous invocation gets + replaced with ... + """ + self.__prev = "" + self.__compress = compress + + def __call__(self): + ftb = traceback.extract_stack(limit=100)[:-2] + entries = [ + [mbasename(x[0]), os.path.dirname(x[0]), str(x[1])] for x in ftb] + entries = [ [e[0], e[2]] for e in entries + if not (e[0] in ['unittest', 'logging.__init__'] + or e[1].endswith('/unittest'))] + + # lets make it more concise + entries_out = [entries[0]] + for entry in entries[1:]: + if entry[0] == entries_out[-1][0]: + entries_out[-1][1] += ',%s' % entry[1] + else: + entries_out.append(entry) + sftb = '>'.join(['%s:%s' % (mbasename(x[0]), + x[1]) for x in entries_out]) + if self.__compress: + # lets remove part which is common with previous invocation + prev_next = sftb + common_prefix = os.path.commonprefix((self.__prev, sftb)) + common_prefix2 = re.sub('>[^>]*$', '', common_prefix) + + if common_prefix2 != "": + sftb = '...' + sftb[len(common_prefix2):] + self.__prev = prev_next + + return sftb + +class FormatterWithTraceBack(logging.Formatter): + """Custom formatter which expands %(tb) and %(tbc) with tracebacks + + TODO: might need locking in case of compressed tracebacks + """ + def __init__(self, fmt, *args, **kwargs): + logging.Formatter.__init__(self, fmt=fmt, *args, **kwargs) + compress = '%(tbc)s' in fmt + self._tb = TraceBack(compress=compress) + + def format(self, record): + record.tbc = record.tb = self._tb() + return logging.Formatter.format(self, record) diff --git a/fail2ban/server/action.py b/fail2ban/server/action.py index 0098c546..fefe2c2c 100644 --- a/fail2ban/server/action.py +++ b/fail2ban/server/action.py @@ -194,6 +194,8 @@ class CommandAction(ActionBase): timeout """ + _escapedTags = set(('matches', 'ipmatches', 'ipjailmatches')) + def __init__(self, jail, name): super(CommandAction, self).__init__(jail, name) self.timeout = 60 @@ -351,8 +353,8 @@ class CommandAction(ActionBase): if not self.executeCmd(stopCmd, self.timeout): raise RuntimeError("Error stopping action") - @staticmethod - def substituteRecursiveTags(tags): + @classmethod + def substituteRecursiveTags(cls, tags): """Sort out tag definitions within other tags. so: becomes: @@ -371,8 +373,11 @@ class CommandAction(ActionBase): within the values recursively replaced. """ t = re.compile(r'<([^ >]+)>') - for tag, value in tags.iteritems(): - value = str(value) + for tag in tags.iterkeys(): + if tag in cls._escapedTags: + # Escaped so won't match + continue + value = str(tags[tag]) m = t.search(value) done = [] #logSys.log(5, 'TAG: %s, value: %s' % (tag, value)) @@ -383,6 +388,9 @@ class CommandAction(ActionBase): # recursive definitions are bad #logSys.log(5, 'recursion fail tag: %s value: %s' % (tag, value) ) return False + elif found_tag in cls._escapedTags: + # Escaped so won't match + continue else: if tags.has_key(found_tag): value = value.replace('<%s>' % found_tag , tags[found_tag]) @@ -441,10 +449,11 @@ class CommandAction(ActionBase): `query` string with tags replaced. """ string = query + aInfo = cls.substituteRecursiveTags(aInfo) for tag in aInfo: if "<%s>" % tag in query: value = str(aInfo[tag]) # assure string - if tag.endswith('matches'): + if tag in cls._escapedTags: # That one needs to be escaped since its content is # out of our control value = cls.escapeTag(value) diff --git a/fail2ban/server/database.py b/fail2ban/server/database.py index 28fd0532..f752561c 100644 --- a/fail2ban/server/database.py +++ b/fail2ban/server/database.py @@ -417,7 +417,7 @@ class Fail2BanDb(object): if jail is not None: query += " AND jail=?" queryArgs.append(jail.name) - if bantime is not None: + if bantime is not None and bantime >= 0: query += " AND timeofban > ?" queryArgs.append(MyTime.time() - bantime) if ip is not None: @@ -436,7 +436,8 @@ class Fail2BanDb(object): Jail that the ban belongs to. Default `None`; all jails. bantime : int Ban time in seconds, such that bans returned would still be - valid now. Default `None`; no limit. + valid now. Negative values are equivalent to `None`. + Default `None`; no limit. ip : str IP Address to filter bans by. Default `None`; all IPs. @@ -464,7 +465,8 @@ class Fail2BanDb(object): Jail that the ban belongs to. Default `None`; all jails. bantime : int Ban time in seconds, such that bans returned would still be - valid now. Default `None`; no limit. + valid now. Negative values are equivalent to `None`. + Default `None`; no limit. ip : str IP Address to filter bans by. Default `None`; all IPs. @@ -475,7 +477,8 @@ class Fail2BanDb(object): in a list. When `ip` argument passed, a single `Ticket` is returned. """ - if bantime is None: + cacheKey = None + if bantime is None or bantime < 0: cacheKey = (ip, jail) if cacheKey in self._bansMergedCache: return self._bansMergedCache[cacheKey] @@ -505,7 +508,7 @@ class Fail2BanDb(object): ticket.setAttempt(failures) tickets.append(ticket) - if bantime is None: + if cacheKey: self._bansMergedCache[cacheKey] = tickets if ip is None else ticket return tickets if ip is None else ticket diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py index a10d5ab3..b9efdfe8 100644 --- a/fail2ban/server/server.py +++ b/fail2ban/server/server.py @@ -529,11 +529,19 @@ class Server: except (AttributeError, ValueError): maxfd = 256 # default maximum - for fd in range(0, maxfd): - try: - os.close(fd) - except OSError: # ERROR (ignore) - pass + # urandom should not be closed in Python 3.4.0. Fixed in 3.4.1 + # http://bugs.python.org/issue21207 + if sys.version_info[0:3] == (3, 4, 0): # pragma: no cover + urandom_fd = os.open("/dev/urandom", os.O_RDONLY) + for fd in range(0, maxfd): + try: + if not os.path.sameopenfile(urandom_fd, fd): + os.close(fd) + except OSError: # ERROR (ignore) + pass + os.close(urandom_fd) + else: + os.closerange(0, maxfd) # Redirect the standard file descriptors to /dev/null. os.open("/dev/null", os.O_RDONLY) # standard input (0) diff --git a/fail2ban/server/strptime.py b/fail2ban/server/strptime.py index 5517e6b0..cf02dad5 100644 --- a/fail2ban/server/strptime.py +++ b/fail2ban/server/strptime.py @@ -190,5 +190,5 @@ def reGroupDictStrptime(found_dict): if gmtoff is not None: return calendar.timegm(date_result.utctimetuple()) else: - return time.mktime(date_result.utctimetuple()) + return time.mktime(date_result.timetuple()) diff --git a/fail2ban/tests/actiontestcase.py b/fail2ban/tests/actiontestcase.py index cb004b4d..f1ea77ce 100644 --- a/fail2ban/tests/actiontestcase.py +++ b/fail2ban/tests/actiontestcase.py @@ -100,17 +100,24 @@ class CommandActionTest(LogCaptureTestCase): {'ipjailmatches': "some >char< should \< be[ escap}ed&\n"}), "some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\n") + + # Recursive + aInfo["ABC"] = "" + self.assertEqual( + self.__action.replaceTag("Text text ABC", aInfo), + "Text 890 text 890 ABC") + # Callable self.assertEqual( - self.__action.replaceTag("09 11", - CallingMap(callme=lambda: str(10))), + self.__action.replaceTag("09 11", + CallingMap(matches=lambda: str(10))), "09 10 11") # As tag not present, therefore callable should not be called # Will raise ValueError if it is self.assertEqual( self.__action.replaceTag("abc", - CallingMap(callme=lambda: int("a"))), "abc") + CallingMap(matches=lambda: int("a"))), "abc") def testExecuteActionBan(self): self.__action.actionstart = "touch /tmp/fail2ban.test" diff --git a/fail2ban/tests/databasetestcase.py b/fail2ban/tests/databasetestcase.py index 3e32683a..07f80e45 100644 --- a/fail2ban/tests/databasetestcase.py +++ b/fail2ban/tests/databasetestcase.py @@ -177,10 +177,15 @@ class DatabaseTest(unittest.TestCase): if Fail2BanDb is None: # pragma: no cover return self.testAddJail() - ticket = FailTicket("127.0.0.1", MyTime.time() - 40, ["abc\n"]) - self.db.addBan(self.jail, ticket) + self.db.addBan( + self.jail, FailTicket("127.0.0.1", MyTime.time() - 60, ["abc\n"])) + self.db.addBan( + self.jail, FailTicket("127.0.0.1", MyTime.time() - 40, ["abc\n"])) self.assertEqual(len(self.db.getBans(jail=self.jail,bantime=50)), 1) self.assertEqual(len(self.db.getBans(jail=self.jail,bantime=20)), 0) + # Negative values are for persistent bans, and such all bans should + # be returned + self.assertEqual(len(self.db.getBans(jail=self.jail,bantime=-1)), 2) def testGetBansMerged(self): if Fail2BanDb is None: # pragma: no cover @@ -251,6 +256,10 @@ class DatabaseTest(unittest.TestCase): self.assertEqual(len(tickets), 1) tickets = self.db.getBansMerged(bantime=5) self.assertEqual(len(tickets), 0) + # Negative values are for persistent bans, and such all bans should + # be returned + tickets = self.db.getBansMerged(bantime=-1) + self.assertEqual(len(tickets), 2) def testPurge(self): if Fail2BanDb is None: # pragma: no cover diff --git a/fail2ban/tests/datedetectortestcase.py b/fail2ban/tests/datedetectortestcase.py index 55f9a823..726e73f8 100644 --- a/fail2ban/tests/datedetectortestcase.py +++ b/fail2ban/tests/datedetectortestcase.py @@ -131,7 +131,7 @@ class DateDetectorTest(unittest.TestCase): # see https://github.com/fail2ban/fail2ban/pull/130 # yoh: unfortunately this test is not really effective to reproduce the # situation but left in place to assure consistent behavior - mu = time.mktime(datetime.datetime(2012, 10, 11, 2, 37, 17).utctimetuple()) + mu = time.mktime(datetime.datetime(2012, 10, 11, 2, 37, 17).timetuple()) logdate = self.__datedetector.getTime('2012/10/11 02:37:17 [error] 18434#0') self.assertNotEqual(logdate, None) ( logTime, logMatch ) = logdate diff --git a/fail2ban/tests/files/logs/dovecot b/fail2ban/tests/files/logs/dovecot index 5c3acb93..6ca31b7c 100644 --- a/fail2ban/tests/files/logs/dovecot +++ b/fail2ban/tests/files/logs/dovecot @@ -1,12 +1,12 @@ -# failJSON: { "time": "2010-09-16T06:51:00", "match": true , "host": "80.187.101.33" } +# failJSON: { "time": "2010-09-16T07:51:00", "match": true , "host": "80.187.101.33" } @400000004c91b044077a9e94 imap-login: Info: Aborted login (auth failed, 1 attempts): user=, method=CRAM-MD5, rip=80.187.101.33, lip=80.254.129.240, TLS -# failJSON: { "time": "2010-09-16T06:51:00", "match": true , "host": "176.61.140.224" } +# failJSON: { "time": "2010-09-16T07:51:00", "match": true , "host": "176.61.140.224" } @400000004c91b044077a9e94 dovecot-auth: pam_unix(dovecot:auth): authentication failure; logname= uid=0 euid=0 tty=dovecot ruser=web rhost=176.61.140.224 # Above example with injected rhost into ruser -- should not match for 1.2.3.4 -# failJSON: { "time": "2010-09-16T06:51:00", "match": true , "host": "192.0.43.10" } +# failJSON: { "time": "2010-09-16T07:51:00", "match": true , "host": "192.0.43.10" } @400000004c91b044077a9e94 dovecot-auth: pam_unix(dovecot:auth): authentication failure; logname= uid=0 euid=0 tty=dovecot ruser=rhost=1.2.3.4 rhost=192.0.43.10 -# failJSON: { "time": "2010-09-16T06:51:00", "match": true , "host": "176.61.140.225" } +# failJSON: { "time": "2010-09-16T07:51:00", "match": true , "host": "176.61.140.225" } @400000004c91b044077a9e94 dovecot-auth: pam_unix(dovecot:auth): authentication failure; logname= uid=0 euid=0 tty=dovecot ruser=root rhost=176.61.140.225 user=root # failJSON: { "time": "2004-12-12T11:19:11", "match": true , "host": "190.210.136.21" } diff --git a/fail2ban/tests/files/logs/monit b/fail2ban/tests/files/logs/monit new file mode 100644 index 00000000..a923b6e2 --- /dev/null +++ b/fail2ban/tests/files/logs/monit @@ -0,0 +1,6 @@ +# failJSON: { "time": "2005-04-16T21:05:29", "match": true , "host": "69.93.127.111" } +[PDT Apr 16 21:05:29] error : Warning: Client '69.93.127.111' supplied unknown user 'foo' accessing monit httpd + +# failJSON: { "time": "2005-04-16T20:59:33", "match": true , "host": "97.113.189.111" } +[PDT Apr 16 20:59:33] error : Warning: Client '97.113.189.111' supplied wrong password for user 'admin' accessing monit httpd + diff --git a/fail2ban/tests/files/logs/selinux-ssh b/fail2ban/tests/files/logs/selinux-ssh index b6db443b..f9e1b828 100644 --- a/fail2ban/tests/files/logs/selinux-ssh +++ b/fail2ban/tests/files/logs/selinux-ssh @@ -1,25 +1,25 @@ -# failJSON: { "time": "2013-07-09T01:45:16", "match": false , "host": "173.242.116.187" } +# failJSON: { "time": "2013-07-09T02:45:16", "match": false , "host": "173.242.116.187" } type=USER_LOGIN msg=audit(1373330716.415:4063): user pid=11998 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=login acct="root" exe="/usr/sbin/sshd" hostname=? addr=173.242.116.187 terminal=ssh res=failed' -# failJSON: { "time": "2013-07-09T01:45:17", "match": false , "host": "173.242.116.187" } +# failJSON: { "time": "2013-07-09T02:45:17", "match": false , "host": "173.242.116.187" } type=USER_LOGIN msg=audit(1373330717.000:4068): user pid=12000 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=login acct=28756E6B6E6F776E207573657229 exe="/usr/sbin/sshd" hostname=? addr=173.242.116.187 terminal=ssh res=failed' -# failJSON: { "time": "2013-07-09T01:45:17", "match": true , "host": "173.242.116.187" } +# failJSON: { "time": "2013-07-09T02:45:17", "match": true , "host": "173.242.116.187" } type=USER_ERR msg=audit(1373330717.000:4070): user pid=12000 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=PAM:bad_ident acct="?" exe="/usr/sbin/sshd" hostname=173.242.116.187 addr=173.242.116.187 terminal=ssh res=failed' -# failJSON: { "time": "2013-07-09T01:45:17", "match": false , "host": "173.242.116.187" } +# failJSON: { "time": "2013-07-09T02:45:17", "match": false , "host": "173.242.116.187" } type=USER_LOGIN msg=audit(1373330717.000:4073): user pid=12000 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=login acct=28696E76616C6964207573657229 exe="/usr/sbin/sshd" hostname=? addr=173.242.116.187 terminal=ssh res=failed' -# failJSON: { "time": "2013-06-30T01:02:08", "match": false , "host": "113.240.248.18" } +# failJSON: { "time": "2013-06-30T02:02:08", "match": false , "host": "113.240.248.18" } type=USER_LOGIN msg=audit(1372546928.000:52008): user pid=21569 uid=0 auid=0 ses=76 subj=unconfined_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=login acct="sshd" exe="/usr/sbin/sshd" hostname=? addr=113.240.248.18 terminal=ssh res=failed' -# failJSON: { "time": "2013-06-30T02:58:20", "match": true , "host": "113.240.248.18" } +# failJSON: { "time": "2013-06-30T03:58:20", "match": true , "host": "113.240.248.18" } type=USER_ERR msg=audit(1372557500.000:61747): user pid=23684 uid=0 auid=0 ses=76 subj=unconfined_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=PAM:bad_ident acct="?" exe="/usr/sbin/sshd" hostname=113.240.248.18 addr=113.240.248.18 terminal=ssh res=failed' -# failJSON: { "time": "2013-06-30T03:58:20", "match": false , "host": "113.240.248.18" } +# failJSON: { "time": "2013-06-30T04:58:20", "match": false , "host": "113.240.248.18" } type=USER_LOGIN msg=audit(1372557500.000:61750): user pid=23684 uid=0 auid=0 ses=76 subj=unconfined_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=login acct=28696E76616C6964207573657229 exe="/usr/sbin/sshd" hostname=? addr=113.240.248.18 terminal=ssh res=failed' -# failJSON: { "time": "2013-07-06T17:48:00", "match": true , "host": "194.228.20.113" } +# failJSON: { "time": "2013-07-06T18:48:00", "match": true , "host": "194.228.20.113" } type=USER_AUTH msg=audit(1373129280.000:9): user pid=1277 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=pubkey acct="root" exe="/usr/sbin/sshd" hostname=? addr=194.228.20.113 terminal=ssh res=failed' # failJSON: { "time": "2013-10-30T07:57:43", "match": true , "host": "192.168.3.100" } diff --git a/fail2ban/tests/files/logs/sshd b/fail2ban/tests/files/logs/sshd index e2246cf8..b9d1b9b4 100644 --- a/fail2ban/tests/files/logs/sshd +++ b/fail2ban/tests/files/logs/sshd @@ -136,3 +136,10 @@ Jul 13 18:44:28 mdop sshd[4931]: Received disconnect from 89.24.13.192: 3: com.j Feb 12 04:09:18 localhost sshd[26713]: Connection from 115.249.163.77 port 51353 # failJSON: { "time": "2005-02-12T04:09:21", "match": true , "host": "115.249.163.77", "desc": "from gh-457" } Feb 12 04:09:21 localhost sshd[26713]: Disconnecting: Too many authentication failures for root [preauth] + +# failJSON: { "match": false } +Apr 27 13:02:04 host sshd[29116]: User root not allowed because account is locked +# failJSON: { "match": false } +Apr 27 13:02:04 host sshd[29116]: input_userauth_request: invalid user root [preauth] +# failJSON: { "time": "2005-04-27T13:02:04", "match": true , "host": "1.2.3.4", "desc": "No Bye-Bye" } +Apr 27 13:02:04 host sshd[29116]: Received disconnect from 1.2.3.4: 11: Normal Shutdown, Thank you for playing [preauth] diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index a0f715cd..c02e8616 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -794,7 +794,7 @@ class GetFailures(unittest.TestCase): FILENAME_MULTILINE = os.path.join(TEST_FILES_DIR, "testcase-multiline.log") # so that they could be reused by other tests - FAILURES_01 = ('193.168.0.128', 3, 1124017199.0, + FAILURES_01 = ('193.168.0.128', 3, 1124013599.0, [u'Aug 14 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 193.168.0.128']*3) def setUp(self): @@ -844,7 +844,7 @@ class GetFailures(unittest.TestCase): def testGetFailures02(self): - output = ('141.3.81.106', 4, 1124017139.0, + output = ('141.3.81.106', 4, 1124013539.0, [u'Aug 14 11:%d:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:141.3.81.106 port 51332 ssh2' % m for m in 53, 54, 57, 58]) @@ -854,7 +854,7 @@ class GetFailures(unittest.TestCase): _assert_correct_last_attempt(self, self.filter, output) def testGetFailures03(self): - output = ('203.162.223.135', 7, 1124017144.0) + output = ('203.162.223.135', 7, 1124013544.0) self.filter.addLogPath(GetFailures.FILENAME_03) self.filter.addFailRegex("error,relay=,.*550 User unknown") @@ -862,7 +862,7 @@ class GetFailures(unittest.TestCase): _assert_correct_last_attempt(self, self.filter, output) def testGetFailures04(self): - output = [('212.41.96.186', 4, 1124017200.0), + output = [('212.41.96.186', 4, 1124013600.0), ('212.41.96.185', 4, 1124017198.0)] self.filter.addLogPath(GetFailures.FILENAME_04) @@ -877,11 +877,11 @@ class GetFailures(unittest.TestCase): def testGetFailuresUseDNS(self): # We should still catch failures with usedns = no ;-) - output_yes = ('93.184.216.119', 2, 1124017139.0, + output_yes = ('93.184.216.119', 2, 1124013539.0, [u'Aug 14 11:54:59 i60p295 sshd[12365]: Failed publickey for roehl from example.com port 51332 ssh2', u'Aug 14 11:58:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:93.184.216.119 port 51332 ssh2']) - output_no = ('93.184.216.119', 1, 1124017139.0, + output_no = ('93.184.216.119', 1, 1124013539.0, [u'Aug 14 11:58:59 i60p295 sshd[12365]: Failed publickey for roehl from ::ffff:93.184.216.119 port 51332 ssh2']) # Actually no exception would be raised -- it will be just set to 'no' @@ -904,7 +904,7 @@ class GetFailures(unittest.TestCase): def testGetFailuresMultiRegex(self): - output = ('141.3.81.106', 8, 1124017141.0) + output = ('141.3.81.106', 8, 1124013541.0) self.filter.addLogPath(GetFailures.FILENAME_02) self.filter.addFailRegex("Failed .* from ") @@ -923,8 +923,8 @@ class GetFailures(unittest.TestCase): self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan) def testGetFailuresMultiLine(self): - output = [("192.0.43.10", 2, 1124017199.0), - ("192.0.43.11", 1, 1124017198.0)] + output = [("192.0.43.10", 2, 1124013599.0), + ("192.0.43.11", 1, 1124013598.0)] self.filter.addLogPath(GetFailures.FILENAME_MULTILINE) self.filter.addFailRegex("^.*rsyncd\[(?P\d+)\]: connect from .+ \(\)$^.+ rsyncd\[(?P=pid)\]: rsync error: .*$") self.filter.setMaxLines(100) @@ -942,7 +942,7 @@ class GetFailures(unittest.TestCase): self.assertEqual(sorted(foundList), sorted(output)) def testGetFailuresMultiLineIgnoreRegex(self): - output = [("192.0.43.10", 2, 1124017199.0)] + output = [("192.0.43.10", 2, 1124013599.0)] self.filter.addLogPath(GetFailures.FILENAME_MULTILINE) self.filter.addFailRegex("^.*rsyncd\[(?P\d+)\]: connect from .+ \(\)$^.+ rsyncd\[(?P=pid)\]: rsync error: .*$") self.filter.addIgnoreRegex("rsync error: Received SIGINT") @@ -956,9 +956,9 @@ class GetFailures(unittest.TestCase): self.assertRaises(FailManagerEmpty, self.filter.failManager.toBan) def testGetFailuresMultiLineMultiRegex(self): - output = [("192.0.43.10", 2, 1124017199.0), - ("192.0.43.11", 1, 1124017198.0), - ("192.0.43.15", 1, 1124017198.0)] + output = [("192.0.43.10", 2, 1124013599.0), + ("192.0.43.11", 1, 1124013598.0), + ("192.0.43.15", 1, 1124013598.0)] self.filter.addLogPath(GetFailures.FILENAME_MULTILINE) self.filter.addFailRegex("^.*rsyncd\[(?P\d+)\]: connect from .+ \(\)$^.+ rsyncd\[(?P=pid)\]: rsync error: .*$") self.filter.addFailRegex("^.* sendmail\[.*, msgid=<(?P[^>]+).*relay=\[\].*$^.+ spamd: result: Y \d+ .*,mid=<(?P=msgid)>(,bayes=[.\d]+)?(,autolearn=\S+)?\s*$") diff --git a/fail2ban/tests/misctestcase.py b/fail2ban/tests/misctestcase.py index 284b684b..ca84eba7 100644 --- a/fail2ban/tests/misctestcase.py +++ b/fail2ban/tests/misctestcase.py @@ -32,8 +32,7 @@ import datetime from glob import glob from StringIO import StringIO -from .utils import mbasename, TraceBack, FormatterWithTraceBack -from ..helpers import formatExceptionInfo +from ..helpers import formatExceptionInfo, mbasename, TraceBack, FormatterWithTraceBack from ..server.datetemplate import DatePatternRegex diff --git a/fail2ban/tests/samplestestcase.py b/fail2ban/tests/samplestestcase.py index 3529fcc2..132ade7b 100644 --- a/fail2ban/tests/samplestestcase.py +++ b/fail2ban/tests/samplestestcase.py @@ -129,7 +129,7 @@ def testSampleRegexsFactory(name): jsonTimeLocal = datetime.datetime.strptime(t, "%Y-%m-%dT%H:%M:%S.%f") - jsonTime = time.mktime(jsonTimeLocal.utctimetuple()) + jsonTime = time.mktime(jsonTimeLocal.timetuple()) jsonTime += jsonTimeLocal.microsecond / 1000000 diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index 8c45a1b6..8e3252e2 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -22,90 +22,17 @@ __author__ = "Yaroslav Halchenko" __copyright__ = "Copyright (c) 2013 Yaroslav Halchenko" __license__ = "GPL" -import logging, os, re, traceback, time, unittest -from os.path import basename, dirname +import logging +import os +import re +import time +import unittest from StringIO import StringIO from ..server.mytime import MyTime logSys = logging.getLogger(__name__) -# -# Following "traceback" functions are adopted from PyMVPA distributed -# under MIT/Expat and copyright by PyMVPA developers (i.e. me and -# Michael). Hereby I re-license derivative work on these pieces under GPL -# to stay in line with the main Fail2Ban license -# -def mbasename(s): - """Custom function to include directory name if filename is too common - - Also strip .py at the end - """ - base = basename(s) - if base.endswith('.py'): - base = base[:-3] - if base in set(['base', '__init__']): - base = basename(dirname(s)) + '.' + base - return base - -class TraceBack(object): - """Customized traceback to be included in debug messages - """ - - def __init__(self, compress=False): - """Initialize TrackBack metric - - Parameters - ---------- - compress : bool - if True then prefix common with previous invocation gets - replaced with ... - """ - self.__prev = "" - self.__compress = compress - - def __call__(self): - ftb = traceback.extract_stack(limit=100)[:-2] - entries = [[mbasename(x[0]), dirname(x[0]), str(x[1])] for x in ftb] - entries = [ [e[0], e[2]] for e in entries - if not (e[0] in ['unittest', 'logging.__init__'] - or e[1].endswith('/unittest'))] - - # lets make it more concise - entries_out = [entries[0]] - for entry in entries[1:]: - if entry[0] == entries_out[-1][0]: - entries_out[-1][1] += ',%s' % entry[1] - else: - entries_out.append(entry) - sftb = '>'.join(['%s:%s' % (mbasename(x[0]), - x[1]) for x in entries_out]) - if self.__compress: - # lets remove part which is common with previous invocation - prev_next = sftb - common_prefix = os.path.commonprefix((self.__prev, sftb)) - common_prefix2 = re.sub('>[^>]*$', '', common_prefix) - - if common_prefix2 != "": - sftb = '...' + sftb[len(common_prefix2):] - self.__prev = prev_next - - return sftb - -class FormatterWithTraceBack(logging.Formatter): - """Custom formatter which expands %(tb) and %(tbc) with tracebacks - - TODO: might need locking in case of compressed tracebacks - """ - def __init__(self, fmt, *args, **kwargs): - logging.Formatter.__init__(self, fmt=fmt, *args, **kwargs) - compress = '%(tbc)s' in fmt - self._tb = TraceBack(compress=compress) - - def format(self, record): - record.tbc = record.tb = self._tb() - return logging.Formatter.format(self, record) - def mtimesleep(): # no sleep now should be necessary since polling tracks now not only # mtime but also ino and size @@ -146,7 +73,6 @@ def gatherTests(regexps=None, no_network=False): if not regexps: # pragma: no cover tests = unittest.TestSuite() else: # pragma: no cover - import re class FilteredTestSuite(unittest.TestSuite): _regexps = [re.compile(r) for r in regexps] def addTest(self, suite): diff --git a/files/fail2ban.upstart b/files/fail2ban.upstart index 1780a810..18fafebd 100644 --- a/files/fail2ban.upstart +++ b/files/fail2ban.upstart @@ -1,13 +1,20 @@ description "fail2ban - ban hosts that cause multiple authentication errors" -start on filesystem and started networking -stop on deconfiguring-networking +start on filesystem and static-network-up +stop on runlevel [016] expect fork respawn -exec /usr/bin/fail2ban-client -x -b start +env RUNDIR=/var/run/fail2ban + +pre-start script + test -d $RUNDIR || mkdir -p $RUNDIR + test ! -e $RUNDIR/fail2ban.sock || rm -f $RUNDIR/fail2ban.sock +end script + +exec /usr/bin/fail2ban-client -f -x start pre-stop exec /usr/bin/fail2ban-client stop -post-stop exec rm -f /var/run/fail2ban/fail2ban.pid +post-stop exec rm -f $RUNDIR/fail2ban.pid diff --git a/man/fail2ban-client.1 b/man/fail2ban-client.1 index ec79d725..32580e20 100644 --- a/man/fail2ban-client.1 +++ b/man/fail2ban-client.1 @@ -34,6 +34,12 @@ decrease verbosity \fB\-x\fR force execution of the server (remove socket file) .TP +\fB\-b\fR +start the server in background mode (default) +.TP +\fB\-f\fR +start the server in foreground mode (note that the client forks once itself) +.TP \fB\-h\fR, \fB\-\-help\fR display this help message .TP