Skip to content

Commit

Permalink
Merge branch 'fail2bangh-2655--f2b-regex-4-jail': implemented loading…
Browse files Browse the repository at this point in the history
… of jail settings in fail2ban-regex;

closes fail2bangh-2655
  • Loading branch information
sebres committed Jan 3, 2024
2 parents 7de1057 + 302252b commit 9bedc3c
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 24 deletions.
8 changes: 8 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -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)

### 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
(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
Expand Down
49 changes: 42 additions & 7 deletions fail2ban/client/fail2banregex.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
FilterSystemd = None

from ..version import version, normVersion
from .filterreader import FilterReader
from .jailreader import FilterReader, JailReader, NoJailError
from ..server.filter import Filter, FileContainer, MyTime
from ..server.failregex import Regex, RegexException

Expand Down Expand Up @@ -312,12 +312,18 @@ def setJournalMatch(self, v):
def _dumpRealOptions(self, reader, fltOpt):
realopts = {}
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):
for k in ['logtype', 'datepattern'] + list(fltOpt.keys()):
# combined options win, but they contain only a sub-set in filter expected keys,
# so get the rest from definition section:
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
pass
self.output("Real filter options : %r" % realopts)
Expand All @@ -330,16 +336,26 @@ def readRegex(self, value, regextype):
fltName = value
fltFile = None
fltOpt = {}
jail = None
if regextype == 'fail':
if re.search(r'(?ms)^/{0,3}[\w/_\-.]+(?:\[.*\])?$', value):
try:
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:]:
tryNames = (fltName,)
else:
tryNames = (fltName, fltName + '.conf', fltName + '.local')
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':
fltFile = os.path.join(basedir, fltFile)
else:
Expand All @@ -354,8 +370,25 @@ def readRegex(self, value, regextype):
output(" while parsing: %s" % (value,))
if self._verbose: raise(e)
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 fltFile is not None:
elif fltFile is not None:
if (basedir == self._opts.config
or os.path.basename(basedir) == 'filter.d'
or ("." not in fltName[~5:] and "/" not in fltName)
Expand All @@ -364,16 +397,17 @@ def readRegex(self, value, regextype):
if os.path.basename(basedir) == 'filter.d':
basedir = os.path.dirname(basedir)
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:
## 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
if not os.path.isabs(fltName): # avoid join with "filter.d" inside FilterReader
fltName = os.path.abspath(fltName)
if 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
try:
if basedir is not None:
Expand All @@ -398,6 +432,7 @@ def readRegex(self, value, regextype):
# to stream:
readercommands = reader.convert()

if readercommands:
regex_values = {}
for opt in readercommands:
if opt[0] == 'multi-set':
Expand Down
31 changes: 23 additions & 8 deletions fail2ban/client/jailreader.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,19 @@
import os.path
import re

from .configreader import ConfigReaderUnshared, ConfigReader
from .configreader import ConfigReaderUnshared, ConfigReader, NoSectionError
from .filterreader import FilterReader
from .actionreader import ActionReader
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.
logSys = getLogger(__name__)


class NoJailError(ValueError):
pass

class JailReader(ConfigReader):

def __init__(self, name, force_enable=False, **kwargs):
Expand All @@ -64,7 +67,7 @@ def read(self):
# Before returning -- verify that requested section
# exists at all
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)
return out

Expand Down Expand Up @@ -117,9 +120,9 @@ def _glob(path):
}
_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()

Expand All @@ -136,6 +139,8 @@ def getOptions(self):
shouldExist=True)
if not self.__opts: # pragma: no cover
raise JailDefError("Init jail options failed")
if addOpts:
self.__opts = _merge_dicts(self.__opts, addOpts)

if not self.isEnabled():
return True
Expand All @@ -147,6 +152,8 @@ def getOptions(self):
filterName, filterOpt = extractOptions(flt)
except ValueError as e:
raise JailDefError("Invalid filter definition %r: %s" % (flt, e))
if addOpts:
filterOpt = _merge_dicts(filterOpt, addOpts)
self.__filter = FilterReader(
filterName, self.__name, filterOpt,
share_config=self.share_config, basedir=basedir)
Expand Down Expand Up @@ -219,6 +226,15 @@ def getOptions(self):
return False
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):
"""Convert read before __opts to the commands stream
Expand All @@ -240,9 +256,10 @@ def convert(self, allow_no_files=False):
stream.extend(self.__filter.convert())
# and using options from jail:
FilterReader._fillStream(stream, self.__opts, self.__name)
backend = self.__opts.get('backend', 'auto')
for opt, value in self.__opts.items():
if opt == "logpath":
if self.__opts.get('backend', '').startswith("systemd"): continue
if backend.startswith("systemd"): continue
found_files = 0
for path in value.split("\n"):
path = path.rsplit(" ", 1)
Expand All @@ -260,8 +277,6 @@ def convert(self, allow_no_files=False):
if not allow_no_files:
raise ValueError(msg)
logSys.warning(msg)
elif opt == "backend":
backend = value
elif opt == "ignoreip":
stream.append(["set", self.__name, "addignoreip"] + splitwords(value))
elif opt not in JailReader._ignoreOpts:
Expand Down
28 changes: 19 additions & 9 deletions fail2ban/tests/fail2banregextestcase.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ def testVerboseFullSshd(self):
"-l", "notice", # put down log-level, because of too many debug-messages
"-v", "--verbose-date", "--print-all-matched", "--print-all-ignored",
"-c", CONFIG_DIR,
FILENAME_SSHD, "sshd"
FILENAME_SSHD, "sshd.conf"
))
# test failure line and not-failure lines both presents:
self.assertLogged("[29116]: User root not allowed because account is locked",
Expand All @@ -269,7 +269,7 @@ def testVerboseFullSshd(self):
"-l", "notice", # put down log-level, because of too many debug-messages
"-vv", "-c", CONFIG_DIR,
"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:
self.assertLogged("Real filter options :", "'logtype': 'short'", all=True)
Expand All @@ -288,6 +288,16 @@ def testFastSshd(self):
"[29116]: User root not allowed because account is locked",
"[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):
# by the way test of missing lines by multiline in `for bufLine in orgLineBuffer[int(fullBuffer):]`
self.assertTrue(_test_exec(
Expand Down Expand Up @@ -431,11 +441,11 @@ def testStalledIPByNoFailFrmtOutput(self):
)
_test = lambda *args: _test_exec(*(opts + args))
# 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.pruneLog()
# 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):
self.assertTrue(_test('-o', 'ID:"<fid>" | IP:<ip> | U:<F-USER>', log,
flt+'[failregex="'
Expand All @@ -455,7 +465,7 @@ def _test_variants(flt="sshd", prefix=""):
# first with sshd and prefregex:
_test_variants()
# 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):
# datepattern doesn't match:
Expand Down Expand Up @@ -490,7 +500,7 @@ def testFrmtOutputWrapML(self):
# complex substitution using tags and message (ip, user, msg):
self.assertTrue(_test_exec('-o', '<ip>, <F-USER>, <msg>',
'-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:
lines = STR_ML_SSHD.split("\n")
self.assertTrue('192.0.2.2' not in lines[-2] and '192.0.2.2' in lines[-1])
Expand All @@ -506,7 +516,7 @@ def testFrmtOutputWrapML(self):
self.pruneLog("[test-phase 1] mode=aggressive & publickey=nofail + OK (accepted)")
self.assertTrue(_test_exec('-o', '<ip>, <F-USER>, <msg>',
'-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(
'192.0.2.2, git, '+lines[-4],
'192.0.2.2, git, '+lines[-3],
Expand All @@ -520,7 +530,7 @@ def testFrmtOutputWrapML(self):
self.pruneLog("[test-phase 2] mode=aggressive & publickey=nofail + FAIL (closed on preauth)")
self.assertTrue(_test_exec('-o', '<ip>, <F-USER>, <msg>',
'-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):
lines = STR_ML_SSHD.split("\n")[0:2] + STR_ML_SSHD_FAIL.split("\n")[-1:]
self.assertLogged(
Expand All @@ -541,7 +551,7 @@ def testOutputNoPendingFailuresAfterGained(self):
'svc[2] connect started 192.0.2.4\n'
'svc[2] connect authorized 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="'
'^started\n'
'^<F-NOFAIL><F-MLFFORGET>finished</F-MLFFORGET></F-NOFAIL> <ADDR>\n'
Expand Down

0 comments on commit 9bedc3c

Please sign in to comment.