Primarily a bugfix release 0.8.8

-----BEGIN PGP SIGNATURE-----
 Version: GnuPG v1.4.12 (GNU/Linux)
 
 iEYEABECAAYFAlDAFpkACgkQjRFFY3XAJMgAgQCg1ZQHPpU7S6EQxM4sxELuJepl
 KV4AnRw/G7RX33ezTvdzAEYutKf+QJVB
 =PFlG
 -----END PGP SIGNATURE-----

Merge tag '0.8.8' into debian

Primarily a bugfix release 0.8.8

* tag '0.8.8': (31 commits)
  Getting ready for 0.8.8 release (changelog, version boost)
  BF: guarantee that IP is stored as a base, non-unicode str (Closes gh-91)
  ENH: BF (forgotten import) for prev commit + removed duplicate Author, adjusted __ fields for that in fail2ban-* scripts
  ENH: until we make it proper module -- adjust sys.path only if system-wide run
  ENH: fail2ban-testcases-all -- pass cmdline options to fail2ban-testcases
  ENH: To help with gh-87 added hints into the log on some failure return codes (e.g. 0x7f00 for this one)
  ENH: trying to go native travis-ci python way to take advantage of virtualenv's with older pythons
  BF: typo
  BF: added a little shell script to excercise tests against all available Python versions
  ENH: travis -- try to run tests against all available python versions
  NF: rudimentary .travis.yml for travis-ci.org service
  BF: do not enable pyinotify backend if pyinotify is too old (Closes gh-80)
  DOC: forgotten --help entry for " unban "
  ENH: downgrade "already banned" from WARN to INFO level (Closes gh-79)
  minor: added a note on now "negative" log entries on "POSSIBLE BREAK-IN ATTEMPT"
  DOC: minor "fixes" in DEVELOP
  Added in while loop to process the Fail Manager after the requested banned IP was added to its queue.  This solves the issue of needing to touch the log file that is being monitored to get the IP to be banned accordingly.  Added in import of FailManagerEmpty exception class.
  ENH: refactored previous commit to make it more Pythonic (With prev commit closes gh-86, gh-81)
  Added in command option to unban and IP, just like using 'banip'.  Command looks like: fail2ban-client set <jail name> unbanip <ip>
  BF: in code we should use MyTime wrapper instead of time module directly
  ...
pull/808/head
Yaroslav Halchenko 2012-12-05 22:53:04 -05:00
commit e484ef0a26
29 changed files with 272 additions and 90 deletions

11
.travis.yml Normal file
View File

@ -0,0 +1,11 @@
# vim ft=yaml
# travis-ci.org definition for Fail2Ban build
language: python
python:
- "2.5"
- "2.6"
- "2.7"
install:
- "pip install pyinotify"
script:
- python ./fail2ban-testcases

View File

@ -4,9 +4,41 @@
|_| \__,_|_|_/___|_.__/\__,_|_||_| |_| \__,_|_|_/___|_.__/\__,_|_||_|
================================================================================ ================================================================================
Fail2Ban (version 0.8.7) 2012/07/31 Fail2Ban (version 0.8.8) 2012/12/06
================================================================================ ================================================================================
ver. 0.8.8 (2012/12/06) - stable
----------
- Fixes:
Alan Jenkins
* [8c38907] Removed 'POSSIBLE BREAK-IN ATTEMPT' from sshd filter to avoid
banning due to misconfigured DNS. Close gh-64
Yaroslav Halchenko
* [83109bc] IMPORTANT: escape the content of <matches> (if used in
custom action files) since its value could contain arbitrary
symbols. Thanks for discovery go to the NBS System security
team
* [0935566,5becaf8] Various python 2.4 and 2.5 compatibility fixes. Close gh-83
* [b159eab] do not enable pyinotify backend if pyinotify < 0.8.3
* [37a2e59] store IP as a base, non-unicode str to avoid spurious messages
in the console. Close gh-91
- New features:
David Engeset
* [2d672d1,6288ec2] 'unbanip' command for the client + avoidance of touching
the log file to take 'banip' or 'unbanip' in effect. Close gh-81, gh-86
Yaroslav Halchenko
- Enhancements:
* [2d66f31] replaced uninformative "Invalid command" message with warning log
exception why command actually failed
* [958a1b0] improved failregex to "support" auth.backend = "htdigest"
* [9e7a3b7] until we make it proper module -- adjusted sys.path only if
system-wide run
* [f52ba99] downgraded "already banned" from WARN to INFO level. Closes gh-79
* [f105379] added hints into the log on some failure return codes (e.g. 0x7f00
for this gh-87)
* Various others: travis-ci integration, script to run tests
against all available Python versions, etc
ver. 0.8.7.1 (2012/07/31) - stable ver. 0.8.7.1 (2012/07/31) - stable
---------- ----------

20
DEVELOP
View File

@ -58,18 +58,20 @@ one)::
RF-Note just a note which might be useful to address while doing RF RF-Note just a note which might be useful to address while doing RF
JailThread -> Filter -> FileFilter -> {FilterPoll, FilterPyinotify, ...} JailThread -> Filter -> FileFilter -> {FilterPoll, FilterPyinotify, ...}
| | * FileContainer | * FileContainer
| + FailManager + FailManager
| + DateDetector + DateDetector
\- -> Actions + Jail (provided in __init__) which contains this Filter
* Actions (used for passing tickets from FailManager to Jail's __queue)
+ BanManager
Server Server
+ Jails + Jails
* Jail * Jail
+ Filter + Filter (in __filter)
* tickets (in __queue) * tickets (in __queue)
+ Actions (in __action)
* Action
+ BanManager
failmanager.py failmanager.py
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~
@ -147,7 +149,7 @@ one way or another provide
except FailManagerEmpty: except FailManagerEmpty:
self.failManager.cleanup(MyTime.time()) self.failManager.cleanup(MyTime.time())
thus channeling "ban tickets" from their failManager to a thus channeling "ban tickets" from their failManager to the
corresponding jail. corresponding jail.
action.py action.py

6
README
View File

@ -4,7 +4,7 @@
|_| \__,_|_|_/___|_.__/\__,_|_||_| |_| \__,_|_|_/___|_.__/\__,_|_||_|
================================================================================ ================================================================================
Fail2Ban (version 0.8.7) 2012/07/31 Fail2Ban (version 0.8.8) 2012/07/31
================================================================================ ================================================================================
Fail2Ban scans log files like /var/log/pwdfail and bans IP that makes too many Fail2Ban scans log files like /var/log/pwdfail and bans IP that makes too many
@ -31,8 +31,8 @@ Optional:
To install, just do: To install, just do:
> tar xvfj fail2ban-0.8.7.tar.bz2 > tar xvfj fail2ban-0.8.8.tar.bz2
> cd fail2ban-0.8.7 > cd fail2ban-0.8.8
> python setup.py install > python setup.py install
This will install Fail2Ban into /usr/share/fail2ban. The executable scripts are This will install Fail2Ban into /usr/share/fail2ban. The executable scripts are

View File

@ -64,6 +64,7 @@ protocol = [
["set <JAIL> bantime <TIME>", "sets the number of seconds <TIME> a host will be banned for <JAIL>"], ["set <JAIL> bantime <TIME>", "sets the number of seconds <TIME> a host will be banned for <JAIL>"],
["set <JAIL> usedns <VALUE>", "sets the usedns mode for <JAIL>"], ["set <JAIL> usedns <VALUE>", "sets the usedns mode for <JAIL>"],
["set <JAIL> banip <IP>", "manually Ban <IP> for <JAIL>"], ["set <JAIL> banip <IP>", "manually Ban <IP> for <JAIL>"],
["set <JAIL> unbanip <IP>", "manually Unban <IP> in <JAIL>"],
["set <JAIL> maxretry <RETRY>", "sets the number of failures <RETRY> before banning the host for <JAIL>"], ["set <JAIL> maxretry <RETRY>", "sets the number of failures <RETRY> before banning the host for <JAIL>"],
["set <JAIL> addaction <ACT>", "adds a new action named <NAME> for <JAIL>"], ["set <JAIL> addaction <ACT>", "adds a new action named <NAME> for <JAIL>"],
["set <JAIL> delaction <ACT>", "removes the action <NAME> from <JAIL>"], ["set <JAIL> delaction <ACT>", "removes the action <NAME> from <JAIL>"],

View File

@ -21,8 +21,8 @@
# #
# $Revision$ # $Revision$
__author__ = "Cyril Jaquier" __author__ = "Cyril Jaquier, Yaroslav Halchenko"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Yaroslav Halchenko"
__license__ = "GPL" __license__ = "GPL"
version = "0.8.7" version = "0.8.8"

View File

@ -12,8 +12,22 @@
# any other addresses found in the whois record, with a few exceptions. # any other addresses found in the whois record, with a few exceptions.
# If no addresses are found, no e-mail is sent. # If no addresses are found, no e-mail is sent.
# #
# $Revision$ # WARNING
# -------
# #
# Please do not use this action unless you are certain that fail2ban
# does not result in "false positives" for your deployment. False
# positive reports could serve a mis-favor to the original cause by
# flooding corresponding contact addresses, and complicating the work
# of administration personnel responsible for handling (verified) legit
# complains.
#
# Please consider using e.g. sendmail-whois-lines.conf action which
# would send the reports with relevant information to you, so the
# report could be first reviewed and then forwarded to a corresponding
# contact if legit.
#
[Definition] [Definition]

View File

@ -9,8 +9,7 @@
# Notes.: regex to match wrong passwords as notified by lighttpd's auth Module # Notes.: regex to match wrong passwords as notified by lighttpd's auth Module
# Values: TEXT # Values: TEXT
# #
failregex = .*http_auth.*password doesn\'t match.*IP: <HOST>\s*$ failregex = .*http_auth.*(password doesn\'t match|wrong password).*IP: <HOST>\s*$
# Option: ignoreregex # Option: ignoreregex
# Notes.: regex to ignore. If this regex matches, the line is ignored. # Notes.: regex to ignore. If this regex matches, the line is ignored.

View File

@ -32,7 +32,6 @@ failregex = ^%(__prefix_line)s(?:error: PAM: )?Authentication failure for .* fro
^%(__prefix_line)sUser .+ from <HOST> not allowed because listed in DenyUsers\s*$ ^%(__prefix_line)sUser .+ from <HOST> not allowed because listed in DenyUsers\s*$
^%(__prefix_line)s(?:pam_unix\(sshd:auth\):\s)?authentication failure; logname=\S* uid=\S* euid=\S* tty=\S* ruser=\S* rhost=<HOST>(?:\s+user=.*)?\s*$ ^%(__prefix_line)s(?:pam_unix\(sshd:auth\):\s)?authentication failure; logname=\S* uid=\S* euid=\S* tty=\S* ruser=\S* rhost=<HOST>(?:\s+user=.*)?\s*$
^%(__prefix_line)srefused connect from \S+ \(<HOST>\)\s*$ ^%(__prefix_line)srefused connect from \S+ \(<HOST>\)\s*$
^%(__prefix_line)sAddress <HOST> .* POSSIBLE BREAK-IN ATTEMPT!*\s*$
^%(__prefix_line)sUser .+ from <HOST> not allowed because none of user's groups are listed in AllowGroups\s*$ ^%(__prefix_line)sUser .+ from <HOST> not allowed because none of user's groups are listed in AllowGroups\s*$
# Option: ignoreregex # Option: ignoreregex

View File

@ -18,13 +18,7 @@
# along with Fail2Ban; if not, write to the Free Software # along with Fail2Ban; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# Author: Cyril Jaquier
#
# $Revision$
__author__ = "Cyril Jaquier" __author__ = "Cyril Jaquier"
__version__ = "$Revision$"
__date__ = "$Date$"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
@ -33,6 +27,8 @@ import getopt, time, shlex, socket
# Inserts our own modules path first in the list # Inserts our own modules path first in the list
# fix for bug #343821 # fix for bug #343821
if os.path.abspath(__file__).startswith('/usr/'):
# makes sense to use system-wide library iff -client is also under /usr/
sys.path.insert(1, "/usr/share/fail2ban") sys.path.insert(1, "/usr/share/fail2ban")
# Now we can import our modules # Now we can import our modules
@ -69,7 +65,7 @@ class Fail2banClient:
def dispVersion(self): def dispVersion(self):
print "Fail2Ban v" + version print "Fail2Ban v" + version
print print
print "Copyright (c) 2004-2008 Cyril Jaquier" print "Copyright (c) 2004-2008 Cyril Jaquier, 2008- Fail2Ban Contributors"
print "Copyright of modifications held by their respective authors." print "Copyright of modifications held by their respective authors."
print "Licensed under the GNU General Public License v2 (GPL)." print "Licensed under the GNU General Public License v2 (GPL)."
print print

View File

@ -15,20 +15,16 @@
# along with Fail2Ban; if not, write to the Free Software # along with Fail2Ban; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# Author: Cyril Jaquier __author__ = "Cyril Jaquier, Yaroslav Halchenko"
# __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2012 Yaroslav Halchenko"
# $Revision$
__author__ = "Cyril Jaquier"
__version__ = "$Revision$"
__date__ = "$Date$"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import getopt, sys, time, logging, os import getopt, sys, time, logging, os
# Inserts our own modules path first in the list # Inserts our own modules path first in the list
# fix for bug #343821 # fix for bug #343821
if os.path.abspath(__file__).startswith('/usr/'):
# makes sense to use system-wide library iff -regex is also under /usr/
sys.path.insert(1, "/usr/share/fail2ban") sys.path.insert(1, "/usr/share/fail2ban")
from client.configparserinc import SafeConfigParserWithIncludes from client.configparserinc import SafeConfigParserWithIncludes

View File

@ -18,20 +18,16 @@
# along with Fail2Ban; if not, write to the Free Software # along with Fail2Ban; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# Author: Cyril Jaquier
#
# $Revision$
__author__ = "Cyril Jaquier" __author__ = "Cyril Jaquier"
__version__ = "$Revision$"
__date__ = "$Date$"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import getopt, sys, logging import getopt, sys, logging, os
# Inserts our own modules path first in the list # Inserts our own modules path first in the list
# fix for bug #343821 # fix for bug #343821
if os.path.abspath(__file__).startswith('/usr/'):
# makes sense to use system-wide library iff -server is also under /usr/
sys.path.insert(1, "/usr/share/fail2ban") sys.path.insert(1, "/usr/share/fail2ban")
from common.version import version from common.version import version
@ -61,7 +57,7 @@ class Fail2banServer:
def dispVersion(self): def dispVersion(self):
print "Fail2Ban v" + version print "Fail2Ban v" + version
print print
print "Copyright (c) 2004-2008 Cyril Jaquier" print "Copyright (c) 2004-2008 Cyril Jaquier, 2008- Fail2Ban Contributors"
print "Copyright of modifications held by their respective authors." print "Copyright of modifications held by their respective authors."
print "Licensed under the GNU General Public License v2 (GPL)." print "Licensed under the GNU General Public License v2 (GPL)."
print print

View File

@ -20,12 +20,8 @@
# along with Fail2Ban; if not, write to the Free Software # along with Fail2Ban; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# Author: Cyril Jaquier
__author__ = "Cyril Jaquier" __author__ = "Cyril Jaquier"
__version__ = "$Revision$" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2012- Yaroslav Halchenko"
__date__ = "$Date$"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
@ -100,7 +96,8 @@ logSys.addHandler(stdout)
# Let know the version # Let know the version
# #
if not opts.log_level or opts.log_level != 'fatal': if not opts.log_level or opts.log_level != 'fatal':
print "Fail2ban " + version + " test suite. Please wait..." print "Fail2ban %s test suite. Python %s. Please wait..." \
% (version, str(sys.version).replace('\n', ''))
# #
@ -125,6 +122,7 @@ tests.addTest(unittest.makeSuite(filtertestcase.LogFile))
tests.addTest(unittest.makeSuite(filtertestcase.LogFileMonitor)) tests.addTest(unittest.makeSuite(filtertestcase.LogFileMonitor))
tests.addTest(unittest.makeSuite(filtertestcase.GetFailures)) tests.addTest(unittest.makeSuite(filtertestcase.GetFailures))
tests.addTest(unittest.makeSuite(filtertestcase.DNSUtilsTests)) tests.addTest(unittest.makeSuite(filtertestcase.DNSUtilsTests))
tests.addTest(unittest.makeSuite(filtertestcase.JailTests))
# DateDetector # DateDetector
tests.addTest(unittest.makeSuite(datedetectortestcase.DateDetectorTest)) tests.addTest(unittest.makeSuite(datedetectortestcase.DateDetectorTest))
@ -142,14 +140,14 @@ filters = [FilterPoll] # always available
try: try:
from server.filtergamin import FilterGamin from server.filtergamin import FilterGamin
filters.append(FilterGamin) filters.append(FilterGamin)
except: except Exception, e:
pass print "I: Skipping gamin backend testing. Got exception '%s'" % e
try: try:
from server.filterpyinotify import FilterPyinotify from server.filterpyinotify import FilterPyinotify
filters.append(FilterPyinotify) filters.append(FilterPyinotify)
except: except Exception, e:
pass print "I: Skipping pyinotify backend testing. Got exception '%s'" % e
for Filter_ in filters: for Filter_ in filters:
tests.addTest(unittest.makeSuite( tests.addTest(unittest.makeSuite(

18
fail2ban-testcases-all Executable file
View File

@ -0,0 +1,18 @@
#!/bin/bash
# Simple helper script to exercise unittests using all available
# (under /usr/bin and /usr/local/bin python2.*)
set -eu
failed=
for python in /usr/{,local/}bin/python2.[0-9]{,.*}{,-dbg}
do
[ -e "$python" ] || continue
echo "Testing using $python"
$python ./fail2ban-testcases "$@" || failed+=" $python"
done
if [ ! -z "$failed" ]; then
echo "E: Failed with $failed"
exit 1
fi

View File

@ -37,6 +37,17 @@ logSys = logging.getLogger("fail2ban.actions.action")
# Create a lock for running system commands # Create a lock for running system commands
_cmd_lock = threading.Lock() _cmd_lock = threading.Lock()
# Some hints on common abnormal exit codes
_RETCODE_HINTS = {
0x7f00: '"Command not found". Make sure that all commands in %(realCmd)r '
'are in the PATH of fail2ban-server process '
'(grep -a PATH= /proc/`pidof -x fail2ban-server`/environ). '
'You may want to start '
'"fail2ban-server -f" separately, initiate it with '
'"fail2ban-client reload" in another shell session and observe if '
'additional informative error messages appear in the terminals.'
}
## ##
# Execute commands. # Execute commands.
# #
@ -231,6 +242,13 @@ class Action:
stopCmd = Action.replaceTag(self.__actionStop, self.__cInfo) stopCmd = Action.replaceTag(self.__actionStop, self.__cInfo)
return Action.executeCmd(stopCmd) return Action.executeCmd(stopCmd)
def escapeTag(tag):
for c in '\\#&;`|*?~<>^()[]{}$\n':
if c in tag:
tag = tag.replace(c, '\\' + c)
return tag
escapeTag = staticmethod(escapeTag)
## ##
# Replaces tags in query with property values in aInfo. # Replaces tags in query with property values in aInfo.
# #
@ -243,8 +261,13 @@ class Action:
""" Replace tags in query """ Replace tags in query
""" """
string = query string = query
for tag in aInfo: for tag, value in aInfo.iteritems():
string = string.replace('<' + tag + '>', str(aInfo[tag])) value = str(value) # assure string
if tag == 'matches':
# That one needs to be escaped since its content is
# out of our control
value = Action.escapeTag(value)
string = string.replace('<' + tag + '>', value)
# New line # New line
string = string.replace("<br>", '\n') string = string.replace("<br>", '\n')
return string return string
@ -318,7 +341,11 @@ class Action:
logSys.debug("%s returned successfully" % realCmd) logSys.debug("%s returned successfully" % realCmd)
return True return True
else: else:
msg = _RETCODE_HINTS.get(retcode, None)
logSys.error("%s returned %x" % (realCmd, retcode)) logSys.error("%s returned %x" % (realCmd, retcode))
if msg:
logSys.info("HINT on %x: %s"
% (retcode, msg % locals()))
except OSError, e: except OSError, e:
logSys.error("%s failed with %s" % (realCmd, e)) logSys.error("%s failed with %s" % (realCmd, e))
finally: finally:

View File

@ -120,6 +120,19 @@ class Actions(JailThread):
def getBanTime(self): def getBanTime(self):
return self.__banManager.getBanTime() return self.__banManager.getBanTime()
##
# Remove a banned IP now, rather than waiting for it to expire, even if set to never expire.
#
# @return the IP string or 'None' if not unbanned.
def removeBannedIP(self, ip):
# Find the ticket with the IP.
ticket = self.__banManager.getTicketByIP(ip)
if ticket is not None:
# Unban the IP.
self.__unBan(ticket)
return ip
return 'None'
## ##
# Main loop. # Main loop.
# #
@ -163,13 +176,13 @@ class Actions(JailThread):
aInfo["time"] = bTicket.getTime() aInfo["time"] = bTicket.getTime()
aInfo["matches"] = "".join(bTicket.getMatches()) aInfo["matches"] = "".join(bTicket.getMatches())
if self.__banManager.addBanTicket(bTicket): if self.__banManager.addBanTicket(bTicket):
logSys.warn("[%s] Ban %s" % (self.jail.getName(), str(aInfo["ip"]))) logSys.warn("[%s] Ban %s" % (self.jail.getName(), aInfo["ip"]))
for action in self.__actions: for action in self.__actions:
action.execActionBan(aInfo) action.execActionBan(aInfo)
return True return True
else: else:
logSys.warn("[%s] %s already banned" % (self.jail.getName(), logSys.info("[%s] %s already banned" % (self.jail.getName(),
str(aInfo["ip"]))) aInfo["ip"]))
return False return False
## ##

View File

@ -223,3 +223,21 @@ class BanManager:
return uBList return uBList
finally: finally:
self.__lock.release() self.__lock.release()
##
# Gets the ticket for the specified IP.
#
# @return the ticket for the IP or False.
def getTicketByIP(self, ip):
try:
self.__lock.acquire()
# Find the ticket the IP goes with and return it
for i, ticket in enumerate(self.__banList):
if ticket.getIP() == ip:
# Return the ticket after removing (popping)
# if from the ban list.
return self.__banList.pop(i)
finally:
self.__lock.release()
return None # if none found

View File

@ -27,6 +27,7 @@ __date__ = "$Date$"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
from failmanager import FailManagerEmpty
from failmanager import FailManager from failmanager import FailManager
from ticket import FailTicket from ticket import FailTicket
from jailthread import JailThread from jailthread import JailThread
@ -220,10 +221,18 @@ class Filter(JailThread):
# to enable banip fail2ban-client BAN command # to enable banip fail2ban-client BAN command
def addBannedIP(self, ip): def addBannedIP(self, ip):
unixTime = time.time() unixTime = MyTime.time()
for i in xrange(self.failManager.getMaxRetry()): for i in xrange(self.failManager.getMaxRetry()):
self.failManager.addFailure(FailTicket(ip, unixTime)) self.failManager.addFailure(FailTicket(ip, unixTime))
# Perform the banning of the IP now.
try:
while True:
ticket = self.failManager.toBan()
self.jail.putFailTicket(ticket)
except FailManagerEmpty:
self.failManager.cleanup(MyTime.time())
return ip return ip
## ##

View File

@ -23,12 +23,18 @@ __author__ = "Cyril Jaquier, Lee Clemens, Yaroslav Halchenko"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Lee Clemens, 2012 Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Lee Clemens, 2012 Yaroslav Halchenko"
__license__ = "GPL" __license__ = "GPL"
from distutils.version import LooseVersion
from failmanager import FailManagerEmpty from failmanager import FailManagerEmpty
from filter import FileFilter from filter import FileFilter
from mytime import MyTime from mytime import MyTime
import time, logging, pyinotify import time, logging, pyinotify
if not hasattr(pyinotify, '__version__') \
or LooseVersion(pyinotify.__version__) < '0.8.3':
raise ImportError("Fail2Ban requires pyinotify >= 0.8.3")
from os.path import dirname, sep as pathsep from os.path import dirname, sep as pathsep
# Gets the instance of the logger. # Gets the instance of the logger.

View File

@ -33,7 +33,9 @@ logSys = logging.getLogger("fail2ban.jail")
class Jail: class Jail:
#Known backends. Each backend should have corresponding __initBackend method #Known backends. Each backend should have corresponding __initBackend method
_BACKENDS = ('pyinotify', 'gamin', 'polling') # yoh: stored in a list instead of a tuple since only
# list had .index until 2.6
_BACKENDS = ['pyinotify', 'gamin', 'polling']
def __init__(self, name, backend = "auto"): def __init__(self, name, backend = "auto"):
self.__name = name self.__name = name

View File

@ -241,6 +241,9 @@ class Server:
def setBanIP(self, name, value): def setBanIP(self, name, value):
return self.__jails.getFilter(name).addBannedIP(value) return self.__jails.getFilter(name).addBannedIP(value)
def setUnbanIP(self, name, value):
return self.__jails.getAction(name).removeBannedIP(value)
def getBanTime(self, name): def getBanTime(self, name):
return self.__jails.getAction(name).getBanTime() return self.__jails.getAction(name).getBanTime()

View File

@ -42,7 +42,7 @@ class Ticket:
@param matches (log) lines caused the ticket @param matches (log) lines caused the ticket
""" """
self.__ip = ip self.setIP(ip)
self.__time = time self.__time = time
self.__attempt = 0 self.__attempt = 0
self.__file = None self.__file = None
@ -54,6 +54,9 @@ class Ticket:
def setIP(self, value): def setIP(self, value):
if isinstance(value, basestring):
# guarantee using regular str instead of unicode for the IP
value = str(value)
self.__ip = value self.__ip = value
def getIP(self): def getIP(self):

View File

@ -55,7 +55,8 @@ class Transmitter:
ret = self.__commandHandler(command) ret = self.__commandHandler(command)
ack = 0, ret ack = 0, ret
except Exception, e: except Exception, e:
logSys.warn("Invalid command: " + `command`) logSys.warn("Command %r has failed. Received %r"
% (command, e))
ack = 1, e ack = 1, e
return ack return ack
@ -174,6 +175,9 @@ class Transmitter:
elif command[1] == "banip": elif command[1] == "banip":
value = command[2] value = command[2]
return self.__server.setBanIP(name,value) return self.__server.setBanIP(name,value)
elif command[1] == "unbanip":
value = command[2]
return self.__server.setUnbanIP(name,value)
elif command[1] == "addaction": elif command[1] == "addaction":
value = command[2] value = command[2]
self.__server.addAction(name, value) self.__server.addAction(name, value)

View File

@ -18,13 +18,7 @@
# along with Fail2Ban; if not, write to the Free Software # along with Fail2Ban; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# Author: Cyril Jaquier
#
# $Revision$
__author__ = "Cyril Jaquier" __author__ = "Cyril Jaquier"
__version__ = "$Revision$"
__date__ = "$Date$"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"

View File

@ -28,7 +28,9 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import unittest, time import unittest, time
import logging, sys
from server.action import Action from server.action import Action
from StringIO import StringIO
class ExecuteAction(unittest.TestCase): class ExecuteAction(unittest.TestCase):
@ -36,15 +38,43 @@ class ExecuteAction(unittest.TestCase):
"""Call before every test case.""" """Call before every test case."""
self.__action = Action("Test") self.__action = Action("Test")
# For extended testing of what gets output into logging
# system, we will redirect it to a string
logSys = logging.getLogger("fail2ban")
# Keep old settings
self._old_level = logSys.level
self._old_handlers = logSys.handlers
# Let's log everything into a string
self._log = StringIO()
logSys.handlers = [logging.StreamHandler(self._log)]
logSys.setLevel(getattr(logging, 'DEBUG'))
def tearDown(self): def tearDown(self):
"""Call after every test case.""" """Call after every test case."""
# print "O: >>%s<<" % self._log.getvalue()
logSys = logging.getLogger("fail2ban")
logSys.handlers = self._old_handlers
logSys.level = self._old_level
self.__action.execActionStop() self.__action.execActionStop()
def _is_logged(self, s):
return s in self._log.getvalue()
def testExecuteActionBan(self): def testExecuteActionBan(self):
self.__action.setActionStart("touch /tmp/fail2ban.test") self.__action.setActionStart("touch /tmp/fail2ban.test")
self.__action.setActionStop("rm -f /tmp/fail2ban.test") self.__action.setActionStop("rm -f /tmp/fail2ban.test")
self.__action.setActionBan("echo -n") self.__action.setActionBan("echo -n")
self.__action.setActionCheck("[ -e /tmp/fail2ban.test ]") self.__action.setActionCheck("[ -e /tmp/fail2ban.test ]")
self.assertTrue(self.__action.execActionBan(None)) self.assertFalse(self._is_logged('returned'))
# no action was actually executed yet
self.assertTrue(self.__action.execActionBan(None))
self.assertTrue(self._is_logged('Invariant check failed'))
self.assertTrue(self._is_logged('returned successfully'))
def testExecuteIncorrectCmd(self):
Action.executeCmd('/bin/ls >/dev/null\nbogusXXX now 2>/dev/null')
self.assertTrue(self._is_logged('HINT on 7f00: "Command not found"'))

View File

@ -35,11 +35,11 @@ class AddFailure(unittest.TestCase):
def setUp(self): def setUp(self):
"""Call before every test case.""" """Call before every test case."""
self.__items = [['193.168.0.128', 1167605999.0], self.__items = [[u'193.168.0.128', 1167605999.0],
['193.168.0.128', 1167605999.0], [u'193.168.0.128', 1167605999.0],
['193.168.0.128', 1167605999.0], [u'193.168.0.128', 1167605999.0],
['193.168.0.128', 1167605999.0], [u'193.168.0.128', 1167605999.0],
['193.168.0.128', 1167605999.0], [u'193.168.0.128', 1167605999.0],
['87.142.124.10', 1167605999.0], ['87.142.124.10', 1167605999.0],
['87.142.124.10', 1167605999.0], ['87.142.124.10', 1167605999.0],
['87.142.124.10', 1167605999.0], ['87.142.124.10', 1167605999.0],
@ -80,6 +80,7 @@ class AddFailure(unittest.TestCase):
#ticket = FailTicket('193.168.0.128', None) #ticket = FailTicket('193.168.0.128', None)
ticket = self.__failManager.toBan() ticket = self.__failManager.toBan()
self.assertEqual(ticket.getIP(), "193.168.0.128") self.assertEqual(ticket.getIP(), "193.168.0.128")
self.assertTrue(isinstance(ticket.getIP(), str))
def testbanNOK(self): def testbanNOK(self):
self.__failManager.setMaxRetry(10) self.__failManager.setMaxRetry(10)

View File

@ -1,2 +1,3 @@
#authentification failure (mod_auth) #authentification failure (mod_auth)
2011-12-25 17:09:20: (http_auth.c.875) password doesn't match for /gitweb/ username: francois, IP: 4.4.4.4 2011-12-25 17:09:20: (http_auth.c.875) password doesn't match for /gitweb/ username: francois, IP: 4.4.4.4
2012-09-26 10:24:35: (http_auth.c.1136) digest: auth failed for xxx : wrong password, IP: 4.4.4.4

View File

@ -22,6 +22,7 @@ Feb 25 14:34:11 belka sshd[31607]: User root from ferrari.inescn.pt not allowed
Nov 11 23:33:27 Server sshd[5174]: refused connect from _U2FsdGVkX19P3BCJmFBHhjLza8BcMH06WCUVwttMHpE=_@::ffff:218.249.210.161 (::ffff:218.249.210.161) Nov 11 23:33:27 Server sshd[5174]: refused connect from _U2FsdGVkX19P3BCJmFBHhjLza8BcMH06WCUVwttMHpE=_@::ffff:218.249.210.161 (::ffff:218.249.210.161)
#7 added exclamation mark to BREAK-IN #7 added exclamation mark to BREAK-IN
# Now should be a negative since we decided not to catch those
Oct 15 19:51:35 server sshd[7592]: Address 1.2.3.4 maps to 1234.bbbbbb.com, but this does not map back to the address - POSSIBLE BREAK-IN ATTEMPT Oct 15 19:51:35 server sshd[7592]: Address 1.2.3.4 maps to 1234.bbbbbb.com, but this does not map back to the address - POSSIBLE BREAK-IN ATTEMPT
Oct 15 19:51:35 server sshd[7592]: Address 1.2.3.4 maps to 1234.bbbbbb.com, but this does not map back to the address - POSSIBLE BREAK-IN ATTEMPT! Oct 15 19:51:35 server sshd[7592]: Address 1.2.3.4 maps to 1234.bbbbbb.com, but this does not map back to the address - POSSIBLE BREAK-IN ATTEMPT!

View File

@ -28,6 +28,7 @@ import sys
import time import time
import tempfile import tempfile
from server.jail import Jail
from server.filterpoll import FilterPoll from server.filterpoll import FilterPoll
from server.filter import FileFilter, DNSUtils from server.filter import FileFilter, DNSUtils
from server.failmanager import FailManager from server.failmanager import FailManager
@ -626,3 +627,10 @@ class DNSUtilsTests(unittest.TestCase):
self.assertEqual(res, ['192.0.43.10']) self.assertEqual(res, ['192.0.43.10'])
else: else:
self.assertEqual(res, []) self.assertEqual(res, [])
class JailTests(unittest.TestCase):
def testSetBackend_gh83(self):
# smoke test
jail = Jail('test', backend='polling') # Must not fail to initiate