Merge remote-tracking branch 'f2b-perfom-prepare-716-cs' into ban-time-incr (+ conflicts resolved)

pull/1460/head
sebres 2016-03-06 15:06:43 +01:00
commit bf0adc1fdf
44 changed files with 2202 additions and 747 deletions

View File

@ -38,7 +38,7 @@ script:
# Keep the legacy setup.py test approach of checking coverage for python2 # Keep the legacy setup.py test approach of checking coverage for python2
- if [[ "$F2B_PY_2" ]]; then coverage run setup.py test; fi - 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 # 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) # Use $VENV_BIN (not python) or else sudo will always run the system's python (2.7)
- sudo $VENV_BIN/pip install . - sudo $VENV_BIN/pip install .
after_success: after_success:

View File

@ -41,6 +41,11 @@ ver. 0.9.4 (2015/XX/XXX) - wanna-be-released
for python version < 3.x (gh-1248) for python version < 3.x (gh-1248)
* Use postfix_log logpath for postfix-rbl jail * Use postfix_log logpath for postfix-rbl jail
* filters.d/postfix.conf - add 'Sender address rejected: Domain not found' failregex * 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 Features:
* New interpolation feature for definition config readers - `<known/parameter>` * New interpolation feature for definition config readers - `<known/parameter>`
@ -50,6 +55,9 @@ ver. 0.9.4 (2015/XX/XXX) - wanna-be-released
filter.d/*.local file. filter.d/*.local file.
As extension to interpolation `%(known/parameter)s`, that does not works for As extension to interpolation `%(known/parameter)s`, that does not works for
filter and action init parameters 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: * New filters:
- openhab - domotic software authentication failure with the - openhab - domotic software authentication failure with the
rest api and web interface (gh-1223) 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) request processing rate (ngx_http_limit_req_module)
- murmur - ban hosts that repeatedly attempt to connect to - murmur - ban hosts that repeatedly attempt to connect to
murmur/mumble-server with an invalid server password or certificate. 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: * New jails:
- murmur - bans TCP and UDP from the bad host on the default murmur port. - murmur - bans TCP and UDP from the bad host on the default murmur port.
* sshd filter got new failregex to match "maximum authentication * sshd filter got new failregex to match "maximum authentication
attempts exceeded" (introduced in openssh 6.8) attempts exceeded" (introduced in openssh 6.8)
* Added filter for Mac OS screen sharing (VNC) daemon
- Enhancements: - Enhancements:
* Do not rotate empty log files * 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). * Performance improvements while monitoring large number of files (gh-1265).
Use associative array (dict) for monitored log files to speed up lookup Use associative array (dict) for monitored log files to speed up lookup
operations. Thanks @kshetragia 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 ver. 0.9.3 (2015/08/01) - lets-all-stay-friends
---------- ----------

View File

@ -165,7 +165,11 @@ fail2ban/client/configparserinc.py
fail2ban/client/configreader.py fail2ban/client/configreader.py
fail2ban/client/configurator.py fail2ban/client/configurator.py
fail2ban/client/csocket.py fail2ban/client/csocket.py
fail2ban/client/fail2banclient.py
fail2ban/client/fail2bancmdline.py
fail2ban/client/fail2banreader.py fail2ban/client/fail2banreader.py
fail2ban/client/fail2banregex.py
fail2ban/client/fail2banserver.py
fail2ban/client/filterreader.py fail2ban/client/filterreader.py
fail2ban/client/jailreader.py fail2ban/client/jailreader.py
fail2ban/client/jailsreader.py fail2ban/client/jailsreader.py

View File

@ -18,456 +18,20 @@
# along with Fail2Ban; if not, write to the Free Software # along with Fail2Ban; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
__author__ = "Cyril Jaquier" """
__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" __license__ = "GPL"
import getopt from fail2ban.client.fail2banclient import exec_command_line, sys
import logging
import os
import pickle
import re
import shlex
import signal
import socket
import string
import sys
import time
from fail2ban.version import version if __name__ == "__main__":
from fail2ban.protocol import printFormatted exec_command_line(sys.argv)
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 <cyril.jaquier@fail2ban.org>."
print "Many contributions by Yaroslav O. Halchenko <debian@onerussian.com>."
def dispUsage(self):
""" Prints Fail2Ban command line options and exits
"""
print "Usage: "+self.__argv[0]+" [OPTIONS] <COMMAND>"
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 <DIR> configuration directory"
print " -s <FILE> socket path"
print " -p <FILE> 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)

View File

@ -18,123 +18,20 @@
# along with Fail2Ban; if not, write to the Free Software # along with Fail2Ban; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
__author__ = "Cyril Jaquier" """
__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" __license__ = "GPL"
import getopt from fail2ban.client.fail2banserver import exec_command_line, sys
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 <cyril.jaquier@fail2ban.org>."
print "Many contributions by Yaroslav O. Halchenko <debian@onerussian.com>."
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 <FILE> socket path"
print " -p <FILE> 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
if __name__ == "__main__": if __name__ == "__main__":
server = Fail2banServer() exec_command_line(sys.argv)
if server.start(sys.argv):
sys.exit(0)
else:
sys.exit(-1)

View File

@ -119,9 +119,13 @@ else:
# Custom log format for the verbose tests runs # Custom log format for the verbose tests runs
if verbosity > 1: # pragma: no cover if verbosity > 1: # pragma: no cover
stdout.setFormatter(Formatter(' %(asctime)-15s %(thread)s' + fmt)) if verbosity > 3:
else: # pragma: no cover fmt = ' | %(module)15.15s-%(levelno)-2d: %(funcName)-20.20s |' + fmt
# just prefix with the space 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)) stdout.setFormatter(Formatter(fmt))
logSys.addHandler(stdout) logSys.addHandler(stdout)

View File

@ -10,7 +10,7 @@
[Definition] [Definition]
actionban = curl --fail --user-agent "fail2ban v0.8.12" http://www.badips.com/add/<category>/<ip> actionban = curl --fail --user-agent "<agent>" http://www.badips.com/add/<category>/<ip>
[Init] [Init]

View File

@ -21,7 +21,6 @@ import sys
if sys.version_info < (2, 7): if sys.version_info < (2, 7):
raise ImportError("badips.py action requires Python >= 2.7") raise ImportError("badips.py action requires Python >= 2.7")
import json import json
from functools import partial
import threading import threading
import logging import logging
if sys.version_info >= (3, ): if sys.version_info >= (3, ):
@ -33,7 +32,6 @@ else:
from urllib import urlencode from urllib import urlencode
from fail2ban.server.actions import ActionBase from fail2ban.server.actions import ActionBase
from fail2ban.version import version as f2bVersion
class BadIPsAction(ActionBase): class BadIPsAction(ActionBase):
@ -72,6 +70,9 @@ class BadIPsAction(ActionBase):
updateperiod : int, optional updateperiod : int, optional
Time in seconds between updating bad IPs blacklist. Time in seconds between updating bad IPs blacklist.
Default 900 (15 minutes) Default 900 (15 minutes)
agent : str, optional
User agent transmitted to server.
Default `Fail2Ban/ver.`
Raises Raises
------ ------
@ -80,13 +81,14 @@ class BadIPsAction(ActionBase):
""" """
_badips = "http://www.badips.com" _badips = "http://www.badips.com"
_Request = partial( def _Request(self, url, **argv):
Request, headers={'User-Agent': "Fail2Ban %s" % f2bVersion}) return Request(url, headers={'User-Agent': self.agent}, **argv)
def __init__(self, jail, name, category, score=3, age="24h", key=None, 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) super(BadIPsAction, self).__init__(jail, name)
self.agent = agent
self.category = category self.category = category
self.score = score self.score = score
self.age = age self.age = age

View File

@ -54,7 +54,7 @@ actioncheck =
# Tags: See jail.conf(5) man page # Tags: See jail.conf(5) man page
# Values: CMD # Values: CMD
# #
actionban = curl --fail --data-urlencode 'server=<email>' --data 'apikey=<apikey>' --data 'service=<service>' --data 'ip=<ip>' --data-urlencode 'logs=<matches>' --data 'format=text' --user-agent "fail2ban v0.8.12" "https://www.blocklist.de/en/httpreports.html" actionban = curl --fail --data-urlencode 'server=<email>' --data 'apikey=<apikey>' --data 'service=<service>' --data 'ip=<ip>' --data-urlencode 'logs=<matches>' --data 'format=text' --user-agent "<agent>" "https://www.blocklist.de/en/httpreports.html"
# Option: actionunban # Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the # Notes.: command executed when unbanning an IP. Take care that the

View File

@ -111,13 +111,17 @@ myip = `ip -4 addr show dev eth0 | grep inet | head -n 1 | sed -r 's/.*inet ([0-
# #
protocol = tcp protocol = tcp
# Option: agent
# Default: Fail2ban
agent = Fail2ban
# Option: getcmd # Option: getcmd
# Notes.: A command to fetch a URL. Should output page to STDOUT # Notes.: A command to fetch a URL. Should output page to STDOUT
# Values: CMD Default: wget # 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=<agent>
# Alternative value: # 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 <agent>
# Option: srcport # Option: srcport
# Notes.: The source port of the attack. You're unlikely to have this info, so # Notes.: The source port of the attack. You're unlikely to have this info, so

View File

@ -0,0 +1,22 @@
# Fail2Ban configuration file
#
# Author: Cyril Jaquier
# Modified: Yaroslav O. Halchenko <debian@onerussian.com>
# made active on all ports from original iptables.conf
# Modified: Alexander Belykh <albel727@ngs.ru>
# 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 <protocol>
[Init]

View File

@ -0,0 +1,119 @@
# Fail2Ban configuration file
#
# Author: Daniel Black
# Author: Cyril Jaquier
# Modified: Yaroslav O. Halchenko <debian@onerussian.com>
# made active on all ports from original iptables.conf
# Modified: Alexander Belykh <albel727@ngs.ru>
# 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 = <protocol> dport \{ <port> \}
# Option: actionstart
# Notes.: command executed once at the start of Fail2Ban.
# Values: CMD
#
actionstart = <nftables> add set <nftables_family> <nftables_table> f2b-<name> \{ type <nftables_type>\; \}
<nftables> insert rule <nftables_family> <nftables_table> <chain> %(nftables_mode)s ip saddr @f2b-<name> <blocktype>
_nft_list = <nftables> --handle --numeric list chain <nftables_family> <nftables_table> <chain>
_nft_get_handle_id = grep -m1 'ip saddr @f2b-<name> <blocktype> # 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)
<nftables> delete rule <nftables_family> <nftables_table> <chain> $HANDLE_ID
<nftables> delete set <nftables_family> <nftables_table> f2b-<name>
# Option: actioncheck
# Notes.: command executed once before each actionban command
# Values: CMD
#
actioncheck = <nftables> list chain <nftables_family> <nftables_table> <chain> | grep -q '@f2b-<name>[ \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 = <nftables> add element <nftables_family> <nftables_table> f2b-<name> \{ <ip> \}
# 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 = <nftables> delete element <nftables_family> <nftables_table> f2b-<name> \{ <ip> \}
[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

View File

@ -0,0 +1,22 @@
# Fail2Ban configuration file
#
# Author: Cyril Jaquier
# Modified: Yaroslav O. Halchenko <debian@onerussian.com>
# made active on all ports from original iptables.conf
# Modified: Alexander Belykh <albel727@ngs.ru>
# adapted for nftables
#
[INCLUDES]
before = nftables-common.conf
[Definition]
# Option: nftables_mode
# Notes.: additional expressions for nftables filter rule
# Values: nftables expressions
#
nftables_mode = <protocol> dport \{ <port> \}
[Init]

View File

@ -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+:)? 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 '<HOST>(:\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)$ failregex = ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Registration from '[^']*' failed for '<HOST>(:\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 '[^']*' \(<HOST>:\d+\) to extension '\d+' rejected because extension not found in context 'default'\.$ ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Call from '[^']*' \(<HOST>:\d+\) to extension '[^']*' rejected because extension not found in context
^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Host <HOST> failed to authenticate as '[^']*'$ ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Host <HOST> failed to authenticate as '[^']*'$
^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s No registration for peer '[^']*' \(from <HOST>\)$ ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s No registration for peer '[^']*' \(from <HOST>\)$
^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Host <HOST> failed MD5 authentication for '[^']*' \([^)]+\)$ ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Host <HOST> failed MD5 authentication for '[^']*' \([^)]+\)$

View File

@ -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 "<HOST>" can
# be used for standard IP/hostname matching and is only an alias for
# (?:::f{4,6}:)?(?P<host>[\w\-.^_]+)
# Values: TEXT
#
failregex = ^%(__prefix_line)s<HOST>.*<NOSRV> -1/-1/-1/-1/\+*\d* 401
# Option: ignoreregex
# Notes.: regex to ignore. If this regex matches, the line is ignored.
# Values: TEXT
#
ignoreregex =

View File

@ -15,6 +15,7 @@ _daemon = postfix/(submission/)?smtp(d|s)
failregex = ^%(__prefix_line)sNOQUEUE: reject: RCPT from \S+\[<HOST>\]: 554 5\.7\.1 .*$ failregex = ^%(__prefix_line)sNOQUEUE: reject: RCPT from \S+\[<HOST>\]: 554 5\.7\.1 .*$
^%(__prefix_line)sNOQUEUE: reject: RCPT from \S+\[<HOST>\]: 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+\[<HOST>\]: 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+\[<HOST>\]: 450 4\.7\.1 : Helo command rejected: Host not found; from=<> to=<> proto=ESMTP helo= *$ ^%(__prefix_line)sNOQUEUE: reject: RCPT from \S+\[<HOST>\]: 450 4\.7\.1 : Helo command rejected: Host not found; from=<> to=<> proto=ESMTP helo= *$
^%(__prefix_line)sNOQUEUE: reject: EHLO from \S+\[<HOST>\]: 504 5\.5\.2 <\S+>: Helo command rejected: need fully-qualified hostname;
^%(__prefix_line)sNOQUEUE: reject: VRFY from \S+\[<HOST>\]: 550 5\.1\.1 .*$ ^%(__prefix_line)sNOQUEUE: reject: VRFY from \S+\[<HOST>\]: 550 5\.1\.1 .*$
^%(__prefix_line)sNOQUEUE: reject: RCPT from \S+\[<HOST>\]: 450 4\.1\.8 <\S*>: Sender address rejected: Domain not found; from=<\S*> to=<\S+> proto=ESMTP helo=<\S*>$ ^%(__prefix_line)sNOQUEUE: reject: RCPT from \S+\[<HOST>\]: 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 [^[]*\[<HOST>\]:?$ ^%(__prefix_line)simproper command pipelining after \S+ from [^[]*\[<HOST>\]:?$

View File

@ -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 "<HOST>" can
# be used for standard IP/hostname matching and is only an alias for
# (?:::f{4,6}:)?(?P<host>[\w\-.^_]+)
# Values: TEXT
#
failregex = ^%(__prefix_line)sAuthentication: FAILED :: User Name: .+ :: Viewer Address: <HOST> :: Type: DH$
# Option: ignoreregex
# Notes.: regex to ignore. If this regex matches, the line is ignored.
# Values: TEXT
#
ignoreregex =

View File

@ -184,6 +184,9 @@ chain = INPUT
# Usually should be overridden in a particular jail # Usually should be overridden in a particular jail
port = 0:65535 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 # 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. # 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"] 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 # ban & send an e-mail with whois report and relevant log lines
# to the destemail. # to the destemail.
action_mwl = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] 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 # 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 # ban IP on CloudFlare & send an e-mail with whois report and relevant log lines
# to the destemail. # to the destemail.
action_cf_mwl = cloudflare[cfuser="%(cfemail)s", cftoken="%(cfapikey)s"] 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 # Report block via blocklist.de fail2ban reporting service API
# #
@ -225,7 +228,7 @@ action_cf_mwl = cloudflare[cfuser="%(cfemail)s", cftoken="%(cfapikey)s"]
# [Init] # [Init]
# blocklist_de_apikey = {api key from registration] # 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 # 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 # NOTE: This action relies on banaction being present on start and therefore
# should be last action defined for a jail. # 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 # 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 # 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 port = ssh
logpath = %(auditd_log)s logpath = %(auditd_log)s
maxretry = 5
# #
@ -304,7 +310,6 @@ maxretry = 1
port = http,https port = http,https
logpath = %(apache_error_log)s logpath = %(apache_error_log)s
maxretry = 6
[apache-overflows] [apache-overflows]
@ -342,18 +347,21 @@ port = http,https
logpath = %(apache_error_log)s logpath = %(apache_error_log)s
maxretry = 2 maxretry = 2
[apache-shellshock] [apache-shellshock]
port = http,https port = http,https
logpath = %(apache_error_log)s logpath = %(apache_error_log)s
maxretry = 1 maxretry = 1
[openhab-auth] [openhab-auth]
filter = openhab filter = openhab
action = iptables-allports[name=NoAuthFailures] action = iptables-allports[name=NoAuthFailures]
logpath = /opt/openhab/logs/request.log logpath = /opt/openhab/logs/request.log
[nginx-http-auth] [nginx-http-auth]
port = http,https port = http,https
@ -373,6 +381,7 @@ port = http,https
logpath = %(nginx_error_log)s logpath = %(nginx_error_log)s
maxretry = 2 maxretry = 2
# Ban attackers that try to use PHP's URL-fopen() functionality # Ban attackers that try to use PHP's URL-fopen() functionality
# through GET/POST variables. - Experimental, with more than a year # through GET/POST variables. - Experimental, with more than a year
# of usage in production environments. # of usage in production environments.
@ -437,7 +446,6 @@ logpath = /var/log/sogo/sogo.log
logpath = /var/log/tine20/tine20.log logpath = /var/log/tine20/tine20.log
port = http,https port = http,https
maxretry = 5
# #
@ -458,7 +466,6 @@ logpath = /var/log/tomcat*/catalina.out
[monit] [monit]
#Ban clients brute-forcing the monit gui login #Ban clients brute-forcing the monit gui login
filter = monit
port = 2812 port = 2812
logpath = /var/log/monit logpath = /var/log/monit
@ -511,7 +518,6 @@ backend = %(proftpd_backend)s
port = ftp,ftp-data,ftps,ftps-data port = ftp,ftp-data,ftps,ftps-data
logpath = %(pureftpd_log)s logpath = %(pureftpd_log)s
backend = %(pureftpd_backend)s backend = %(pureftpd_backend)s
maxretry = 6
[gssftpd] [gssftpd]
@ -519,7 +525,6 @@ maxretry = 6
port = ftp,ftp-data,ftps,ftps-data port = ftp,ftp-data,ftps,ftps-data
logpath = %(syslog_daemon)s logpath = %(syslog_daemon)s
backend = %(syslog_backend)s backend = %(syslog_backend)s
maxretry = 6
[wuftpd] [wuftpd]
@ -527,7 +532,6 @@ maxretry = 6
port = ftp,ftp-data,ftps,ftps-data port = ftp,ftp-data,ftps,ftps-data
logpath = %(wuftpd_log)s logpath = %(wuftpd_log)s
backend = %(wuftpd_backend)s backend = %(wuftpd_backend)s
maxretry = 6
[vsftpd] [vsftpd]
@ -762,7 +766,6 @@ maxretry = 10
port = 3306 port = 3306
logpath = %(mysql_log)s logpath = %(mysql_log)s
backend = %(mysql_backend)s backend = %(mysql_backend)s
maxretry = 5
# Jail for more extended banning of persistent abusers # Jail for more extended banning of persistent abusers
@ -778,7 +781,6 @@ logpath = /var/log/fail2ban.log
banaction = %(banaction_allports)s banaction = %(banaction_allports)s
bantime = 1w bantime = 1w
findtime = 1d findtime = 1d
maxretry = 5
# Generic filter for PAM. Has to be used with action which bans all # 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 # nobody except your own Nagios server should ever probe nrpe
[nagios] [nagios]
enabled = false
logpath = %(syslog_daemon)s ; nrpe.cfg may define a different log_facility logpath = %(syslog_daemon)s ; nrpe.cfg may define a different log_facility
backend = %(syslog_backend)s backend = %(syslog_backend)s
maxretry = 1 maxretry = 1
@ -832,18 +833,14 @@ maxretry = 1
[oracleims] [oracleims]
# see "oracleims" filter file for configuration requirement for Oracle IMS v6 and above # see "oracleims" filter file for configuration requirement for Oracle IMS v6 and above
enabled = false
logpath = /opt/sun/comms/messaging64/log/mail.log_current logpath = /opt/sun/comms/messaging64/log/mail.log_current
maxretry = 6
banaction = %(banaction_allports)s banaction = %(banaction_allports)s
[directadmin] [directadmin]
enabled = false
logpath = /var/log/directadmin/login.log logpath = /var/log/directadmin/login.log
port = 2222 port = 2222
[portsentry] [portsentry]
enabled = false
logpath = /var/lib/portsentry/portsentry.history logpath = /var/lib/portsentry/portsentry.history
maxretry = 1 maxretry = 1
@ -864,7 +861,19 @@ findtime = 1
[murmur] [murmur]
# AKA mumble-server # AKA mumble-server
port = 64738 port = 64738
filter = murmur
action = %(banaction)s[name=%(__name__)s-tcp, port="%(port)s", protocol=tcp, chain="%(chain)s", actname=%(banaction)s-tcp] 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] %(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 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

View File

@ -68,6 +68,8 @@ class Beautifier:
msg = "Added jail " + response msg = "Added jail " + response
elif inC[0] == "flushlogs": elif inC[0] == "flushlogs":
msg = "logs: " + response msg = "logs: " + response
elif inC[0] == "echo":
msg = ' '.join(msg)
elif inC[0:1] == ['status']: elif inC[0:1] == ['status']:
if len(inC) > 1: if len(inC) > 1:
# Display information # Display information

View File

@ -208,7 +208,7 @@ class ConfigReaderUnshared(SafeConfigParserWithIncludes):
# 1 -> the name of the option # 1 -> the name of the option
# 2 -> the default value for 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() values = dict()
for option in options: for option in options:
try: try:
@ -222,6 +222,8 @@ class ConfigReaderUnshared(SafeConfigParserWithIncludes):
continue continue
values[option[1]] = v values[option[1]] = v
except NoSectionError, e: except NoSectionError, e:
if shouldExist:
raise
# No "Definition" section or wrong basedir # No "Definition" section or wrong basedir
logSys.error(e) logSys.error(e)
values[option[1]] = option[2] values[option[1]] = option[2]

View File

@ -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)

View File

@ -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 <cyril.jaquier@fail2ban.org>.")
output("Many contributions by Yaroslav O. Halchenko <debian@onerussian.com>.")
def dispUsage(self):
""" Prints Fail2Ban command line options and exits
"""
caller = os.path.basename(self._argv[0])
output("Usage: "+caller+" [OPTIONS]" + (" <COMMAND>" 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 <DIR> configuration directory")
output(" -s <FILE> socket path")
output(" -p <FILE> pidfile path")
output(" --loglevel <LEVEL> logging level")
output(" --logtarget <FILE>|STDOUT|STDERR|SYSLOG")
output(" --syslogsocket auto|<FILE>")
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

View File

@ -40,8 +40,13 @@ class Fail2banReader(ConfigReader):
ConfigReader.read(self, "fail2ban") ConfigReader.read(self, "fail2ban")
def getEarlyOptions(self): def getEarlyOptions(self):
opts = [["string", "socket", "/var/run/fail2ban/fail2ban.sock"], opts = [
["string", "pidfile", "/var/run/fail2ban/fail2ban.pid"]] ["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) return ConfigReader.getOptions(self, "Definition", opts)
def getOptions(self): def getOptions(self):

View File

@ -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)

View File

@ -32,6 +32,7 @@ import re
from .configreader import ConfigReaderUnshared, ConfigReader from .configreader import ConfigReaderUnshared, ConfigReader
from .filterreader import FilterReader from .filterreader import FilterReader
from .actionreader import ActionReader from .actionreader import ActionReader
from ..version import version
from ..helpers import getLogger from ..helpers import getLogger
from ..helpers import splitcommaspace from ..helpers import splitcommaspace
@ -115,8 +116,12 @@ class JailReader(ConfigReader):
["string", "filter", ""], ["string", "filter", ""],
["string", "action", ""]] ["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): # 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: if not self.__opts:
return False return False

View File

@ -26,6 +26,9 @@ __license__ = "GPL"
import textwrap import textwrap
def output(s):
print(s)
## ##
# Describes the protocol used to communicate with the server. # Describes the protocol used to communicate with the server.
@ -42,11 +45,13 @@ CSPROTO = dotdict({
protocol = [ protocol = [
['', "BASIC", ""], ['', "BASIC", ""],
["start", "starts the server and the jails"], ["start", "starts the server and the jails"],
["reload", "reloads the configuration"], ["restart", "restarts the server"],
["reload", "reloads the configuration without restart"],
["reload <JAIL>", "reloads the jail <JAIL>"], ["reload <JAIL>", "reloads the jail <JAIL>"],
["stop", "stops all jails and terminate the server"], ["stop", "stops all jails and terminate the server"],
["status", "gets the current status of 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"], ["help", "return this output"],
["version", "return the server version"], ["version", "return the server version"],
['', "LOGGING", ""], ['', "LOGGING", ""],
@ -141,7 +146,7 @@ def printFormatted():
firstHeading = False firstHeading = False
for m in protocol: for m in protocol:
if m[0] == '' and firstHeading: if m[0] == '' and firstHeading:
print output("")
firstHeading = True firstHeading = True
first = True first = True
if len(m[0]) >= MARGIN: if len(m[0]) >= MARGIN:
@ -152,7 +157,7 @@ def printFormatted():
first = False first = False
else: else:
line = ' ' * (INDENT + MARGIN) + n.strip() line = ' ' * (INDENT + MARGIN) + n.strip()
print line output(line)
## ##
@ -163,20 +168,20 @@ def printWiki():
for m in protocol: for m in protocol:
if m[0] == '': if m[0] == '':
if firstHeading: if firstHeading:
print "|}" output("|}")
__printWikiHeader(m[1], m[2]) __printWikiHeader(m[1], m[2])
firstHeading = True firstHeading = True
else: else:
print "|-" output("|-")
print "| <span style=\"white-space:nowrap;\"><tt>" + m[0] + "</tt></span> || || " + m[1] output("| <span style=\"white-space:nowrap;\"><tt>" + m[0] + "</tt></span> || || " + m[1])
print "|}" output("|}")
def __printWikiHeader(section, desc): def __printWikiHeader(section, desc):
print output("")
print "=== " + section + " ===" output("=== " + section + " ===")
print output("")
print desc output(desc)
print output("")
print "{|" output("{|")
print "| '''Command''' || || '''Description'''" output("| '''Command''' || || '''Description'''")

View File

@ -95,6 +95,7 @@ def loop(active, timeout=None, use_poll=False):
# Use poll instead of loop, because of recognition of active flag, # Use poll instead of loop, because of recognition of active flag,
# because of loop timeout mistake: different in poll and poll2 (sec vs ms), # 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) # and to prevent sporadical errors like EBADF 'Bad file descriptor' etc. (see gh-161)
errCount = 0
if timeout is None: if timeout is None:
timeout = Utils.DEFAULT_SLEEP_TIME timeout = Utils.DEFAULT_SLEEP_TIME
poll = asyncore.poll poll = asyncore.poll
@ -107,11 +108,20 @@ def loop(active, timeout=None, use_poll=False):
while active(): while active():
try: try:
poll(timeout) poll(timeout)
if errCount:
errCount -= 1
except Exception as e: # pragma: no cover except Exception as e: # pragma: no cover
if not active():
break
errCount += 1
if errCount < 20:
if e.args[0] in (errno.ENOTCONN, errno.EBADF): # (errno.EBADF, 'Bad file descriptor') if e.args[0] in (errno.ENOTCONN, errno.EBADF): # (errno.EBADF, 'Bad file descriptor')
logSys.info('Server connection was closed: %s', str(e)) logSys.info('Server connection was closed: %s', str(e))
else: else:
logSys.error('Server connection was closed: %s', str(e)) 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") logSys.error("Fail2ban seems to be already running")
if force: if force:
logSys.warning("Forcing execution of the server") logSys.warning("Forcing execution of the server")
os.remove(sock) self._remove_sock()
else: else:
raise AsyncServerException("Server already running") raise AsyncServerException("Server already running")
# Creates the socket. # Creates the socket.
@ -175,20 +185,22 @@ class AsyncServer(asyncore.dispatcher):
AsyncServer.__markCloseOnExec(self.socket) AsyncServer.__markCloseOnExec(self.socket)
self.listen(1) self.listen(1)
# Sets the init flag. # Sets the init flag.
self.__init = self.__active = True self.__init = self.__loop = self.__active = True
# Event loop as long as active: # Event loop as long as active:
loop(lambda: self.__active) loop(lambda: self.__loop)
self.__active = False
# Cleanup all # Cleanup all
self.stop() self.stop()
def close(self): def close(self):
if self.__active: if self.__active:
self.__loop = False
asyncore.dispatcher.close(self) asyncore.dispatcher.close(self)
# Remove socket (file) only if it was created: # Remove socket (file) only if it was created:
if self.__init and os.path.exists(self.__sock): if self.__init and os.path.exists(self.__sock):
logSys.debug("Removed socket file " + self.__sock) logSys.debug("Removed socket file " + self.__sock)
os.remove(self.__sock) self._remove_sock()
logSys.debug("Socket shutdown") logSys.debug("Socket shutdown")
self.__active = False self.__active = False
@ -201,6 +213,17 @@ class AsyncServer(asyncore.dispatcher):
def isActive(self): def isActive(self):
return self.__active 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 # Marks socket as close-on-exec to avoid leaking file descriptors when
# running actions involving command execution. # running actions involving command execution.

View File

@ -51,6 +51,8 @@ class JailThread(Thread):
def __init__(self, name=None): def __init__(self, name=None):
super(JailThread, self).__init__(name=name) super(JailThread, self).__init__(name=name)
## Should going with main thread also:
self.daemon = True
## Control the state of the thread. ## Control the state of the thread.
self.active = False self.active = False
## Control the idle state of the thread. ## Control the idle state of the thread.

View File

@ -24,6 +24,7 @@ __author__ = "Cyril Jaquier"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import threading
from threading import Lock, RLock from threading import Lock, RLock
import logging import logging
import logging.handlers import logging.handlers
@ -43,6 +44,10 @@ from ..helpers import getLogger, excepthook
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = getLogger(__name__) logSys = getLogger(__name__)
DEF_SYSLOGSOCKET = "auto"
DEF_LOGLEVEL = "INFO"
DEF_LOGTARGET = "STDOUT"
try: try:
from .database import Fail2BanDb from .database import Fail2BanDb
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
@ -50,6 +55,10 @@ except ImportError: # pragma: no cover
Fail2BanDb = None Fail2BanDb = None
def _thread_name():
return threading.current_thread().__class__.__name__
class Server: class Server:
def __init__(self, daemon=False): def __init__(self, daemon=False):
@ -59,7 +68,8 @@ class Server:
self.__db = None self.__db = None
self.__daemon = daemon self.__daemon = daemon
self.__transm = Transmitter(self) self.__transm = Transmitter(self)
self.__asyncServer = AsyncServer(self.__transm) #self.__asyncServer = AsyncServer(self.__transm)
self.__asyncServer = None
self.__logLevel = None self.__logLevel = None
self.__logTarget = None self.__logTarget = None
self.__syslogSocket = None self.__syslogSocket = None
@ -68,10 +78,7 @@ class Server:
'FreeBSD': '/var/run/log', 'FreeBSD': '/var/run/log',
'Linux': '/dev/log', 'Linux': '/dev/log',
} }
self.setSyslogSocket("auto") self.__prev_signals = {}
# Set logging level
self.setLogLevel("INFO")
self.setLogTarget("STDOUT")
def __sigTERMhandler(self, signum, frame): def __sigTERMhandler(self, signum, frame):
logSys.debug("Caught signal %d. Exiting" % signum) logSys.debug("Caught signal %d. Exiting" % signum)
@ -81,27 +88,44 @@ class Server:
logSys.debug("Caught signal %d. Flushing logs" % signum) logSys.debug("Caught signal %d. Flushing logs" % signum)
self.flushLogs() self.flushLogs()
def start(self, sock, pidfile, force=False, observer=True): def start(self, sock, pidfile, force=False, observer=True, conf={}):
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
# First set the mask to only allow access to owner # First set the mask to only allow access to owner
os.umask(0077) os.umask(0077)
# Second daemonize before logging etc, because it will close all handles:
if self.__daemon: # pragma: no cover if self.__daemon: # pragma: no cover
logSys.info("Starting in daemon mode") logSys.info("Starting in daemon mode")
ret = self.__createDaemon() ret = self.__createDaemon()
if ret: # 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") logSys.info("Daemon started")
else:
logSys.error("Could not create daemon") # Install signal handlers
raise ServerInitializationError("Could not create daemon") 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. # Creates a PID file.
try: try:
@ -121,6 +145,7 @@ class Server:
# Start the communication # Start the communication
logSys.debug("Starting communication") logSys.debug("Starting communication")
try: try:
self.__asyncServer = AsyncServer(self.__transm)
self.__asyncServer.start(sock, force) self.__asyncServer.start(sock, force)
except AsyncServerException, e: except AsyncServerException, e:
logSys.error("Could not start server: %s", e) logSys.error("Could not start server: %s", e)
@ -143,17 +168,25 @@ class Server:
# communications first (which should be ok anyways since we # communications first (which should be ok anyways since we
# are exiting) # are exiting)
# See https://github.com/fail2ban/fail2ban/issues/7 # See https://github.com/fail2ban/fail2ban/issues/7
if self.__asyncServer is not None:
self.__asyncServer.stop() self.__asyncServer.stop()
self.__asyncServer = None
# Now stop all the jails # Now stop all the jails
self.stopAllJail() self.stopAllJail()
# Only now shutdown the logging. # Only now shutdown the logging.
try: if self.__logTarget is not None:
self.__loggingLock.acquire() with self.__loggingLock:
logging.shutdown() logging.shutdown()
finally:
self.__loggingLock.release() # 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): def addJail(self, name, backend):
# Add jail hereafter: # Add jail hereafter:
@ -341,6 +374,9 @@ class Server:
def getBanTimeExtra(self, name, opt): def getBanTimeExtra(self, name, opt):
return self.__jails[name].getBanTimeExtra(opt) return self.__jails[name].getBanTimeExtra(opt)
def isStarted(self):
self.__asyncServer.isActive()
def isAlive(self, jailnum=None): def isAlive(self, jailnum=None):
if jailnum is not None and len(self.__jails) != jailnum: if jailnum is not None and len(self.__jails) != jailnum:
return 0 return 0
@ -379,16 +415,15 @@ class Server:
# @param value the level # @param value the level
def setLogLevel(self, value): def setLogLevel(self, value):
value = value.upper()
with self.__loggingLock:
if self.__logLevel == value:
return
try: try:
self.__loggingLock.acquire() getLogger("fail2ban").setLevel(getattr(logging, value))
getLogger("fail2ban").setLevel( self.__logLevel = value
getattr(logging, value.upper()))
except AttributeError: except AttributeError:
raise ValueError("Invalid log level") raise ValueError("Invalid log level")
else:
self.__logLevel = value.upper()
finally:
self.__loggingLock.release()
## ##
# Get the logging level. # Get the logging level.
@ -397,11 +432,8 @@ class Server:
# @return the log level # @return the log level
def getLogLevel(self): def getLogLevel(self):
try: with self.__loggingLock:
self.__loggingLock.acquire()
return self.__logLevel return self.__logLevel
finally:
self.__loggingLock.release()
## ##
# Sets the logging target. # Sets the logging target.
@ -410,8 +442,14 @@ class Server:
# @param target the logging target # @param target the logging target
def setLogTarget(self, target): def setLogTarget(self, target):
try: with self.__loggingLock:
self.__loggingLock.acquire() # 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 # set a format which is simpler for console use
formatter = logging.Formatter("%(asctime)s %(name)-24s[%(process)d]: %(levelname)-7s %(message)s") formatter = logging.Formatter("%(asctime)s %(name)-24s[%(process)d]: %(levelname)-7s %(message)s")
if target == "SYSLOG": if target == "SYSLOG":
@ -479,8 +517,6 @@ class Server:
# Sets the logging target. # Sets the logging target.
self.__logTarget = target self.__logTarget = target
return True return True
finally:
self.__loggingLock.release()
## ##
# Sets the syslog socket. # Sets the syslog socket.
@ -488,24 +524,21 @@ class Server:
# syslogsocket is the full path to the syslog socket # syslogsocket is the full path to the syslog socket
# @param syslogsocket the syslog socket path # @param syslogsocket the syslog socket path
def setSyslogSocket(self, syslogsocket): def setSyslogSocket(self, syslogsocket):
with self.__loggingLock:
if self.__syslogSocket == syslogsocket:
return True
self.__syslogSocket = syslogsocket self.__syslogSocket = syslogsocket
# Conditionally reload, logtarget depends on socket path when SYSLOG # Conditionally reload, logtarget depends on socket path when SYSLOG
return self.__logTarget != "SYSLOG"\ return self.__logTarget != "SYSLOG"\
or self.setLogTarget(self.__logTarget) or self.setLogTarget(self.__logTarget)
def getLogTarget(self): def getLogTarget(self):
try: with self.__loggingLock:
self.__loggingLock.acquire()
return self.__logTarget return self.__logTarget
finally:
self.__loggingLock.release()
def getSyslogSocket(self): def getSyslogSocket(self):
try: with self.__loggingLock:
self.__loggingLock.acquire()
return self.__syslogSocket return self.__syslogSocket
finally:
self.__loggingLock.release()
def flushLogs(self): def flushLogs(self):
if self.__logTarget not in ['STDERR', 'STDOUT', 'SYSLOG']: if self.__logTarget not in ['STDERR', 'STDOUT', 'SYSLOG']:
@ -561,7 +594,9 @@ class Server:
# We need to set this in the parent process, so it gets inherited by the # 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 # child process, and this makes sure that it is effect even if the parent
# terminates quickly. # 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: try:
# Fork a child process so the parent can exit. This will return control # Fork a child process so the parent can exit. This will return control
@ -572,7 +607,7 @@ class Server:
# PGID. # PGID.
pid = os.fork() pid = os.fork()
except OSError, e: 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. if pid == 0: # The first child.
@ -593,7 +628,7 @@ class Server:
# preventing the daemon from ever acquiring a controlling terminal. # preventing the daemon from ever acquiring a controlling terminal.
pid = os.fork() # Fork a second child. pid = os.fork() # Fork a second child.
except OSError, e: 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. if (pid == 0): # The second child.
# Ensure that the daemon doesn't keep any directory in use. Failure # Ensure that the daemon doesn't keep any directory in use. Failure
@ -602,7 +637,8 @@ class Server:
else: else:
os._exit(0) # Exit parent (the first child) of the second child. os._exit(0) # Exit parent (the first child) of the second child.
else: 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, # 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 # for the maximum number of open files to close. If it doesn't exist, use
@ -630,7 +666,7 @@ class Server:
os.open("/dev/null", os.O_RDONLY) # standard input (0) 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 output (1)
os.open("/dev/null", os.O_RDWR) # standard error (2) os.open("/dev/null", os.O_RDWR) # standard error (2)
return True return (True,)
class ServerInitializationError(Exception): class ServerInitializationError(Exception):

View File

@ -93,6 +93,8 @@ class Transmitter:
name = command[1] name = command[1]
self.__server.stopJail(name) self.__server.stopJail(name)
return None return None
elif command[0] == "echo":
return command[1:]
elif command[0] == "sleep": elif command[0] == "sleep":
value = command[1] value = command[1]
time.sleep(float(value)) time.sleep(float(value))

View File

@ -125,6 +125,7 @@ class Utils():
timeout_expr = lambda: time.time() - stime <= timeout timeout_expr = lambda: time.time() - stime <= timeout
else: else:
timeout_expr = timeout timeout_expr = timeout
popen = None
try: try:
popen = subprocess.Popen( popen = subprocess.Popen(
realCmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell, 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): if retcode is None and not Utils.pid_exists(pgid):
retcode = signal.SIGKILL retcode = signal.SIGKILL
except OSError as e: 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 std_level = retcode == 0 and logging.DEBUG or logging.ERROR
# if we need output (to return or to log it): # if we need output (to return or to log it):
@ -164,8 +168,10 @@ class Utils():
stdout = popen.stdout.read() stdout = popen.stdout.read()
except IOError as e: except IOError as e:
logSys.error(" ... -- failed to read stdout %s", e) logSys.error(" ... -- failed to read stdout %s", e)
if stdout is not None and stdout != '': if stdout is not None and stdout != '' and std_level >= logSys.getEffectiveLevel():
logSys.log(std_level, "%s -- stdout: %r", realCmd, stdout) logSys.log(std_level, "%s -- stdout:", realCmd)
for l in stdout.splitlines():
logSys.log(std_level, " -- stdout: %r", l)
popen.stdout.close() popen.stdout.close()
if popen.stderr: if popen.stderr:
try: try:
@ -174,8 +180,10 @@ class Utils():
stderr = popen.stderr.read() stderr = popen.stderr.read()
except IOError as e: except IOError as e:
logSys.error(" ... -- failed to read stderr %s", e) logSys.error(" ... -- failed to read stderr %s", e)
if stderr is not None and stderr != '': if stderr is not None and stderr != '' and std_level >= logSys.getEffectiveLevel():
logSys.log(std_level, "%s -- stderr: %r", realCmd, stderr) logSys.log(std_level, "%s -- stderr:", realCmd)
for l in stderr.splitlines():
logSys.log(std_level, " -- stderr: %r", l)
popen.stderr.close() popen.stderr.close()
if retcode == 0: if retcode == 0:

View File

@ -65,6 +65,7 @@ class SMTPActionTest(unittest.TestCase):
self._active = True self._active = True
self._loop_thread = threading.Thread( self._loop_thread = threading.Thread(
target=asyncserver.loop, kwargs={'active': lambda: self._active}) target=asyncserver.loop, kwargs={'active': lambda: self._active})
self._loop_thread.daemon = True
self._loop_thread.start() self._loop_thread.start()
def tearDown(self): def tearDown(self):

View File

@ -44,8 +44,8 @@ class CommandActionTest(LogCaptureTestCase):
def tearDown(self): def tearDown(self):
"""Call after every test case.""" """Call after every test case."""
LogCaptureTestCase.tearDown(self)
self.__action.stop() self.__action.stop()
LogCaptureTestCase.tearDown(self)
def testSubstituteRecursiveTags(self): def testSubstituteRecursiveTags(self):
aInfo = { aInfo = {
@ -271,11 +271,11 @@ class CommandActionTest(LogCaptureTestCase):
def testCaptureStdOutErr(self): def testCaptureStdOutErr(self):
CommandAction.executeCmd('echo "How now brown cow"') 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( CommandAction.executeCmd(
'echo "The rain in Spain stays mainly in the plain" 1>&2') 'echo "The rain in Spain stays mainly in the plain" 1>&2')
self.assertLogged( 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): def testCallingMap(self):
mymap = CallingMap(callme=lambda: str(10), error=lambda: int('a'), mymap = CallingMap(callme=lambda: str(10), error=lambda: int('a'),

View File

@ -28,13 +28,15 @@ import re
import shutil import shutil
import tempfile import tempfile
import unittest import unittest
from ..client.configreader import ConfigReaderUnshared from ..client.configreader import ConfigReader, ConfigReaderUnshared
from ..client import configparserinc from ..client import configparserinc
from ..client.jailreader import JailReader from ..client.jailreader import JailReader
from ..client.filterreader import FilterReader from ..client.filterreader import FilterReader
from ..client.jailsreader import JailsReader from ..client.jailsreader import JailsReader
from ..client.actionreader import ActionReader from ..client.actionreader import ActionReader
from ..client.configurator import Configurator from ..client.configurator import Configurator
from ..server.mytime import MyTime
from ..version import version
from .utils import LogCaptureTestCase from .utils import LogCaptureTestCase
TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files")
@ -253,6 +255,34 @@ class JailReaderTest(LogCaptureTestCase):
result = JailReader.extractOptions(option) result = JailReader.extractOptions(option)
self.assertEqual(expected, result) 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 <HOST>$'),
('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', '<wrong>'), useragent)
self.assertEqual(act[3], ['set', 'blocklisttest', 'action', 'mynetwatchman', 'agent', useragent])
def testGlob(self): def testGlob(self):
d = tempfile.mkdtemp(prefix="f2b-temp") d = tempfile.mkdtemp(prefix="f2b-temp")
# Generate few files # Generate few files
@ -596,6 +626,12 @@ class JailsReaderTest(LogCaptureTestCase):
# by default we have lots of jails ;) # by default we have lots of jails ;)
self.assertTrue(len(comm_commands)) 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 # and we know even some of them by heart
for j in ['sshd', 'recidive']: for j in ['sshd', 'recidive']:
# by default we have 'auto' backend ATM # by default we have 'auto' backend ATM

View File

@ -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)

View File

@ -23,19 +23,7 @@ __author__ = "Serg Brester"
__copyright__ = "Copyright (c) 2015 Serg G. Brester (sebres), 2008- Fail2Ban Contributors" __copyright__ = "Copyright (c) 2015 Serg G. Brester (sebres), 2008- Fail2Ban Contributors"
__license__ = "GPL" __license__ = "GPL"
from __builtin__ import open as fopen
import unittest
import getpass
import os 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 import fail2banregex
from ..client.fail2banregex import Fail2banRegex, get_opt_parser, output from ..client.fail2banregex import Fail2banRegex, get_opt_parser, output

View File

@ -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 # match UTF-8 in SessionID
# failJSON: { "time": "2015-05-25T07:52:36", "match": true, "host": "10.250.251.252" } # 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" [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'.

View File

@ -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/<NOSRV> -1/-1/-1/-1/0 401 248 - - PR-- 0/0/0/0/0 0/0 "GET / HTTP/1.1"

View File

@ -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" } # 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 <some@nonexistant.tld>: Sender address rejected: Domain not found; from=<some@nonexistant.tld> to=<goodguy@example.com> proto=ESMTP helo=<1-2-3-4.example.com> 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 <some@nonexistant.tld>: Sender address rejected: Domain not found; from=<some@nonexistant.tld> to=<goodguy@example.com> 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 <User>: Helo command rejected: need fully-qualified hostname; proto=SMTP helo=<User>

View File

@ -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

View File

@ -62,25 +62,18 @@ class TransmitterBase(unittest.TestCase):
def setUp(self): def setUp(self):
"""Call before every test case.""" """Call before every test case."""
#super(TransmitterBase, self).setUp()
self.transm = self.server._Server__transm self.transm = self.server._Server__transm
self.tmp_files = [] # To test thransmitter we don't need to start server...
sock_fd, sock_name = tempfile.mkstemp('fail2ban.sock', 'transmitter') #self.server.start('/dev/null', '/dev/null', force=False)
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)
self.jailName = "TestJail1" self.jailName = "TestJail1"
self.server.addJail(self.jailName, FAST_BACKEND) self.server.addJail(self.jailName, FAST_BACKEND)
def tearDown(self): def tearDown(self):
"""Call after every test case.""" """Call after every test case."""
# stop jails, etc.
self.server.quit() self.server.quit()
for f in self.tmp_files: #super(TransmitterBase, self).tearDown()
if os.path.exists(f):
os.remove(f)
def setGetTest(self, cmd, inValue, outValue=(None,), outCode=0, jail=None, repr_=False): def setGetTest(self, cmd, inValue, outValue=(None,), outCode=0, jail=None, repr_=False):
"""Process set/get commands and compare both return values """Process set/get commands and compare both return values
@ -160,7 +153,6 @@ class Transmitter(TransmitterBase):
def setUp(self): def setUp(self):
self.server = TestServer() self.server = TestServer()
self.server_start_args = {'force':False, 'observer':False}
super(Transmitter, self).setUp() super(Transmitter, self).setUp()
def testStopServer(self): def testStopServer(self):
@ -793,11 +785,10 @@ class TransmitterLogging(TransmitterBase):
def setUp(self): def setUp(self):
self.server = Server() self.server = Server()
super(TransmitterLogging, self).setUp()
self.server.setLogTarget("/dev/null") self.server.setLogTarget("/dev/null")
self.server.setLogLevel("CRITICAL") self.server.setLogLevel("CRITICAL")
self.server.setSyslogSocket("auto") self.server.setSyslogSocket("auto")
self.server_start_args = {'force':False, 'observer':False}
super(TransmitterLogging, self).setUp()
def testLogTarget(self): def testLogTarget(self):
logTargets = [] logTargets = []
@ -915,17 +906,6 @@ class TransmitterLogging(TransmitterBase):
self.setGetTest("bantime.overalljails", "true", "true", jail=self.jailName) 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): class JailTests(unittest.TestCase):
def testLongName(self): def testLongName(self):
@ -985,3 +965,21 @@ class LoggingTests(LogCaptureTestCase):
sys.__excepthook__ = prev_exchook sys.__excepthook__ = prev_exchook
self.assertEqual(len(x), 1) self.assertEqual(len(x), 1)
self.assertEqual(x[0][0], RuntimeError) 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)

View File

@ -26,10 +26,14 @@ import logging
import optparse import optparse
import os import os
import re import re
import tempfile
import shutil
import sys import sys
import time import time
import unittest import unittest
from StringIO import StringIO from StringIO import StringIO
from functools import wraps
from ..helpers import getLogger from ..helpers import getLogger
from ..server.filter import DNSUtils from ..server.filter import DNSUtils
@ -50,6 +54,10 @@ if not CONFIG_DIR:
else: else:
CONFIG_DIR = '/etc/fail2ban' 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): class F2B(optparse.Values):
def __init__(self, opts={}): def __init__(self, opts={}):
@ -71,6 +79,17 @@ class F2B(optparse.Values):
return wtime 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): def initTests(opts):
unittest.F2B = F2B(opts) unittest.F2B = F2B(opts)
# --fast : # --fast :
@ -146,6 +165,7 @@ def gatherTests(regexps=None, opts=None):
from . import databasetestcase from . import databasetestcase
from . import observertestcase from . import observertestcase
from . import samplestestcase from . import samplestestcase
from . import fail2banclienttestcase
from . import fail2banregextestcase from . import fail2banregextestcase
if not regexps: # pragma: no cover if not regexps: # pragma: no cover
@ -170,7 +190,6 @@ def gatherTests(regexps=None, opts=None):
# Server # Server
tests.addTest(unittest.makeSuite(servertestcase.Transmitter)) tests.addTest(unittest.makeSuite(servertestcase.Transmitter))
tests.addTest(unittest.makeSuite(servertestcase.TransmitterWithObserver))
tests.addTest(unittest.makeSuite(servertestcase.JailTests)) tests.addTest(unittest.makeSuite(servertestcase.JailTests))
tests.addTest(unittest.makeSuite(servertestcase.RegexTests)) tests.addTest(unittest.makeSuite(servertestcase.RegexTests))
tests.addTest(unittest.makeSuite(servertestcase.LoggingTests)) tests.addTest(unittest.makeSuite(servertestcase.LoggingTests))
@ -228,6 +247,9 @@ def gatherTests(regexps=None, opts=None):
# Filter Regex tests with sample logs # Filter Regex tests with sample logs
tests.addTest(unittest.makeSuite(samplestestcase.FilterSamplesRegex)) 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 # bin/fail2ban-regex
tests.addTest(unittest.makeSuite(fail2banregextestcase.Fail2banRegexTest)) tests.addTest(unittest.makeSuite(fail2banregextestcase.Fail2banRegexTest))
@ -298,13 +320,16 @@ class LogCaptureTestCase(unittest.TestCase):
# Let's log everything into a string # Let's log everything into a string
self._log = StringIO() self._log = StringIO()
logSys.handlers = [logging.StreamHandler(self._log)] 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.handlers += self._old_handlers
logSys.debug('='*10 + ' %s ' + '='*20, self.id())
logSys.setLevel(getattr(logging, 'DEBUG')) logSys.setLevel(getattr(logging, 'DEBUG'))
def tearDown(self): def tearDown(self):
"""Call after every test case.""" """Call after every test case."""
# print "O: >>%s<<" % self._log.getvalue() # print "O: >>%s<<" % self._log.getvalue()
self.pruneLog()
logSys = getLogger("fail2ban") logSys = getLogger("fail2ban")
logSys.handlers = self._old_handlers logSys.handlers = self._old_handlers
logSys.level = self._old_level 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)) 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): def getLog(self):
return self._log.getvalue() return self._log.getvalue()

View File

@ -6,11 +6,9 @@
# https://github.com/fail2ban/fail2ban/blob/debian/debian/fail2ban.logrotate # https://github.com/fail2ban/fail2ban/blob/debian/debian/fail2ban.logrotate
/var/log/fail2ban.log { /var/log/fail2ban.log {
rotate 7
missingok missingok
notifempty notifempty
compress
postrotate postrotate
/usr/bin/fail2ban-client flushlogs 1>/dev/null || true /usr/bin/fail2ban-client flushlogs >/dev/null || true
endscript endscript
} }

View File

@ -34,19 +34,19 @@ start() {
# remove stalled sock file after system crash # remove stalled sock file after system crash
# bug 347477 # bug 347477
rm -f /var/run/fail2ban/fail2ban.sock || return 1 rm -f /var/run/fail2ban/fail2ban.sock || return 1
${FAIL2BAN} start &> /dev/null ${FAIL2BAN} start
eend $? "Failed to start fail2ban" eend $? "Failed to start fail2ban"
} }
stop() { stop() {
ebegin "Stopping fail2ban" ebegin "Stopping fail2ban"
${FAIL2BAN} stop &> /dev/null ${FAIL2BAN} stop
eend $? "Failed to stop fail2ban" eend $? "Failed to stop fail2ban"
} }
reload() { reload() {
ebegin "Reloading fail2ban" ebegin "Reloading fail2ban"
${FAIL2BAN} reload > /dev/null ${FAIL2BAN} reload
eend $? "Failed to reload fail2ban" eend $? "Failed to reload fail2ban"
} }