diff --git a/.travis.yml b/.travis.yml index adb41e7d..71a0a05f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,7 @@ script: # Keep the legacy setup.py test approach of checking coverage for python2 - if [[ "$F2B_PY_2" ]]; then coverage run setup.py test; fi # Coverage doesn't pick up setup.py test with python3, so run it directly - - if [[ "$F2B_PY_3" ]]; then coverage run bin/fail2ban-testcases; fi + - if [[ "$F2B_PY_3" ]]; then coverage run bin/fail2ban-testcases -l debug; fi # Use $VENV_BIN (not python) or else sudo will always run the system's python (2.7) - sudo $VENV_BIN/pip install . after_success: diff --git a/ChangeLog b/ChangeLog index d15d8af6..eabfe1c8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -41,6 +41,11 @@ ver. 0.9.4 (2015/XX/XXX) - wanna-be-released for python version < 3.x (gh-1248) * Use postfix_log logpath for postfix-rbl jail * filters.d/postfix.conf - add 'Sender address rejected: Domain not found' failregex + * use `fail2ban_agent` as user-agent in actions badips, blocklist_de, etc (gh-1271) + * Fix ignoring the sender option by action_mw, action_mwl and action_c_mwl + * Changed filter.d/asterisk regex for "Call from ..." (few vulnerable now) + * Removed compression and rotation count from logrotate (inherit them from + the global logrotate config) - New Features: * New interpolation feature for definition config readers - `` @@ -50,6 +55,9 @@ ver. 0.9.4 (2015/XX/XXX) - wanna-be-released filter.d/*.local file. As extension to interpolation `%(known/parameter)s`, that does not works for filter and action init parameters + * New actions: + - nftables-multiport and nftables-allports - filtering using nftables + framework. Note: it requires a pre-existing chain for the filtering rule. * New filters: - openhab - domotic software authentication failure with the rest api and web interface (gh-1223) @@ -57,10 +65,13 @@ ver. 0.9.4 (2015/XX/XXX) - wanna-be-released request processing rate (ngx_http_limit_req_module) - murmur - ban hosts that repeatedly attempt to connect to murmur/mumble-server with an invalid server password or certificate. + - haproxy-http-auth - filter to match failed HTTP Authentications against a + HAProxy server * New jails: - murmur - bans TCP and UDP from the bad host on the default murmur port. * sshd filter got new failregex to match "maximum authentication attempts exceeded" (introduced in openssh 6.8) + * Added filter for Mac OS screen sharing (VNC) daemon - Enhancements: * Do not rotate empty log files @@ -82,6 +93,12 @@ ver. 0.9.4 (2015/XX/XXX) - wanna-be-released * Performance improvements while monitoring large number of files (gh-1265). Use associative array (dict) for monitored log files to speed up lookup operations. Thanks @kshetragia + * Specified that fail2ban is PartOf iptables.service firewalld.service in + .service file -- would reload fail2ban if those services are restarted + * Provides new default `fail2ban_version` and interpolation variable + `fail2ban_agent` in jail.conf + * Enhance filter 'postfix' to ban incoming SMTP client with no fqdn hostname + ver. 0.9.3 (2015/08/01) - lets-all-stay-friends ---------- diff --git a/MANIFEST b/MANIFEST index fb70bb4b..f77caad6 100644 --- a/MANIFEST +++ b/MANIFEST @@ -165,7 +165,11 @@ fail2ban/client/configparserinc.py fail2ban/client/configreader.py fail2ban/client/configurator.py fail2ban/client/csocket.py +fail2ban/client/fail2banclient.py +fail2ban/client/fail2bancmdline.py fail2ban/client/fail2banreader.py +fail2ban/client/fail2banregex.py +fail2ban/client/fail2banserver.py fail2ban/client/filterreader.py fail2ban/client/jailreader.py fail2ban/client/jailsreader.py diff --git a/bin/fail2ban-client b/bin/fail2ban-client index 7f3f5639..f5ae7946 100755 --- a/bin/fail2ban-client +++ b/bin/fail2ban-client @@ -18,456 +18,20 @@ # along with Fail2Ban; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -__author__ = "Cyril Jaquier" -__copyright__ = "Copyright (c) 2004 Cyril Jaquier" +""" +Fail2Ban reads log file that contains password failure report +and bans the corresponding IP addresses using firewall rules. + +This tools starts/stops fail2ban server or does client/server communication, +to change/read parameters of the server or jails. + +""" + +__author__ = "Fail2Ban Developers" +__copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halchenko, 2014-2016 Serg G. Brester" __license__ = "GPL" -import getopt -import logging -import os -import pickle -import re -import shlex -import signal -import socket -import string -import sys -import time +from fail2ban.client.fail2banclient import exec_command_line, sys -from fail2ban.version import version -from fail2ban.protocol import printFormatted -from fail2ban.client.csocket import CSocket -from fail2ban.client.configurator import Configurator -from fail2ban.client.beautifier import Beautifier -from fail2ban.helpers import getLogger - -# Gets the instance of the logger. -logSys = getLogger("fail2ban") - -## -# -# @todo This class needs cleanup. - -class Fail2banClient: - - SERVER = "fail2ban-server" - PROMPT = "fail2ban> " - - def __init__(self): - self.__argv = None - self.__stream = None - self.__configurator = Configurator() - self.__conf = dict() - 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 - self.__conf["pidfile"] = None - - def dispVersion(self): - print "Fail2Ban v" + version - print - print "Copyright (c) 2004-2008 Cyril Jaquier, 2008- Fail2Ban Contributors" - print "Copyright of modifications held by their respective authors." - print "Licensed under the GNU General Public License v2 (GPL)." - print - print "Written by Cyril Jaquier ." - print "Many contributions by Yaroslav O. Halchenko ." - - def dispUsage(self): - """ Prints Fail2Ban command line options and exits - """ - print "Usage: "+self.__argv[0]+" [OPTIONS] " - print - print "Fail2Ban v" + version + " reads log file that contains password failure report" - print "and bans the corresponding IP addresses using firewall rules." - print - print "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" - 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 - print "Command:" - - # Prints the protocol - printFormatted() - - print - print "Report bugs to https://github.com/fail2ban/fail2ban/issues" - - def dispInteractive(self): - print "Fail2Ban v" + version + " reads log file that contains password failure report" - print "and bans the corresponding IP addresses using firewall rules." - print - - def __sigTERMhandler(self, signum, frame): - # Print a new line because we probably come from wait - print - logSys.warning("Caught signal %d. Exiting" % signum) - sys.exit(-1) - - def __getCmdLineOptions(self, optList): - """ Gets the command line options - """ - for opt in optList: - if opt[0] == "-c": - 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": - self.__conf["verbose"] = self.__conf["verbose"] + 1 - elif opt[0] == "-q": - self.__conf["verbose"] = self.__conf["verbose"] - 1 - elif opt[0] == "-x": - 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) - elif opt[0] in ["-V", "--version"]: - self.dispVersion() - sys.exit(0) - - def __ping(self): - return self.__processCmd([["ping"]], False) - - def __processCmd(self, cmd, showRet = True): - client = None - try: - beautifier = Beautifier() - streamRet = True - for c in cmd: - beautifier.setInputCmd(c) - try: - if not client: - client = CSocket(self.__conf["socket"]) - ret = client.send(c) - if ret[0] == 0: - logSys.debug("OK : " + `ret[1]`) - if showRet: - print beautifier.beautify(ret[1]) - else: - logSys.error("NOK: " + `ret[1].args`) - if showRet: - print beautifier.beautifyError(ret[1]) - streamRet = False - except socket.error: - if showRet: - self.__logSocketError() - return False - except Exception, e: - if showRet: - logSys.error(e) - return False - finally: - if client: - client.close() - return streamRet - - def __logSocketError(self): - try: - if os.access(self.__conf["socket"], os.F_OK): - # This doesn't check if path is a socket, - # but socket.error should be raised - if os.access(self.__conf["socket"], os.W_OK): - # Permissions look good, but socket.error was raised - logSys.error("Unable to contact server. Is it running?") - else: - logSys.error("Permission denied to socket: %s," - " (you must be root)", self.__conf["socket"]) - else: - logSys.error("Failed to access socket path: %s." - " Is fail2ban running?", - self.__conf["socket"]) - except Exception as e: - logSys.error("Exception while checking socket access: %s", - self.__conf["socket"]) - logSys.error(e) - - ## - # Process a command line. - # - # Process one command line and exit. - # @param cmd the command line - - def __processCommand(self, cmd): - if len(cmd) == 1 and cmd[0] == "start": - if self.__ping(): - logSys.error("Server already running") - return False - else: - # Read the config - ret = self.__readConfig() - # 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"], - self.__conf["force"], - self.__conf["background"]) - try: - # Wait for the server to start - self.__waitOnServer() - # Configure the server - 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 " - "server, adding the -x option will do it") - return False - elif len(cmd) == 1 and cmd[0] == "reload": - if self.__ping(): - ret = self.__readConfig() - # Do not continue if configuration is not 100% valid - if not ret: - return False - self.__processCmd([['stop', 'all']], False) - # Configure the server - return self.__processCmd(self.__stream, False) - else: - logSys.error("Could not find server") - return False - elif len(cmd) == 2 and cmd[0] == "reload": - if self.__ping(): - jail = cmd[1] - ret = self.__readConfig(jail) - # Do not continue if configuration is not 100% valid - if not ret: - return False - self.__processCmd([['stop', jail]], False) - # Configure the server - return self.__processCmd(self.__stream, False) - else: - logSys.error("Could not find server") - return False - else: - return self.__processCmd([cmd]) - - - ## - # Start Fail2Ban server. - # - # Start the Fail2ban server in daemon mode. - - 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) - # 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") - # 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)) - 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: - logSys.error("Could not start %s" % self.SERVER) - os.exit(-1) - - def __waitOnServer(self): - # Wait for the server to start - cnt = 0 - if self.__conf["verbose"] > 1: - pos = 0 - delta = 1 - mask = "[ ]" - while not self.__ping(): - # Wonderful visual :) - if self.__conf["verbose"] > 1: - pos += delta - sys.stdout.write("\rINFO " + mask[:pos] + '#' + mask[pos+1:] + - " Waiting on the server...") - sys.stdout.flush() - if pos > len(mask)-3: - delta = -1 - elif pos < 2: - delta = 1 - # The server has 30 seconds to start. - if cnt >= 300: - if self.__conf["verbose"] > 1: - sys.stdout.write('\n') - raise ServerExecutionException("Failed to start server") - time.sleep(0.1) - cnt += 1 - if self.__conf["verbose"] > 1: - sys.stdout.write('\n') - - - def start(self, argv): - # Command line options - self.__argv = argv - - # Install signal handlers - signal.signal(signal.SIGTERM, self.__sigTERMhandler) - signal.signal(signal.SIGINT, self.__sigTERMhandler) - - # Reads the command line options. - try: - cmdOpts = 'hc:s:p:xfbdviqV' - cmdLongOpts = ['help', 'version'] - optList, args = getopt.getopt(self.__argv[1:], cmdOpts, cmdLongOpts) - except getopt.GetoptError: - self.dispUsage() - return False - - self.__getCmdLineOptions(optList) - - verbose = self.__conf["verbose"] - if verbose <= 0: - logSys.setLevel(logging.ERROR) - elif verbose == 1: - logSys.setLevel(logging.WARNING) - elif verbose == 2: - logSys.setLevel(logging.INFO) - else: - logSys.setLevel(logging.DEBUG) - # Add the default logging handler to dump to stderr - logout = logging.StreamHandler(sys.stderr) - # set a format which is simpler for console use - formatter = logging.Formatter('%(levelname)-6s %(message)s') - # tell the handler to use this format - logout.setFormatter(formatter) - logSys.addHandler(logout) - - # Set the configuration path - self.__configurator.setBaseDir(self.__conf["conf"]) - - # Set socket path - self.__configurator.readEarly() - conf = self.__configurator.getEarlyOptions() - if self.__conf["socket"] is None: - self.__conf["socket"] = conf["socket"] - if self.__conf["pidfile"] is None: - self.__conf["pidfile"] = conf["pidfile"] - logSys.info("Using socket file " + self.__conf["socket"]) - - if self.__conf["dump"]: - ret = self.__readConfig() - self.dumpConfig(self.__stream) - return ret - - # Interactive mode - if self.__conf["interactive"]: - try: - import readline - except ImportError: - logSys.error("Readline not available") - return False - try: - ret = True - if len(args) > 0: - ret = self.__processCommand(args) - if ret: - readline.parse_and_bind("tab: complete") - self.dispInteractive() - while True: - cmd = raw_input(self.PROMPT) - if cmd == "exit" or cmd == "quit": - # Exit - return True - if cmd == "help": - self.dispUsage() - elif not cmd == "": - try: - self.__processCommand(shlex.split(cmd)) - except Exception, e: - logSys.error(e) - except (EOFError, KeyboardInterrupt): - print - return True - # Single command mode - else: - if len(args) < 1: - self.dispUsage() - return False - return self.__processCommand(args) - - def __readConfig(self, jail=None): - # Read the configuration - # TODO: get away from stew of return codes and exception - # handling -- handle via exceptions - try: - self.__configurator.Reload() - self.__configurator.readAll() - ret = self.__configurator.getOptions(jail) - self.__configurator.convertToProtocol() - self.__stream = self.__configurator.getConfigStream() - except Exception, e: - logSys.error("Failed during configuration: %s" % e) - ret = False - return ret - - @staticmethod - def dumpConfig(cmd): - for c in cmd: - print c - return True - - -class ServerExecutionException(Exception): - pass - -if __name__ == "__main__": # pragma: no cover - can't test main - client = Fail2banClient() - # Exit with correct return value - if client.start(sys.argv): - sys.exit(0) - else: - sys.exit(-1) +if __name__ == "__main__": + exec_command_line(sys.argv) diff --git a/bin/fail2ban-server b/bin/fail2ban-server index f522f418..ffafabe2 100755 --- a/bin/fail2ban-server +++ b/bin/fail2ban-server @@ -18,123 +18,20 @@ # along with Fail2Ban; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -__author__ = "Cyril Jaquier" -__copyright__ = "Copyright (c) 2004 Cyril Jaquier" +""" +Fail2Ban reads log file that contains password failure report +and bans the corresponding IP addresses using firewall rules. + +This tools starts/stops fail2ban server or does client/server communication, +to change/read parameters of the server or jails. + +""" + +__author__ = "Fail2Ban Developers" +__copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halchenko, 2014-2016 Serg G. Brester" __license__ = "GPL" -import getopt -import os -import sys - -from fail2ban.version import version -from fail2ban.server.server import Server -from fail2ban.helpers import getLogger - -# Gets the instance of the logger. -logSys = getLogger("fail2ban") - -## -# \mainpage Fail2Ban -# -# \section Introduction -# -# Fail2ban is designed to protect your server against brute force attacks. -# Its first goal was to protect a SSH server. - -class Fail2banServer: - - def __init__(self): - self.__server = None - self.__argv = None - self.__conf = dict() - 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 - print - print "Copyright (c) 2004-2008 Cyril Jaquier, 2008- Fail2Ban Contributors" - print "Copyright of modifications held by their respective authors." - print "Licensed under the GNU General Public License v2 (GPL)." - print - print "Written by Cyril Jaquier ." - print "Many contributions by Yaroslav O. Halchenko ." - - def dispUsage(self): - """ Prints Fail2Ban command line options and exits - """ - print "Usage: "+self.__argv[0]+" [OPTIONS]" - print - print "Fail2Ban v" + version + " reads log file that contains password failure report" - print "and bans the corresponding IP addresses using firewall rules." - print - print "Only use this command for debugging purpose. Start the server with" - print "fail2ban-client instead. The default behaviour is to start the server" - print "in background." - print - print "Options:" - 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" - print - print "Report bugs to https://github.com/fail2ban/fail2ban/issues" - - def __getCmdLineOptions(self, optList): - """ Gets the command line options - """ - for opt in optList: - if opt[0] == "-b": - self.__conf["background"] = True - if opt[0] == "-f": - 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"]: - self.dispUsage() - sys.exit(0) - if opt[0] in ["-V", "--version"]: - self.dispVersion() - sys.exit(0) - - def start(self, argv): - # Command line options - self.__argv = argv - - # Reads the command line options. - try: - cmdOpts = 'bfs:p:xhV' - cmdLongOpts = ['help', 'version'] - optList, args = getopt.getopt(self.__argv[1:], cmdOpts, cmdLongOpts) - except getopt.GetoptError: - self.dispUsage() - sys.exit(-1) - - self.__getCmdLineOptions(optList) - - try: - self.__server = Server(self.__conf["background"]) - self.__server.start(self.__conf["socket"], - self.__conf["pidfile"], - self.__conf["force"]) - return True - except Exception, e: - logSys.exception(e) - self.__server.quit() - return False +from fail2ban.client.fail2banserver import exec_command_line, sys if __name__ == "__main__": - server = Fail2banServer() - if server.start(sys.argv): - sys.exit(0) - else: - sys.exit(-1) + exec_command_line(sys.argv) diff --git a/bin/fail2ban-testcases b/bin/fail2ban-testcases index 606b0b06..3b18b7c2 100755 --- a/bin/fail2ban-testcases +++ b/bin/fail2ban-testcases @@ -119,10 +119,14 @@ else: # Custom log format for the verbose tests runs if verbosity > 1: # pragma: no cover - stdout.setFormatter(Formatter(' %(asctime)-15s %(thread)s' + fmt)) -else: # pragma: no cover - # just prefix with the space - stdout.setFormatter(Formatter(fmt)) + if verbosity > 3: + fmt = ' | %(module)15.15s-%(levelno)-2d: %(funcName)-20.20s |' + fmt + if verbosity > 2: + fmt = ' +%(relativeCreated)5d %(thread)X %(levelname)-5.5s' + fmt + else: + fmt = ' %(asctime)-15s %(thread)X %(levelname)-5.5s' + fmt +# +stdout.setFormatter(Formatter(fmt)) logSys.addHandler(stdout) # diff --git a/config/action.d/badips.conf b/config/action.d/badips.conf index 4a5c0f97..70b46546 100644 --- a/config/action.d/badips.conf +++ b/config/action.d/badips.conf @@ -10,7 +10,7 @@ [Definition] -actionban = curl --fail --user-agent "fail2ban v0.8.12" http://www.badips.com/add// +actionban = curl --fail --user-agent "" http://www.badips.com/add// [Init] diff --git a/config/action.d/badips.py b/config/action.d/badips.py index 99e1866a..025289ca 100644 --- a/config/action.d/badips.py +++ b/config/action.d/badips.py @@ -21,7 +21,6 @@ import sys if sys.version_info < (2, 7): raise ImportError("badips.py action requires Python >= 2.7") import json -from functools import partial import threading import logging if sys.version_info >= (3, ): @@ -33,7 +32,6 @@ else: from urllib import urlencode from fail2ban.server.actions import ActionBase -from fail2ban.version import version as f2bVersion class BadIPsAction(ActionBase): @@ -72,6 +70,9 @@ class BadIPsAction(ActionBase): updateperiod : int, optional Time in seconds between updating bad IPs blacklist. Default 900 (15 minutes) + agent : str, optional + User agent transmitted to server. + Default `Fail2Ban/ver.` Raises ------ @@ -80,13 +81,14 @@ class BadIPsAction(ActionBase): """ _badips = "http://www.badips.com" - _Request = partial( - Request, headers={'User-Agent': "Fail2Ban %s" % f2bVersion}) + def _Request(self, url, **argv): + return Request(url, headers={'User-Agent': self.agent}, **argv) def __init__(self, jail, name, category, score=3, age="24h", key=None, - banaction=None, bancategory=None, bankey=None, updateperiod=900): + banaction=None, bancategory=None, bankey=None, updateperiod=900, agent="Fail2Ban"): super(BadIPsAction, self).__init__(jail, name) + self.agent = agent self.category = category self.score = score self.age = age diff --git a/config/action.d/blocklist_de.conf b/config/action.d/blocklist_de.conf index 6d520694..2f31d8b9 100644 --- a/config/action.d/blocklist_de.conf +++ b/config/action.d/blocklist_de.conf @@ -54,7 +54,7 @@ actioncheck = # Tags: See jail.conf(5) man page # Values: CMD # -actionban = curl --fail --data-urlencode 'server=' --data 'apikey=' --data 'service=' --data 'ip=' --data-urlencode 'logs=' --data 'format=text' --user-agent "fail2ban v0.8.12" "https://www.blocklist.de/en/httpreports.html" +actionban = curl --fail --data-urlencode 'server=' --data 'apikey=' --data 'service=' --data 'ip=' --data-urlencode 'logs=' --data 'format=text' --user-agent "" "https://www.blocklist.de/en/httpreports.html" # Option: actionunban # Notes.: command executed when unbanning an IP. Take care that the diff --git a/config/action.d/mynetwatchman.conf b/config/action.d/mynetwatchman.conf index 5245a4e3..8f3edf9e 100644 --- a/config/action.d/mynetwatchman.conf +++ b/config/action.d/mynetwatchman.conf @@ -111,13 +111,17 @@ myip = `ip -4 addr show dev eth0 | grep inet | head -n 1 | sed -r 's/.*inet ([0- # protocol = tcp +# Option: agent +# Default: Fail2ban +agent = Fail2ban + # Option: getcmd # Notes.: A command to fetch a URL. Should output page to STDOUT # Values: CMD Default: wget # -getcmd = wget --no-verbose --tries=3 --waitretry=10 --connect-timeout=10 --read-timeout=60 --retry-connrefused --output-document=- --user-agent=Fail2Ban +getcmd = wget --no-verbose --tries=3 --waitretry=10 --connect-timeout=10 --read-timeout=60 --retry-connrefused --output-document=- --user-agent= # Alternative value: -# getcmd = curl --silent --show-error --retry 3 --connect-timeout 10 --max-time 60 --user-agent Fail2Ban +# getcmd = curl --silent --show-error --retry 3 --connect-timeout 10 --max-time 60 --user-agent # Option: srcport # Notes.: The source port of the attack. You're unlikely to have this info, so diff --git a/config/action.d/nftables-allports.conf b/config/action.d/nftables-allports.conf new file mode 100644 index 00000000..afd0ca84 --- /dev/null +++ b/config/action.d/nftables-allports.conf @@ -0,0 +1,22 @@ +# Fail2Ban configuration file +# +# Author: Cyril Jaquier +# Modified: Yaroslav O. Halchenko +# made active on all ports from original iptables.conf +# Modified: Alexander Belykh +# adapted for nftables +# + +[INCLUDES] + +before = nftables-common.conf + +[Definition] + +# Option: nftables_mode +# Notes.: additional expressions for nftables filter rule +# Values: nftables expressions +# +nftables_mode = ip protocol + +[Init] diff --git a/config/action.d/nftables-common.conf b/config/action.d/nftables-common.conf new file mode 100644 index 00000000..80657c5c --- /dev/null +++ b/config/action.d/nftables-common.conf @@ -0,0 +1,119 @@ +# Fail2Ban configuration file +# +# Author: Daniel Black +# Author: Cyril Jaquier +# Modified: Yaroslav O. Halchenko +# made active on all ports from original iptables.conf +# Modified: Alexander Belykh +# adapted for nftables +# +# This is a included configuration file and includes the definitions for the nftables +# used in all nftables based actions by default. +# +# The user can override the defaults in nftables-common.local + +[INCLUDES] + +after = nftables-common.local + +[Definition] + +# Option: nftables_mode +# Notes.: additional expressions for nftables filter rule +# Values: nftables expressions +# +nftables_mode = dport \{ \} + +# Option: actionstart +# Notes.: command executed once at the start of Fail2Ban. +# Values: CMD +# +actionstart = add set f2b- \{ type \; \} + insert rule %(nftables_mode)s ip saddr @f2b- + +_nft_list = --handle --numeric list chain +_nft_get_handle_id = grep -m1 'ip saddr @f2b- # handle' | grep -oe ' handle [0-9]*' + +# Option: actionstop +# Notes.: command executed once at the end of Fail2Ban +# Values: CMD +# +actionstop = HANDLE_ID=$(%(_nft_list)s | %(_nft_get_handle_id)s) + delete rule $HANDLE_ID + delete set f2b- + +# Option: actioncheck +# Notes.: command executed once before each actionban command +# Values: CMD +# +actioncheck = list chain | grep -q '@f2b-[ \t]' + +# Option: actionban +# Notes.: command executed when banning an IP. Take care that the +# command is executed with Fail2Ban user rights. +# Tags: See jail.conf(5) man page +# Values: CMD +# +actionban = add element f2b- \{ \} + +# Option: actionunban +# Notes.: command executed when unbanning an IP. Take care that the +# command is executed with Fail2Ban user rights. +# Tags: See jail.conf(5) man page +# Values: CMD +# +actionunban = delete element f2b- \{ \} + +[Init] + +# Option: nftables_type +# Notes.: address type to work with +# Values: [ipv4_addr | ipv6_addr] Default: ipv4_addr +# +nftables_type = ipv4_addr + +# Option: nftables_family +# Notes.: address family to work in +# Values: [ip | ip6 | inet] Default: inet +# +nftables_family = inet + +# Option: nftables_table +# Notes.: table in the address family to work in +# Values: STRING Default: filter +# +nftables_table = filter + +# Option: chain +# Notes specifies the nftables chain to which the Fail2Ban rules should be +# added +# Values: STRING Default: input +chain = input + +# Default name of the filtering set +# +name = default + +# Option: port +# Notes.: specifies port to monitor +# Values: [ NUM | STRING ] Default: +# +port = ssh + +# Option: protocol +# Notes.: internally used by config reader for interpolations. +# Values: [ tcp | udp ] Default: tcp +# +protocol = tcp + +# Option: blocktype +# Note: This is what the action does with rules. This can be any jump target +# as per the nftables man page (section 8). Common values are drop +# reject, reject with icmp type host-unreachable +# Values: STRING +blocktype = reject + +# Option: nftables +# Notes.: Actual command to be executed, including common to all calls options +# Values: STRING +nftables = nft diff --git a/config/action.d/nftables-multiport.conf b/config/action.d/nftables-multiport.conf new file mode 100644 index 00000000..d1afafb3 --- /dev/null +++ b/config/action.d/nftables-multiport.conf @@ -0,0 +1,22 @@ +# Fail2Ban configuration file +# +# Author: Cyril Jaquier +# Modified: Yaroslav O. Halchenko +# made active on all ports from original iptables.conf +# Modified: Alexander Belykh +# adapted for nftables +# + +[INCLUDES] + +before = nftables-common.conf + +[Definition] + +# Option: nftables_mode +# Notes.: additional expressions for nftables filter rule +# Values: nftables expressions +# +nftables_mode = dport \{ \} + +[Init] diff --git a/config/filter.d/asterisk.conf b/config/filter.d/asterisk.conf index b446c44e..3975fb29 100644 --- a/config/filter.d/asterisk.conf +++ b/config/filter.d/asterisk.conf @@ -19,7 +19,7 @@ iso8601 = \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+[+-]\d{4} log_prefix= (?:NOTICE|SECURITY)%(__pid_re)s:?(?:\[C-[\da-f]*\])? \S+:\d*( in \w+:)? failregex = ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Registration from '[^']*' failed for '(:\d+)?' - (Wrong password|Username/auth name mismatch|No matching peer found|Not a local domain|Device does not match ACL|Peer is not supposed to register|ACL error \(permit/deny\)|Not a local domain)$ - ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Call from '[^']*' \(:\d+\) to extension '\d+' rejected because extension not found in context 'default'\.$ + ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Call from '[^']*' \(:\d+\) to extension '[^']*' rejected because extension not found in context ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Host failed to authenticate as '[^']*'$ ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s No registration for peer '[^']*' \(from \)$ ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Host failed MD5 authentication for '[^']*' \([^)]+\)$ diff --git a/config/filter.d/haproxy-http-auth.conf b/config/filter.d/haproxy-http-auth.conf new file mode 100644 index 00000000..298ca292 --- /dev/null +++ b/config/filter.d/haproxy-http-auth.conf @@ -0,0 +1,37 @@ +# Fail2Ban filter configuration file to match failed login attempts to +# HAProxy HTTP Authentication protected servers. +# +# PLEASE NOTE - When a user first hits the HTTP Auth a 401 is returned by the server +# which prompts their browser to ask for login details. +# This initial 401 is logged by HAProxy. +# In other words, even successful logins will have at least 1 fail regex match. +# Please keep this in mind when setting findtime and maxretry for jails. +# +# Author: Jordan Moeser +# + +[INCLUDES] + +# Read common prefixes. If any customizations available -- read them from +# common.local +before = common.conf + + +[Definition] + +_daemon = haproxy + +# Option: failregex +# Notes.: regex to match the password failures messages in the logfile. The +# host must be matched by a group named "host". The tag "" can +# be used for standard IP/hostname matching and is only an alias for +# (?:::f{4,6}:)?(?P[\w\-.^_]+) +# Values: TEXT +# +failregex = ^%(__prefix_line)s.* -1/-1/-1/-1/\+*\d* 401 + +# Option: ignoreregex +# Notes.: regex to ignore. If this regex matches, the line is ignored. +# Values: TEXT +# +ignoreregex = diff --git a/config/filter.d/postfix.conf b/config/filter.d/postfix.conf index f6a8578b..25141863 100644 --- a/config/filter.d/postfix.conf +++ b/config/filter.d/postfix.conf @@ -15,6 +15,7 @@ _daemon = postfix/(submission/)?smtp(d|s) failregex = ^%(__prefix_line)sNOQUEUE: reject: RCPT from \S+\[\]: 554 5\.7\.1 .*$ ^%(__prefix_line)sNOQUEUE: reject: RCPT from \S+\[\]: 450 4\.7\.1 Client host rejected: cannot find your hostname, (\[\S*\]); from=<\S*> to=<\S+> proto=ESMTP helo=<\S*>$ ^%(__prefix_line)sNOQUEUE: reject: RCPT from \S+\[\]: 450 4\.7\.1 : Helo command rejected: Host not found; from=<> to=<> proto=ESMTP helo= *$ + ^%(__prefix_line)sNOQUEUE: reject: EHLO from \S+\[\]: 504 5\.5\.2 <\S+>: Helo command rejected: need fully-qualified hostname; ^%(__prefix_line)sNOQUEUE: reject: VRFY from \S+\[\]: 550 5\.1\.1 .*$ ^%(__prefix_line)sNOQUEUE: reject: RCPT from \S+\[\]: 450 4\.1\.8 <\S*>: Sender address rejected: Domain not found; from=<\S*> to=<\S+> proto=ESMTP helo=<\S*>$ ^%(__prefix_line)simproper command pipelining after \S+ from [^[]*\[\]:?$ diff --git a/config/filter.d/screensharingd.conf b/config/filter.d/screensharingd.conf new file mode 100644 index 00000000..4cd76465 --- /dev/null +++ b/config/filter.d/screensharingd.conf @@ -0,0 +1,31 @@ +# Fail2Ban configuration file +# +# Author: Simon Brown +# +# Filter for Mac OS X Screen Sharing service + +[INCLUDES] + +# Read common prefixes. If any customizations available -- read them from +# common.local +before = common.conf + + +[Definition] + +_daemon = screensharingd + +# Option: failregex +# Notes.: regex to match the password failures messages in the logfile. The +# host must be matched by a group named "host". The tag "" can +# be used for standard IP/hostname matching and is only an alias for +# (?:::f{4,6}:)?(?P[\w\-.^_]+) +# Values: TEXT +# +failregex = ^%(__prefix_line)sAuthentication: FAILED :: User Name: .+ :: Viewer Address: :: Type: DH$ + +# Option: ignoreregex +# Notes.: regex to ignore. If this regex matches, the line is ignored. +# Values: TEXT +# +ignoreregex = diff --git a/config/jail.conf b/config/jail.conf index c6007559..5b18fdd5 100644 --- a/config/jail.conf +++ b/config/jail.conf @@ -184,6 +184,9 @@ chain = INPUT # Usually should be overridden in a particular jail port = 0:65535 +# Format of user-agent https://tools.ietf.org/html/rfc7231#section-5.5.3 +fail2ban_agent = Fail2Ban/%(fail2ban_version)s + # # Action shortcuts. To be used to define action parameter @@ -199,12 +202,12 @@ action_ = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s # ban & send an e-mail with whois report to the destemail. action_mw = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] - %(mta)s-whois[name=%(__name__)s, dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s"] + %(mta)s-whois[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s"] # ban & send an e-mail with whois report and relevant log lines # to the destemail. action_mwl = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] - %(mta)s-whois-lines[name=%(__name__)s, dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"] + %(mta)s-whois-lines[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"] # See the IMPORTANT note in action.d/xarf-login-attack for when to use this action # @@ -216,7 +219,7 @@ action_xarf = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(po # ban IP on CloudFlare & send an e-mail with whois report and relevant log lines # to the destemail. action_cf_mwl = cloudflare[cfuser="%(cfemail)s", cftoken="%(cfapikey)s"] - %(mta)s-whois-lines[name=%(__name__)s, dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"] + %(mta)s-whois-lines[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"] # Report block via blocklist.de fail2ban reporting service API # @@ -225,7 +228,7 @@ action_cf_mwl = cloudflare[cfuser="%(cfemail)s", cftoken="%(cfapikey)s"] # [Init] # blocklist_de_apikey = {api key from registration] # -action_blocklist_de = blocklist_de[email="%(sender)s", service=%(filter)s, apikey="%(blocklist_de_apikey)s"] +action_blocklist_de = blocklist_de[email="%(sender)s", service=%(filter)s, apikey="%(blocklist_de_apikey)s", agent="%(fail2ban_agent)s"] # Report ban via badips.com, and use as blacklist # @@ -235,7 +238,11 @@ action_blocklist_de = blocklist_de[email="%(sender)s", service=%(filter)s, apik # NOTE: This action relies on banaction being present on start and therefore # should be last action defined for a jail. # -action_badips = badips.py[category="%(name)s", banaction="%(banaction)s"] +action_badips = badips.py[category="%(__name__)s", banaction="%(banaction)s", agent="%(fail2ban_agent)s"] +# +# Report ban via badips.com (uses action.d/badips.conf for reporting only) +# +action_badips_report = badips[category="%(__name__)s", agent="%(fail2ban_agent)s"] # Choose default action. To change, just override value of 'action' with the # interpolation to the chosen action shortcut (e.g. action_mw, action_mwl, etc) in jail.local @@ -278,7 +285,6 @@ backend = %(dropbear_backend)s port = ssh logpath = %(auditd_log)s -maxretry = 5 # @@ -304,7 +310,6 @@ maxretry = 1 port = http,https logpath = %(apache_error_log)s -maxretry = 6 [apache-overflows] @@ -342,18 +347,21 @@ port = http,https logpath = %(apache_error_log)s maxretry = 2 + [apache-shellshock] port = http,https logpath = %(apache_error_log)s maxretry = 1 + [openhab-auth] filter = openhab action = iptables-allports[name=NoAuthFailures] logpath = /opt/openhab/logs/request.log + [nginx-http-auth] port = http,https @@ -373,6 +381,7 @@ port = http,https logpath = %(nginx_error_log)s maxretry = 2 + # Ban attackers that try to use PHP's URL-fopen() functionality # through GET/POST variables. - Experimental, with more than a year # of usage in production environments. @@ -437,7 +446,6 @@ logpath = /var/log/sogo/sogo.log logpath = /var/log/tine20/tine20.log port = http,https -maxretry = 5 # @@ -458,7 +466,6 @@ logpath = /var/log/tomcat*/catalina.out [monit] #Ban clients brute-forcing the monit gui login -filter = monit port = 2812 logpath = /var/log/monit @@ -511,7 +518,6 @@ backend = %(proftpd_backend)s port = ftp,ftp-data,ftps,ftps-data logpath = %(pureftpd_log)s backend = %(pureftpd_backend)s -maxretry = 6 [gssftpd] @@ -519,7 +525,6 @@ maxretry = 6 port = ftp,ftp-data,ftps,ftps-data logpath = %(syslog_daemon)s backend = %(syslog_backend)s -maxretry = 6 [wuftpd] @@ -527,7 +532,6 @@ maxretry = 6 port = ftp,ftp-data,ftps,ftps-data logpath = %(wuftpd_log)s backend = %(wuftpd_backend)s -maxretry = 6 [vsftpd] @@ -762,7 +766,6 @@ maxretry = 10 port = 3306 logpath = %(mysql_log)s backend = %(mysql_backend)s -maxretry = 5 # Jail for more extended banning of persistent abusers @@ -778,7 +781,6 @@ logpath = /var/log/fail2ban.log banaction = %(banaction_allports)s bantime = 1w findtime = 1d -maxretry = 5 # Generic filter for PAM. Has to be used with action which bans all @@ -824,7 +826,6 @@ action = %(banaction)s[name=%(__name__)s-tcp, port="%(tcpport)s", protocol="tcp # nobody except your own Nagios server should ever probe nrpe [nagios] -enabled = false logpath = %(syslog_daemon)s ; nrpe.cfg may define a different log_facility backend = %(syslog_backend)s maxretry = 1 @@ -832,18 +833,14 @@ maxretry = 1 [oracleims] # see "oracleims" filter file for configuration requirement for Oracle IMS v6 and above -enabled = false logpath = /opt/sun/comms/messaging64/log/mail.log_current -maxretry = 6 banaction = %(banaction_allports)s [directadmin] -enabled = false logpath = /var/log/directadmin/login.log port = 2222 [portsentry] -enabled = false logpath = /var/lib/portsentry/portsentry.history maxretry = 1 @@ -864,7 +861,19 @@ findtime = 1 [murmur] # AKA mumble-server port = 64738 -filter = murmur action = %(banaction)s[name=%(__name__)s-tcp, port="%(port)s", protocol=tcp, chain="%(chain)s", actname=%(banaction)s-tcp] %(banaction)s[name=%(__name__)s-udp, port="%(port)s", protocol=udp, chain="%(chain)s", actname=%(banaction)s-udp] logpath = /var/log/mumble-server/mumble-server.log + + +[screensharingd] +# For Mac OS Screen Sharing Service (VNC) +logpath = /var/log/system.log +logencoding = utf-8 + +[haproxy-http-auth] +# HAProxy by default doesn't log to file you'll need to set it up to forward +# logs to a syslog server which would then write them to disk. +# See "haproxy-http-auth" filter for a brief cautionary note when setting +# maxretry and findtime. +logpath = /var/log/haproxy.log diff --git a/fail2ban/client/beautifier.py b/fail2ban/client/beautifier.py index 812fbe65..08ff484d 100644 --- a/fail2ban/client/beautifier.py +++ b/fail2ban/client/beautifier.py @@ -68,6 +68,8 @@ class Beautifier: msg = "Added jail " + response elif inC[0] == "flushlogs": msg = "logs: " + response + elif inC[0] == "echo": + msg = ' '.join(msg) elif inC[0:1] == ['status']: if len(inC) > 1: # Display information diff --git a/fail2ban/client/configreader.py b/fail2ban/client/configreader.py index c6dd1b60..f333cdc1 100644 --- a/fail2ban/client/configreader.py +++ b/fail2ban/client/configreader.py @@ -208,7 +208,7 @@ class ConfigReaderUnshared(SafeConfigParserWithIncludes): # 1 -> the name of the option # 2 -> the default value for the option - def getOptions(self, sec, options, pOptions=None): + def getOptions(self, sec, options, pOptions=None, shouldExist=False): values = dict() for option in options: try: @@ -222,6 +222,8 @@ class ConfigReaderUnshared(SafeConfigParserWithIncludes): continue values[option[1]] = v except NoSectionError, e: + if shouldExist: + raise # No "Definition" section or wrong basedir logSys.error(e) values[option[1]] = option[2] diff --git a/fail2ban/client/fail2banclient.py b/fail2ban/client/fail2banclient.py new file mode 100644 index 00000000..a8e0a331 --- /dev/null +++ b/fail2ban/client/fail2banclient.py @@ -0,0 +1,462 @@ +#!/usr/bin/python +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*- +# vi: set ft=python sts=4 ts=4 sw=4 noet : +# +# This file is part of Fail2Ban. +# +# Fail2Ban is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Fail2Ban is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Fail2Ban; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +__author__ = "Fail2Ban Developers" +__copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halchenko, 2014-2016 Serg G. Brester" +__license__ = "GPL" + +import os +import shlex +import signal +import socket +import sys +import time + +import threading +from threading import Thread + +from ..version import version +from .csocket import CSocket +from .beautifier import Beautifier +from .fail2bancmdline import Fail2banCmdLine, ServerExecutionException, ExitException, \ + logSys, PRODUCTION, exit, output + +PROMPT = "fail2ban> " + + +def _thread_name(): + return threading.current_thread().__class__.__name__ + +def input_command(): # pragma: no cover + return raw_input(PROMPT) + +## +# +# @todo This class needs cleanup. + +class Fail2banClient(Fail2banCmdLine, Thread): + + def __init__(self): + Fail2banCmdLine.__init__(self) + Thread.__init__(self) + self._alive = True + self._server = None + self._beautifier = None + + def dispInteractive(self): + output("Fail2Ban v" + version + " reads log file that contains password failure report") + output("and bans the corresponding IP addresses using firewall rules.") + output("") + + def __sigTERMhandler(self, signum, frame): # pragma: no cover + # Print a new line because we probably come from wait + output("") + logSys.warning("Caught signal %d. Exiting" % signum) + exit(-1) + + def __ping(self): + return self.__processCmd([["ping"]], False) + + @property + def beautifier(self): + if self._beautifier: + return self._beautifier + self._beautifier = Beautifier() + return self._beautifier + + def __processCmd(self, cmd, showRet=True): + client = None + try: + beautifier = self.beautifier + streamRet = True + for c in cmd: + beautifier.setInputCmd(c) + try: + if not client: + client = CSocket(self._conf["socket"]) + ret = client.send(c) + if ret[0] == 0: + logSys.debug("OK : %r", ret[1]) + if showRet or c[0] == 'echo': + output(beautifier.beautify(ret[1])) + else: + logSys.error("NOK: %r", ret[1].args) + if showRet: + output(beautifier.beautifyError(ret[1])) + streamRet = False + except socket.error as e: + if showRet or self._conf["verbose"] > 1: + if showRet or c != ["ping"]: + self.__logSocketError() + else: + logSys.debug(" -- ping failed -- %r", e) + return False + except Exception as e: # pragma: no cover + if showRet or self._conf["verbose"] > 1: + if self._conf["verbose"] > 1: + logSys.exception(e) + else: + logSys.error(e) + return False + finally: + # prevent errors by close during shutdown (on exit command): + if client: + try : + client.close() + except Exception as e: + if showRet or self._conf["verbose"] > 1: + logSys.debug(e) + if showRet or c[0] == 'echo': + sys.stdout.flush() + return streamRet + + def __logSocketError(self): + try: + if os.access(self._conf["socket"], os.F_OK): # pragma: no cover + # This doesn't check if path is a socket, + # but socket.error should be raised + if os.access(self._conf["socket"], os.W_OK): + # Permissions look good, but socket.error was raised + logSys.error("Unable to contact server. Is it running?") + else: + logSys.error("Permission denied to socket: %s," + " (you must be root)", self._conf["socket"]) + else: + logSys.error("Failed to access socket path: %s." + " Is fail2ban running?", + self._conf["socket"]) + except Exception as e: # pragma: no cover + logSys.error("Exception while checking socket access: %s", + self._conf["socket"]) + logSys.error(e) + + ## + def __prepareStartServer(self): + if self.__ping(): + logSys.error("Server already running") + return None + + # Read the config + ret, stream = self.readConfig() + # Do not continue if configuration is not 100% valid + if not ret: + return None + + # 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 None + if not os.access(socket_dir, os.W_OK | os.X_OK): # pragma: no cover + logSys.error( + "Directory %s exists but not accessible for writing" + % (socket_dir,)) + return None + + # Check already running + if not self._conf["force"] and os.path.exists(self._conf["socket"]): + logSys.error("Fail2ban seems to be in unexpected state (not running but the socket exists)") + return None + + stream.append(['echo', 'Server ready']) + return stream + + ## + def __startServer(self, background=True): + from .fail2banserver import Fail2banServer + stream = self.__prepareStartServer() + self._alive = True + if not stream: + return False + # Start the server or just initialize started one: + try: + if background: + # Start server daemon as fork of client process: + Fail2banServer.startServerAsync(self._conf) + # Send config stream to server: + if not self.__processStartStreamAfterWait(stream, False): + return False + else: + # In foreground mode we should make server/client communication in different threads: + th = Thread(target=Fail2banClient.__processStartStreamAfterWait, args=(self, stream, False)) + th.daemon = True + th.start() + # Mark current (main) thread as daemon: + self.setDaemon(True) + # Start server direct here in main thread (not fork): + self._server = Fail2banServer.startServerDirect(self._conf, False) + + except ExitException: # pragma: no cover + pass + except Exception as e: # pragma: no cover + output("") + logSys.error("Exception while starting server " + ("background" if background else "foreground")) + if self._conf["verbose"] > 1: + logSys.exception(e) + else: + logSys.error(e) + return False + + return True + + ## + def configureServer(self, async=True, phase=None): + # if asynchron start this operation in the new thread: + if async: + th = Thread(target=Fail2banClient.configureServer, args=(self, False, phase)) + th.daemon = True + return th.start() + # prepare: read config, check configuration is valid, etc.: + if phase is not None: + phase['start'] = True + logSys.debug('-- client phase %s', phase) + stream = self.__prepareStartServer() + if phase is not None: + phase['ready'] = phase['start'] = (True if stream else False) + logSys.debug('-- client phase %s', phase) + if not stream: + return False + # configure server with config stream: + ret = self.__processStartStreamAfterWait(stream, False) + if phase is not None: + phase['done'] = ret + return ret + + ## + # Process a command line. + # + # Process one command line and exit. + # @param cmd the command line + + def __processCommand(self, cmd): + if len(cmd) == 1 and cmd[0] == "start": + + ret = self.__startServer(self._conf["background"]) + if not ret: + return False + return ret + + elif len(cmd) == 1 and cmd[0] == "restart": + + if self._conf.get("interactive", False): + output(' ## stop ... ') + self.__processCommand(['stop']) + if not self.__waitOnServer(False): # pragma: no cover + logSys.error("Could not stop server") + return False + # in interactive mode reset config, to make full-reload if there something changed: + if self._conf.get("interactive", False): + output(' ## load configuration ... ') + self.resetConf() + ret = self.initCmdLine(self._argv) + if ret is not None: + return ret + if self._conf.get("interactive", False): + output(' ## start ... ') + return self.__processCommand(['start']) + + elif len(cmd) >= 1 and cmd[0] == "reload": + if self.__ping(): + if len(cmd) == 1: + jail = 'all' + ret, stream = self.readConfig() + else: + jail = cmd[1] + ret, stream = self.readConfig(jail) + # Do not continue if configuration is not 100% valid + if not ret: + return False + self.__processCmd([['stop', jail]], False) + # Configure the server + return self.__processCmd(stream, True) + else: + logSys.error("Could not find server") + return False + + else: + return self.__processCmd([cmd]) + + + def __processStartStreamAfterWait(self, *args): + try: + # Wait for the server to start + if not self.__waitOnServer(): # pragma: no cover + logSys.error("Could not find server, waiting failed") + return False + # Configure the server + self.__processCmd(*args) + except ServerExecutionException as e: # pragma: no cover + if self._conf["verbose"] > 1: + logSys.exception(e) + 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") + if self._server: + self._server.quit() + return False + return True + + def __waitOnServer(self, alive=True, maxtime=None): + if maxtime is None: + maxtime = self._conf["timeout"] + # Wait for the server to start (the server has 30 seconds to answer ping) + starttime = time.time() + logSys.debug("__waitOnServer: %r", (alive, maxtime)) + test = lambda: os.path.exists(self._conf["socket"]) and self.__ping() + with VisualWait(self._conf["verbose"]) as vis: + sltime = 0.0125 / 2 + while self._alive: + runf = test() + if runf == alive: + return True + now = time.time() + # Wonderful visual :) + if now > starttime + 1: + vis.heartbeat() + # f end time reached: + if now - starttime >= maxtime: + raise ServerExecutionException("Failed to start server") + sltime = min(sltime * 2, 0.5) + time.sleep(sltime) + return False + + def start(self, argv): + # Install signal handlers + _prev_signals = {} + if _thread_name() == '_MainThread': + for s in (signal.SIGTERM, signal.SIGINT): + _prev_signals[s] = signal.getsignal(s) + signal.signal(s, self.__sigTERMhandler) + try: + # Command line options + if self._argv is None: + ret = self.initCmdLine(argv) + if ret is not None: + if ret: + return True + raise ServerExecutionException("Init of command line failed") + + # Commands + args = self._args + + # Interactive mode + if self._conf.get("interactive", False): + # no readline in test: + if PRODUCTION: # pragma: no cover + try: + import readline + except ImportError: + raise ServerExecutionException("Readline not available") + try: + ret = True + if len(args) > 0: + ret = self.__processCommand(args) + if ret: + if PRODUCTION: # pragma: no cover + readline.parse_and_bind("tab: complete") + self.dispInteractive() + while True: + cmd = input_command() + if cmd == "exit" or cmd == "quit": + # Exit + return True + if cmd == "help": + self.dispUsage() + elif not cmd == "": + try: + self.__processCommand(shlex.split(cmd)) + except Exception, e: # pragma: no cover + if self._conf["verbose"] > 1: + logSys.exception(e) + else: + logSys.error(e) + except (EOFError, KeyboardInterrupt): # pragma: no cover + output("") + raise + # Single command mode + else: + if len(args) < 1: + self.dispUsage() + return False + return self.__processCommand(args) + except Exception as e: + if self._conf["verbose"] > 1: + logSys.exception(e) + else: + logSys.error(e) + return False + finally: + self._alive = False + for s, sh in _prev_signals.iteritems(): + signal.signal(s, sh) + + +## +# Wonderful visual :) +# + +class _VisualWait: + pos = 0 + delta = 1 + def __init__(self, maxpos=10): + self.maxpos = maxpos + def __enter__(self): + return self + def __exit__(self, *args): + if self.pos: + sys.stdout.write('\r'+(' '*(35+self.maxpos))+'\r') + sys.stdout.flush() + def heartbeat(self): + if not self.pos: + sys.stdout.write("\nINFO [#" + (' '*self.maxpos) + "] Waiting on the server...\r\x1b[8C") + self.pos += self.delta + if self.delta > 0: + s = " #\x1b[1D" if self.pos > 1 else "# \x1b[2D" + else: + s = "\x1b[1D# \x1b[2D" + sys.stdout.write(s) + sys.stdout.flush() + if self.pos > self.maxpos: + self.delta = -1 + elif self.pos < 2: + self.delta = 1 +class _NotVisualWait: + def __enter__(self): + return self + def __exit__(self, *args): + pass + def heartbeat(self): + pass + +def VisualWait(verbose, *args, **kwargs): + return _VisualWait(*args, **kwargs) if verbose > 1 else _NotVisualWait() + + +def exec_command_line(argv): + client = Fail2banClient() + # Exit with correct return value + if client.start(argv): + exit(0) + else: + exit(-1) + diff --git a/fail2ban/client/fail2bancmdline.py b/fail2ban/client/fail2bancmdline.py new file mode 100644 index 00000000..a7754bc3 --- /dev/null +++ b/fail2ban/client/fail2bancmdline.py @@ -0,0 +1,275 @@ +#!/usr/bin/python +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*- +# vi: set ft=python sts=4 ts=4 sw=4 noet : +# +# This file is part of Fail2Ban. +# +# Fail2Ban is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Fail2Ban is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Fail2Ban; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +__author__ = "Fail2Ban Developers" +__copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halchenko, 2014-2016 Serg G. Brester" +__license__ = "GPL" + +import getopt +import logging +import os +import sys + +from ..version import version +from ..protocol import printFormatted +from ..helpers import getLogger + +# Gets the instance of the logger. +logSys = getLogger("fail2ban") + +def output(s): # pragma: no cover + print(s) + +CONFIG_PARAMS = ("socket", "pidfile", "logtarget", "loglevel", "syslogsocket",) +# Used to signal - we are in test cases (ex: prevents change logging params, log capturing, etc) +PRODUCTION = True + +MAX_WAITTIME = 30 + +class Fail2banCmdLine(): + + def __init__(self): + self._argv = self._args = None + self._configurator = None + self.resetConf() + + def resetConf(self): + self._conf = { + "async": False, + "conf": "/etc/fail2ban", + "force": False, + "background": True, + "verbose": 1, + "socket": None, + "pidfile": None, + "timeout": MAX_WAITTIME + } + + @property + def configurator(self): + if self._configurator: + return self._configurator + # New configurator + from .configurator import Configurator + self._configurator = Configurator() + # Set the configuration path + self._configurator.setBaseDir(self._conf["conf"]) + return self._configurator + + + def applyMembers(self, obj): + for o in obj.__dict__: + self.__dict__[o] = obj.__dict__[o] + + def dispVersion(self): + output("Fail2Ban v" + version) + output("") + output("Copyright (c) 2004-2008 Cyril Jaquier, 2008- Fail2Ban Contributors") + output("Copyright of modifications held by their respective authors.") + output("Licensed under the GNU General Public License v2 (GPL).") + output("") + output("Written by Cyril Jaquier .") + output("Many contributions by Yaroslav O. Halchenko .") + + def dispUsage(self): + """ Prints Fail2Ban command line options and exits + """ + caller = os.path.basename(self._argv[0]) + output("Usage: "+caller+" [OPTIONS]" + (" " if not caller.endswith('server') else "")) + output("") + output("Fail2Ban v" + version + " reads log file that contains password failure report") + output("and bans the corresponding IP addresses using firewall rules.") + output("") + output("Options:") + output(" -c configuration directory") + output(" -s socket path") + output(" -p pidfile path") + output(" --loglevel logging level") + output(" --logtarget |STDOUT|STDERR|SYSLOG") + output(" --syslogsocket auto|") + output(" -d dump configuration. For debugging") + output(" -i interactive mode") + output(" -v increase verbosity") + output(" -q decrease verbosity") + output(" -x force execution of the server (remove socket file)") + output(" -b start server in background (default)") + output(" -f start server in foreground") + output(" --async start server in async mode (for internal usage only, don't read configuration)") + output(" --timeout timeout to wait for the server (for internal usage only, don't read configuration)") + output(" -h, --help display this help message") + output(" -V, --version print the version") + + if not caller.endswith('server'): + output("") + output("Command:") + # Prints the protocol + printFormatted() + + output("") + output("Report bugs to https://github.com/fail2ban/fail2ban/issues") + + def __getCmdLineOptions(self, optList): + """ Gets the command line options + """ + for opt in optList: + o = opt[0] + if o == "-c": + self._conf["conf"] = opt[1] + elif o == "-s": + self._conf["socket"] = opt[1] + elif o == "-p": + self._conf["pidfile"] = opt[1] + elif o.startswith("--log") or o.startswith("--sys"): + self._conf[ o[2:] ] = opt[1] + elif o == "-d": + self._conf["dump"] = True + elif o == "-v": + self._conf["verbose"] += 1 + elif o == "-q": + self._conf["verbose"] -= 1 + elif o == "-x": + self._conf["force"] = True + elif o == "-i": + self._conf["interactive"] = True + elif o == "-b": + self._conf["background"] = True + elif o == "-f": + self._conf["background"] = False + elif o == "--async": + self._conf["async"] = True + elif o == "-timeout": + from ..mytime import MyTime + self._conf["timeout"] = MyTime.str2seconds(opt[1]) + elif o in ["-h", "--help"]: + self.dispUsage() + return True + elif o in ["-V", "--version"]: + self.dispVersion() + return True + return None + + def initCmdLine(self, argv): + try: + # First time? + initial = (self._argv is None) + + # Command line options + self._argv = argv + logSys.info("Using start params %s", argv[1:]) + + # Reads the command line options. + try: + cmdOpts = 'hc:s:p:xfbdviqV' + cmdLongOpts = ['loglevel=', 'logtarget=', 'syslogsocket=', 'async', 'timeout=', 'help', 'version'] + optList, self._args = getopt.getopt(self._argv[1:], cmdOpts, cmdLongOpts) + except getopt.GetoptError: + self.dispUsage() + return False + + ret = self.__getCmdLineOptions(optList) + if ret is not None: + return ret + + logSys.debug("-- conf: %r, args: %r", self._conf, self._args) + + if initial and PRODUCTION: # pragma: no cover - can't test + verbose = self._conf["verbose"] + if verbose <= 0: + logSys.setLevel(logging.ERROR) + elif verbose == 1: + logSys.setLevel(logging.WARNING) + elif verbose == 2: + logSys.setLevel(logging.INFO) + else: + logSys.setLevel(logging.DEBUG) + # Add the default logging handler to dump to stderr + logout = logging.StreamHandler(sys.stderr) + # set a format which is simpler for console use + formatter = logging.Formatter('%(levelname)-6s %(message)s') + # tell the handler to use this format + logout.setFormatter(formatter) + logSys.addHandler(logout) + + # Set expected parameters (like socket, pidfile, etc) from configuration, + # if those not yet specified, in which read configuration only if needed here: + conf = None + for o in CONFIG_PARAMS: + if self._conf.get(o, None) is None: + if not conf: + self.configurator.readEarly() + conf = self.configurator.getEarlyOptions() + self._conf[o] = conf[o] + + logSys.info("Using socket file %s", self._conf["socket"]) + + logSys.info("Using pid file %s, [%s] logging to %s", + self._conf["pidfile"], self._conf["loglevel"], self._conf["logtarget"]) + + if self._conf.get("dump", False): + ret, stream = self.readConfig() + self.dumpConfig(stream) + return ret + + # Nothing to do here, process in client/server + return None + except Exception as e: + output("ERROR: %s" % (e,)) + #logSys.exception(e) + return False + + def readConfig(self, jail=None): + # Read the configuration + # TODO: get away from stew of return codes and exception + # handling -- handle via exceptions + stream = None + try: + self.configurator.Reload() + self.configurator.readAll() + ret = self.configurator.getOptions(jail) + self.configurator.convertToProtocol() + stream = self.configurator.getConfigStream() + except Exception, e: + logSys.error("Failed during configuration: %s" % e) + ret = False + return ret, stream + + @staticmethod + def dumpConfig(cmd): + for c in cmd: + output(c) + return True + + @staticmethod + def exit(code=0): # pragma: no cover - can't test + logSys.debug("Exit with code %s", code) + if os._exit: + os._exit(code) + else: + sys.exit(code) + +# global exit handler: +exit = Fail2banCmdLine.exit + + +class ExitException(Exception): + pass + + +class ServerExecutionException(Exception): + pass diff --git a/fail2ban/client/fail2banreader.py b/fail2ban/client/fail2banreader.py index c55f65ea..b3012c9b 100644 --- a/fail2ban/client/fail2banreader.py +++ b/fail2ban/client/fail2banreader.py @@ -40,8 +40,13 @@ class Fail2banReader(ConfigReader): ConfigReader.read(self, "fail2ban") def getEarlyOptions(self): - opts = [["string", "socket", "/var/run/fail2ban/fail2ban.sock"], - ["string", "pidfile", "/var/run/fail2ban/fail2ban.pid"]] + opts = [ + ["string", "socket", "/var/run/fail2ban/fail2ban.sock"], + ["string", "pidfile", "/var/run/fail2ban/fail2ban.pid"], + ["string", "loglevel", "INFO"], + ["string", "logtarget", "/var/log/fail2ban.log"], + ["string", "syslogsocket", "auto"] + ] return ConfigReader.getOptions(self, "Definition", opts) def getOptions(self): diff --git a/fail2ban/client/fail2banserver.py b/fail2ban/client/fail2banserver.py new file mode 100644 index 00000000..a511e017 --- /dev/null +++ b/fail2ban/client/fail2banserver.py @@ -0,0 +1,226 @@ +#!/usr/bin/python +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*- +# vi: set ft=python sts=4 ts=4 sw=4 noet : +# +# This file is part of Fail2Ban. +# +# Fail2Ban is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Fail2Ban is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Fail2Ban; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +__author__ = "Fail2Ban Developers" +__copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halchenko, 2014-2016 Serg G. Brester" +__license__ = "GPL" + +import os +import sys + +from .fail2bancmdline import Fail2banCmdLine, ServerExecutionException, \ + logSys, PRODUCTION, exit + +SERVER = "fail2ban-server" + +## +# \mainpage Fail2Ban +# +# \section Introduction +# +class Fail2banServer(Fail2banCmdLine): + + # def __init__(self): + # Fail2banCmdLine.__init__(self) + + ## + # Start Fail2Ban server in main thread without fork (direct, it can fork itself in Server if daemon=True). + # + # Start the Fail2ban server in background/foreground (daemon mode or not). + + @staticmethod + def startServerDirect(conf, daemon=True): + logSys.debug("-- direct starting of server in %s, deamon: %s", os.getpid(), daemon) + from ..server.server import Server + server = None + try: + # Start it in foreground (current thread, not new process), + # server object will internally fork self if daemon is True + server = Server(daemon) + server.start(conf["socket"], + conf["pidfile"], conf["force"], + conf=conf) + except Exception as e: # pragma: no cover + try: + if server: + server.quit() + except Exception as e2: + if conf["verbose"] > 1: + logSys.exception(e2) + raise + + return server + + ## + # Start Fail2Ban server. + # + # Start the Fail2ban server in daemon mode (background, start from client). + + @staticmethod + def startServerAsync(conf): + # Forks the current process, don't fork if async specified (ex: test cases) + pid = 0 + frk = not conf["async"] and PRODUCTION + if frk: # pragma: no cover + pid = os.fork() + logSys.debug("-- async starting of server in %s, fork: %s - %s", os.getpid(), frk, pid) + if pid == 0: + args = list() + args.append(SERVER) + # Start async (don't read config) and in background as requested. + args.append("--async") + args.append("-b") + # Set the socket path. + args.append("-s") + args.append(conf["socket"]) + # Set the pidfile + args.append("-p") + args.append(conf["pidfile"]) + # Force the execution if needed. + if conf["force"]: + args.append("-x") + # Logging parameters: + for o in ('loglevel', 'logtarget', 'syslogsocket'): + args.append("--"+o) + args.append(conf[o]) + try: + # Directory of client (to try the first start from current or the same directory as client, and from relative bin): + exe = Fail2banServer.getServerPath() + if not frk: + # Wrapr args to use the same python version in client/server (important for multi-python systems): + args[0] = exe + exe = sys.executable + args[0:0] = [exe] + logSys.debug("Starting %r with args %r", exe, args) + if frk: # pragma: no cover + os.execv(exe, args) + else: + # use P_WAIT instead of P_NOWAIT (to prevent defunct-zomby process), it startet as daemon, so parent exit fast after fork): + ret = os.spawnv(os.P_WAIT, exe, args) + if ret != 0: # pragma: no cover + raise OSError(ret, "Unknown error by executing server %r with %r" % (args[1], exe)) + except OSError as e: # pragma: no cover + if not frk: #not PRODUCTION: + raise + # Use the PATH env. + logSys.warning("Initial start attempt failed (%s). Starting %r with the same args", e, SERVER) + if frk: # pragma: no cover + os.execvp(SERVER, args) + + @staticmethod + def getServerPath(): + startdir = sys.path[0] + exe = os.path.abspath(os.path.join(startdir, SERVER)) + if not os.path.isfile(exe): # may be uresolved in test-cases, so get relative starter (client): + startdir = os.path.dirname(sys.argv[0]) + exe = os.path.abspath(os.path.join(startdir, SERVER)) + if not os.path.isfile(exe): # may be uresolved in test-cases, so try to get relative bin-directory: + startdir = os.path.dirname(os.path.abspath(__file__)) + startdir = os.path.join(os.path.dirname(os.path.dirname(startdir)), "bin") + exe = os.path.abspath(os.path.join(startdir, SERVER)) + return exe + + def _Fail2banClient(self): + from .fail2banclient import Fail2banClient + cli = Fail2banClient() + cli.applyMembers(self) + return cli + + def start(self, argv): + # Command line options + ret = self.initCmdLine(argv) + if ret is not None: + return ret + + # Commands + args = self._args + + cli = None + # Just start: + if len(args) == 1 and args[0] == 'start' and not self._conf.get("interactive", False): + pass + else: + # If client mode - whole processing over client: + if len(args) or self._conf.get("interactive", False): + cli = self._Fail2banClient() + return cli.start(argv) + + # Start the server: + server = None + try: + from ..server.utils import Utils + # background = True, if should be new process running in background, otherwise start in foreground + # process will be forked in daemonize, inside of Server module. + # async = True, if started from client, should... + background = self._conf["background"] + async = self._conf.get("async", False) + # If was started not from the client: + if not async: + # Start new thread with client to read configuration and + # transfer it to the server: + cli = self._Fail2banClient() + phase = dict() + logSys.debug('Configure via async client thread') + cli.configureServer(async=True, phase=phase) + # wait, do not continue if configuration is not 100% valid: + Utils.wait_for(lambda: phase.get('ready', None) is not None, self._conf["timeout"]) + if not phase.get('start', False): + raise ServerExecutionException('Async configuration of server failed') + + # Start server, daemonize it, etc. + pid = os.getpid() + server = Fail2banServer.startServerDirect(self._conf, background) + # If forked - just exit other processes + if pid != os.getpid(): # pragma: no cover + os._exit(0) + if cli: + cli._server = server + + # wait for client answer "done": + if not async and cli: + Utils.wait_for(lambda: phase.get('done', None) is not None, self._conf["timeout"]) + if not phase.get('done', False): + if server: # pragma: no cover + server.quit() + exit(-1) + logSys.debug('Starting server done') + + except Exception, e: + if self._conf["verbose"] > 1: + logSys.exception(e) + else: + logSys.error(e) + if server: # pragma: no cover + server.quit() + exit(-1) + + return True + + @staticmethod + def exit(code=0): # pragma: no cover + if code != 0: + logSys.error("Could not start %s", SERVER) + exit(code) + +def exec_command_line(argv): + server = Fail2banServer() + if server.start(argv): + exit(0) + else: + exit(-1) diff --git a/fail2ban/client/jailreader.py b/fail2ban/client/jailreader.py index 08289596..c8e20ab2 100644 --- a/fail2ban/client/jailreader.py +++ b/fail2ban/client/jailreader.py @@ -32,6 +32,7 @@ import re from .configreader import ConfigReaderUnshared, ConfigReader from .filterreader import FilterReader from .actionreader import ActionReader +from ..version import version from ..helpers import getLogger from ..helpers import splitcommaspace @@ -115,8 +116,12 @@ class JailReader(ConfigReader): ["string", "filter", ""], ["string", "action", ""]] + # Before interpolation (substitution) add static options always available as default: + defsec = self._cfg.get_defaults() + defsec["fail2ban_version"] = version + # Read first options only needed for merge defaults ('known/...' from filter): - self.__opts = ConfigReader.getOptions(self, self.__name, opts1st) + self.__opts = ConfigReader.getOptions(self, self.__name, opts1st, shouldExist=True) if not self.__opts: return False diff --git a/fail2ban/protocol.py b/fail2ban/protocol.py index 5d9fdd65..648666a1 100644 --- a/fail2ban/protocol.py +++ b/fail2ban/protocol.py @@ -26,6 +26,9 @@ __license__ = "GPL" import textwrap +def output(s): + print(s) + ## # Describes the protocol used to communicate with the server. @@ -42,11 +45,13 @@ CSPROTO = dotdict({ protocol = [ ['', "BASIC", ""], ["start", "starts the server and the jails"], -["reload", "reloads the configuration"], +["restart", "restarts the server"], +["reload", "reloads the configuration without restart"], ["reload ", "reloads the jail "], ["stop", "stops all jails and terminate the server"], ["status", "gets the current status of the server"], -["ping", "tests if the server is alive"], +["ping", "tests if the server is alive"], +["echo", "for internal usage, returns back and outputs a given string"], ["help", "return this output"], ["version", "return the server version"], ['', "LOGGING", ""], @@ -141,7 +146,7 @@ def printFormatted(): firstHeading = False for m in protocol: if m[0] == '' and firstHeading: - print + output("") firstHeading = True first = True if len(m[0]) >= MARGIN: @@ -152,7 +157,7 @@ def printFormatted(): first = False else: line = ' ' * (INDENT + MARGIN) + n.strip() - print line + output(line) ## @@ -163,20 +168,20 @@ def printWiki(): for m in protocol: if m[0] == '': if firstHeading: - print "|}" + output("|}") __printWikiHeader(m[1], m[2]) firstHeading = True else: - print "|-" - print "| " + m[0] + " || || " + m[1] - print "|}" + output("|-") + output("| " + m[0] + " || || " + m[1]) + output("|}") def __printWikiHeader(section, desc): - print - print "=== " + section + " ===" - print - print desc - print - print "{|" - print "| '''Command''' || || '''Description'''" + output("") + output("=== " + section + " ===") + output("") + output(desc) + output("") + output("{|") + output("| '''Command''' || || '''Description'''") diff --git a/fail2ban/server/asyncserver.py b/fail2ban/server/asyncserver.py index ad37544a..6454ef1c 100644 --- a/fail2ban/server/asyncserver.py +++ b/fail2ban/server/asyncserver.py @@ -95,6 +95,7 @@ def loop(active, timeout=None, use_poll=False): # Use poll instead of loop, because of recognition of active flag, # because of loop timeout mistake: different in poll and poll2 (sec vs ms), # and to prevent sporadical errors like EBADF 'Bad file descriptor' etc. (see gh-161) + errCount = 0 if timeout is None: timeout = Utils.DEFAULT_SLEEP_TIME poll = asyncore.poll @@ -107,11 +108,20 @@ def loop(active, timeout=None, use_poll=False): while active(): try: poll(timeout) + if errCount: + errCount -= 1 except Exception as e: # pragma: no cover - if e.args[0] in (errno.ENOTCONN, errno.EBADF): # (errno.EBADF, 'Bad file descriptor') - logSys.info('Server connection was closed: %s', str(e)) - else: - logSys.error('Server connection was closed: %s', str(e)) + if not active(): + break + errCount += 1 + if errCount < 20: + if e.args[0] in (errno.ENOTCONN, errno.EBADF): # (errno.EBADF, 'Bad file descriptor') + logSys.info('Server connection was closed: %s', str(e)) + else: + logSys.error('Server connection was closed: %s', str(e)) + elif errCount == 20: + logSys.info('Too many errors - stop logging connection errors') + logSys.exception(e) ## @@ -162,7 +172,7 @@ class AsyncServer(asyncore.dispatcher): logSys.error("Fail2ban seems to be already running") if force: logSys.warning("Forcing execution of the server") - os.remove(sock) + self._remove_sock() else: raise AsyncServerException("Server already running") # Creates the socket. @@ -175,20 +185,22 @@ class AsyncServer(asyncore.dispatcher): AsyncServer.__markCloseOnExec(self.socket) self.listen(1) # Sets the init flag. - self.__init = self.__active = True + self.__init = self.__loop = self.__active = True # Event loop as long as active: - loop(lambda: self.__active) + loop(lambda: self.__loop) + self.__active = False # Cleanup all self.stop() def close(self): if self.__active: + self.__loop = False asyncore.dispatcher.close(self) # Remove socket (file) only if it was created: if self.__init and os.path.exists(self.__sock): logSys.debug("Removed socket file " + self.__sock) - os.remove(self.__sock) + self._remove_sock() logSys.debug("Socket shutdown") self.__active = False @@ -201,6 +213,17 @@ class AsyncServer(asyncore.dispatcher): def isActive(self): return self.__active + + ## + # Safe remove (in multithreaded mode): + + def _remove_sock(self): + try: + os.remove(self.__sock) + except OSError as e: + if e.errno != errno.ENOENT: + raise + ## # Marks socket as close-on-exec to avoid leaking file descriptors when # running actions involving command execution. diff --git a/fail2ban/server/jailthread.py b/fail2ban/server/jailthread.py index eb43e453..39a86c2b 100644 --- a/fail2ban/server/jailthread.py +++ b/fail2ban/server/jailthread.py @@ -51,6 +51,8 @@ class JailThread(Thread): def __init__(self, name=None): super(JailThread, self).__init__(name=name) + ## Should going with main thread also: + self.daemon = True ## Control the state of the thread. self.active = False ## Control the idle state of the thread. diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py index e5b81db8..7535c0e6 100644 --- a/fail2ban/server/server.py +++ b/fail2ban/server/server.py @@ -24,6 +24,7 @@ __author__ = "Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" +import threading from threading import Lock, RLock import logging import logging.handlers @@ -43,6 +44,10 @@ from ..helpers import getLogger, excepthook # Gets the instance of the logger. logSys = getLogger(__name__) +DEF_SYSLOGSOCKET = "auto" +DEF_LOGLEVEL = "INFO" +DEF_LOGTARGET = "STDOUT" + try: from .database import Fail2BanDb except ImportError: # pragma: no cover @@ -50,6 +55,10 @@ except ImportError: # pragma: no cover Fail2BanDb = None +def _thread_name(): + return threading.current_thread().__class__.__name__ + + class Server: def __init__(self, daemon=False): @@ -59,7 +68,8 @@ class Server: self.__db = None self.__daemon = daemon self.__transm = Transmitter(self) - self.__asyncServer = AsyncServer(self.__transm) + #self.__asyncServer = AsyncServer(self.__transm) + self.__asyncServer = None self.__logLevel = None self.__logTarget = None self.__syslogSocket = None @@ -68,10 +78,7 @@ class Server: 'FreeBSD': '/var/run/log', 'Linux': '/dev/log', } - self.setSyslogSocket("auto") - # Set logging level - self.setLogLevel("INFO") - self.setLogTarget("STDOUT") + self.__prev_signals = {} def __sigTERMhandler(self, signum, frame): logSys.debug("Caught signal %d. Exiting" % signum) @@ -81,28 +88,45 @@ class Server: logSys.debug("Caught signal %d. Flushing logs" % signum) self.flushLogs() - def start(self, sock, pidfile, force=False, observer=True): - logSys.info("Starting Fail2ban v%s", version.version) - - # Install signal handlers - signal.signal(signal.SIGTERM, self.__sigTERMhandler) - signal.signal(signal.SIGINT, self.__sigTERMhandler) - signal.signal(signal.SIGUSR1, self.__sigUSR1handler) - - # Ensure unhandled exceptions are logged - sys.excepthook = excepthook - + def start(self, sock, pidfile, force=False, observer=True, conf={}): # First set the mask to only allow access to owner os.umask(0077) + # Second daemonize before logging etc, because it will close all handles: if self.__daemon: # pragma: no cover logSys.info("Starting in daemon mode") ret = self.__createDaemon() - if ret: - logSys.info("Daemon started") - else: - logSys.error("Could not create daemon") - raise ServerInitializationError("Could not create daemon") + # If forked parent - return here (parent process will configure server later): + if ret is None: + return False + # If error: + if not ret[0]: + err = "Could not create daemon %s", ret[1:] + logSys.error(err) + raise ServerInitializationError(err) + # We are daemon. + # Set all logging parameters (or use default if not specified): + self.setSyslogSocket(conf.get("syslogsocket", + self.__syslogSocket if self.__syslogSocket is not None else DEF_SYSLOGSOCKET)) + self.setLogLevel(conf.get("loglevel", + self.__logLevel if self.__logLevel is not None else DEF_LOGLEVEL)) + self.setLogTarget(conf.get("logtarget", + self.__logTarget if self.__logTarget is not None else DEF_LOGTARGET)) + + logSys.info("-"*50) + logSys.info("Starting Fail2ban v%s", version.version) + + if self.__daemon: # pragma: no cover + logSys.info("Daemon started") + + # Install signal handlers + if _thread_name() == '_MainThread': + for s in (signal.SIGTERM, signal.SIGINT, signal.SIGUSR1): + self.__prev_signals[s] = signal.getsignal(s) + signal.signal(s, self.__sigTERMhandler if s != signal.SIGUSR1 else self.__sigUSR1handler) + # Ensure unhandled exceptions are logged + sys.excepthook = excepthook + # Creates a PID file. try: logSys.debug("Creating PID file %s" % pidfile) @@ -121,6 +145,7 @@ class Server: # Start the communication logSys.debug("Starting communication") try: + self.__asyncServer = AsyncServer(self.__transm) self.__asyncServer.start(sock, force) except AsyncServerException, e: logSys.error("Could not start server: %s", e) @@ -143,17 +168,25 @@ class Server: # communications first (which should be ok anyways since we # are exiting) # See https://github.com/fail2ban/fail2ban/issues/7 - self.__asyncServer.stop() + if self.__asyncServer is not None: + self.__asyncServer.stop() + self.__asyncServer = None # Now stop all the jails self.stopAllJail() # Only now shutdown the logging. - try: - self.__loggingLock.acquire() - logging.shutdown() - finally: - self.__loggingLock.release() + if self.__logTarget is not None: + with self.__loggingLock: + logging.shutdown() + + # Restore default signal handlers: + if _thread_name() == '_MainThread': + for s, sh in self.__prev_signals.iteritems(): + signal.signal(s, sh) + + # Prevent to call quit twice: + self.quit = lambda: False def addJail(self, name, backend): # Add jail hereafter: @@ -341,6 +374,9 @@ class Server: def getBanTimeExtra(self, name, opt): return self.__jails[name].getBanTimeExtra(opt) + def isStarted(self): + self.__asyncServer.isActive() + def isAlive(self, jailnum=None): if jailnum is not None and len(self.__jails) != jailnum: return 0 @@ -379,16 +415,15 @@ class Server: # @param value the level def setLogLevel(self, value): - try: - self.__loggingLock.acquire() - getLogger("fail2ban").setLevel( - getattr(logging, value.upper())) - except AttributeError: - raise ValueError("Invalid log level") - else: - self.__logLevel = value.upper() - finally: - self.__loggingLock.release() + value = value.upper() + with self.__loggingLock: + if self.__logLevel == value: + return + try: + getLogger("fail2ban").setLevel(getattr(logging, value)) + self.__logLevel = value + except AttributeError: + raise ValueError("Invalid log level") ## # Get the logging level. @@ -397,11 +432,8 @@ class Server: # @return the log level def getLogLevel(self): - try: - self.__loggingLock.acquire() + with self.__loggingLock: return self.__logLevel - finally: - self.__loggingLock.release() ## # Sets the logging target. @@ -410,8 +442,14 @@ class Server: # @param target the logging target def setLogTarget(self, target): - try: - self.__loggingLock.acquire() + with self.__loggingLock: + # don't set new handlers if already the same + # or if "INHERITED" (foreground worker of the test cases, to prevent stop logging): + if self.__logTarget == target: + return True + if target == "INHERITED": + self.__logTarget = target + return True # set a format which is simpler for console use formatter = logging.Formatter("%(asctime)s %(name)-24s[%(process)d]: %(levelname)-7s %(message)s") if target == "SYSLOG": @@ -479,8 +517,6 @@ class Server: # Sets the logging target. self.__logTarget = target return True - finally: - self.__loggingLock.release() ## # Sets the syslog socket. @@ -488,24 +524,21 @@ class Server: # syslogsocket is the full path to the syslog socket # @param syslogsocket the syslog socket path def setSyslogSocket(self, syslogsocket): - self.__syslogSocket = syslogsocket - # Conditionally reload, logtarget depends on socket path when SYSLOG - return self.__logTarget != "SYSLOG"\ - or self.setLogTarget(self.__logTarget) + with self.__loggingLock: + if self.__syslogSocket == syslogsocket: + return True + self.__syslogSocket = syslogsocket + # Conditionally reload, logtarget depends on socket path when SYSLOG + return self.__logTarget != "SYSLOG"\ + or self.setLogTarget(self.__logTarget) def getLogTarget(self): - try: - self.__loggingLock.acquire() + with self.__loggingLock: return self.__logTarget - finally: - self.__loggingLock.release() def getSyslogSocket(self): - try: - self.__loggingLock.acquire() + with self.__loggingLock: return self.__syslogSocket - finally: - self.__loggingLock.release() def flushLogs(self): if self.__logTarget not in ['STDERR', 'STDOUT', 'SYSLOG']: @@ -561,8 +594,10 @@ class Server: # We need to set this in the parent process, so it gets inherited by the # child process, and this makes sure that it is effect even if the parent # terminates quickly. - signal.signal(signal.SIGHUP, signal.SIG_IGN) - + for s in (signal.SIGHUP,): + self.__prev_signals[s] = signal.getsignal(s) + signal.signal(s, signal.SIG_IGN) + try: # Fork a child process so the parent can exit. This will return control # to the command line or shell. This is required so that the new process @@ -572,7 +607,7 @@ class Server: # PGID. pid = os.fork() except OSError, e: - return((e.errno, e.strerror)) # ERROR (return a tuple) + return (False, (e.errno, e.strerror)) # ERROR (return a tuple) if pid == 0: # The first child. @@ -593,7 +628,7 @@ class Server: # preventing the daemon from ever acquiring a controlling terminal. pid = os.fork() # Fork a second child. except OSError, e: - return((e.errno, e.strerror)) # ERROR (return a tuple) + return (False, (e.errno, e.strerror)) # ERROR (return a tuple) if (pid == 0): # The second child. # Ensure that the daemon doesn't keep any directory in use. Failure @@ -602,8 +637,9 @@ class Server: else: os._exit(0) # Exit parent (the first child) of the second child. else: - os._exit(0) # Exit parent of the first child. - + # Signal to exit, parent of the first child. + return None + # Close all open files. Try the system configuration variable, SC_OPEN_MAX, # for the maximum number of open files to close. If it doesn't exist, use # the default value (configurable). @@ -630,7 +666,7 @@ class Server: os.open("/dev/null", os.O_RDONLY) # standard input (0) os.open("/dev/null", os.O_RDWR) # standard output (1) os.open("/dev/null", os.O_RDWR) # standard error (2) - return True + return (True,) class ServerInitializationError(Exception): diff --git a/fail2ban/server/transmitter.py b/fail2ban/server/transmitter.py index d6283a8e..006c6047 100644 --- a/fail2ban/server/transmitter.py +++ b/fail2ban/server/transmitter.py @@ -93,6 +93,8 @@ class Transmitter: name = command[1] self.__server.stopJail(name) return None + elif command[0] == "echo": + return command[1:] elif command[0] == "sleep": value = command[1] time.sleep(float(value)) diff --git a/fail2ban/server/utils.py b/fail2ban/server/utils.py index 45d1c09d..fcf54d5f 100644 --- a/fail2ban/server/utils.py +++ b/fail2ban/server/utils.py @@ -125,6 +125,7 @@ class Utils(): timeout_expr = lambda: time.time() - stime <= timeout else: timeout_expr = timeout + popen = None try: popen = subprocess.Popen( realCmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell, @@ -151,7 +152,10 @@ class Utils(): if retcode is None and not Utils.pid_exists(pgid): retcode = signal.SIGKILL except OSError as e: - logSys.error("%s -- failed with %s" % (realCmd, e)) + stderr = "%s -- failed with %s" % (realCmd, e) + logSys.error(stderr) + if not popen: + return False if not output else (False, stdout, stderr, retcode) std_level = retcode == 0 and logging.DEBUG or logging.ERROR # if we need output (to return or to log it): @@ -164,8 +168,10 @@ class Utils(): stdout = popen.stdout.read() except IOError as e: logSys.error(" ... -- failed to read stdout %s", e) - if stdout is not None and stdout != '': - logSys.log(std_level, "%s -- stdout: %r", realCmd, stdout) + if stdout is not None and stdout != '' and std_level >= logSys.getEffectiveLevel(): + logSys.log(std_level, "%s -- stdout:", realCmd) + for l in stdout.splitlines(): + logSys.log(std_level, " -- stdout: %r", l) popen.stdout.close() if popen.stderr: try: @@ -174,8 +180,10 @@ class Utils(): stderr = popen.stderr.read() except IOError as e: logSys.error(" ... -- failed to read stderr %s", e) - if stderr is not None and stderr != '': - logSys.log(std_level, "%s -- stderr: %r", realCmd, stderr) + if stderr is not None and stderr != '' and std_level >= logSys.getEffectiveLevel(): + logSys.log(std_level, "%s -- stderr:", realCmd) + for l in stderr.splitlines(): + logSys.log(std_level, " -- stderr: %r", l) popen.stderr.close() if retcode == 0: diff --git a/fail2ban/tests/action_d/test_smtp.py b/fail2ban/tests/action_d/test_smtp.py index 37fe0138..1385fe82 100644 --- a/fail2ban/tests/action_d/test_smtp.py +++ b/fail2ban/tests/action_d/test_smtp.py @@ -65,6 +65,7 @@ class SMTPActionTest(unittest.TestCase): self._active = True self._loop_thread = threading.Thread( target=asyncserver.loop, kwargs={'active': lambda: self._active}) + self._loop_thread.daemon = True self._loop_thread.start() def tearDown(self): diff --git a/fail2ban/tests/actiontestcase.py b/fail2ban/tests/actiontestcase.py index 6d8fcc82..39984169 100644 --- a/fail2ban/tests/actiontestcase.py +++ b/fail2ban/tests/actiontestcase.py @@ -44,8 +44,8 @@ class CommandActionTest(LogCaptureTestCase): def tearDown(self): """Call after every test case.""" - LogCaptureTestCase.tearDown(self) self.__action.stop() + LogCaptureTestCase.tearDown(self) def testSubstituteRecursiveTags(self): aInfo = { @@ -271,11 +271,11 @@ class CommandActionTest(LogCaptureTestCase): def testCaptureStdOutErr(self): CommandAction.executeCmd('echo "How now brown cow"') - self.assertLogged("'How now brown cow\\n'") + self.assertLogged("stdout: 'How now brown cow'\n", "stdout: b'How now brown cow'\n") CommandAction.executeCmd( 'echo "The rain in Spain stays mainly in the plain" 1>&2') self.assertLogged( - "'The rain in Spain stays mainly in the plain\\n'") + "stderr: 'The rain in Spain stays mainly in the plain'\n", "stderr: b'The rain in Spain stays mainly in the plain'\n") def testCallingMap(self): mymap = CallingMap(callme=lambda: str(10), error=lambda: int('a'), diff --git a/fail2ban/tests/clientreadertestcase.py b/fail2ban/tests/clientreadertestcase.py index 78c9a582..bd734c1b 100644 --- a/fail2ban/tests/clientreadertestcase.py +++ b/fail2ban/tests/clientreadertestcase.py @@ -28,13 +28,15 @@ import re import shutil import tempfile import unittest -from ..client.configreader import ConfigReaderUnshared +from ..client.configreader import ConfigReader, ConfigReaderUnshared from ..client import configparserinc from ..client.jailreader import JailReader from ..client.filterreader import FilterReader from ..client.jailsreader import JailsReader from ..client.actionreader import ActionReader from ..client.configurator import Configurator +from ..server.mytime import MyTime +from ..version import version from .utils import LogCaptureTestCase TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") @@ -253,6 +255,34 @@ class JailReaderTest(LogCaptureTestCase): result = JailReader.extractOptions(option) self.assertEqual(expected, result) + def testVersionAgent(self): + jail = JailReader('blocklisttest', force_enable=True, basedir=CONFIG_DIR) + # emulate jail.read(), because such jail not exists: + ConfigReader.read(jail, "jail"); + sections = jail._cfg.get_sections() + sections['blocklisttest'] = dict((('__name__', 'blocklisttest'), + ('filter', ''), ('failregex', '^test $'), + ('sender', 'f2b-test@example.com'), ('blocklist_de_apikey', 'test-key'), + ('action', + '%(action_blocklist_de)s\n' + '%(action_badips_report)s\n' + '%(action_badips)s\n' + 'mynetwatchman[port=1234,protocol=udp,agent="%(fail2ban_agent)s"]' + ), + )) + # get options: + self.assertTrue(jail.getOptions()) + # convert and get stream + stream = jail.convert() + # get action and retrieve agent from it, compare with agent saved in version: + act = [o for o in stream if len(o) > 4 and (o[4] == 'agent' or o[4].endswith('badips.py'))] + useragent = 'Fail2Ban/%s' % version + self.assertEqual(len(act), 4) + self.assertEqual(act[0], ['set', 'blocklisttest', 'action', 'blocklist_de', 'agent', useragent]) + self.assertEqual(act[1], ['set', 'blocklisttest', 'action', 'badips', 'agent', useragent]) + self.assertEqual(eval(act[2][5]).get('agent', ''), useragent) + self.assertEqual(act[3], ['set', 'blocklisttest', 'action', 'mynetwatchman', 'agent', useragent]) + def testGlob(self): d = tempfile.mkdtemp(prefix="f2b-temp") # Generate few files @@ -596,6 +626,12 @@ class JailsReaderTest(LogCaptureTestCase): # by default we have lots of jails ;) self.assertTrue(len(comm_commands)) + # some common sanity checks for commands + for command in comm_commands: + if len(command) >= 3 and [command[0], command[2]] == ['set', 'bantime']: + self.assertTrue(MyTime.str2seconds(command[3]) > 0) + + # and we know even some of them by heart for j in ['sshd', 'recidive']: # by default we have 'auto' backend ATM diff --git a/fail2ban/tests/fail2banclienttestcase.py b/fail2ban/tests/fail2banclienttestcase.py new file mode 100644 index 00000000..82ffe9b3 --- /dev/null +++ b/fail2ban/tests/fail2banclienttestcase.py @@ -0,0 +1,593 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*- +# vi: set ft=python sts=4 ts=4 sw=4 noet : + +# This file is part of Fail2Ban. +# +# Fail2Ban is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Fail2Ban is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Fail2Ban; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +# Fail2Ban developers + +__author__ = "Serg Brester" +__copyright__ = "Copyright (c) 2014- Serg G. Brester (sebres), 2008- Fail2Ban Contributors" +__license__ = "GPL" + +import fileinput +import os +import re +import sys +import time +import unittest + +from threading import Thread + +from ..client import fail2banclient, fail2banserver, fail2bancmdline +from ..client.fail2banclient import Fail2banClient, exec_command_line as _exec_client, VisualWait +from ..client.fail2banserver import Fail2banServer, exec_command_line as _exec_server +from .. import protocol +from ..server import server +from ..server.utils import Utils +from .utils import LogCaptureTestCase, logSys, withtmpdir, shutil, logging + + +STOCK_CONF_DIR = "config" +STOCK = os.path.exists(os.path.join(STOCK_CONF_DIR,'fail2ban.conf')) + +CLIENT = "fail2ban-client" +SERVER = "fail2ban-server" +BIN = os.path.dirname(Fail2banServer.getServerPath()) + +MAX_WAITTIME = 30 if not unittest.F2B.fast else 5 + +## +# Several wrappers and settings for proper testing: +# + +fail2bancmdline.MAX_WAITTIME = MAX_WAITTIME-1 + +fail2bancmdline.logSys = \ +fail2banclient.logSys = \ +fail2banserver.logSys = logSys + +server.DEF_LOGTARGET = "/dev/null" + +def _test_output(*args): + logSys.info(args[0]) +fail2bancmdline.output = \ +fail2banclient.output = \ +fail2banserver.output = \ +protocol.output = _test_output + +def _test_exit(code=0): + logSys.debug("Exit with code %s", code) + if code == 0: + raise ExitException() + else: + raise FailExitException() +fail2bancmdline.exit = \ +fail2banclient.exit = \ +fail2banserver.exit = _test_exit + +INTERACT = [] +def _test_input_command(*args): + if len(INTERACT): + #logSys.debug('--- interact command: %r', INTERACT[0]) + return INTERACT.pop(0) + else: + return "exit" +fail2banclient.input_command = _test_input_command + +# prevents change logging params, log capturing, etc: +fail2bancmdline.PRODUCTION = \ +fail2banclient.PRODUCTION = \ +fail2banserver.PRODUCTION = False + + +class ExitException(fail2bancmdline.ExitException): + pass +class FailExitException(fail2bancmdline.ExitException): + pass + + +def _out_file(fn): # pragma: no cover + logSys.debug('---- ' + fn + ' ----') + for line in fileinput.input(fn): + line = line.rstrip('\n') + logSys.debug(line) + logSys.debug('-'*30) + +def _start_params(tmp, use_stock=False, logtarget="/dev/null"): + cfg = tmp+"/config" + if use_stock and STOCK: + # copy config (sub-directories as alias): + def ig_dirs(dir, files): + return [f for f in files if os.path.isdir(os.path.join(dir, f))] + shutil.copytree(STOCK_CONF_DIR, cfg, ignore=ig_dirs) + os.symlink(STOCK_CONF_DIR+"/action.d", cfg+"/action.d") + os.symlink(STOCK_CONF_DIR+"/filter.d", cfg+"/filter.d") + # replace fail2ban params (database with memory): + r = re.compile(r'^dbfile\s*=') + for line in fileinput.input(cfg+"/fail2ban.conf", inplace=True): + line = line.rstrip('\n') + if r.match(line): + line = "dbfile = :memory:" + print(line) + # replace jail params (polling as backend to be fast in initialize): + r = re.compile(r'^backend\s*=') + for line in fileinput.input(cfg+"/jail.conf", inplace=True): + line = line.rstrip('\n') + if r.match(line): + line = "backend = polling" + print(line) + else: + # just empty config directory without anything (only fail2ban.conf/jail.conf): + os.mkdir(cfg) + f = open(cfg+"/fail2ban.conf", "w") + f.write('\n'.join(( + "[Definition]", + "loglevel = INFO", + "logtarget = " + logtarget, + "syslogsocket = auto", + "socket = "+tmp+"/f2b.sock", + "pidfile = "+tmp+"/f2b.pid", + "backend = polling", + "dbfile = :memory:", + "dbpurgeage = 1d", + "", + ))) + f.close() + f = open(cfg+"/jail.conf", "w") + f.write('\n'.join(( + "[INCLUDES]", "", + "[DEFAULT]", "", + "", + ))) + f.close() + if logSys.level < logging.DEBUG: # if HEAVYDEBUG + _out_file(cfg+"/fail2ban.conf") + _out_file(cfg+"/jail.conf") + # parameters (sock/pid and config, increase verbosity, set log, etc.): + return ("-c", cfg, "-s", tmp+"/f2b.sock", "-p", tmp+"/f2b.pid", + "-vv", "--logtarget", logtarget, "--loglevel", "DEBUG", "--syslogsocket", "auto", + "--timeout", str(fail2bancmdline.MAX_WAITTIME), + ) + +def _kill_srv(pidfile): # pragma: no cover + def _pid_exists(pid): + try: + os.kill(pid, 0) + return True + except OSError: + return False + logSys.debug("-- cleanup: %r", (pidfile, os.path.isdir(pidfile))) + if os.path.isdir(pidfile): + piddir = pidfile + pidfile = piddir + "/f2b.pid" + if not os.path.isfile(pidfile): + pidfile = piddir + "/fail2ban.pid" + if not os.path.isfile(pidfile): + logSys.debug("--- cleanup: no pidfile for %r", piddir) + return True + f = pid = None + try: + logSys.debug("--- cleanup pidfile: %r", pidfile) + f = open(pidfile) + pid = f.read().split()[1] + pid = int(pid) + logSys.debug("--- cleanup pid: %r", pid) + if pid <= 0: + raise ValueError('pid %s of %s is invalid' % (pid, pidfile)) + if not _pid_exists(pid): + return True + ## try to preper stop (have signal handler): + os.kill(pid, signal.SIGTERM) + ## check still exists after small timeout: + if not Utils.wait_for(lambda: not _pid_exists(pid), 1): + ## try to kill hereafter: + os.kill(pid, signal.SIGKILL) + return not _pid_exists(pid) + except Exception as e: + logSys.debug(e) + finally: + if f is not None: + f.close() + return True + + +class Fail2banClientServerBase(LogCaptureTestCase): + + def setUp(self): + """Call before every test case.""" + LogCaptureTestCase.setUp(self) + + def tearDown(self): + """Call after every test case.""" + LogCaptureTestCase.tearDown(self) + + def _wait_for_srv(self, tmp, ready=True, startparams=None): + try: + sock = tmp+"/f2b.sock" + # wait for server (socket): + ret = Utils.wait_for(lambda: os.path.exists(sock), MAX_WAITTIME) + if not ret: + raise Exception('Unexpected: Socket file does not exists.\nStart failed: %r' % (startparams,)) + if ready: + # wait for communication with worker ready: + ret = Utils.wait_for(lambda: "Server ready" in self.getLog(), MAX_WAITTIME) + if not ret: + raise Exception('Unexpected: Server ready was not found.\nStart failed: %r' % (startparams,)) + except: # pragma: no cover + log = tmp+"/f2b.log" + if os.path.isfile(log): + _out_file(log) + else: + logSys.debug("No log file %s to examine details of error", log) + raise + + +class Fail2banClientTest(Fail2banClientServerBase): + + def testConsistency(self): + self.assertTrue(os.path.isfile(os.path.join(os.path.join(BIN), CLIENT))) + self.assertTrue(os.path.isfile(os.path.join(os.path.join(BIN), SERVER))) + + def testClientUsage(self): + self.assertRaises(ExitException, _exec_client, + (CLIENT, "-h",)) + self.assertLogged("Usage: " + CLIENT) + self.assertLogged("Report bugs to ") + self.pruneLog() + self.assertRaises(ExitException, _exec_client, + (CLIENT, "-vq", "-V",)) + self.assertLogged("Fail2Ban v" + fail2bancmdline.version) + + @withtmpdir + def testClientDump(self, tmp): + # use here the stock configuration (if possible) + startparams = _start_params(tmp, True) + self.assertRaises(ExitException, _exec_client, + ((CLIENT,) + startparams + ("-vvd",))) + self.assertLogged("Loading files") + self.assertLogged("logtarget") + + @withtmpdir + def testClientStartBackgroundInside(self, tmp): + try: + # use once the stock configuration (to test starting also) + startparams = _start_params(tmp, True) + # start: + self.assertRaises(ExitException, _exec_client, + (CLIENT, "-b") + startparams + ("start",)) + # wait for server (socket and ready): + self._wait_for_srv(tmp, True, startparams=startparams) + self.assertLogged("Server ready") + self.assertLogged("Exit with code 0") + try: + self.assertRaises(ExitException, _exec_client, + (CLIENT,) + startparams + ("echo", "TEST-ECHO",)) + self.assertRaises(FailExitException, _exec_client, + (CLIENT,) + startparams + ("~~unknown~cmd~failed~~",)) + self.pruneLog() + # start again (should fail): + self.assertRaises(FailExitException, _exec_client, + (CLIENT, "-b") + startparams + ("start",)) + self.assertLogged("Server already running") + finally: + self.pruneLog() + # stop: + self.assertRaises(ExitException, _exec_client, + (CLIENT,) + startparams + ("stop",)) + self.assertLogged("Shutdown successful") + self.assertLogged("Exit with code 0") + + self.pruneLog() + # stop again (should fail): + self.assertRaises(FailExitException, _exec_client, + (CLIENT,) + startparams + ("stop",)) + self.assertLogged("Failed to access socket path") + self.assertLogged("Is fail2ban running?") + finally: + _kill_srv(tmp) + + @withtmpdir + def testClientStartBackgroundCall(self, tmp): + try: + global INTERACT + startparams = _start_params(tmp, logtarget=tmp+"/f2b.log") + # start (in new process, using the same python version): + cmd = (sys.executable, os.path.join(os.path.join(BIN), CLIENT)) + logSys.debug('Start %s ...', cmd) + cmd = cmd + startparams + ("--async", "start",) + ret = Utils.executeCmd(cmd, timeout=MAX_WAITTIME, shell=False, output=True) + self.assertTrue(len(ret) and ret[0]) + # wait for server (socket and ready): + self._wait_for_srv(tmp, True, startparams=cmd) + self.assertLogged("Server ready") + self.pruneLog() + try: + # echo from client (inside): + self.assertRaises(ExitException, _exec_client, + (CLIENT,) + startparams + ("echo", "TEST-ECHO",)) + self.assertLogged("TEST-ECHO") + self.assertLogged("Exit with code 0") + self.pruneLog() + # interactive client chat with started server: + INTERACT += [ + "echo INTERACT-ECHO", + "status", + "exit" + ] + self.assertRaises(ExitException, _exec_client, + (CLIENT,) + startparams + ("-i",)) + self.assertLogged("INTERACT-ECHO") + self.assertLogged("Status", "Number of jail:") + self.assertLogged("Exit with code 0") + self.pruneLog() + # test reload and restart over interactive client: + INTERACT += [ + "reload", + "restart", + "exit" + ] + self.assertRaises(ExitException, _exec_client, + (CLIENT,) + startparams + ("-i",)) + self.assertLogged("Reading config files:") + self.assertLogged("Shutdown successful") + self.assertLogged("Server ready") + self.assertLogged("Exit with code 0") + self.pruneLog() + # test reload missing jail (interactive): + INTERACT += [ + "reload ~~unknown~jail~fail~~", + "exit" + ] + self.assertRaises(ExitException, _exec_client, + (CLIENT,) + startparams + ("-i",)) + self.assertLogged("Failed during configuration: No section: '~~unknown~jail~fail~~'") + self.pruneLog() + # test reload missing jail (direct): + self.assertRaises(FailExitException, _exec_client, + (CLIENT,) + startparams + ("reload", "~~unknown~jail~fail~~")) + self.assertLogged("Failed during configuration: No section: '~~unknown~jail~fail~~'") + self.assertLogged("Exit with code -1") + self.pruneLog() + finally: + self.pruneLog() + # stop: + self.assertRaises(ExitException, _exec_client, + (CLIENT,) + startparams + ("stop",)) + self.assertLogged("Shutdown successful") + self.assertLogged("Exit with code 0") + finally: + _kill_srv(tmp) + + def _testClientStartForeground(self, tmp, startparams, phase): + # start and wait to end (foreground): + logSys.debug("-- start of test worker") + phase['start'] = True + self.assertRaises(fail2bancmdline.ExitException, _exec_client, + (CLIENT, "-f") + startparams + ("start",)) + # end : + phase['end'] = True + logSys.debug("-- end of test worker") + + @withtmpdir + def testClientStartForeground(self, tmp): + th = None + try: + # started directly here, so prevent overwrite test cases logger with "INHERITED" + startparams = _start_params(tmp, logtarget="INHERITED") + # because foreground block execution - start it in thread: + phase = dict() + th = Thread(name="_TestCaseWorker", + target=Fail2banClientTest._testClientStartForeground, args=(self, tmp, startparams, phase)) + th.daemon = True + th.start() + try: + # wait for start thread: + Utils.wait_for(lambda: phase.get('start', None) is not None, MAX_WAITTIME) + self.assertTrue(phase.get('start', None)) + # wait for server (socket and ready): + self._wait_for_srv(tmp, True, startparams=startparams) + self.pruneLog() + # several commands to server: + self.assertRaises(ExitException, _exec_client, + (CLIENT,) + startparams + ("ping",)) + self.assertRaises(FailExitException, _exec_client, + (CLIENT,) + startparams + ("~~unknown~cmd~failed~~",)) + self.assertRaises(ExitException, _exec_client, + (CLIENT,) + startparams + ("echo", "TEST-ECHO",)) + finally: + self.pruneLog() + # stop: + self.assertRaises(ExitException, _exec_client, + (CLIENT,) + startparams + ("stop",)) + # wait for end: + Utils.wait_for(lambda: phase.get('end', None) is not None, MAX_WAITTIME) + self.assertTrue(phase.get('end', None)) + self.assertLogged("Shutdown successful", "Exiting Fail2ban") + finally: + _kill_srv(tmp) + if th: + th.join() + + @withtmpdir + def testClientFailStart(self, tmp): + try: + # started directly here, so prevent overwrite test cases logger with "INHERITED" + startparams = _start_params(tmp, logtarget="INHERITED") + + ## wrong config directory + self.assertRaises(FailExitException, _exec_client, + (CLIENT, "--async", "-c", tmp+"/miss", "start",)) + self.assertLogged("Base configuration directory " + tmp+"/miss" + " does not exist") + self.pruneLog() + + ## wrong socket + self.assertRaises(FailExitException, _exec_client, + (CLIENT, "--async", "-c", tmp+"/config", "-s", tmp+"/miss/f2b.sock", "start",)) + self.assertLogged("There is no directory " + tmp+"/miss" + " to contain the socket file") + self.pruneLog() + + ## not running + self.assertRaises(FailExitException, _exec_client, + (CLIENT, "-c", tmp+"/config", "-s", tmp+"/f2b.sock", "reload",)) + self.assertLogged("Could not find server") + self.pruneLog() + + ## already exists: + open(tmp+"/f2b.sock", 'a').close() + self.assertRaises(FailExitException, _exec_client, + (CLIENT, "--async", "-c", tmp+"/config", "-s", tmp+"/f2b.sock", "start",)) + self.assertLogged("Fail2ban seems to be in unexpected state (not running but the socket exists)") + self.pruneLog() + os.remove(tmp+"/f2b.sock") + + ## wrong option: + self.assertRaises(FailExitException, _exec_client, + (CLIENT, "-s",)) + self.assertLogged("Usage: ") + self.pruneLog() + + finally: + _kill_srv(tmp) + + def testVisualWait(self): + sleeptime = 0.035 + for verbose in (2, 0): + cntr = 15 + with VisualWait(verbose, 5) as vis: + while cntr: + vis.heartbeat() + if verbose and not unittest.F2B.fast: + time.sleep(sleeptime) + cntr -= 1 + + +class Fail2banServerTest(Fail2banClientServerBase): + + def testServerUsage(self): + self.assertRaises(ExitException, _exec_server, + (SERVER, "-h",)) + self.assertLogged("Usage: " + SERVER) + self.assertLogged("Report bugs to ") + + @withtmpdir + def testServerStartBackground(self, tmp): + try: + # to prevent fork of test-cases process, start server in background via command: + startparams = _start_params(tmp, logtarget=tmp+"/f2b.log") + # start (in new process, using the same python version): + cmd = (sys.executable, os.path.join(os.path.join(BIN), SERVER)) + logSys.debug('Start %s ...', cmd) + cmd = cmd + startparams + ("-b",) + ret = Utils.executeCmd(cmd, timeout=MAX_WAITTIME, shell=False, output=True) + self.assertTrue(len(ret) and ret[0]) + # wait for server (socket and ready): + self._wait_for_srv(tmp, True, startparams=cmd) + self.assertLogged("Server ready") + self.pruneLog() + try: + self.assertRaises(ExitException, _exec_server, + (SERVER,) + startparams + ("echo", "TEST-ECHO",)) + self.assertRaises(FailExitException, _exec_server, + (SERVER,) + startparams + ("~~unknown~cmd~failed~~",)) + finally: + self.pruneLog() + # stop: + self.assertRaises(ExitException, _exec_server, + (SERVER,) + startparams + ("stop",)) + self.assertLogged("Shutdown successful") + self.assertLogged("Exit with code 0") + finally: + _kill_srv(tmp) + + def _testServerStartForeground(self, tmp, startparams, phase): + # start and wait to end (foreground): + logSys.debug("-- start of test worker") + phase['start'] = True + self.assertRaises(fail2bancmdline.ExitException, _exec_server, + (SERVER, "-f") + startparams + ("start",)) + # end : + phase['end'] = True + logSys.debug("-- end of test worker") + + @withtmpdir + def testServerStartForeground(self, tmp): + th = None + try: + # started directly here, so prevent overwrite test cases logger with "INHERITED" + startparams = _start_params(tmp, logtarget="INHERITED") + # because foreground block execution - start it in thread: + phase = dict() + th = Thread(name="_TestCaseWorker", + target=Fail2banServerTest._testServerStartForeground, args=(self, tmp, startparams, phase)) + th.daemon = True + th.start() + try: + # wait for start thread: + Utils.wait_for(lambda: phase.get('start', None) is not None, MAX_WAITTIME) + self.assertTrue(phase.get('start', None)) + # wait for server (socket and ready): + self._wait_for_srv(tmp, True, startparams=startparams) + self.pruneLog() + # several commands to server: + self.assertRaises(ExitException, _exec_server, + (SERVER,) + startparams + ("ping",)) + self.assertRaises(FailExitException, _exec_server, + (SERVER,) + startparams + ("~~unknown~cmd~failed~~",)) + self.assertRaises(ExitException, _exec_server, + (SERVER,) + startparams + ("echo", "TEST-ECHO",)) + finally: + self.pruneLog() + # stop: + self.assertRaises(ExitException, _exec_server, + (SERVER,) + startparams + ("stop",)) + # wait for end: + Utils.wait_for(lambda: phase.get('end', None) is not None, MAX_WAITTIME) + self.assertTrue(phase.get('end', None)) + self.assertLogged("Shutdown successful", "Exiting Fail2ban") + finally: + _kill_srv(tmp) + if th: + th.join() + + @withtmpdir + def testServerFailStart(self, tmp): + try: + # started directly here, so prevent overwrite test cases logger with "INHERITED" + startparams = _start_params(tmp, logtarget="INHERITED") + + ## wrong config directory + self.assertRaises(FailExitException, _exec_server, + (SERVER, "-c", tmp+"/miss",)) + self.assertLogged("Base configuration directory " + tmp+"/miss" + " does not exist") + self.pruneLog() + + ## wrong socket + self.assertRaises(FailExitException, _exec_server, + (SERVER, "-c", tmp+"/config", "-x", "-s", tmp+"/miss/f2b.sock",)) + self.assertLogged("There is no directory " + tmp+"/miss" + " to contain the socket file") + self.pruneLog() + + ## already exists: + open(tmp+"/f2b.sock", 'a').close() + self.assertRaises(FailExitException, _exec_server, + (SERVER, "-c", tmp+"/config", "-s", tmp+"/f2b.sock",)) + self.assertLogged("Fail2ban seems to be in unexpected state (not running but the socket exists)") + self.pruneLog() + os.remove(tmp+"/f2b.sock") + + finally: + _kill_srv(tmp) diff --git a/fail2ban/tests/fail2banregextestcase.py b/fail2ban/tests/fail2banregextestcase.py index 2fd362c7..a1dcb4da 100644 --- a/fail2ban/tests/fail2banregextestcase.py +++ b/fail2ban/tests/fail2banregextestcase.py @@ -23,19 +23,7 @@ __author__ = "Serg Brester" __copyright__ = "Copyright (c) 2015 Serg G. Brester (sebres), 2008- Fail2Ban Contributors" __license__ = "GPL" -from __builtin__ import open as fopen -import unittest -import getpass import os -import sys -import time -import tempfile -import uuid - -try: - from systemd import journal -except ImportError: - journal = None from ..client import fail2banregex from ..client.fail2banregex import Fail2banRegex, get_opt_parser, output diff --git a/fail2ban/tests/files/logs/asterisk b/fail2ban/tests/files/logs/asterisk index ab018ba9..aa32a290 100644 --- a/fail2ban/tests/files/logs/asterisk +++ b/fail2ban/tests/files/logs/asterisk @@ -59,3 +59,11 @@ Nov 4 18:30:40 localhost asterisk[32229]: NOTICE[32257]: chan_sip.c:23417 in han # match UTF-8 in SessionID # failJSON: { "time": "2015-05-25T07:52:36", "match": true, "host": "10.250.251.252" } [2015-05-25 07:52:36] SECURITY[6988] res_security_log.c: SecurityEvent="InvalidAccountID",EventTV="2015-05-25T07:52:36.888+0300",Severity="Error",Service="PJSIP",EventVersion="1",AccountID="70000180",SessionID="Негодяй",LocalAddress="IPV4/UDP/1.2.3.4/5060",RemoteAddress="IPV4/UDP/10.250.251.252/5061" + +# match phone numbers with + symbol (and without number, or other context) +# failJSON: { "time": "2016-01-28T10:22:27", "match": true , "host": "1.2.3.4" } +[2016-01-28 10:22:27] NOTICE[3477][C-000003bb] chan_sip.c: Call from '' (1.2.3.4:10836) to extension '++441772285411' rejected because extension not found in context 'default'. +# failJSON: { "time": "2016-01-28T10:34:31", "match": true , "host": "1.2.3.4" } +[2016-01-28 10:34:31] NOTICE[3477][C-000003c3] chan_sip.c: Call from '' (1.2.3.4:10836) to extension '0+441772285407' rejected because extension not found in context 'default'. +# failJSON: { "time": "2016-01-28T10:34:33", "match": true , "host": "1.2.3.4" } +[2016-01-28 10:34:33] NOTICE[3477][C-000003c3] chan_sip.c: Call from '' (1.2.3.4:10836) to extension '' rejected because extension not found in context 'my-context'. diff --git a/fail2ban/tests/files/logs/haproxy-http-auth b/fail2ban/tests/files/logs/haproxy-http-auth new file mode 100644 index 00000000..298f1972 --- /dev/null +++ b/fail2ban/tests/files/logs/haproxy-http-auth @@ -0,0 +1,4 @@ +# failJSON: { "match": false } +Nov 14 22:45:27 test haproxy[760]: 192.168.33.1:58444 [14/Nov/2015:22:45:25.439] main app/app1 1939/0/1/0/1940 403 5168 - - ---- 3/3/0/0/0 0/0 "GET / HTTP/1.1" +# failJSON: { "time": "2004-11-14T22:45:11", "match": true , "host": "192.168.33.1" } +Nov 14 22:45:11 test haproxy[760]: 192.168.33.1:58430 [14/Nov/2015:22:45:11.608] main main/ -1/-1/-1/-1/0 401 248 - - PR-- 0/0/0/0/0 0/0 "GET / HTTP/1.1" diff --git a/fail2ban/tests/files/logs/postfix b/fail2ban/tests/files/logs/postfix index 4934a29e..800c7f0c 100644 --- a/fail2ban/tests/files/logs/postfix +++ b/fail2ban/tests/files/logs/postfix @@ -26,3 +26,6 @@ Dec 21 21:17:29 xxx postfix/smtpd[7150]: NOQUEUE: reject: RCPT from badserver.ex # failJSON: { "time": "2004-11-22T22:33:44", "match": true , "host": "1.2.3.4" } Nov 22 22:33:44 xxx postfix/smtpd[11111]: NOQUEUE: reject: RCPT from 1-2-3-4.example.com[1.2.3.4]: 450 4.1.8 : Sender address rejected: Domain not found; from= to= proto=ESMTP helo=<1-2-3-4.example.com> + +# failJSON: { "time": "2005-01-31T13:55:24", "match": true , "host": "78.107.251.238" } +Jan 31 13:55:24 xxx postfix/smtpd[3462]: NOQUEUE: reject: EHLO from s271272.static.corbina.ru[78.107.251.238]: 504 5.5.2 : Helo command rejected: need fully-qualified hostname; proto=SMTP helo= diff --git a/fail2ban/tests/files/logs/screensharingd b/fail2ban/tests/files/logs/screensharingd new file mode 100644 index 00000000..0ec0ebd6 --- /dev/null +++ b/fail2ban/tests/files/logs/screensharingd @@ -0,0 +1,12 @@ +# NOTE: dates here include years -- this is not the typical configuration for the system.log +# file on Mac OS. However, without it the test routines will use 2004 as the year and matches will not pass. +# +# failJSON: { "match": false } +Oct 27 2015 09:24:46 test1.beezwax.net screensharingd[1170]: Authentication: SUCCEEDED :: User Name: simon :: Viewer Address: 192.168.5.247 :: Type: DH +# +# failJSON: { "time": "2015-10-27T12:35:40", "match": true , "host": "192.168.5.247" } +Oct 27 2015 12:35:40 test1.beezwax.net screensharingd[1170]: Authentication: FAILED :: User Name: sdfsdfs () mro :: Viewer Address: 192.168.5.247 :: Type: DH +# failJSON: { "time": "2015-10-27T12:35:50", "match": true , "host": "192.168.5.247" } +Oct 27 2015 12:35:50 test1.beezwax.net screensharingd[1170]: Authentication: FAILED :: User Name: brown_s :: :: Viewer Address: 192.168.5.247 :: Type: DH +# failJSON: { "time": "2015-10-27T12:26:01", "match": true , "host": "192.168.5.247" } +Oct 27 2015 12:26:01 test1.beezwax.net screensharingd[1170]: Authentication: FAILED :: User Name: brown @! s:: :: Viewer Address: 192.168.5.247 :: Type: DH diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index dbe80c32..d593355e 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -62,25 +62,18 @@ class TransmitterBase(unittest.TestCase): def setUp(self): """Call before every test case.""" + #super(TransmitterBase, self).setUp() self.transm = self.server._Server__transm - self.tmp_files = [] - sock_fd, sock_name = tempfile.mkstemp('fail2ban.sock', 'transmitter') - os.close(sock_fd) - self.tmp_files.append(sock_name) - pidfile_fd, pidfile_name = tempfile.mkstemp( - 'fail2ban.pid', 'transmitter') - os.close(pidfile_fd) - self.tmp_files.append(pidfile_name) - self.server.start(sock_name, pidfile_name, **self.server_start_args) + # To test thransmitter we don't need to start server... + #self.server.start('/dev/null', '/dev/null', force=False) self.jailName = "TestJail1" self.server.addJail(self.jailName, FAST_BACKEND) def tearDown(self): """Call after every test case.""" + # stop jails, etc. self.server.quit() - for f in self.tmp_files: - if os.path.exists(f): - os.remove(f) + #super(TransmitterBase, self).tearDown() def setGetTest(self, cmd, inValue, outValue=(None,), outCode=0, jail=None, repr_=False): """Process set/get commands and compare both return values @@ -160,7 +153,6 @@ class Transmitter(TransmitterBase): def setUp(self): self.server = TestServer() - self.server_start_args = {'force':False, 'observer':False} super(Transmitter, self).setUp() def testStopServer(self): @@ -793,11 +785,10 @@ class TransmitterLogging(TransmitterBase): def setUp(self): self.server = Server() + super(TransmitterLogging, self).setUp() self.server.setLogTarget("/dev/null") self.server.setLogLevel("CRITICAL") self.server.setSyslogSocket("auto") - self.server_start_args = {'force':False, 'observer':False} - super(TransmitterLogging, self).setUp() def testLogTarget(self): logTargets = [] @@ -915,17 +906,6 @@ class TransmitterLogging(TransmitterBase): self.setGetTest("bantime.overalljails", "true", "true", jail=self.jailName) -class TransmitterWithObserver(TransmitterBase): - - def setUp(self): - self.server = TestServer() - self.server_start_args = {'force':False, 'observer':True} - super(TransmitterWithObserver, self).setUp() - - def testObserver(self): - pass - - class JailTests(unittest.TestCase): def testLongName(self): @@ -985,3 +965,21 @@ class LoggingTests(LogCaptureTestCase): sys.__excepthook__ = prev_exchook self.assertEqual(len(x), 1) self.assertEqual(x[0][0], RuntimeError) + + def testStartFailedSockExists(self): + tmp_files = [] + sock_fd, sock_name = tempfile.mkstemp('fail2ban.sock', 'f2b-test') + os.close(sock_fd) + tmp_files.append(sock_name) + pidfile_fd, pidfile_name = tempfile.mkstemp('fail2ban.pid', 'f2b-test') + os.close(pidfile_fd) + tmp_files.append(pidfile_name) + server = TestServer() + try: + server.start(sock_name, pidfile_name, force=False) + self.assertLogged("Server already running") + finally: + server.quit() + for f in tmp_files: + if os.path.exists(f): + os.remove(f) diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index 0ca61183..c68e6e2e 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -26,10 +26,14 @@ import logging import optparse import os import re +import tempfile +import shutil import sys import time import unittest + from StringIO import StringIO +from functools import wraps from ..helpers import getLogger from ..server.filter import DNSUtils @@ -50,6 +54,10 @@ if not CONFIG_DIR: else: CONFIG_DIR = '/etc/fail2ban' +# In not installed env (setup, test-cases) use fail2ban modules from main directory: +if 1 or os.environ.get('PYTHONPATH', None) is None: + os.putenv('PYTHONPATH', os.path.dirname(os.path.dirname(os.path.dirname( + os.path.abspath(__file__))))) class F2B(optparse.Values): def __init__(self, opts={}): @@ -71,6 +79,17 @@ class F2B(optparse.Values): return wtime +def withtmpdir(f): + @wraps(f) + def wrapper(self, *args, **kwargs): + tmp = tempfile.mkdtemp(prefix="f2b-temp") + try: + return f(self, tmp, *args, **kwargs) + finally: + # clean up + shutil.rmtree(tmp) + return wrapper + def initTests(opts): unittest.F2B = F2B(opts) # --fast : @@ -146,6 +165,7 @@ def gatherTests(regexps=None, opts=None): from . import databasetestcase from . import observertestcase from . import samplestestcase + from . import fail2banclienttestcase from . import fail2banregextestcase if not regexps: # pragma: no cover @@ -170,7 +190,6 @@ def gatherTests(regexps=None, opts=None): # Server tests.addTest(unittest.makeSuite(servertestcase.Transmitter)) - tests.addTest(unittest.makeSuite(servertestcase.TransmitterWithObserver)) tests.addTest(unittest.makeSuite(servertestcase.JailTests)) tests.addTest(unittest.makeSuite(servertestcase.RegexTests)) tests.addTest(unittest.makeSuite(servertestcase.LoggingTests)) @@ -228,6 +247,9 @@ def gatherTests(regexps=None, opts=None): # Filter Regex tests with sample logs tests.addTest(unittest.makeSuite(samplestestcase.FilterSamplesRegex)) + # bin/fail2ban-client, bin/fail2ban-server + tests.addTest(unittest.makeSuite(fail2banclienttestcase.Fail2banClientTest)) + tests.addTest(unittest.makeSuite(fail2banclienttestcase.Fail2banServerTest)) # bin/fail2ban-regex tests.addTest(unittest.makeSuite(fail2banregextestcase.Fail2banRegexTest)) @@ -298,13 +320,16 @@ class LogCaptureTestCase(unittest.TestCase): # Let's log everything into a string self._log = StringIO() logSys.handlers = [logging.StreamHandler(self._log)] - if self._old_level < logging.DEBUG: # so if HEAVYDEBUG etc -- show them! + if self._old_level <= logging.DEBUG: # so if DEBUG etc -- show them (and log it in travis)! + print("") logSys.handlers += self._old_handlers + logSys.debug('='*10 + ' %s ' + '='*20, self.id()) logSys.setLevel(getattr(logging, 'DEBUG')) def tearDown(self): """Call after every test case.""" # print "O: >>%s<<" % self._log.getvalue() + self.pruneLog() logSys = getLogger("fail2ban") logSys.handlers = self._old_handlers logSys.level = self._old_level @@ -345,6 +370,9 @@ class LogCaptureTestCase(unittest.TestCase): raise AssertionError("All of the %r were found present in the log: %r" % (s, logged)) + def pruneLog(self): + self._log.truncate(0) + def getLog(self): return self._log.getvalue() diff --git a/files/fail2ban-logrotate b/files/fail2ban-logrotate index 8d94a8b3..13a94537 100644 --- a/files/fail2ban-logrotate +++ b/files/fail2ban-logrotate @@ -6,11 +6,9 @@ # https://github.com/fail2ban/fail2ban/blob/debian/debian/fail2ban.logrotate /var/log/fail2ban.log { - rotate 7 missingok notifempty - compress postrotate - /usr/bin/fail2ban-client flushlogs 1>/dev/null || true + /usr/bin/fail2ban-client flushlogs >/dev/null || true endscript } diff --git a/files/gentoo-initd b/files/gentoo-initd index 98c5edf9..e939b987 100755 --- a/files/gentoo-initd +++ b/files/gentoo-initd @@ -34,19 +34,19 @@ start() { # remove stalled sock file after system crash # bug 347477 rm -f /var/run/fail2ban/fail2ban.sock || return 1 - ${FAIL2BAN} start &> /dev/null + ${FAIL2BAN} start eend $? "Failed to start fail2ban" } stop() { ebegin "Stopping fail2ban" - ${FAIL2BAN} stop &> /dev/null + ${FAIL2BAN} stop eend $? "Failed to stop fail2ban" } reload() { ebegin "Reloading fail2ban" - ${FAIL2BAN} reload > /dev/null + ${FAIL2BAN} reload eend $? "Failed to reload fail2ban" }