mirror of https://github.com/fail2ban/fail2ban
Merge remote-tracking branch 'f2b-perfom-prepare-716-cs' into ban-time-incr (+ conflicts resolved)
commit
bf0adc1fdf
|
@ -38,7 +38,7 @@ script:
|
|||
# Keep the legacy setup.py test approach of checking coverage for python2
|
||||
- if [[ "$F2B_PY_2" ]]; then coverage run setup.py test; fi
|
||||
# Coverage doesn't pick up setup.py test with python3, so run it directly
|
||||
- if [[ "$F2B_PY_3" ]]; then coverage run bin/fail2ban-testcases; fi
|
||||
- if [[ "$F2B_PY_3" ]]; then coverage run bin/fail2ban-testcases -l debug; fi
|
||||
# Use $VENV_BIN (not python) or else sudo will always run the system's python (2.7)
|
||||
- sudo $VENV_BIN/pip install .
|
||||
after_success:
|
||||
|
|
17
ChangeLog
17
ChangeLog
|
@ -41,6 +41,11 @@ ver. 0.9.4 (2015/XX/XXX) - wanna-be-released
|
|||
for python version < 3.x (gh-1248)
|
||||
* Use postfix_log logpath for postfix-rbl jail
|
||||
* filters.d/postfix.conf - add 'Sender address rejected: Domain not found' failregex
|
||||
* use `fail2ban_agent` as user-agent in actions badips, blocklist_de, etc (gh-1271)
|
||||
* Fix ignoring the sender option by action_mw, action_mwl and action_c_mwl
|
||||
* Changed filter.d/asterisk regex for "Call from ..." (few vulnerable now)
|
||||
* Removed compression and rotation count from logrotate (inherit them from
|
||||
the global logrotate config)
|
||||
|
||||
- New Features:
|
||||
* New interpolation feature for definition config readers - `<known/parameter>`
|
||||
|
@ -50,6 +55,9 @@ ver. 0.9.4 (2015/XX/XXX) - wanna-be-released
|
|||
filter.d/*.local file.
|
||||
As extension to interpolation `%(known/parameter)s`, that does not works for
|
||||
filter and action init parameters
|
||||
* New actions:
|
||||
- nftables-multiport and nftables-allports - filtering using nftables
|
||||
framework. Note: it requires a pre-existing chain for the filtering rule.
|
||||
* New filters:
|
||||
- openhab - domotic software authentication failure with the
|
||||
rest api and web interface (gh-1223)
|
||||
|
@ -57,10 +65,13 @@ ver. 0.9.4 (2015/XX/XXX) - wanna-be-released
|
|||
request processing rate (ngx_http_limit_req_module)
|
||||
- murmur - ban hosts that repeatedly attempt to connect to
|
||||
murmur/mumble-server with an invalid server password or certificate.
|
||||
- haproxy-http-auth - filter to match failed HTTP Authentications against a
|
||||
HAProxy server
|
||||
* New jails:
|
||||
- murmur - bans TCP and UDP from the bad host on the default murmur port.
|
||||
* sshd filter got new failregex to match "maximum authentication
|
||||
attempts exceeded" (introduced in openssh 6.8)
|
||||
* Added filter for Mac OS screen sharing (VNC) daemon
|
||||
|
||||
- Enhancements:
|
||||
* Do not rotate empty log files
|
||||
|
@ -82,6 +93,12 @@ ver. 0.9.4 (2015/XX/XXX) - wanna-be-released
|
|||
* Performance improvements while monitoring large number of files (gh-1265).
|
||||
Use associative array (dict) for monitored log files to speed up lookup
|
||||
operations. Thanks @kshetragia
|
||||
* Specified that fail2ban is PartOf iptables.service firewalld.service in
|
||||
.service file -- would reload fail2ban if those services are restarted
|
||||
* Provides new default `fail2ban_version` and interpolation variable
|
||||
`fail2ban_agent` in jail.conf
|
||||
* Enhance filter 'postfix' to ban incoming SMTP client with no fqdn hostname
|
||||
|
||||
|
||||
ver. 0.9.3 (2015/08/01) - lets-all-stay-friends
|
||||
----------
|
||||
|
|
4
MANIFEST
4
MANIFEST
|
@ -165,7 +165,11 @@ fail2ban/client/configparserinc.py
|
|||
fail2ban/client/configreader.py
|
||||
fail2ban/client/configurator.py
|
||||
fail2ban/client/csocket.py
|
||||
fail2ban/client/fail2banclient.py
|
||||
fail2ban/client/fail2bancmdline.py
|
||||
fail2ban/client/fail2banreader.py
|
||||
fail2ban/client/fail2banregex.py
|
||||
fail2ban/client/fail2banserver.py
|
||||
fail2ban/client/filterreader.py
|
||||
fail2ban/client/jailreader.py
|
||||
fail2ban/client/jailsreader.py
|
||||
|
|
|
@ -18,456 +18,20 @@
|
|||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
__author__ = "Cyril Jaquier"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
"""
|
||||
Fail2Ban reads log file that contains password failure report
|
||||
and bans the corresponding IP addresses using firewall rules.
|
||||
|
||||
This tools starts/stops fail2ban server or does client/server communication,
|
||||
to change/read parameters of the server or jails.
|
||||
|
||||
"""
|
||||
|
||||
__author__ = "Fail2Ban Developers"
|
||||
__copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halchenko, 2014-2016 Serg G. Brester"
|
||||
__license__ = "GPL"
|
||||
|
||||
import getopt
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
import re
|
||||
import shlex
|
||||
import signal
|
||||
import socket
|
||||
import string
|
||||
import sys
|
||||
import time
|
||||
from fail2ban.client.fail2banclient import exec_command_line, sys
|
||||
|
||||
from fail2ban.version import version
|
||||
from fail2ban.protocol import printFormatted
|
||||
from fail2ban.client.csocket import CSocket
|
||||
from fail2ban.client.configurator import Configurator
|
||||
from fail2ban.client.beautifier import Beautifier
|
||||
from fail2ban.helpers import getLogger
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = getLogger("fail2ban")
|
||||
|
||||
##
|
||||
#
|
||||
# @todo This class needs cleanup.
|
||||
|
||||
class Fail2banClient:
|
||||
|
||||
SERVER = "fail2ban-server"
|
||||
PROMPT = "fail2ban> "
|
||||
|
||||
def __init__(self):
|
||||
self.__argv = None
|
||||
self.__stream = None
|
||||
self.__configurator = Configurator()
|
||||
self.__conf = dict()
|
||||
self.__conf["conf"] = "/etc/fail2ban"
|
||||
self.__conf["dump"] = False
|
||||
self.__conf["force"] = False
|
||||
self.__conf["background"] = True
|
||||
self.__conf["verbose"] = 1
|
||||
self.__conf["interactive"] = False
|
||||
self.__conf["socket"] = None
|
||||
self.__conf["pidfile"] = None
|
||||
|
||||
def dispVersion(self):
|
||||
print "Fail2Ban v" + version
|
||||
print
|
||||
print "Copyright (c) 2004-2008 Cyril Jaquier, 2008- Fail2Ban Contributors"
|
||||
print "Copyright of modifications held by their respective authors."
|
||||
print "Licensed under the GNU General Public License v2 (GPL)."
|
||||
print
|
||||
print "Written by Cyril Jaquier <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)
|
||||
if __name__ == "__main__":
|
||||
exec_command_line(sys.argv)
|
||||
|
|
|
@ -18,123 +18,20 @@
|
|||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
__author__ = "Cyril Jaquier"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
"""
|
||||
Fail2Ban reads log file that contains password failure report
|
||||
and bans the corresponding IP addresses using firewall rules.
|
||||
|
||||
This tools starts/stops fail2ban server or does client/server communication,
|
||||
to change/read parameters of the server or jails.
|
||||
|
||||
"""
|
||||
|
||||
__author__ = "Fail2Ban Developers"
|
||||
__copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halchenko, 2014-2016 Serg G. Brester"
|
||||
__license__ = "GPL"
|
||||
|
||||
import getopt
|
||||
import os
|
||||
import sys
|
||||
|
||||
from fail2ban.version import version
|
||||
from fail2ban.server.server import Server
|
||||
from fail2ban.helpers import getLogger
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = getLogger("fail2ban")
|
||||
|
||||
##
|
||||
# \mainpage Fail2Ban
|
||||
#
|
||||
# \section Introduction
|
||||
#
|
||||
# Fail2ban is designed to protect your server against brute force attacks.
|
||||
# Its first goal was to protect a SSH server.
|
||||
|
||||
class Fail2banServer:
|
||||
|
||||
def __init__(self):
|
||||
self.__server = None
|
||||
self.__argv = None
|
||||
self.__conf = dict()
|
||||
self.__conf["background"] = True
|
||||
self.__conf["force"] = False
|
||||
self.__conf["socket"] = "/var/run/fail2ban/fail2ban.sock"
|
||||
self.__conf["pidfile"] = "/var/run/fail2ban/fail2ban.pid"
|
||||
|
||||
def dispVersion(self):
|
||||
print "Fail2Ban v" + version
|
||||
print
|
||||
print "Copyright (c) 2004-2008 Cyril Jaquier, 2008- Fail2Ban Contributors"
|
||||
print "Copyright of modifications held by their respective authors."
|
||||
print "Licensed under the GNU General Public License v2 (GPL)."
|
||||
print
|
||||
print "Written by Cyril Jaquier <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
|
||||
from fail2ban.client.fail2banserver import exec_command_line, sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
server = Fail2banServer()
|
||||
if server.start(sys.argv):
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(-1)
|
||||
exec_command_line(sys.argv)
|
||||
|
|
|
@ -119,10 +119,14 @@ else:
|
|||
|
||||
# Custom log format for the verbose tests runs
|
||||
if verbosity > 1: # pragma: no cover
|
||||
stdout.setFormatter(Formatter(' %(asctime)-15s %(thread)s' + fmt))
|
||||
else: # pragma: no cover
|
||||
# just prefix with the space
|
||||
stdout.setFormatter(Formatter(fmt))
|
||||
if verbosity > 3:
|
||||
fmt = ' | %(module)15.15s-%(levelno)-2d: %(funcName)-20.20s |' + fmt
|
||||
if verbosity > 2:
|
||||
fmt = ' +%(relativeCreated)5d %(thread)X %(levelname)-5.5s' + fmt
|
||||
else:
|
||||
fmt = ' %(asctime)-15s %(thread)X %(levelname)-5.5s' + fmt
|
||||
#
|
||||
stdout.setFormatter(Formatter(fmt))
|
||||
logSys.addHandler(stdout)
|
||||
|
||||
#
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
[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]
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ import sys
|
|||
if sys.version_info < (2, 7):
|
||||
raise ImportError("badips.py action requires Python >= 2.7")
|
||||
import json
|
||||
from functools import partial
|
||||
import threading
|
||||
import logging
|
||||
if sys.version_info >= (3, ):
|
||||
|
@ -33,7 +32,6 @@ else:
|
|||
from urllib import urlencode
|
||||
|
||||
from fail2ban.server.actions import ActionBase
|
||||
from fail2ban.version import version as f2bVersion
|
||||
|
||||
|
||||
class BadIPsAction(ActionBase):
|
||||
|
@ -72,6 +70,9 @@ class BadIPsAction(ActionBase):
|
|||
updateperiod : int, optional
|
||||
Time in seconds between updating bad IPs blacklist.
|
||||
Default 900 (15 minutes)
|
||||
agent : str, optional
|
||||
User agent transmitted to server.
|
||||
Default `Fail2Ban/ver.`
|
||||
|
||||
Raises
|
||||
------
|
||||
|
@ -80,13 +81,14 @@ class BadIPsAction(ActionBase):
|
|||
"""
|
||||
|
||||
_badips = "http://www.badips.com"
|
||||
_Request = partial(
|
||||
Request, headers={'User-Agent': "Fail2Ban %s" % f2bVersion})
|
||||
def _Request(self, url, **argv):
|
||||
return Request(url, headers={'User-Agent': self.agent}, **argv)
|
||||
|
||||
def __init__(self, jail, name, category, score=3, age="24h", key=None,
|
||||
banaction=None, bancategory=None, bankey=None, updateperiod=900):
|
||||
banaction=None, bancategory=None, bankey=None, updateperiod=900, agent="Fail2Ban"):
|
||||
super(BadIPsAction, self).__init__(jail, name)
|
||||
|
||||
self.agent = agent
|
||||
self.category = category
|
||||
self.score = score
|
||||
self.age = age
|
||||
|
|
|
@ -54,7 +54,7 @@ actioncheck =
|
|||
# Tags: See jail.conf(5) man page
|
||||
# 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
|
||||
# Notes.: command executed when unbanning an IP. Take care that the
|
||||
|
|
|
@ -111,13 +111,17 @@ myip = `ip -4 addr show dev eth0 | grep inet | head -n 1 | sed -r 's/.*inet ([0-
|
|||
#
|
||||
protocol = tcp
|
||||
|
||||
# Option: agent
|
||||
# Default: Fail2ban
|
||||
agent = Fail2ban
|
||||
|
||||
# Option: getcmd
|
||||
# Notes.: A command to fetch a URL. Should output page to STDOUT
|
||||
# Values: CMD Default: wget
|
||||
#
|
||||
getcmd = wget --no-verbose --tries=3 --waitretry=10 --connect-timeout=10 --read-timeout=60 --retry-connrefused --output-document=- --user-agent=Fail2Ban
|
||||
getcmd = wget --no-verbose --tries=3 --waitretry=10 --connect-timeout=10 --read-timeout=60 --retry-connrefused --output-document=- --user-agent=<agent>
|
||||
# Alternative value:
|
||||
# getcmd = curl --silent --show-error --retry 3 --connect-timeout 10 --max-time 60 --user-agent Fail2Ban
|
||||
# getcmd = curl --silent --show-error --retry 3 --connect-timeout 10 --max-time 60 --user-agent <agent>
|
||||
|
||||
# Option: srcport
|
||||
# Notes.: The source port of the attack. You're unlikely to have this info, so
|
||||
|
|
|
@ -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]
|
|
@ -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
|
|
@ -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]
|
|
@ -19,7 +19,7 @@ iso8601 = \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+[+-]\d{4}
|
|||
log_prefix= (?:NOTICE|SECURITY)%(__pid_re)s:?(?:\[C-[\da-f]*\])? \S+:\d*( in \w+:)?
|
||||
|
||||
failregex = ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Registration from '[^']*' failed for '<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 No registration for peer '[^']*' \(from <HOST>\)$
|
||||
^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Host <HOST> failed MD5 authentication for '[^']*' \([^)]+\)$
|
||||
|
|
|
@ -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 =
|
|
@ -15,6 +15,7 @@ _daemon = postfix/(submission/)?smtp(d|s)
|
|||
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 : 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: 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>\]:?$
|
||||
|
|
|
@ -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 =
|
|
@ -184,6 +184,9 @@ chain = INPUT
|
|||
# Usually should be overridden in a particular jail
|
||||
port = 0:65535
|
||||
|
||||
# Format of user-agent https://tools.ietf.org/html/rfc7231#section-5.5.3
|
||||
fail2ban_agent = Fail2Ban/%(fail2ban_version)s
|
||||
|
||||
#
|
||||
# Action shortcuts. To be used to define action parameter
|
||||
|
||||
|
@ -199,12 +202,12 @@ action_ = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s
|
|||
|
||||
# ban & send an e-mail with whois report to the destemail.
|
||||
action_mw = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
|
||||
%(mta)s-whois[name=%(__name__)s, dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s"]
|
||||
%(mta)s-whois[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s"]
|
||||
|
||||
# ban & send an e-mail with whois report and relevant log lines
|
||||
# to the destemail.
|
||||
action_mwl = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
|
||||
%(mta)s-whois-lines[name=%(__name__)s, dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"]
|
||||
%(mta)s-whois-lines[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"]
|
||||
|
||||
# See the IMPORTANT note in action.d/xarf-login-attack for when to use this action
|
||||
#
|
||||
|
@ -216,7 +219,7 @@ action_xarf = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(po
|
|||
# ban IP on CloudFlare & send an e-mail with whois report and relevant log lines
|
||||
# to the destemail.
|
||||
action_cf_mwl = cloudflare[cfuser="%(cfemail)s", cftoken="%(cfapikey)s"]
|
||||
%(mta)s-whois-lines[name=%(__name__)s, dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"]
|
||||
%(mta)s-whois-lines[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"]
|
||||
|
||||
# Report block via blocklist.de fail2ban reporting service API
|
||||
#
|
||||
|
@ -225,7 +228,7 @@ action_cf_mwl = cloudflare[cfuser="%(cfemail)s", cftoken="%(cfapikey)s"]
|
|||
# [Init]
|
||||
# blocklist_de_apikey = {api key from registration]
|
||||
#
|
||||
action_blocklist_de = blocklist_de[email="%(sender)s", service=%(filter)s, apikey="%(blocklist_de_apikey)s"]
|
||||
action_blocklist_de = blocklist_de[email="%(sender)s", service=%(filter)s, apikey="%(blocklist_de_apikey)s", agent="%(fail2ban_agent)s"]
|
||||
|
||||
# Report ban via badips.com, and use as blacklist
|
||||
#
|
||||
|
@ -235,7 +238,11 @@ action_blocklist_de = blocklist_de[email="%(sender)s", service=%(filter)s, apik
|
|||
# NOTE: This action relies on banaction being present on start and therefore
|
||||
# should be last action defined for a jail.
|
||||
#
|
||||
action_badips = badips.py[category="%(name)s", banaction="%(banaction)s"]
|
||||
action_badips = badips.py[category="%(__name__)s", banaction="%(banaction)s", agent="%(fail2ban_agent)s"]
|
||||
#
|
||||
# Report ban via badips.com (uses action.d/badips.conf for reporting only)
|
||||
#
|
||||
action_badips_report = badips[category="%(__name__)s", agent="%(fail2ban_agent)s"]
|
||||
|
||||
# Choose default action. To change, just override value of 'action' with the
|
||||
# interpolation to the chosen action shortcut (e.g. action_mw, action_mwl, etc) in jail.local
|
||||
|
@ -278,7 +285,6 @@ backend = %(dropbear_backend)s
|
|||
|
||||
port = ssh
|
||||
logpath = %(auditd_log)s
|
||||
maxretry = 5
|
||||
|
||||
|
||||
#
|
||||
|
@ -304,7 +310,6 @@ maxretry = 1
|
|||
|
||||
port = http,https
|
||||
logpath = %(apache_error_log)s
|
||||
maxretry = 6
|
||||
|
||||
|
||||
[apache-overflows]
|
||||
|
@ -342,18 +347,21 @@ port = http,https
|
|||
logpath = %(apache_error_log)s
|
||||
maxretry = 2
|
||||
|
||||
|
||||
[apache-shellshock]
|
||||
|
||||
port = http,https
|
||||
logpath = %(apache_error_log)s
|
||||
maxretry = 1
|
||||
|
||||
|
||||
[openhab-auth]
|
||||
|
||||
filter = openhab
|
||||
action = iptables-allports[name=NoAuthFailures]
|
||||
logpath = /opt/openhab/logs/request.log
|
||||
|
||||
|
||||
[nginx-http-auth]
|
||||
|
||||
port = http,https
|
||||
|
@ -373,6 +381,7 @@ port = http,https
|
|||
logpath = %(nginx_error_log)s
|
||||
maxretry = 2
|
||||
|
||||
|
||||
# Ban attackers that try to use PHP's URL-fopen() functionality
|
||||
# through GET/POST variables. - Experimental, with more than a year
|
||||
# of usage in production environments.
|
||||
|
@ -437,7 +446,6 @@ logpath = /var/log/sogo/sogo.log
|
|||
|
||||
logpath = /var/log/tine20/tine20.log
|
||||
port = http,https
|
||||
maxretry = 5
|
||||
|
||||
|
||||
#
|
||||
|
@ -458,7 +466,6 @@ logpath = /var/log/tomcat*/catalina.out
|
|||
|
||||
[monit]
|
||||
#Ban clients brute-forcing the monit gui login
|
||||
filter = monit
|
||||
port = 2812
|
||||
logpath = /var/log/monit
|
||||
|
||||
|
@ -511,7 +518,6 @@ backend = %(proftpd_backend)s
|
|||
port = ftp,ftp-data,ftps,ftps-data
|
||||
logpath = %(pureftpd_log)s
|
||||
backend = %(pureftpd_backend)s
|
||||
maxretry = 6
|
||||
|
||||
|
||||
[gssftpd]
|
||||
|
@ -519,7 +525,6 @@ maxretry = 6
|
|||
port = ftp,ftp-data,ftps,ftps-data
|
||||
logpath = %(syslog_daemon)s
|
||||
backend = %(syslog_backend)s
|
||||
maxretry = 6
|
||||
|
||||
|
||||
[wuftpd]
|
||||
|
@ -527,7 +532,6 @@ maxretry = 6
|
|||
port = ftp,ftp-data,ftps,ftps-data
|
||||
logpath = %(wuftpd_log)s
|
||||
backend = %(wuftpd_backend)s
|
||||
maxretry = 6
|
||||
|
||||
|
||||
[vsftpd]
|
||||
|
@ -762,7 +766,6 @@ maxretry = 10
|
|||
port = 3306
|
||||
logpath = %(mysql_log)s
|
||||
backend = %(mysql_backend)s
|
||||
maxretry = 5
|
||||
|
||||
|
||||
# Jail for more extended banning of persistent abusers
|
||||
|
@ -778,7 +781,6 @@ logpath = /var/log/fail2ban.log
|
|||
banaction = %(banaction_allports)s
|
||||
bantime = 1w
|
||||
findtime = 1d
|
||||
maxretry = 5
|
||||
|
||||
|
||||
# Generic filter for PAM. Has to be used with action which bans all
|
||||
|
@ -824,7 +826,6 @@ action = %(banaction)s[name=%(__name__)s-tcp, port="%(tcpport)s", protocol="tcp
|
|||
# nobody except your own Nagios server should ever probe nrpe
|
||||
[nagios]
|
||||
|
||||
enabled = false
|
||||
logpath = %(syslog_daemon)s ; nrpe.cfg may define a different log_facility
|
||||
backend = %(syslog_backend)s
|
||||
maxretry = 1
|
||||
|
@ -832,18 +833,14 @@ maxretry = 1
|
|||
|
||||
[oracleims]
|
||||
# see "oracleims" filter file for configuration requirement for Oracle IMS v6 and above
|
||||
enabled = false
|
||||
logpath = /opt/sun/comms/messaging64/log/mail.log_current
|
||||
maxretry = 6
|
||||
banaction = %(banaction_allports)s
|
||||
|
||||
[directadmin]
|
||||
enabled = false
|
||||
logpath = /var/log/directadmin/login.log
|
||||
port = 2222
|
||||
|
||||
[portsentry]
|
||||
enabled = false
|
||||
logpath = /var/lib/portsentry/portsentry.history
|
||||
maxretry = 1
|
||||
|
||||
|
@ -864,7 +861,19 @@ findtime = 1
|
|||
[murmur]
|
||||
# AKA mumble-server
|
||||
port = 64738
|
||||
filter = murmur
|
||||
action = %(banaction)s[name=%(__name__)s-tcp, port="%(port)s", protocol=tcp, chain="%(chain)s", actname=%(banaction)s-tcp]
|
||||
%(banaction)s[name=%(__name__)s-udp, port="%(port)s", protocol=udp, chain="%(chain)s", actname=%(banaction)s-udp]
|
||||
logpath = /var/log/mumble-server/mumble-server.log
|
||||
|
||||
|
||||
[screensharingd]
|
||||
# For Mac OS Screen Sharing Service (VNC)
|
||||
logpath = /var/log/system.log
|
||||
logencoding = utf-8
|
||||
|
||||
[haproxy-http-auth]
|
||||
# HAProxy by default doesn't log to file you'll need to set it up to forward
|
||||
# logs to a syslog server which would then write them to disk.
|
||||
# See "haproxy-http-auth" filter for a brief cautionary note when setting
|
||||
# maxretry and findtime.
|
||||
logpath = /var/log/haproxy.log
|
||||
|
|
|
@ -68,6 +68,8 @@ class Beautifier:
|
|||
msg = "Added jail " + response
|
||||
elif inC[0] == "flushlogs":
|
||||
msg = "logs: " + response
|
||||
elif inC[0] == "echo":
|
||||
msg = ' '.join(msg)
|
||||
elif inC[0:1] == ['status']:
|
||||
if len(inC) > 1:
|
||||
# Display information
|
||||
|
|
|
@ -208,7 +208,7 @@ class ConfigReaderUnshared(SafeConfigParserWithIncludes):
|
|||
# 1 -> the name of the option
|
||||
# 2 -> the default value for the option
|
||||
|
||||
def getOptions(self, sec, options, pOptions=None):
|
||||
def getOptions(self, sec, options, pOptions=None, shouldExist=False):
|
||||
values = dict()
|
||||
for option in options:
|
||||
try:
|
||||
|
@ -222,6 +222,8 @@ class ConfigReaderUnshared(SafeConfigParserWithIncludes):
|
|||
continue
|
||||
values[option[1]] = v
|
||||
except NoSectionError, e:
|
||||
if shouldExist:
|
||||
raise
|
||||
# No "Definition" section or wrong basedir
|
||||
logSys.error(e)
|
||||
values[option[1]] = option[2]
|
||||
|
|
|
@ -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)
|
||||
|
|
@ -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
|
|
@ -40,8 +40,13 @@ class Fail2banReader(ConfigReader):
|
|||
ConfigReader.read(self, "fail2ban")
|
||||
|
||||
def getEarlyOptions(self):
|
||||
opts = [["string", "socket", "/var/run/fail2ban/fail2ban.sock"],
|
||||
["string", "pidfile", "/var/run/fail2ban/fail2ban.pid"]]
|
||||
opts = [
|
||||
["string", "socket", "/var/run/fail2ban/fail2ban.sock"],
|
||||
["string", "pidfile", "/var/run/fail2ban/fail2ban.pid"],
|
||||
["string", "loglevel", "INFO"],
|
||||
["string", "logtarget", "/var/log/fail2ban.log"],
|
||||
["string", "syslogsocket", "auto"]
|
||||
]
|
||||
return ConfigReader.getOptions(self, "Definition", opts)
|
||||
|
||||
def getOptions(self):
|
||||
|
|
|
@ -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)
|
|
@ -32,6 +32,7 @@ import re
|
|||
from .configreader import ConfigReaderUnshared, ConfigReader
|
||||
from .filterreader import FilterReader
|
||||
from .actionreader import ActionReader
|
||||
from ..version import version
|
||||
from ..helpers import getLogger
|
||||
from ..helpers import splitcommaspace
|
||||
|
||||
|
@ -115,8 +116,12 @@ class JailReader(ConfigReader):
|
|||
["string", "filter", ""],
|
||||
["string", "action", ""]]
|
||||
|
||||
# Before interpolation (substitution) add static options always available as default:
|
||||
defsec = self._cfg.get_defaults()
|
||||
defsec["fail2ban_version"] = version
|
||||
|
||||
# Read first options only needed for merge defaults ('known/...' from filter):
|
||||
self.__opts = ConfigReader.getOptions(self, self.__name, opts1st)
|
||||
self.__opts = ConfigReader.getOptions(self, self.__name, opts1st, shouldExist=True)
|
||||
if not self.__opts:
|
||||
return False
|
||||
|
||||
|
|
|
@ -26,6 +26,9 @@ __license__ = "GPL"
|
|||
|
||||
import textwrap
|
||||
|
||||
def output(s):
|
||||
print(s)
|
||||
|
||||
##
|
||||
# Describes the protocol used to communicate with the server.
|
||||
|
||||
|
@ -42,11 +45,13 @@ CSPROTO = dotdict({
|
|||
protocol = [
|
||||
['', "BASIC", ""],
|
||||
["start", "starts the server and the jails"],
|
||||
["reload", "reloads the configuration"],
|
||||
["restart", "restarts the server"],
|
||||
["reload", "reloads the configuration without restart"],
|
||||
["reload <JAIL>", "reloads the jail <JAIL>"],
|
||||
["stop", "stops all jails and terminate the server"],
|
||||
["status", "gets the current status of the server"],
|
||||
["ping", "tests if the server is alive"],
|
||||
["echo", "for internal usage, returns back and outputs a given string"],
|
||||
["help", "return this output"],
|
||||
["version", "return the server version"],
|
||||
['', "LOGGING", ""],
|
||||
|
@ -141,7 +146,7 @@ def printFormatted():
|
|||
firstHeading = False
|
||||
for m in protocol:
|
||||
if m[0] == '' and firstHeading:
|
||||
print
|
||||
output("")
|
||||
firstHeading = True
|
||||
first = True
|
||||
if len(m[0]) >= MARGIN:
|
||||
|
@ -152,7 +157,7 @@ def printFormatted():
|
|||
first = False
|
||||
else:
|
||||
line = ' ' * (INDENT + MARGIN) + n.strip()
|
||||
print line
|
||||
output(line)
|
||||
|
||||
|
||||
##
|
||||
|
@ -163,20 +168,20 @@ def printWiki():
|
|||
for m in protocol:
|
||||
if m[0] == '':
|
||||
if firstHeading:
|
||||
print "|}"
|
||||
output("|}")
|
||||
__printWikiHeader(m[1], m[2])
|
||||
firstHeading = True
|
||||
else:
|
||||
print "|-"
|
||||
print "| <span style=\"white-space:nowrap;\"><tt>" + m[0] + "</tt></span> || || " + m[1]
|
||||
print "|}"
|
||||
output("|-")
|
||||
output("| <span style=\"white-space:nowrap;\"><tt>" + m[0] + "</tt></span> || || " + m[1])
|
||||
output("|}")
|
||||
|
||||
|
||||
def __printWikiHeader(section, desc):
|
||||
print
|
||||
print "=== " + section + " ==="
|
||||
print
|
||||
print desc
|
||||
print
|
||||
print "{|"
|
||||
print "| '''Command''' || || '''Description'''"
|
||||
output("")
|
||||
output("=== " + section + " ===")
|
||||
output("")
|
||||
output(desc)
|
||||
output("")
|
||||
output("{|")
|
||||
output("| '''Command''' || || '''Description'''")
|
||||
|
|
|
@ -95,6 +95,7 @@ def loop(active, timeout=None, use_poll=False):
|
|||
# Use poll instead of loop, because of recognition of active flag,
|
||||
# because of loop timeout mistake: different in poll and poll2 (sec vs ms),
|
||||
# and to prevent sporadical errors like EBADF 'Bad file descriptor' etc. (see gh-161)
|
||||
errCount = 0
|
||||
if timeout is None:
|
||||
timeout = Utils.DEFAULT_SLEEP_TIME
|
||||
poll = asyncore.poll
|
||||
|
@ -107,11 +108,20 @@ def loop(active, timeout=None, use_poll=False):
|
|||
while active():
|
||||
try:
|
||||
poll(timeout)
|
||||
if errCount:
|
||||
errCount -= 1
|
||||
except Exception as e: # pragma: no cover
|
||||
if not active():
|
||||
break
|
||||
errCount += 1
|
||||
if errCount < 20:
|
||||
if e.args[0] in (errno.ENOTCONN, errno.EBADF): # (errno.EBADF, 'Bad file descriptor')
|
||||
logSys.info('Server connection was closed: %s', str(e))
|
||||
else:
|
||||
logSys.error('Server connection was closed: %s', str(e))
|
||||
elif errCount == 20:
|
||||
logSys.info('Too many errors - stop logging connection errors')
|
||||
logSys.exception(e)
|
||||
|
||||
|
||||
##
|
||||
|
@ -162,7 +172,7 @@ class AsyncServer(asyncore.dispatcher):
|
|||
logSys.error("Fail2ban seems to be already running")
|
||||
if force:
|
||||
logSys.warning("Forcing execution of the server")
|
||||
os.remove(sock)
|
||||
self._remove_sock()
|
||||
else:
|
||||
raise AsyncServerException("Server already running")
|
||||
# Creates the socket.
|
||||
|
@ -175,20 +185,22 @@ class AsyncServer(asyncore.dispatcher):
|
|||
AsyncServer.__markCloseOnExec(self.socket)
|
||||
self.listen(1)
|
||||
# Sets the init flag.
|
||||
self.__init = self.__active = True
|
||||
self.__init = self.__loop = self.__active = True
|
||||
# Event loop as long as active:
|
||||
loop(lambda: self.__active)
|
||||
loop(lambda: self.__loop)
|
||||
self.__active = False
|
||||
# Cleanup all
|
||||
self.stop()
|
||||
|
||||
|
||||
def close(self):
|
||||
if self.__active:
|
||||
self.__loop = False
|
||||
asyncore.dispatcher.close(self)
|
||||
# Remove socket (file) only if it was created:
|
||||
if self.__init and os.path.exists(self.__sock):
|
||||
logSys.debug("Removed socket file " + self.__sock)
|
||||
os.remove(self.__sock)
|
||||
self._remove_sock()
|
||||
logSys.debug("Socket shutdown")
|
||||
self.__active = False
|
||||
|
||||
|
@ -201,6 +213,17 @@ class AsyncServer(asyncore.dispatcher):
|
|||
def isActive(self):
|
||||
return self.__active
|
||||
|
||||
|
||||
##
|
||||
# Safe remove (in multithreaded mode):
|
||||
|
||||
def _remove_sock(self):
|
||||
try:
|
||||
os.remove(self.__sock)
|
||||
except OSError as e:
|
||||
if e.errno != errno.ENOENT:
|
||||
raise
|
||||
|
||||
##
|
||||
# Marks socket as close-on-exec to avoid leaking file descriptors when
|
||||
# running actions involving command execution.
|
||||
|
|
|
@ -51,6 +51,8 @@ class JailThread(Thread):
|
|||
|
||||
def __init__(self, name=None):
|
||||
super(JailThread, self).__init__(name=name)
|
||||
## Should going with main thread also:
|
||||
self.daemon = True
|
||||
## Control the state of the thread.
|
||||
self.active = False
|
||||
## Control the idle state of the thread.
|
||||
|
|
|
@ -24,6 +24,7 @@ __author__ = "Cyril Jaquier"
|
|||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
__license__ = "GPL"
|
||||
|
||||
import threading
|
||||
from threading import Lock, RLock
|
||||
import logging
|
||||
import logging.handlers
|
||||
|
@ -43,6 +44,10 @@ from ..helpers import getLogger, excepthook
|
|||
# Gets the instance of the logger.
|
||||
logSys = getLogger(__name__)
|
||||
|
||||
DEF_SYSLOGSOCKET = "auto"
|
||||
DEF_LOGLEVEL = "INFO"
|
||||
DEF_LOGTARGET = "STDOUT"
|
||||
|
||||
try:
|
||||
from .database import Fail2BanDb
|
||||
except ImportError: # pragma: no cover
|
||||
|
@ -50,6 +55,10 @@ except ImportError: # pragma: no cover
|
|||
Fail2BanDb = None
|
||||
|
||||
|
||||
def _thread_name():
|
||||
return threading.current_thread().__class__.__name__
|
||||
|
||||
|
||||
class Server:
|
||||
|
||||
def __init__(self, daemon=False):
|
||||
|
@ -59,7 +68,8 @@ class Server:
|
|||
self.__db = None
|
||||
self.__daemon = daemon
|
||||
self.__transm = Transmitter(self)
|
||||
self.__asyncServer = AsyncServer(self.__transm)
|
||||
#self.__asyncServer = AsyncServer(self.__transm)
|
||||
self.__asyncServer = None
|
||||
self.__logLevel = None
|
||||
self.__logTarget = None
|
||||
self.__syslogSocket = None
|
||||
|
@ -68,10 +78,7 @@ class Server:
|
|||
'FreeBSD': '/var/run/log',
|
||||
'Linux': '/dev/log',
|
||||
}
|
||||
self.setSyslogSocket("auto")
|
||||
# Set logging level
|
||||
self.setLogLevel("INFO")
|
||||
self.setLogTarget("STDOUT")
|
||||
self.__prev_signals = {}
|
||||
|
||||
def __sigTERMhandler(self, signum, frame):
|
||||
logSys.debug("Caught signal %d. Exiting" % signum)
|
||||
|
@ -81,27 +88,44 @@ class Server:
|
|||
logSys.debug("Caught signal %d. Flushing logs" % signum)
|
||||
self.flushLogs()
|
||||
|
||||
def start(self, sock, pidfile, force=False, observer=True):
|
||||
logSys.info("Starting Fail2ban v%s", version.version)
|
||||
|
||||
# Install signal handlers
|
||||
signal.signal(signal.SIGTERM, self.__sigTERMhandler)
|
||||
signal.signal(signal.SIGINT, self.__sigTERMhandler)
|
||||
signal.signal(signal.SIGUSR1, self.__sigUSR1handler)
|
||||
|
||||
# Ensure unhandled exceptions are logged
|
||||
sys.excepthook = excepthook
|
||||
|
||||
def start(self, sock, pidfile, force=False, observer=True, conf={}):
|
||||
# First set the mask to only allow access to owner
|
||||
os.umask(0077)
|
||||
# Second daemonize before logging etc, because it will close all handles:
|
||||
if self.__daemon: # pragma: no cover
|
||||
logSys.info("Starting in daemon mode")
|
||||
ret = self.__createDaemon()
|
||||
if ret:
|
||||
# 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")
|
||||
else:
|
||||
logSys.error("Could not create daemon")
|
||||
raise ServerInitializationError("Could not create daemon")
|
||||
|
||||
# Install signal handlers
|
||||
if _thread_name() == '_MainThread':
|
||||
for s in (signal.SIGTERM, signal.SIGINT, signal.SIGUSR1):
|
||||
self.__prev_signals[s] = signal.getsignal(s)
|
||||
signal.signal(s, self.__sigTERMhandler if s != signal.SIGUSR1 else self.__sigUSR1handler)
|
||||
# Ensure unhandled exceptions are logged
|
||||
sys.excepthook = excepthook
|
||||
|
||||
# Creates a PID file.
|
||||
try:
|
||||
|
@ -121,6 +145,7 @@ class Server:
|
|||
# Start the communication
|
||||
logSys.debug("Starting communication")
|
||||
try:
|
||||
self.__asyncServer = AsyncServer(self.__transm)
|
||||
self.__asyncServer.start(sock, force)
|
||||
except AsyncServerException, e:
|
||||
logSys.error("Could not start server: %s", e)
|
||||
|
@ -143,17 +168,25 @@ class Server:
|
|||
# communications first (which should be ok anyways since we
|
||||
# are exiting)
|
||||
# See https://github.com/fail2ban/fail2ban/issues/7
|
||||
if self.__asyncServer is not None:
|
||||
self.__asyncServer.stop()
|
||||
self.__asyncServer = None
|
||||
|
||||
# Now stop all the jails
|
||||
self.stopAllJail()
|
||||
|
||||
# Only now shutdown the logging.
|
||||
try:
|
||||
self.__loggingLock.acquire()
|
||||
if self.__logTarget is not None:
|
||||
with self.__loggingLock:
|
||||
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):
|
||||
# Add jail hereafter:
|
||||
|
@ -341,6 +374,9 @@ class Server:
|
|||
def getBanTimeExtra(self, name, opt):
|
||||
return self.__jails[name].getBanTimeExtra(opt)
|
||||
|
||||
def isStarted(self):
|
||||
self.__asyncServer.isActive()
|
||||
|
||||
def isAlive(self, jailnum=None):
|
||||
if jailnum is not None and len(self.__jails) != jailnum:
|
||||
return 0
|
||||
|
@ -379,16 +415,15 @@ class Server:
|
|||
# @param value the level
|
||||
|
||||
def setLogLevel(self, value):
|
||||
value = value.upper()
|
||||
with self.__loggingLock:
|
||||
if self.__logLevel == value:
|
||||
return
|
||||
try:
|
||||
self.__loggingLock.acquire()
|
||||
getLogger("fail2ban").setLevel(
|
||||
getattr(logging, value.upper()))
|
||||
getLogger("fail2ban").setLevel(getattr(logging, value))
|
||||
self.__logLevel = value
|
||||
except AttributeError:
|
||||
raise ValueError("Invalid log level")
|
||||
else:
|
||||
self.__logLevel = value.upper()
|
||||
finally:
|
||||
self.__loggingLock.release()
|
||||
|
||||
##
|
||||
# Get the logging level.
|
||||
|
@ -397,11 +432,8 @@ class Server:
|
|||
# @return the log level
|
||||
|
||||
def getLogLevel(self):
|
||||
try:
|
||||
self.__loggingLock.acquire()
|
||||
with self.__loggingLock:
|
||||
return self.__logLevel
|
||||
finally:
|
||||
self.__loggingLock.release()
|
||||
|
||||
##
|
||||
# Sets the logging target.
|
||||
|
@ -410,8 +442,14 @@ class Server:
|
|||
# @param target the logging target
|
||||
|
||||
def setLogTarget(self, target):
|
||||
try:
|
||||
self.__loggingLock.acquire()
|
||||
with self.__loggingLock:
|
||||
# don't set new handlers if already the same
|
||||
# or if "INHERITED" (foreground worker of the test cases, to prevent stop logging):
|
||||
if self.__logTarget == target:
|
||||
return True
|
||||
if target == "INHERITED":
|
||||
self.__logTarget = target
|
||||
return True
|
||||
# set a format which is simpler for console use
|
||||
formatter = logging.Formatter("%(asctime)s %(name)-24s[%(process)d]: %(levelname)-7s %(message)s")
|
||||
if target == "SYSLOG":
|
||||
|
@ -479,8 +517,6 @@ class Server:
|
|||
# Sets the logging target.
|
||||
self.__logTarget = target
|
||||
return True
|
||||
finally:
|
||||
self.__loggingLock.release()
|
||||
|
||||
##
|
||||
# Sets the syslog socket.
|
||||
|
@ -488,24 +524,21 @@ class Server:
|
|||
# syslogsocket is the full path to the syslog socket
|
||||
# @param syslogsocket the syslog socket path
|
||||
def setSyslogSocket(self, syslogsocket):
|
||||
with self.__loggingLock:
|
||||
if self.__syslogSocket == syslogsocket:
|
||||
return True
|
||||
self.__syslogSocket = syslogsocket
|
||||
# Conditionally reload, logtarget depends on socket path when SYSLOG
|
||||
return self.__logTarget != "SYSLOG"\
|
||||
or self.setLogTarget(self.__logTarget)
|
||||
|
||||
def getLogTarget(self):
|
||||
try:
|
||||
self.__loggingLock.acquire()
|
||||
with self.__loggingLock:
|
||||
return self.__logTarget
|
||||
finally:
|
||||
self.__loggingLock.release()
|
||||
|
||||
def getSyslogSocket(self):
|
||||
try:
|
||||
self.__loggingLock.acquire()
|
||||
with self.__loggingLock:
|
||||
return self.__syslogSocket
|
||||
finally:
|
||||
self.__loggingLock.release()
|
||||
|
||||
def flushLogs(self):
|
||||
if self.__logTarget not in ['STDERR', 'STDOUT', 'SYSLOG']:
|
||||
|
@ -561,7 +594,9 @@ class Server:
|
|||
# We need to set this in the parent process, so it gets inherited by the
|
||||
# child process, and this makes sure that it is effect even if the parent
|
||||
# terminates quickly.
|
||||
signal.signal(signal.SIGHUP, signal.SIG_IGN)
|
||||
for s in (signal.SIGHUP,):
|
||||
self.__prev_signals[s] = signal.getsignal(s)
|
||||
signal.signal(s, signal.SIG_IGN)
|
||||
|
||||
try:
|
||||
# Fork a child process so the parent can exit. This will return control
|
||||
|
@ -572,7 +607,7 @@ class Server:
|
|||
# PGID.
|
||||
pid = os.fork()
|
||||
except OSError, e:
|
||||
return((e.errno, e.strerror)) # ERROR (return a tuple)
|
||||
return (False, (e.errno, e.strerror)) # ERROR (return a tuple)
|
||||
|
||||
if pid == 0: # The first child.
|
||||
|
||||
|
@ -593,7 +628,7 @@ class Server:
|
|||
# preventing the daemon from ever acquiring a controlling terminal.
|
||||
pid = os.fork() # Fork a second child.
|
||||
except OSError, e:
|
||||
return((e.errno, e.strerror)) # ERROR (return a tuple)
|
||||
return (False, (e.errno, e.strerror)) # ERROR (return a tuple)
|
||||
|
||||
if (pid == 0): # The second child.
|
||||
# Ensure that the daemon doesn't keep any directory in use. Failure
|
||||
|
@ -602,7 +637,8 @@ class Server:
|
|||
else:
|
||||
os._exit(0) # Exit parent (the first child) of the second child.
|
||||
else:
|
||||
os._exit(0) # Exit parent of the first child.
|
||||
# Signal to exit, parent of the first child.
|
||||
return None
|
||||
|
||||
# Close all open files. Try the system configuration variable, SC_OPEN_MAX,
|
||||
# for the maximum number of open files to close. If it doesn't exist, use
|
||||
|
@ -630,7 +666,7 @@ class Server:
|
|||
os.open("/dev/null", os.O_RDONLY) # standard input (0)
|
||||
os.open("/dev/null", os.O_RDWR) # standard output (1)
|
||||
os.open("/dev/null", os.O_RDWR) # standard error (2)
|
||||
return True
|
||||
return (True,)
|
||||
|
||||
|
||||
class ServerInitializationError(Exception):
|
||||
|
|
|
@ -93,6 +93,8 @@ class Transmitter:
|
|||
name = command[1]
|
||||
self.__server.stopJail(name)
|
||||
return None
|
||||
elif command[0] == "echo":
|
||||
return command[1:]
|
||||
elif command[0] == "sleep":
|
||||
value = command[1]
|
||||
time.sleep(float(value))
|
||||
|
|
|
@ -125,6 +125,7 @@ class Utils():
|
|||
timeout_expr = lambda: time.time() - stime <= timeout
|
||||
else:
|
||||
timeout_expr = timeout
|
||||
popen = None
|
||||
try:
|
||||
popen = subprocess.Popen(
|
||||
realCmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell,
|
||||
|
@ -151,7 +152,10 @@ class Utils():
|
|||
if retcode is None and not Utils.pid_exists(pgid):
|
||||
retcode = signal.SIGKILL
|
||||
except OSError as e:
|
||||
logSys.error("%s -- failed with %s" % (realCmd, e))
|
||||
stderr = "%s -- failed with %s" % (realCmd, e)
|
||||
logSys.error(stderr)
|
||||
if not popen:
|
||||
return False if not output else (False, stdout, stderr, retcode)
|
||||
|
||||
std_level = retcode == 0 and logging.DEBUG or logging.ERROR
|
||||
# if we need output (to return or to log it):
|
||||
|
@ -164,8 +168,10 @@ class Utils():
|
|||
stdout = popen.stdout.read()
|
||||
except IOError as e:
|
||||
logSys.error(" ... -- failed to read stdout %s", e)
|
||||
if stdout is not None and stdout != '':
|
||||
logSys.log(std_level, "%s -- stdout: %r", realCmd, stdout)
|
||||
if stdout is not None and stdout != '' and std_level >= logSys.getEffectiveLevel():
|
||||
logSys.log(std_level, "%s -- stdout:", realCmd)
|
||||
for l in stdout.splitlines():
|
||||
logSys.log(std_level, " -- stdout: %r", l)
|
||||
popen.stdout.close()
|
||||
if popen.stderr:
|
||||
try:
|
||||
|
@ -174,8 +180,10 @@ class Utils():
|
|||
stderr = popen.stderr.read()
|
||||
except IOError as e:
|
||||
logSys.error(" ... -- failed to read stderr %s", e)
|
||||
if stderr is not None and stderr != '':
|
||||
logSys.log(std_level, "%s -- stderr: %r", realCmd, stderr)
|
||||
if stderr is not None and stderr != '' and std_level >= logSys.getEffectiveLevel():
|
||||
logSys.log(std_level, "%s -- stderr:", realCmd)
|
||||
for l in stderr.splitlines():
|
||||
logSys.log(std_level, " -- stderr: %r", l)
|
||||
popen.stderr.close()
|
||||
|
||||
if retcode == 0:
|
||||
|
|
|
@ -65,6 +65,7 @@ class SMTPActionTest(unittest.TestCase):
|
|||
self._active = True
|
||||
self._loop_thread = threading.Thread(
|
||||
target=asyncserver.loop, kwargs={'active': lambda: self._active})
|
||||
self._loop_thread.daemon = True
|
||||
self._loop_thread.start()
|
||||
|
||||
def tearDown(self):
|
||||
|
|
|
@ -44,8 +44,8 @@ class CommandActionTest(LogCaptureTestCase):
|
|||
|
||||
def tearDown(self):
|
||||
"""Call after every test case."""
|
||||
LogCaptureTestCase.tearDown(self)
|
||||
self.__action.stop()
|
||||
LogCaptureTestCase.tearDown(self)
|
||||
|
||||
def testSubstituteRecursiveTags(self):
|
||||
aInfo = {
|
||||
|
@ -271,11 +271,11 @@ class CommandActionTest(LogCaptureTestCase):
|
|||
|
||||
def testCaptureStdOutErr(self):
|
||||
CommandAction.executeCmd('echo "How now brown cow"')
|
||||
self.assertLogged("'How now brown cow\\n'")
|
||||
self.assertLogged("stdout: 'How now brown cow'\n", "stdout: b'How now brown cow'\n")
|
||||
CommandAction.executeCmd(
|
||||
'echo "The rain in Spain stays mainly in the plain" 1>&2')
|
||||
self.assertLogged(
|
||||
"'The rain in Spain stays mainly in the plain\\n'")
|
||||
"stderr: 'The rain in Spain stays mainly in the plain'\n", "stderr: b'The rain in Spain stays mainly in the plain'\n")
|
||||
|
||||
def testCallingMap(self):
|
||||
mymap = CallingMap(callme=lambda: str(10), error=lambda: int('a'),
|
||||
|
|
|
@ -28,13 +28,15 @@ import re
|
|||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from ..client.configreader import ConfigReaderUnshared
|
||||
from ..client.configreader import ConfigReader, ConfigReaderUnshared
|
||||
from ..client import configparserinc
|
||||
from ..client.jailreader import JailReader
|
||||
from ..client.filterreader import FilterReader
|
||||
from ..client.jailsreader import JailsReader
|
||||
from ..client.actionreader import ActionReader
|
||||
from ..client.configurator import Configurator
|
||||
from ..server.mytime import MyTime
|
||||
from ..version import version
|
||||
from .utils import LogCaptureTestCase
|
||||
|
||||
TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files")
|
||||
|
@ -253,6 +255,34 @@ class JailReaderTest(LogCaptureTestCase):
|
|||
result = JailReader.extractOptions(option)
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def testVersionAgent(self):
|
||||
jail = JailReader('blocklisttest', force_enable=True, basedir=CONFIG_DIR)
|
||||
# emulate jail.read(), because such jail not exists:
|
||||
ConfigReader.read(jail, "jail");
|
||||
sections = jail._cfg.get_sections()
|
||||
sections['blocklisttest'] = dict((('__name__', 'blocklisttest'),
|
||||
('filter', ''), ('failregex', '^test <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):
|
||||
d = tempfile.mkdtemp(prefix="f2b-temp")
|
||||
# Generate few files
|
||||
|
@ -596,6 +626,12 @@ class JailsReaderTest(LogCaptureTestCase):
|
|||
# by default we have lots of jails ;)
|
||||
self.assertTrue(len(comm_commands))
|
||||
|
||||
# some common sanity checks for commands
|
||||
for command in comm_commands:
|
||||
if len(command) >= 3 and [command[0], command[2]] == ['set', 'bantime']:
|
||||
self.assertTrue(MyTime.str2seconds(command[3]) > 0)
|
||||
|
||||
|
||||
# and we know even some of them by heart
|
||||
for j in ['sshd', 'recidive']:
|
||||
# by default we have 'auto' backend ATM
|
||||
|
|
|
@ -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)
|
|
@ -23,19 +23,7 @@ __author__ = "Serg Brester"
|
|||
__copyright__ = "Copyright (c) 2015 Serg G. Brester (sebres), 2008- Fail2Ban Contributors"
|
||||
__license__ = "GPL"
|
||||
|
||||
from __builtin__ import open as fopen
|
||||
import unittest
|
||||
import getpass
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import tempfile
|
||||
import uuid
|
||||
|
||||
try:
|
||||
from systemd import journal
|
||||
except ImportError:
|
||||
journal = None
|
||||
|
||||
from ..client import fail2banregex
|
||||
from ..client.fail2banregex import Fail2banRegex, get_opt_parser, output
|
||||
|
|
|
@ -59,3 +59,11 @@ Nov 4 18:30:40 localhost asterisk[32229]: NOTICE[32257]: chan_sip.c:23417 in han
|
|||
# match UTF-8 in SessionID
|
||||
# failJSON: { "time": "2015-05-25T07:52:36", "match": true, "host": "10.250.251.252" }
|
||||
[2015-05-25 07:52:36] SECURITY[6988] res_security_log.c: SecurityEvent="InvalidAccountID",EventTV="2015-05-25T07:52:36.888+0300",Severity="Error",Service="PJSIP",EventVersion="1",AccountID="70000180",SessionID="Негодяй",LocalAddress="IPV4/UDP/1.2.3.4/5060",RemoteAddress="IPV4/UDP/10.250.251.252/5061"
|
||||
|
||||
# match phone numbers with + symbol (and without number, or other context)
|
||||
# failJSON: { "time": "2016-01-28T10:22:27", "match": true , "host": "1.2.3.4" }
|
||||
[2016-01-28 10:22:27] NOTICE[3477][C-000003bb] chan_sip.c: Call from '' (1.2.3.4:10836) to extension '++441772285411' rejected because extension not found in context 'default'.
|
||||
# failJSON: { "time": "2016-01-28T10:34:31", "match": true , "host": "1.2.3.4" }
|
||||
[2016-01-28 10:34:31] NOTICE[3477][C-000003c3] chan_sip.c: Call from '' (1.2.3.4:10836) to extension '0+441772285407' rejected because extension not found in context 'default'.
|
||||
# failJSON: { "time": "2016-01-28T10:34:33", "match": true , "host": "1.2.3.4" }
|
||||
[2016-01-28 10:34:33] NOTICE[3477][C-000003c3] chan_sip.c: Call from '' (1.2.3.4:10836) to extension '' rejected because extension not found in context 'my-context'.
|
||||
|
|
|
@ -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"
|
|
@ -26,3 +26,6 @@ Dec 21 21:17:29 xxx postfix/smtpd[7150]: NOQUEUE: reject: RCPT from badserver.ex
|
|||
|
||||
# failJSON: { "time": "2004-11-22T22:33:44", "match": true , "host": "1.2.3.4" }
|
||||
Nov 22 22:33:44 xxx postfix/smtpd[11111]: NOQUEUE: reject: RCPT from 1-2-3-4.example.com[1.2.3.4]: 450 4.1.8 <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>
|
||||
|
|
|
@ -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
|
|
@ -62,25 +62,18 @@ class TransmitterBase(unittest.TestCase):
|
|||
|
||||
def setUp(self):
|
||||
"""Call before every test case."""
|
||||
#super(TransmitterBase, self).setUp()
|
||||
self.transm = self.server._Server__transm
|
||||
self.tmp_files = []
|
||||
sock_fd, sock_name = tempfile.mkstemp('fail2ban.sock', 'transmitter')
|
||||
os.close(sock_fd)
|
||||
self.tmp_files.append(sock_name)
|
||||
pidfile_fd, pidfile_name = tempfile.mkstemp(
|
||||
'fail2ban.pid', 'transmitter')
|
||||
os.close(pidfile_fd)
|
||||
self.tmp_files.append(pidfile_name)
|
||||
self.server.start(sock_name, pidfile_name, **self.server_start_args)
|
||||
# To test thransmitter we don't need to start server...
|
||||
#self.server.start('/dev/null', '/dev/null', force=False)
|
||||
self.jailName = "TestJail1"
|
||||
self.server.addJail(self.jailName, FAST_BACKEND)
|
||||
|
||||
def tearDown(self):
|
||||
"""Call after every test case."""
|
||||
# stop jails, etc.
|
||||
self.server.quit()
|
||||
for f in self.tmp_files:
|
||||
if os.path.exists(f):
|
||||
os.remove(f)
|
||||
#super(TransmitterBase, self).tearDown()
|
||||
|
||||
def setGetTest(self, cmd, inValue, outValue=(None,), outCode=0, jail=None, repr_=False):
|
||||
"""Process set/get commands and compare both return values
|
||||
|
@ -160,7 +153,6 @@ class Transmitter(TransmitterBase):
|
|||
|
||||
def setUp(self):
|
||||
self.server = TestServer()
|
||||
self.server_start_args = {'force':False, 'observer':False}
|
||||
super(Transmitter, self).setUp()
|
||||
|
||||
def testStopServer(self):
|
||||
|
@ -793,11 +785,10 @@ class TransmitterLogging(TransmitterBase):
|
|||
|
||||
def setUp(self):
|
||||
self.server = Server()
|
||||
super(TransmitterLogging, self).setUp()
|
||||
self.server.setLogTarget("/dev/null")
|
||||
self.server.setLogLevel("CRITICAL")
|
||||
self.server.setSyslogSocket("auto")
|
||||
self.server_start_args = {'force':False, 'observer':False}
|
||||
super(TransmitterLogging, self).setUp()
|
||||
|
||||
def testLogTarget(self):
|
||||
logTargets = []
|
||||
|
@ -915,17 +906,6 @@ class TransmitterLogging(TransmitterBase):
|
|||
self.setGetTest("bantime.overalljails", "true", "true", jail=self.jailName)
|
||||
|
||||
|
||||
class TransmitterWithObserver(TransmitterBase):
|
||||
|
||||
def setUp(self):
|
||||
self.server = TestServer()
|
||||
self.server_start_args = {'force':False, 'observer':True}
|
||||
super(TransmitterWithObserver, self).setUp()
|
||||
|
||||
def testObserver(self):
|
||||
pass
|
||||
|
||||
|
||||
class JailTests(unittest.TestCase):
|
||||
|
||||
def testLongName(self):
|
||||
|
@ -985,3 +965,21 @@ class LoggingTests(LogCaptureTestCase):
|
|||
sys.__excepthook__ = prev_exchook
|
||||
self.assertEqual(len(x), 1)
|
||||
self.assertEqual(x[0][0], RuntimeError)
|
||||
|
||||
def testStartFailedSockExists(self):
|
||||
tmp_files = []
|
||||
sock_fd, sock_name = tempfile.mkstemp('fail2ban.sock', 'f2b-test')
|
||||
os.close(sock_fd)
|
||||
tmp_files.append(sock_name)
|
||||
pidfile_fd, pidfile_name = tempfile.mkstemp('fail2ban.pid', 'f2b-test')
|
||||
os.close(pidfile_fd)
|
||||
tmp_files.append(pidfile_name)
|
||||
server = TestServer()
|
||||
try:
|
||||
server.start(sock_name, pidfile_name, force=False)
|
||||
self.assertLogged("Server already running")
|
||||
finally:
|
||||
server.quit()
|
||||
for f in tmp_files:
|
||||
if os.path.exists(f):
|
||||
os.remove(f)
|
||||
|
|
|
@ -26,10 +26,14 @@ import logging
|
|||
import optparse
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
import unittest
|
||||
|
||||
from StringIO import StringIO
|
||||
from functools import wraps
|
||||
|
||||
from ..helpers import getLogger
|
||||
from ..server.filter import DNSUtils
|
||||
|
@ -50,6 +54,10 @@ if not CONFIG_DIR:
|
|||
else:
|
||||
CONFIG_DIR = '/etc/fail2ban'
|
||||
|
||||
# In not installed env (setup, test-cases) use fail2ban modules from main directory:
|
||||
if 1 or os.environ.get('PYTHONPATH', None) is None:
|
||||
os.putenv('PYTHONPATH', os.path.dirname(os.path.dirname(os.path.dirname(
|
||||
os.path.abspath(__file__)))))
|
||||
|
||||
class F2B(optparse.Values):
|
||||
def __init__(self, opts={}):
|
||||
|
@ -71,6 +79,17 @@ class F2B(optparse.Values):
|
|||
return wtime
|
||||
|
||||
|
||||
def withtmpdir(f):
|
||||
@wraps(f)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
tmp = tempfile.mkdtemp(prefix="f2b-temp")
|
||||
try:
|
||||
return f(self, tmp, *args, **kwargs)
|
||||
finally:
|
||||
# clean up
|
||||
shutil.rmtree(tmp)
|
||||
return wrapper
|
||||
|
||||
def initTests(opts):
|
||||
unittest.F2B = F2B(opts)
|
||||
# --fast :
|
||||
|
@ -146,6 +165,7 @@ def gatherTests(regexps=None, opts=None):
|
|||
from . import databasetestcase
|
||||
from . import observertestcase
|
||||
from . import samplestestcase
|
||||
from . import fail2banclienttestcase
|
||||
from . import fail2banregextestcase
|
||||
|
||||
if not regexps: # pragma: no cover
|
||||
|
@ -170,7 +190,6 @@ def gatherTests(regexps=None, opts=None):
|
|||
|
||||
# Server
|
||||
tests.addTest(unittest.makeSuite(servertestcase.Transmitter))
|
||||
tests.addTest(unittest.makeSuite(servertestcase.TransmitterWithObserver))
|
||||
tests.addTest(unittest.makeSuite(servertestcase.JailTests))
|
||||
tests.addTest(unittest.makeSuite(servertestcase.RegexTests))
|
||||
tests.addTest(unittest.makeSuite(servertestcase.LoggingTests))
|
||||
|
@ -228,6 +247,9 @@ def gatherTests(regexps=None, opts=None):
|
|||
# Filter Regex tests with sample logs
|
||||
tests.addTest(unittest.makeSuite(samplestestcase.FilterSamplesRegex))
|
||||
|
||||
# bin/fail2ban-client, bin/fail2ban-server
|
||||
tests.addTest(unittest.makeSuite(fail2banclienttestcase.Fail2banClientTest))
|
||||
tests.addTest(unittest.makeSuite(fail2banclienttestcase.Fail2banServerTest))
|
||||
# bin/fail2ban-regex
|
||||
tests.addTest(unittest.makeSuite(fail2banregextestcase.Fail2banRegexTest))
|
||||
|
||||
|
@ -298,13 +320,16 @@ class LogCaptureTestCase(unittest.TestCase):
|
|||
# Let's log everything into a string
|
||||
self._log = StringIO()
|
||||
logSys.handlers = [logging.StreamHandler(self._log)]
|
||||
if self._old_level < logging.DEBUG: # so if HEAVYDEBUG etc -- show them!
|
||||
if self._old_level <= logging.DEBUG: # so if DEBUG etc -- show them (and log it in travis)!
|
||||
print("")
|
||||
logSys.handlers += self._old_handlers
|
||||
logSys.debug('='*10 + ' %s ' + '='*20, self.id())
|
||||
logSys.setLevel(getattr(logging, 'DEBUG'))
|
||||
|
||||
def tearDown(self):
|
||||
"""Call after every test case."""
|
||||
# print "O: >>%s<<" % self._log.getvalue()
|
||||
self.pruneLog()
|
||||
logSys = getLogger("fail2ban")
|
||||
logSys.handlers = self._old_handlers
|
||||
logSys.level = self._old_level
|
||||
|
@ -345,6 +370,9 @@ class LogCaptureTestCase(unittest.TestCase):
|
|||
raise AssertionError("All of the %r were found present in the log: %r" % (s, logged))
|
||||
|
||||
|
||||
def pruneLog(self):
|
||||
self._log.truncate(0)
|
||||
|
||||
def getLog(self):
|
||||
return self._log.getvalue()
|
||||
|
||||
|
|
|
@ -6,11 +6,9 @@
|
|||
# https://github.com/fail2ban/fail2ban/blob/debian/debian/fail2ban.logrotate
|
||||
|
||||
/var/log/fail2ban.log {
|
||||
rotate 7
|
||||
missingok
|
||||
notifempty
|
||||
compress
|
||||
postrotate
|
||||
/usr/bin/fail2ban-client flushlogs 1>/dev/null || true
|
||||
/usr/bin/fail2ban-client flushlogs >/dev/null || true
|
||||
endscript
|
||||
}
|
||||
|
|
|
@ -34,19 +34,19 @@ start() {
|
|||
# remove stalled sock file after system crash
|
||||
# bug 347477
|
||||
rm -f /var/run/fail2ban/fail2ban.sock || return 1
|
||||
${FAIL2BAN} start &> /dev/null
|
||||
${FAIL2BAN} start
|
||||
eend $? "Failed to start fail2ban"
|
||||
}
|
||||
|
||||
stop() {
|
||||
ebegin "Stopping fail2ban"
|
||||
${FAIL2BAN} stop &> /dev/null
|
||||
${FAIL2BAN} stop
|
||||
eend $? "Failed to stop fail2ban"
|
||||
}
|
||||
|
||||
reload() {
|
||||
ebegin "Reloading fail2ban"
|
||||
${FAIL2BAN} reload > /dev/null
|
||||
${FAIL2BAN} reload
|
||||
eend $? "Failed to reload fail2ban"
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue