Merge branch '0.10' into 0.11

pull/2935/head
sebres 2021-02-03 14:47:56 +01:00
commit 6198b4566c
13 changed files with 136 additions and 45 deletions

View File

@ -55,6 +55,12 @@ socket = /var/run/fail2ban/fail2ban.sock
# #
pidfile = /var/run/fail2ban/fail2ban.pid pidfile = /var/run/fail2ban/fail2ban.pid
# Option: allowipv6
# Notes.: Allows IPv6 interface:
# Default: auto
# Values: [ auto yes (on, true, 1) no (off, false, 0) ] Default: auto
#allowipv6 = auto
# Options: dbfile # Options: dbfile
# Notes.: Set the file for the fail2ban persistent data to be stored. # Notes.: Set the file for the fail2ban persistent data to be stored.
# A value of ":memory:" means database is only stored in memory # A value of ":memory:" means database is only stored in memory

View File

@ -53,6 +53,7 @@ class Fail2banReader(ConfigReader):
opts = [["string", "loglevel", "INFO" ], opts = [["string", "loglevel", "INFO" ],
["string", "logtarget", "STDERR"], ["string", "logtarget", "STDERR"],
["string", "syslogsocket", "auto"], ["string", "syslogsocket", "auto"],
["string", "allowipv6", "auto"],
["string", "dbfile", "/var/lib/fail2ban/fail2ban.sqlite3"], ["string", "dbfile", "/var/lib/fail2ban/fail2ban.sqlite3"],
["int", "dbmaxmatches", None], ["int", "dbmaxmatches", None],
["string", "dbpurgeage", "1d"]] ["string", "dbpurgeage", "1d"]]
@ -74,6 +75,7 @@ class Fail2banReader(ConfigReader):
# Also dbfile should be set before all other database options. # Also dbfile should be set before all other database options.
# So adding order indices into items, to be stripped after sorting, upon return # So adding order indices into items, to be stripped after sorting, upon return
order = {"thread":0, "syslogsocket":11, "loglevel":12, "logtarget":13, order = {"thread":0, "syslogsocket":11, "loglevel":12, "logtarget":13,
"allowipv6": 14,
"dbfile":50, "dbmaxmatches":51, "dbpurgeage":51} "dbfile":50, "dbmaxmatches":51, "dbpurgeage":51}
stream = list() stream = list()
for opt in self.__opts: for opt in self.__opts:

View File

@ -35,6 +35,7 @@ __license__ = "GPL"
import getopt import getopt
import logging import logging
import re
import os import os
import shlex import shlex
import sys import sys
@ -329,26 +330,33 @@ class Fail2banRegex(object):
regex = regextype + 'regex' regex = regextype + 'regex'
# try to check - we've case filter?[options...]?: # try to check - we've case filter?[options...]?:
basedir = self._opts.config basedir = self._opts.config
fltName = value
fltFile = None fltFile = None
fltOpt = {} fltOpt = {}
if regextype == 'fail': if regextype == 'fail':
fltName, fltOpt = extractOptions(value) if re.search(r'^/{0,3}[\w/_\-.]+(?:\[.*\])?$', value):
if fltName is not None: try:
if "." in fltName[~5:]: fltName, fltOpt = extractOptions(value)
tryNames = (fltName,) if "." in fltName[~5:]:
else: tryNames = (fltName,)
tryNames = (fltName, fltName + '.conf', fltName + '.local')
for fltFile in tryNames:
if not "/" in fltFile:
if os.path.basename(basedir) == 'filter.d':
fltFile = os.path.join(basedir, fltFile)
else:
fltFile = os.path.join(basedir, 'filter.d', fltFile)
else: else:
basedir = os.path.dirname(fltFile) tryNames = (fltName, fltName + '.conf', fltName + '.local')
if os.path.isfile(fltFile): for fltFile in tryNames:
break if not "/" in fltFile:
fltFile = None if os.path.basename(basedir) == 'filter.d':
fltFile = os.path.join(basedir, fltFile)
else:
fltFile = os.path.join(basedir, 'filter.d', fltFile)
else:
basedir = os.path.dirname(fltFile)
if os.path.isfile(fltFile):
break
fltFile = None
except Exception as e:
output("ERROR: Wrong filter name or options: %s" % (str(e),))
output(" while parsing: %s" % (value,))
if self._verbose: raise(e)
return False
# if it is filter file: # if it is filter file:
if fltFile is not None: if fltFile is not None:
if (basedir == self._opts.config if (basedir == self._opts.config

View File

@ -140,9 +140,10 @@ class JailReader(ConfigReader):
# Read filter # Read filter
flt = self.__opts["filter"] flt = self.__opts["filter"]
if flt: if flt:
filterName, filterOpt = extractOptions(flt) try:
if not filterName: filterName, filterOpt = extractOptions(flt)
raise JailDefError("Invalid filter definition %r" % flt) except ValueError as e:
raise JailDefError("Invalid filter definition %r: %s" % (flt, e))
self.__filter = FilterReader( self.__filter = FilterReader(
filterName, self.__name, filterOpt, filterName, self.__name, filterOpt,
share_config=self.share_config, basedir=self.getBaseDir()) share_config=self.share_config, basedir=self.getBaseDir())
@ -174,10 +175,10 @@ class JailReader(ConfigReader):
if not act: # skip empty actions if not act: # skip empty actions
continue continue
# join with previous line if needed (consider possible new-line): # join with previous line if needed (consider possible new-line):
actName, actOpt = extractOptions(act) try:
prevln = '' actName, actOpt = extractOptions(act)
if not actName: except ValueError as e:
raise JailDefError("Invalid action definition %r" % act) raise JailDefError("Invalid action definition %r: %s" % (act, e))
if actName.endswith(".py"): if actName.endswith(".py"):
self.__actions.append([ self.__actions.append([
"set", "set",

View File

@ -371,7 +371,7 @@ OPTION_CRE = re.compile(r"^([^\[]+)(?:\[(.*)\])?\s*$", re.DOTALL)
# since v0.10 separator extended with `]\s*[` for support of multiple option groups, syntax # since v0.10 separator extended with `]\s*[` for support of multiple option groups, syntax
# `action = act[p1=...][p2=...]` # `action = act[p1=...][p2=...]`
OPTION_EXTRACT_CRE = re.compile( OPTION_EXTRACT_CRE = re.compile(
r'([\w\-_\.]+)=(?:"([^"]*)"|\'([^\']*)\'|([^,\]]*))(?:,|\]\s*\[|$)', re.DOTALL) r'\s*([\w\-_\.]+)=(?:"([^"]*)"|\'([^\']*)\'|([^,\]]*))(?:,|\]\s*\[|$|(?P<wrngA>.+))|,?\s*$|(?P<wrngB>.+)', re.DOTALL)
# split by new-line considering possible new-lines within options [...]: # split by new-line considering possible new-lines within options [...]:
OPTION_SPLIT_CRE = re.compile( OPTION_SPLIT_CRE = re.compile(
r'(?:[^\[\s]+(?:\s*\[\s*(?:[\w\-_\.]+=(?:"[^"]*"|\'[^\']*\'|[^,\]]*)\s*(?:,|\]\s*\[)?\s*)*\])?\s*|\S+)(?=\n\s*|\s+|$)', re.DOTALL) r'(?:[^\[\s]+(?:\s*\[\s*(?:[\w\-_\.]+=(?:"[^"]*"|\'[^\']*\'|[^,\]]*)\s*(?:,|\]\s*\[)?\s*)*\])?\s*|\S+)(?=\n\s*|\s+|$)', re.DOTALL)
@ -379,13 +379,19 @@ OPTION_SPLIT_CRE = re.compile(
def extractOptions(option): def extractOptions(option):
match = OPTION_CRE.match(option) match = OPTION_CRE.match(option)
if not match: if not match:
# TODO proper error handling raise ValueError("unexpected option syntax")
return None, None
option_name, optstr = match.groups() option_name, optstr = match.groups()
option_opts = dict() option_opts = dict()
if optstr: if optstr:
for optmatch in OPTION_EXTRACT_CRE.finditer(optstr): for optmatch in OPTION_EXTRACT_CRE.finditer(optstr):
if optmatch.group("wrngA"):
raise ValueError("unexpected syntax at %d after option %r: %s" % (
optmatch.start("wrngA"), optmatch.group(1), optmatch.group("wrngA")[0:25]))
if optmatch.group("wrngB"):
raise ValueError("expected option, wrong syntax at %d: %s" % (
optmatch.start("wrngB"), optmatch.group("wrngB")[0:25]))
opt = optmatch.group(1) opt = optmatch.group(1)
if not opt: continue
value = [ value = [
val for val in optmatch.group(2,3,4) if val is not None][0] val for val in optmatch.group(2,3,4) if val is not None][0]
option_opts[opt.strip()] = value.strip() option_opts[opt.strip()] = value.strip()

View File

@ -169,27 +169,31 @@ class DNSUtils:
DNSUtils.CACHE_ipToName.set(key, name) DNSUtils.CACHE_ipToName.set(key, name)
return name return name
# key find cached own hostnames (this tuple-key cannot be used elsewhere):
_getSelfNames_key = ('self','dns')
@staticmethod @staticmethod
def getSelfNames(): def getSelfNames():
"""Get own host names of self""" """Get own host names of self"""
# try find cached own hostnames (this tuple-key cannot be used elsewhere): # try find cached own hostnames:
key = ('self','dns') names = DNSUtils.CACHE_ipToName.get(DNSUtils._getSelfNames_key)
names = DNSUtils.CACHE_ipToName.get(key)
# get it using different ways (a set with names of localhost, hostname, fully qualified): # get it using different ways (a set with names of localhost, hostname, fully qualified):
if names is None: if names is None:
names = set([ names = set([
'localhost', DNSUtils.getHostname(False), DNSUtils.getHostname(True) 'localhost', DNSUtils.getHostname(False), DNSUtils.getHostname(True)
]) - set(['']) # getHostname can return '' ]) - set(['']) # getHostname can return ''
# cache and return : # cache and return :
DNSUtils.CACHE_ipToName.set(key, names) DNSUtils.CACHE_ipToName.set(DNSUtils._getSelfNames_key, names)
return names return names
# key to find cached own IPs (this tuple-key cannot be used elsewhere):
_getSelfIPs_key = ('self','ips')
@staticmethod @staticmethod
def getSelfIPs(): def getSelfIPs():
"""Get own IP addresses of self""" """Get own IP addresses of self"""
# try find cached own IPs (this tuple-key cannot be used elsewhere): # to find cached own IPs:
key = ('self','ips') ips = DNSUtils.CACHE_nameToIp.get(DNSUtils._getSelfIPs_key)
ips = DNSUtils.CACHE_nameToIp.get(key)
# get it using different ways (a set with IPs of localhost, hostname, fully qualified): # get it using different ways (a set with IPs of localhost, hostname, fully qualified):
if ips is None: if ips is None:
ips = set() ips = set()
@ -199,13 +203,30 @@ class DNSUtils:
except Exception as e: # pragma: no cover except Exception as e: # pragma: no cover
logSys.warning("Retrieving own IPs of %s failed: %s", hostname, e) logSys.warning("Retrieving own IPs of %s failed: %s", hostname, e)
# cache and return : # cache and return :
DNSUtils.CACHE_nameToIp.set(key, ips) DNSUtils.CACHE_nameToIp.set(DNSUtils._getSelfIPs_key, ips)
return ips return ips
_IPv6IsAllowed = None
@staticmethod
def setIPv6IsAllowed(value):
DNSUtils._IPv6IsAllowed = value
logSys.debug("IPv6 is %s", ('on' if value else 'off') if value is not None else 'auto')
return value
# key to find cached value of IPv6 allowance (this tuple-key cannot be used elsewhere):
_IPv6IsAllowed_key = ('self','ipv6-allowed')
@staticmethod @staticmethod
def IPv6IsAllowed(): def IPv6IsAllowed():
# return os.path.exists("/proc/net/if_inet6") || any((':' in ip) for ip in DNSUtils.getSelfIPs()) if DNSUtils._IPv6IsAllowed is not None:
return any((':' in ip.ntoa) for ip in DNSUtils.getSelfIPs()) return DNSUtils._IPv6IsAllowed
v = DNSUtils.CACHE_nameToIp.get(DNSUtils._IPv6IsAllowed_key)
if v is not None:
return v
v = any((':' in ip.ntoa) for ip in DNSUtils.getSelfIPs())
DNSUtils.CACHE_nameToIp.set(DNSUtils._IPv6IsAllowed_key, v)
return v
## ##

View File

@ -34,7 +34,7 @@ import sys
from .observer import Observers, ObserverThread from .observer import Observers, ObserverThread
from .jails import Jails from .jails import Jails
from .filter import FileFilter, JournalFilter from .filter import DNSUtils, FileFilter, JournalFilter
from .transmitter import Transmitter from .transmitter import Transmitter
from .asyncserver import AsyncServer, AsyncServerException from .asyncserver import AsyncServer, AsyncServerException
from .. import version from .. import version
@ -293,6 +293,11 @@ class Server:
for name in self.__jails.keys(): for name in self.__jails.keys():
self.delJail(name, stop=False, join=True) self.delJail(name, stop=False, join=True)
def clearCaches(self):
# we need to clear caches, to be able to recognize new IPs/families etc:
DNSUtils.CACHE_nameToIp.clear()
DNSUtils.CACHE_ipToName.clear()
def reloadJails(self, name, opts, begin): def reloadJails(self, name, opts, begin):
if begin: if begin:
# begin reload: # begin reload:
@ -314,6 +319,8 @@ class Server:
if "--restart" in opts: if "--restart" in opts:
self.stopJail(name) self.stopJail(name)
else: else:
# invalidate caches by reload
self.clearCaches()
# first unban all ips (will be not restored after (re)start): # first unban all ips (will be not restored after (re)start):
if "--unban" in opts: if "--unban" in opts:
self.setUnbanIP() self.setUnbanIP()
@ -803,6 +810,11 @@ class Server:
logSys.info("flush performed on %s" % self.__logTarget) logSys.info("flush performed on %s" % self.__logTarget)
return "flushed" return "flushed"
@staticmethod
def setIPv6IsAllowed(value):
value = _as_bool(value) if value != 'auto' else None
return DNSUtils.setIPv6IsAllowed(value)
def setThreadOptions(self, value): def setThreadOptions(self, value):
for o, v in value.iteritems(): for o, v in value.iteritems():
if o == 'stacksize': if o == 'stacksize':

View File

@ -173,6 +173,11 @@ class Transmitter:
return self.__server.getSyslogSocket() return self.__server.getSyslogSocket()
else: else:
raise Exception("Failed to change syslog socket") raise Exception("Failed to change syslog socket")
elif name == "allowipv6":
value = command[1]
self.__server.setIPv6IsAllowed(value)
if self.__quiet: return
return value
#Thread #Thread
elif name == "thread": elif name == "thread":
value = command[1] value = command[1]

View File

@ -381,13 +381,16 @@ class JailReaderTest(LogCaptureTestCase):
self.assertEqual(('mail.who_is', {'a':'cat', 'b':'dog'}), extractOptions("mail.who_is[a=cat,b=dog]")) self.assertEqual(('mail.who_is', {'a':'cat', 'b':'dog'}), extractOptions("mail.who_is[a=cat,b=dog]"))
self.assertEqual(('mail--ho_is', {}), extractOptions("mail--ho_is")) self.assertEqual(('mail--ho_is', {}), extractOptions("mail--ho_is"))
self.assertEqual(('mail--ho_is', {}), extractOptions("mail--ho_is['s']"))
#print(self.getLog())
#self.assertLogged("Invalid argument ['s'] in ''s''")
self.assertEqual(('mail', {'a': ','}), extractOptions("mail[a=',']")) self.assertEqual(('mail', {'a': ','}), extractOptions("mail[a=',']"))
self.assertEqual(('mail', {'a': 'b'}), extractOptions("mail[a=b, ]"))
#self.assertRaises(ValueError, extractOptions ,'mail-how[') self.assertRaises(ValueError, extractOptions ,'mail-how[')
self.assertRaises(ValueError, extractOptions, """mail[a="test with interim (wrong) "" quotes"]""")
self.assertRaises(ValueError, extractOptions, """mail[a='test with interim (wrong) '' quotes']""")
self.assertRaises(ValueError, extractOptions, """mail[a='x, y, z', b=x, y, z]""")
self.assertRaises(ValueError, extractOptions, """mail['s']""")
# Empty option # Empty option
option = "abc[]" option = "abc[]"
@ -752,9 +755,9 @@ class JailsReaderTest(LogCaptureTestCase):
['add', 'tz_correct', 'auto'], ['add', 'tz_correct', 'auto'],
['start', 'tz_correct'], ['start', 'tz_correct'],
['config-error', ['config-error',
"Jail 'brokenactiondef' skipped, because of wrong configuration: Invalid action definition 'joho[foo'"], "Jail 'brokenactiondef' skipped, because of wrong configuration: Invalid action definition 'joho[foo': unexpected option syntax"],
['config-error', ['config-error',
"Jail 'brokenfilterdef' skipped, because of wrong configuration: Invalid filter definition 'flt[test'"], "Jail 'brokenfilterdef' skipped, because of wrong configuration: Invalid filter definition 'flt[test': unexpected option syntax"],
['config-error', ['config-error',
"Jail 'missingaction' skipped, because of wrong configuration: Unable to read action 'noactionfileforthisaction'"], "Jail 'missingaction' skipped, because of wrong configuration: Unable to read action 'noactionfileforthisaction'"],
['config-error', ['config-error',
@ -975,6 +978,7 @@ class JailsReaderTest(LogCaptureTestCase):
['set', 'syslogsocket', 'auto'], ['set', 'syslogsocket', 'auto'],
['set', 'loglevel', "INFO"], ['set', 'loglevel', "INFO"],
['set', 'logtarget', '/var/log/fail2ban.log'], ['set', 'logtarget', '/var/log/fail2ban.log'],
['set', 'allowipv6', 'auto'],
['set', 'dbfile', '/var/lib/fail2ban/fail2ban.sqlite3'], ['set', 'dbfile', '/var/lib/fail2ban/fail2ban.sqlite3'],
['set', 'dbmaxmatches', 10], ['set', 'dbmaxmatches', 10],
['set', 'dbpurgeage', '1d'], ['set', 'dbpurgeage', '1d'],

View File

@ -141,6 +141,12 @@ class Fail2banRegexTest(LogCaptureTestCase):
)) ))
self.assertLogged("Unable to compile regular expression") self.assertLogged("Unable to compile regular expression")
def testWrongFilterOptions(self):
self.assertFalse(_test_exec(
"test", "flt[a='x,y,z',b=z,y,x]"
))
self.assertLogged("Wrong filter name or options", "wrong syntax at 14: y,x", all=True)
def testDirectFound(self): def testDirectFound(self):
self.assertTrue(_test_exec( self.assertTrue(_test_exec(
"--datepattern", r"^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?", "--datepattern", r"^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",

View File

@ -35,7 +35,7 @@ import platform
from ..server.failregex import Regex, FailRegex, RegexException from ..server.failregex import Regex, FailRegex, RegexException
from ..server import actions as _actions from ..server import actions as _actions
from ..server.server import Server from ..server.server import Server
from ..server.ipdns import IPAddr from ..server.ipdns import DNSUtils, IPAddr
from ..server.jail import Jail from ..server.jail import Jail
from ..server.jailthread import JailThread from ..server.jailthread import JailThread
from ..server.ticket import BanTicket from ..server.ticket import BanTicket
@ -175,6 +175,19 @@ class Transmitter(TransmitterBase):
def testVersion(self): def testVersion(self):
self.assertEqual(self.transm.proceed(["version"]), (0, version.version)) self.assertEqual(self.transm.proceed(["version"]), (0, version.version))
def testSetIPv6(self):
try:
self.assertEqual(self.transm.proceed(["set", "allowipv6", 'yes']), (0, 'yes'))
self.assertTrue(DNSUtils.IPv6IsAllowed())
self.assertLogged("IPv6 is on"); self.pruneLog()
self.assertEqual(self.transm.proceed(["set", "allowipv6", 'no']), (0, 'no'))
self.assertFalse(DNSUtils.IPv6IsAllowed())
self.assertLogged("IPv6 is off"); self.pruneLog()
finally:
# restore back to auto:
self.assertEqual(self.transm.proceed(["set", "allowipv6", "auto"]), (0, "auto"))
self.assertLogged("IPv6 is auto"); self.pruneLog()
def testSleep(self): def testSleep(self):
if not unittest.F2B.fast: if not unittest.F2B.fast:
t0 = time.time() t0 = time.time()

View File

@ -320,6 +320,7 @@ def initTests(opts):
# precache all invalid ip's (TEST-NET-1, ..., TEST-NET-3 according to RFC 5737): # precache all invalid ip's (TEST-NET-1, ..., TEST-NET-3 according to RFC 5737):
c = DNSUtils.CACHE_ipToName c = DNSUtils.CACHE_ipToName
c.clear = lambda: logSys.warn('clear CACHE_ipToName is disabled in test suite')
# increase max count and max time (too many entries, long time testing): # increase max count and max time (too many entries, long time testing):
c.setOptions(maxCount=10000, maxTime=5*60) c.setOptions(maxCount=10000, maxTime=5*60)
for i in xrange(256): for i in xrange(256):
@ -337,6 +338,7 @@ def initTests(opts):
c.set('8.8.4.4', 'dns.google') c.set('8.8.4.4', 'dns.google')
# precache all dns to ip's used in test cases: # precache all dns to ip's used in test cases:
c = DNSUtils.CACHE_nameToIp c = DNSUtils.CACHE_nameToIp
c.clear = lambda: logSys.warn('clear CACHE_nameToIp is disabled in test suite')
for i in ( for i in (
('999.999.999.999', set()), ('999.999.999.999', set()),
('abcdef.abcdef', set()), ('abcdef.abcdef', set()),

View File

@ -151,6 +151,11 @@ PID filename. Default: /var/run/fail2ban/fail2ban.pid
.br .br
This is used to store the process ID of the fail2ban server. This is used to store the process ID of the fail2ban server.
.TP .TP
.B allowipv6
option to allow IPv6 interface - auto, yes (on, true, 1) or no (off, false, 0). Default: auto
.br
This value can be used to declare fail2ban whether IPv6 is allowed or not.
.TP
.B dbfile .B dbfile
Database filename. Default: /var/lib/fail2ban/fail2ban.sqlite3 Database filename. Default: /var/lib/fail2ban/fail2ban.sqlite3
.br .br