Merge branch 'gh-2655--f2b-regex-4-jail': implemented loading of jail settings in fail2ban-regex;

closes gh-2655
pull/3641/head
sebres 11 months ago
commit 9bedc3c383

@ -16,6 +16,14 @@ ver. 1.0.3-dev-1 (20??/??/??) - development nightly edition
* `action.d/*ipset*`: make `maxelem` ipset option configurable through banaction arguments (gh-3564) * `action.d/*ipset*`: make `maxelem` ipset option configurable through banaction arguments (gh-3564)
### New Features and Enhancements ### New Features and Enhancements
* `fail2ban-regex` extended to load settings from jail (by simple name it'd prefer jail to the filter now, gh-2655);
to load the settings from filter one could use:
```diff
- fail2ban-regex ... sshd ; # jail
+ fail2ban-regex ... sshd.conf ; # filter
# or:
+ fail2ban-regex ... filter.d/sshd ; # filter
```
* better auto-detection for IPv6 support (`allowipv6 = auto` by default), trying to check sysctl net.ipv6.conf.all.disable_ipv6 * better auto-detection for IPv6 support (`allowipv6 = auto` by default), trying to check sysctl net.ipv6.conf.all.disable_ipv6
(value read from `/proc/sys/net/ipv6/conf/all/disable_ipv6`) if available, otherwise seeks over local IPv6 from network interfaces (value read from `/proc/sys/net/ipv6/conf/all/disable_ipv6`) if available, otherwise seeks over local IPv6 from network interfaces
if available for platform and uses DNS to find local IPv6 as a fallback only if available for platform and uses DNS to find local IPv6 as a fallback only

@ -51,7 +51,7 @@ except ImportError:
FilterSystemd = None FilterSystemd = None
from ..version import version, normVersion from ..version import version, normVersion
from .filterreader import FilterReader from .jailreader import FilterReader, JailReader, NoJailError
from ..server.filter import Filter, FileContainer, MyTime from ..server.filter import Filter, FileContainer, MyTime
from ..server.failregex import Regex, RegexException from ..server.failregex import Regex, RegexException
@ -312,12 +312,18 @@ class Fail2banRegex(object):
def _dumpRealOptions(self, reader, fltOpt): def _dumpRealOptions(self, reader, fltOpt):
realopts = {} realopts = {}
combopts = reader.getCombined() combopts = reader.getCombined()
if isinstance(reader, FilterReader):
_get_opt = lambda k: reader.get('Definition', k)
elif reader.filter: # JailReader for jail with filter:
_get_opt = lambda k: reader.filter.get('Definition', k)
else: # JailReader for jail without filter:
_get_opt = lambda k: None
# output all options that are specified in filter-argument as well as some special (mostly interested): # output all options that are specified in filter-argument as well as some special (mostly interested):
for k in ['logtype', 'datepattern'] + list(fltOpt.keys()): for k in ['logtype', 'datepattern'] + list(fltOpt.keys()):
# combined options win, but they contain only a sub-set in filter expected keys, # combined options win, but they contain only a sub-set in filter expected keys,
# so get the rest from definition section: # so get the rest from definition section:
try: try:
realopts[k] = combopts[k] if k in combopts else reader.get('Definition', k) realopts[k] = combopts[k] if k in combopts else _get_opt(k)
except NoOptionError: # pragma: no cover except NoOptionError: # pragma: no cover
pass pass
self.output("Real filter options : %r" % realopts) self.output("Real filter options : %r" % realopts)
@ -330,16 +336,26 @@ class Fail2banRegex(object):
fltName = value fltName = value
fltFile = None fltFile = None
fltOpt = {} fltOpt = {}
jail = None
if regextype == 'fail': if regextype == 'fail':
if re.search(r'(?ms)^/{0,3}[\w/_\-.]+(?:\[.*\])?$', value): if re.search(r'(?ms)^/{0,3}[\w/_\-.]+(?:\[.*\])?$', value):
try: try:
fltName, fltOpt = extractOptions(value) fltName, fltOpt = extractOptions(value)
if re.search(r'(?ms)^[\w/_\-]+$', fltName): # name of jail?
try:
jail = JailReader(fltName, force_enable=True,
share_config=self.share_config, basedir=basedir)
jail.read()
except NoJailError:
jail = None
if "." in fltName[~5:]: if "." in fltName[~5:]:
tryNames = (fltName,) tryNames = (fltName,)
else: else:
tryNames = (fltName, fltName + '.conf', fltName + '.local') tryNames = (fltName, fltName + '.conf', fltName + '.local')
for fltFile in tryNames: for fltFile in tryNames:
if not "/" in fltFile: if os.path.dirname(fltFile) == 'filter.d':
fltFile = os.path.join(basedir, fltFile)
elif not "/" in fltFile:
if os.path.basename(basedir) == 'filter.d': if os.path.basename(basedir) == 'filter.d':
fltFile = os.path.join(basedir, fltFile) fltFile = os.path.join(basedir, fltFile)
else: else:
@ -354,8 +370,25 @@ class Fail2banRegex(object):
output(" while parsing: %s" % (value,)) output(" while parsing: %s" % (value,))
if self._verbose: raise(e) if self._verbose: raise(e)
return False return False
readercommands = None
# if it is jail:
if jail:
self.output( "Use %11s jail : %s" % ('', fltName) )
if fltOpt:
self.output( "Use jail/flt options : %r" % fltOpt )
if not fltOpt: fltOpt = {}
fltOpt['backend'] = self._backend
ret = jail.getOptions(addOpts=fltOpt)
if not ret:
output('ERROR: Failed to get jail for %r' % (value,))
return False
# show real options if expected:
if self._verbose > 1 or logSys.getEffectiveLevel()<=logging.DEBUG:
self._dumpRealOptions(jail, fltOpt)
readercommands = jail.convert(allow_no_files=True)
# if it is filter file: # if it is filter file:
if fltFile is not None: elif fltFile is not None:
if (basedir == self._opts.config if (basedir == self._opts.config
or os.path.basename(basedir) == 'filter.d' or os.path.basename(basedir) == 'filter.d'
or ("." not in fltName[~5:] and "/" not in fltName) or ("." not in fltName[~5:] and "/" not in fltName)
@ -364,16 +397,17 @@ class Fail2banRegex(object):
if os.path.basename(basedir) == 'filter.d': if os.path.basename(basedir) == 'filter.d':
basedir = os.path.dirname(basedir) basedir = os.path.dirname(basedir)
fltName = os.path.splitext(os.path.basename(fltName))[0] fltName = os.path.splitext(os.path.basename(fltName))[0]
self.output( "Use %11s filter file : %s, basedir: %s" % (regex, fltName, basedir) ) self.output( "Use %11s file : %s, basedir: %s" % ('filter', fltName, basedir) )
else: else:
## foreign file - readexplicit this file and includes if possible: ## foreign file - readexplicit this file and includes if possible:
self.output( "Use %11s file : %s" % (regex, fltName) ) self.output( "Use %11s file : %s" % ('filter', fltName) )
basedir = None basedir = None
if not os.path.isabs(fltName): # avoid join with "filter.d" inside FilterReader if not os.path.isabs(fltName): # avoid join with "filter.d" inside FilterReader
fltName = os.path.abspath(fltName) fltName = os.path.abspath(fltName)
if fltOpt: if fltOpt:
self.output( "Use filter options : %r" % fltOpt ) self.output( "Use filter options : %r" % fltOpt )
reader = FilterReader(fltName, 'fail2ban-regex-jail', fltOpt, share_config=self.share_config, basedir=basedir) reader = FilterReader(fltName, 'fail2ban-regex-jail', fltOpt,
share_config=self.share_config, basedir=basedir)
ret = None ret = None
try: try:
if basedir is not None: if basedir is not None:
@ -398,6 +432,7 @@ class Fail2banRegex(object):
# to stream: # to stream:
readercommands = reader.convert() readercommands = reader.convert()
if readercommands:
regex_values = {} regex_values = {}
for opt in readercommands: for opt in readercommands:
if opt[0] == 'multi-set': if opt[0] == 'multi-set':

@ -29,16 +29,19 @@ import json
import os.path import os.path
import re import re
from .configreader import ConfigReaderUnshared, ConfigReader from .configreader import ConfigReaderUnshared, ConfigReader, NoSectionError
from .filterreader import FilterReader from .filterreader import FilterReader
from .actionreader import ActionReader from .actionreader import ActionReader
from ..version import version from ..version import version
from ..helpers import getLogger, extractOptions, splitWithOptions, splitwords from ..helpers import _merge_dicts, getLogger, extractOptions, splitWithOptions, splitwords
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = getLogger(__name__) logSys = getLogger(__name__)
class NoJailError(ValueError):
pass
class JailReader(ConfigReader): class JailReader(ConfigReader):
def __init__(self, name, force_enable=False, **kwargs): def __init__(self, name, force_enable=False, **kwargs):
@ -64,7 +67,7 @@ class JailReader(ConfigReader):
# Before returning -- verify that requested section # Before returning -- verify that requested section
# exists at all # exists at all
if not (self.__name in self.sections()): if not (self.__name in self.sections()):
raise ValueError("Jail %r was not found among available" raise NoJailError("Jail %r was not found among available"
% self.__name) % self.__name)
return out return out
@ -117,9 +120,9 @@ class JailReader(ConfigReader):
} }
_configOpts.update(FilterReader._configOpts) _configOpts.update(FilterReader._configOpts)
_ignoreOpts = set(['action', 'filter', 'enabled'] + list(FilterReader._configOpts.keys())) _ignoreOpts = set(['action', 'filter', 'enabled', 'backend'] + list(FilterReader._configOpts.keys()))
def getOptions(self): def getOptions(self, addOpts=None):
basedir = self.getBaseDir() basedir = self.getBaseDir()
@ -136,6 +139,8 @@ class JailReader(ConfigReader):
shouldExist=True) shouldExist=True)
if not self.__opts: # pragma: no cover if not self.__opts: # pragma: no cover
raise JailDefError("Init jail options failed") raise JailDefError("Init jail options failed")
if addOpts:
self.__opts = _merge_dicts(self.__opts, addOpts)
if not self.isEnabled(): if not self.isEnabled():
return True return True
@ -147,6 +152,8 @@ class JailReader(ConfigReader):
filterName, filterOpt = extractOptions(flt) filterName, filterOpt = extractOptions(flt)
except ValueError as e: except ValueError as e:
raise JailDefError("Invalid filter definition %r: %s" % (flt, e)) raise JailDefError("Invalid filter definition %r: %s" % (flt, e))
if addOpts:
filterOpt = _merge_dicts(filterOpt, addOpts)
self.__filter = FilterReader( self.__filter = FilterReader(
filterName, self.__name, filterOpt, filterName, self.__name, filterOpt,
share_config=self.share_config, basedir=basedir) share_config=self.share_config, basedir=basedir)
@ -219,6 +226,15 @@ class JailReader(ConfigReader):
return False return False
return True return True
@property
def filter(self):
return self.__filter
def getCombined(self):
if not self.__filter:
return self.__opts
return _merge_dicts(self.__opts, self.__filter.getCombined())
def convert(self, allow_no_files=False): def convert(self, allow_no_files=False):
"""Convert read before __opts to the commands stream """Convert read before __opts to the commands stream
@ -240,9 +256,10 @@ class JailReader(ConfigReader):
stream.extend(self.__filter.convert()) stream.extend(self.__filter.convert())
# and using options from jail: # and using options from jail:
FilterReader._fillStream(stream, self.__opts, self.__name) FilterReader._fillStream(stream, self.__opts, self.__name)
backend = self.__opts.get('backend', 'auto')
for opt, value in self.__opts.items(): for opt, value in self.__opts.items():
if opt == "logpath": if opt == "logpath":
if self.__opts.get('backend', '').startswith("systemd"): continue if backend.startswith("systemd"): continue
found_files = 0 found_files = 0
for path in value.split("\n"): for path in value.split("\n"):
path = path.rsplit(" ", 1) path = path.rsplit(" ", 1)
@ -260,8 +277,6 @@ class JailReader(ConfigReader):
if not allow_no_files: if not allow_no_files:
raise ValueError(msg) raise ValueError(msg)
logSys.warning(msg) logSys.warning(msg)
elif opt == "backend":
backend = value
elif opt == "ignoreip": elif opt == "ignoreip":
stream.append(["set", self.__name, "addignoreip"] + splitwords(value)) stream.append(["set", self.__name, "addignoreip"] + splitwords(value))
elif opt not in JailReader._ignoreOpts: elif opt not in JailReader._ignoreOpts:

@ -258,7 +258,7 @@ class Fail2banRegexTest(LogCaptureTestCase):
"-l", "notice", # put down log-level, because of too many debug-messages "-l", "notice", # put down log-level, because of too many debug-messages
"-v", "--verbose-date", "--print-all-matched", "--print-all-ignored", "-v", "--verbose-date", "--print-all-matched", "--print-all-ignored",
"-c", CONFIG_DIR, "-c", CONFIG_DIR,
FILENAME_SSHD, "sshd" FILENAME_SSHD, "sshd.conf"
)) ))
# test failure line and not-failure lines both presents: # test failure line and not-failure lines both presents:
self.assertLogged("[29116]: User root not allowed because account is locked", self.assertLogged("[29116]: User root not allowed because account is locked",
@ -269,7 +269,7 @@ class Fail2banRegexTest(LogCaptureTestCase):
"-l", "notice", # put down log-level, because of too many debug-messages "-l", "notice", # put down log-level, because of too many debug-messages
"-vv", "-c", CONFIG_DIR, "-vv", "-c", CONFIG_DIR,
"Dec 31 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 192.0.2.1", "Dec 31 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 192.0.2.1",
"sshd[logtype=short]" "filter.d/sshd[logtype=short]"
)) ))
# tet logtype is specified and set in real options: # tet logtype is specified and set in real options:
self.assertLogged("Real filter options :", "'logtype': 'short'", all=True) self.assertLogged("Real filter options :", "'logtype': 'short'", all=True)
@ -288,6 +288,16 @@ class Fail2banRegexTest(LogCaptureTestCase):
"[29116]: User root not allowed because account is locked", "[29116]: User root not allowed because account is locked",
"[29116]: Received disconnect from 192.0.2.4", all=True) "[29116]: Received disconnect from 192.0.2.4", all=True)
def testLoadFromJail(self):
self.assertTrue(_test_exec(
"-l", "notice", # put down log-level, because of too many debug-messages
"-c", CONFIG_DIR, '-vv',
FILENAME_ZZZ_SSHD, "sshd[logtype=short]"
))
# test it was jail not filter:
self.assertLogged(
"Use %11s jail : %s" % ('','sshd'))
def testMultilineSshd(self): def testMultilineSshd(self):
# by the way test of missing lines by multiline in `for bufLine in orgLineBuffer[int(fullBuffer):]` # by the way test of missing lines by multiline in `for bufLine in orgLineBuffer[int(fullBuffer):]`
self.assertTrue(_test_exec( self.assertTrue(_test_exec(
@ -431,11 +441,11 @@ class Fail2banRegexTest(LogCaptureTestCase):
) )
_test = lambda *args: _test_exec(*(opts + args)) _test = lambda *args: _test_exec(*(opts + args))
# with MLFID from prefregex and IP after failure obtained from F-NOFAIL RE: # with MLFID from prefregex and IP after failure obtained from F-NOFAIL RE:
self.assertTrue(_test('-o', 'IP:<ip>', log, 'sshd')) self.assertTrue(_test('-o', 'IP:<ip>', log, 'sshd.conf'))
self.assertLogged('IP:192.0.2.76') self.assertLogged('IP:192.0.2.76')
self.pruneLog() self.pruneLog()
# test diverse ID/IP constellations: # test diverse ID/IP constellations:
def _test_variants(flt="sshd", prefix=""): def _test_variants(flt="sshd.conf", prefix=""):
# with different ID/IP from failregex (ID/User from first, IP from second message): # with different ID/IP from failregex (ID/User from first, IP from second message):
self.assertTrue(_test('-o', 'ID:"<fid>" | IP:<ip> | U:<F-USER>', log, self.assertTrue(_test('-o', 'ID:"<fid>" | IP:<ip> | U:<F-USER>', log,
flt+'[failregex="' flt+'[failregex="'
@ -455,7 +465,7 @@ class Fail2banRegexTest(LogCaptureTestCase):
# first with sshd and prefregex: # first with sshd and prefregex:
_test_variants() _test_variants()
# the same without prefregex and MLFID directly in failregex (no merge with prefregex groups): # the same without prefregex and MLFID directly in failregex (no merge with prefregex groups):
_test_variants('common', prefix=r"\s*\S+ sshd\[<F-MLFID>\d+</F-MLFID>\]:\s+") _test_variants('common.conf', prefix=r"\s*\S+ sshd\[<F-MLFID>\d+</F-MLFID>\]:\s+")
def testNoDateTime(self): def testNoDateTime(self):
# datepattern doesn't match: # datepattern doesn't match:
@ -490,7 +500,7 @@ class Fail2banRegexTest(LogCaptureTestCase):
# complex substitution using tags and message (ip, user, msg): # complex substitution using tags and message (ip, user, msg):
self.assertTrue(_test_exec('-o', '<ip>, <F-USER>, <msg>', self.assertTrue(_test_exec('-o', '<ip>, <F-USER>, <msg>',
'-c', CONFIG_DIR, '--usedns', 'no', '-c', CONFIG_DIR, '--usedns', 'no',
STR_ML_SSHD + "\n" + STR_ML_SSHD_OK, 'sshd[logtype=short, publickey=invalid]')) STR_ML_SSHD + "\n" + STR_ML_SSHD_OK, 'sshd.conf[logtype=short, publickey=invalid]'))
# be sure we don't have IP in one line and have it in another: # be sure we don't have IP in one line and have it in another:
lines = STR_ML_SSHD.split("\n") lines = STR_ML_SSHD.split("\n")
self.assertTrue('192.0.2.2' not in lines[-2] and '192.0.2.2' in lines[-1]) self.assertTrue('192.0.2.2' not in lines[-2] and '192.0.2.2' in lines[-1])
@ -506,7 +516,7 @@ class Fail2banRegexTest(LogCaptureTestCase):
self.pruneLog("[test-phase 1] mode=aggressive & publickey=nofail + OK (accepted)") self.pruneLog("[test-phase 1] mode=aggressive & publickey=nofail + OK (accepted)")
self.assertTrue(_test_exec('-o', '<ip>, <F-USER>, <msg>', self.assertTrue(_test_exec('-o', '<ip>, <F-USER>, <msg>',
'-c', CONFIG_DIR, '--usedns', 'no', '-c', CONFIG_DIR, '--usedns', 'no',
STR_ML_SSHD + "\n" + STR_ML_SSHD_OK, 'sshd[logtype=short, mode=aggressive]')) STR_ML_SSHD + "\n" + STR_ML_SSHD_OK, 'sshd.conf[logtype=short, mode=aggressive]'))
self.assertLogged( self.assertLogged(
'192.0.2.2, git, '+lines[-4], '192.0.2.2, git, '+lines[-4],
'192.0.2.2, git, '+lines[-3], '192.0.2.2, git, '+lines[-3],
@ -520,7 +530,7 @@ class Fail2banRegexTest(LogCaptureTestCase):
self.pruneLog("[test-phase 2] mode=aggressive & publickey=nofail + FAIL (closed on preauth)") self.pruneLog("[test-phase 2] mode=aggressive & publickey=nofail + FAIL (closed on preauth)")
self.assertTrue(_test_exec('-o', '<ip>, <F-USER>, <msg>', self.assertTrue(_test_exec('-o', '<ip>, <F-USER>, <msg>',
'-c', CONFIG_DIR, '--usedns', 'no', '-c', CONFIG_DIR, '--usedns', 'no',
STR_ML_SSHD + "\n" + STR_ML_SSHD_FAIL, 'sshd[logtype=short, mode=aggressive]')) STR_ML_SSHD + "\n" + STR_ML_SSHD_FAIL, 'sshd.conf[logtype=short, mode=aggressive]'))
# 192.0.2.1 should be found for every failure (2x failed key + 1x closed): # 192.0.2.1 should be found for every failure (2x failed key + 1x closed):
lines = STR_ML_SSHD.split("\n")[0:2] + STR_ML_SSHD_FAIL.split("\n")[-1:] lines = STR_ML_SSHD.split("\n")[0:2] + STR_ML_SSHD_FAIL.split("\n")[-1:]
self.assertLogged( self.assertLogged(
@ -541,7 +551,7 @@ class Fail2banRegexTest(LogCaptureTestCase):
'svc[2] connect started 192.0.2.4\n' 'svc[2] connect started 192.0.2.4\n'
'svc[2] connect authorized 192.0.2.4\n' 'svc[2] connect authorized 192.0.2.4\n'
'svc[2] connect finished 192.0.2.4\n', 'svc[2] connect finished 192.0.2.4\n',
r'common[prefregex="^svc\[<F-MLFID>\d+</F-MLFID>\] connect <F-CONTENT>.+</F-CONTENT>$"' r'common.conf[prefregex="^svc\[<F-MLFID>\d+</F-MLFID>\] connect <F-CONTENT>.+</F-CONTENT>$"'
', failregex="' ', failregex="'
'^started\n' '^started\n'
'^<F-NOFAIL><F-MLFFORGET>finished</F-MLFFORGET></F-NOFAIL> <ADDR>\n' '^<F-NOFAIL><F-MLFFORGET>finished</F-MLFFORGET></F-NOFAIL> <ADDR>\n'

Loading…
Cancel
Save