Merge pull request #2588 from sebres/0.10-invariant-improve

0.10 auto-reban, improved invariant check and conditional operations
pull/2601/head
Sergey G. Brester 2020-01-08 21:04:42 +01:00 committed by GitHub
commit a15832e773
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 802 additions and 187 deletions

View File

@ -80,6 +80,10 @@ ver. 0.10.5-dev-1 (20??/??/??) - development edition
* `filter.d/sendmail-auth.conf`, `filter.d/sendmail-reject.conf` :
- ID in prefix can be longer as 14 characters (gh-2563);
* all filters would accept square brackets around IPv4 addresses also (e. g. monit-filter, gh-2494)
* avoids unhandled exception during flush (gh-2588)
* fixes pass2allow-ftp jail - due to inverted handling, action should prohibit access per default for any IP,
therefore reset start on demand parameter for this action (it will be started immediately by repair);
* auto-detection of IPv6 subsystem availability (important for not on-demand actions or jails, like pass2allow);
### New Features
* new replacement tags for failregex to match subnets in form of IP-addresses with CIDR mask (gh-2559):
@ -136,6 +140,18 @@ filter = flt[logtype=short]
* samplestestcase.py (testSampleRegexsFactory) extended:
- allow coverage of journal logtype;
- new option `fileOptions` to set common filter/test options for whole test-file;
* large enhancement: auto-reban, improved invariant check and conditional operations (gh-2588):
- improves invariant check and repair (avoid unhandled exception, consider family on conditional operations, etc),
prepared for bulk re-ban in repair case (if bulk-ban becomes implemented);
- automatic reban (repeat banning action) after repair/restore sane environment, if already logged ticket causes
new failures (via new action operation `actionreban` or `actionban` if still not defined in action);
* introduces banning epoch for actions and tickets (to distinguish or recognize removed set of the tickets);
* invariant check avoids repair by unban/stop (unless parameter `actionrepair_on_unban` set to `true`);
* better handling for all conditional operations (distinguish families for certain operations like
repair/flush/stop, prepared for other families, e. g. if different handling for subnets expected, etc);
* partially implements gh-980 (more breakdown safe handling);
* closes gh-1680 (better as large-scale banning implementation with on-demand reban by failure,
at least unless a bulk-ban gets implemented);
ver. 0.10.4 (2018/10/04) - ten-four-on-due-date-ten-four

View File

@ -861,7 +861,8 @@ filter = apache-pass[knocking_url="%(knocking_url)s"]
logpath = %(apache_access_log)s
blocktype = RETURN
returntype = DROP
action = %(action_)s[blocktype=%(blocktype)s, returntype=%(returntype)s]
action = %(action_)s[blocktype=%(blocktype)s, returntype=%(returntype)s,
actionstart_on_demand=false, actionrepair_on_unban=true]
bantime = 1h
maxretry = 1
findtime = 1

View File

@ -44,7 +44,9 @@ class ActionReader(DefinitionInitConfigReader):
"actionreload": ["string", None],
"actioncheck": ["string", None],
"actionrepair": ["string", None],
"actionrepair_on_unban": ["string", None],
"actionban": ["string", None],
"actionreban": ["string", None],
"actionunban": ["string", None],
"norestored": ["string", None],
}
@ -78,7 +80,7 @@ class ActionReader(DefinitionInitConfigReader):
opts = self.getCombined(
ignore=CommandAction._escapedTags | set(('timeout', 'bantime')))
# type-convert only after combined (otherwise boolean converting prevents substitution):
for o in ('norestored', 'actionstart_on_demand'):
for o in ('norestored', 'actionstart_on_demand', 'actionrepair_on_unban'):
if opts.get(o):
opts[o] = self._convert_to_boolean(opts[o])

View File

@ -33,7 +33,7 @@ from .configreader import ConfigReaderUnshared, ConfigReader
from .filterreader import FilterReader
from .actionreader import ActionReader
from ..version import version
from ..helpers import getLogger, extractOptions, splitwords
from ..helpers import getLogger, extractOptions, splitWithOptions, splitwords
# Gets the instance of the logger.
logSys = getLogger(__name__)
@ -164,21 +164,15 @@ class JailReader(ConfigReader):
self.__filter.getOptions(self.__opts)
# Read action
prevln = ''
actlst = self.__opts["action"].split('\n')
for n, act in enumerate(actlst):
for act in splitWithOptions(self.__opts["action"]):
try:
act = act.strip()
if not act: # skip empty actions
continue
# join with previous line if needed (consider possible new-line):
if prevln: act = prevln + '\n' + act
actName, actOpt = extractOptions(act)
prevln = ''
if not actName:
# consider possible new-line, so repeat with joined next line's:
if n < len(actlst) - 1:
prevln = act
continue
raise JailDefError("Invalid action definition %r" % act)
if actName.endswith(".py"):
self.__actions.append([

View File

@ -336,6 +336,9 @@ OPTION_CRE = re.compile(r"^([^\[]+)(?:\[(.*)\])?\s*$", re.DOTALL)
# `action = act[p1=...][p2=...]`
OPTION_EXTRACT_CRE = re.compile(
r'([\w\-_\.]+)=(?:"([^"]*)"|\'([^\']*)\'|([^,\]]*))(?:,|\]\s*\[|$)', re.DOTALL)
# split by new-line considering possible new-lines within options [...]:
OPTION_SPLIT_CRE = re.compile(
r'(?:[^\[\n]+(?:\s*\[\s*(?:[\w\-_\.]+=(?:"[^"]*"|\'[^\']*\'|[^,\]]*)\s*(?:,|\]\s*\[)?\s*)*\])?\s*|[^\n]+)(?=\n\s*|$)', re.DOTALL)
def extractOptions(option):
match = OPTION_CRE.match(option)
@ -352,6 +355,9 @@ def extractOptions(option):
option_opts[opt.strip()] = value.strip()
return option_name, option_opts
def splitWithOptions(option):
return OPTION_SPLIT_CRE.findall(option)
#
# Following facilities used for safe recursive interpolation of
# tags (<tag>) in tagged options.
@ -386,8 +392,7 @@ def substituteRecursiveTags(inptags, conditional='',
"""
#logSys = getLogger("fail2ban")
tre_search = TAG_CRE.search
# copy return tags dict to prevent modifying of inptags:
tags = inptags.copy()
tags = inptags
# init:
ignore = set(ignore)
done = set()
@ -449,6 +454,9 @@ def substituteRecursiveTags(inptags, conditional='',
# check still contains any tag - should be repeated (possible embedded-recursive substitution):
if tre_search(value):
repFlag = True
# copy return tags dict to prevent modifying of inptags:
if id(tags) == id(inptags):
tags = inptags.copy()
tags[tag] = value
# no more sub tags (and no possible composite), add this tag to done set (just to be faster):
if '<' not in value: done.add(tag)

View File

@ -33,10 +33,11 @@ from abc import ABCMeta
from collections import MutableMapping
from .failregex import mapTag2Opt
from .ipdns import asip, DNSUtils
from .ipdns import DNSUtils
from .mytime import MyTime
from .utils import Utils
from ..helpers import getLogger, _merge_copy_dicts, uni_string, substituteRecursiveTags, TAG_CRE, MAX_TAG_REPLACE_COUNT
from ..helpers import getLogger, _merge_copy_dicts, \
splitwords, substituteRecursiveTags, uni_string, TAG_CRE, MAX_TAG_REPLACE_COUNT
# Gets the instance of the logger.
logSys = getLogger(__name__)
@ -44,13 +45,14 @@ logSys = getLogger(__name__)
# Create a lock for running system commands
_cmd_lock = threading.Lock()
# Todo: make it configurable resp. automatically set, ex.: `[ -f /proc/net/if_inet6 ] && echo 'yes' || echo 'no'`:
allowed_ipv6 = True
# Specifies whether IPv6 subsystem is available:
allowed_ipv6 = DNSUtils.IPv6IsAllowed
# capture groups from filter for map to ticket data:
FCUSTAG_CRE = re.compile(r'<F-([A-Z0-9_\-]+)>'); # currently uppercase only
CONDITIONAL_FAM_RE = re.compile(r"^(\w+)\?(family)=")
COND_FAMILIES = ('inet4', 'inet6')
CONDITIONAL_FAM_RE = re.compile(r"^(\w+)\?(family)=(.*)$")
# Special tags:
DYN_REPL_TAGS = {
@ -173,7 +175,7 @@ class CallingMap(MutableMapping, object):
def __len__(self):
return len(self.data)
def copy(self): # pragma: no cover
def copy(self):
return self.__class__(_merge_copy_dicts(self.data, self.storage))
@ -215,6 +217,7 @@ class ActionBase(object):
"start",
"stop",
"ban",
"reban",
"unban",
)
for method in required:
@ -248,6 +251,17 @@ class ActionBase(object):
"""
pass
def reban(self, aInfo): # pragma: no cover - abstract
"""Executed when a ban occurs.
Parameters
----------
aInfo : dict
Dictionary which includes information in relation to
the ban.
"""
return self.ban(aInfo)
def unban(self, aInfo): # pragma: no cover - abstract
"""Executed when a ban expires.
@ -279,6 +293,7 @@ class CommandAction(ActionBase):
----------
actionban
actioncheck
actionreban
actionreload
actionrepair
actionstart
@ -299,6 +314,7 @@ class CommandAction(ActionBase):
self.actionstart = ''
## Command executed when ticket gets banned.
self.actionban = ''
self.actionreban = ''
## Command executed when ticket gets removed.
self.actionunban = ''
## Command executed in order to check requirements.
@ -340,6 +356,8 @@ class CommandAction(ActionBase):
# set:
self.__dict__[name] = value
__setitem__ = __setattr__
def __delattr__(self, name):
if not name.startswith('_'):
# parameters changed - clear properties and substitution cache:
@ -363,8 +381,8 @@ class CommandAction(ActionBase):
self.__properties = dict(
(key, getattr(self, key))
for key in dir(self)
if not key.startswith("_") and not callable(getattr(self, key)))
#
if not key.startswith("_") and not callable(getattr(self, key))
)
return self.__properties
@property
@ -372,10 +390,40 @@ class CommandAction(ActionBase):
return self.__substCache
def _getOperation(self, tag, family):
# replace operation tag (interpolate all values), be sure family is enclosed as conditional value
# (as lambda in addrepl so only if not overwritten in action):
return self.replaceTag(tag, self._properties,
conditional=('family=' + family), cache=self.__substCache)
conditional=('family='+family if family else ''),
addrepl=(lambda tag:family if tag == 'family' else None),
cache=self.__substCache)
def _executeOperation(self, tag, operation, family=[]):
def _operationExecuted(self, tag, family, *args):
""" Get, set or delete command of operation considering family.
"""
key = ('__eOpCmd',tag)
if not len(args): # get
if not callable(family): # pragma: no cover
return self.__substCache.get(key, {}).get(family)
# family as expression - use it to filter values:
return [v for f, v in self.__substCache.get(key, {}).iteritems() if family(f)]
cmd = args[0]
if cmd: # set:
try:
famd = self.__substCache[key]
except KeyError:
famd = self.__substCache[key] = {}
famd[family] = cmd
else: # delete (given family and all other with same command):
try:
famd = self.__substCache[key]
cmd = famd.pop(family)
for family, v in famd.items():
if v == cmd:
del famd[family]
except KeyError: # pragma: no cover
pass
def _executeOperation(self, tag, operation, family=[], afterExec=None):
"""Executes the operation commands (like "actionstart", "actionstop", etc).
Replace the tags in the action command with actions properties
@ -383,24 +431,53 @@ class CommandAction(ActionBase):
"""
# check valid tags in properties (raises ValueError if self recursion, etc.):
res = True
try:
# common (resp. ipv4):
cmd = self._getOperation(tag, 'inet4')
if not family or 'inet4' in family:
if cmd:
res &= self.executeCmd(cmd, self.timeout)
# execute ipv6 operation if available (and not the same as ipv4):
if allowed_ipv6 and (not family or 'inet6' in family):
cmd6 = self._getOperation(tag, 'inet6')
if cmd6 and cmd6 != cmd: # - avoid double execution of same command
res &= self.executeCmd(cmd6, self.timeout)
if not res:
raise RuntimeError("Error %s action %s/%s" % (operation, self._jail, self._name,))
except ValueError as e:
raise RuntimeError("Error %s action %s/%s: %r" % (operation, self._jail, self._name, e))
err = 'Script error'
if not family: # all started:
family = [famoper for (famoper,v) in self.__started.iteritems() if v]
for famoper in family:
try:
cmd = self._getOperation(tag, famoper)
ret = True
# avoid double execution of same command for both families:
if cmd and cmd not in self._operationExecuted(tag, lambda f: f != famoper):
ret = self.executeCmd(cmd, self.timeout)
res &= ret
if afterExec: afterExec(famoper, ret)
self._operationExecuted(tag, famoper, cmd if ret else None)
except ValueError as e:
res = False
err = e
if not res:
raise RuntimeError("Error %s action %s/%s: %r" % (operation, self._jail, self._name, err))
return res
COND_FAMILIES = ('inet4', 'inet6')
@property
def _hasCondSection(self):
v = self._properties.get('__hasCondSection')
if v is not None:
return v
v = False
for n in self._properties:
if CONDITIONAL_FAM_RE.match(n):
v = True
break
self._properties['__hasCondSection'] = v
return v
@property
def _families(self):
v = self._properties.get('__families')
if v: return v
v = self._properties.get('families')
if v and not isinstance(v, (list,set)): # pragma: no cover - still unused
v = splitwords(v)
elif self._hasCondSection: # all conditional families:
# todo: check it is needed at all # common (resp. ipv4) + ipv6 if allowed:
v = ['inet4', 'inet6'] if allowed_ipv6() else ['inet4']
else: # all action tags seems to be the same
v = ['']
self._properties['__families'] = v
return v
@property
def _startOnDemand(self):
@ -409,15 +486,19 @@ class CommandAction(ActionBase):
if v is not None:
return v
# not set - auto-recognize (depending on conditional):
v = False
for n in self._properties:
if CONDITIONAL_FAM_RE.match(n):
v = True
break
v = self._hasCondSection
self._properties['actionstart_on_demand'] = v
return v
def start(self, family=None, forceStart=False):
def start(self):
"""Executes the "actionstart" command.
Replace the tags in the action command with actions properties
and executes the resulting command.
"""
return self._start()
def _start(self, family=None, forceStart=False):
"""Executes the "actionstart" command.
Replace the tags in the action command with actions properties
@ -427,14 +508,18 @@ class CommandAction(ActionBase):
if self._startOnDemand:
if not forceStart:
return True
elif self.__started.get(family): # pragma: no cover - normally unreachable
elif not forceStart and self.__started.get(family): # pragma: no cover - normally unreachable
return True
ret = self._executeOperation('<actionstart>', 'starting', family=family)
self.__started[family] = ret
family = [family] if family is not None else self._families
def _started(family, ret):
if ret:
self._operationExecuted('<actionstop>', family, None)
self.__started[family] = 1
ret = self._executeOperation('<actionstart>', 'starting', family=family, afterExec=_started)
return ret
def ban(self, aInfo):
"""Executes the "actionban" command.
def ban(self, aInfo, cmd='<actionban>'):
"""Executes the given command ("actionban" or "actionreban").
Replaces the tags in the action command with actions properties
and ban information, and executes the resulting command.
@ -446,13 +531,14 @@ class CommandAction(ActionBase):
the ban.
"""
# if we should start the action on demand (conditional by family):
family = aInfo.get('family', '')
if self._startOnDemand:
family = aInfo.get('family')
if not self.__started.get(family):
self.start(family, forceStart=True)
self._start(family, forceStart=True)
# ban:
if not self._processCmd('<actionban>', aInfo):
if not self._processCmd(cmd, aInfo):
raise RuntimeError("Error banning %(ip)s" % aInfo)
self.__started[family] = self.__started.get(family, 0) | 3; # started and contains items
def unban(self, aInfo):
"""Executes the "actionunban" command.
@ -466,8 +552,25 @@ class CommandAction(ActionBase):
Dictionary which includes information in relation to
the ban.
"""
if not self._processCmd('<actionunban>', aInfo):
raise RuntimeError("Error unbanning %(ip)s" % aInfo)
family = aInfo.get('family', '')
if self.__started.get(family, 0) & 2: # contains items
if not self._processCmd('<actionunban>', aInfo):
raise RuntimeError("Error unbanning %(ip)s" % aInfo)
def reban(self, aInfo):
"""Executes the "actionreban" command if available, otherwise simply repeat "actionban".
Replaces the tags in the action command with actions properties
and ban information, and executes the resulting command.
Parameters
----------
aInfo : dict
Dictionary which includes information in relation to
the ban.
"""
# re-ban:
return self.ban(aInfo, '<actionreban>' if self.actionreban else '<actionban>')
def flush(self):
"""Executes the "actionflush" command.
@ -478,13 +581,15 @@ class CommandAction(ActionBase):
Replaces the tags in the action command with actions properties
and executes the resulting command.
"""
family = []
# collect started families, if started on demand (conditional):
if self._startOnDemand:
family = [f for (f,v) in self.__started.iteritems() if v]
# if no started (on demand) actions:
if not family: return True
return self._executeOperation('<actionflush>', 'flushing', family=family)
# collect started families, may be started on demand (conditional):
family = [f for (f,v) in self.__started.iteritems() if v & 3 == 3]; # started and contains items
# if nothing contains items:
if not family: return True
# flush:
def _afterFlush(family, ret):
if ret and self.__started.get(family):
self.__started[family] &= ~2; # no items anymore
return self._executeOperation('<actionflush>', 'flushing', family=family, afterExec=_afterFlush)
def stop(self):
"""Executes the "actionstop" command.
@ -492,14 +597,30 @@ class CommandAction(ActionBase):
Replaces the tags in the action command with actions properties
and executes the resulting command.
"""
family = []
return self._stop()
def _stop(self, family=None):
"""Executes the "actionstop" command.
Replaces the tags in the action command with actions properties
and executes the resulting command.
"""
# collect started families, if started on demand (conditional):
if self._startOnDemand:
if family is None:
family = [f for (f,v) in self.__started.iteritems() if v]
# if no started (on demand) actions:
if not family: return True
self.__started = {}
return self._executeOperation('<actionstop>', 'stopping', family=family)
self.__started = {}
else:
try:
self.__started[family] &= 0
family = [family]
except KeyError: # pragma: no cover
return True
def _stopped(family, ret):
if ret:
self._operationExecuted('<actionstart>', family, None)
return self._executeOperation('<actionstop>', 'stopping', family=family, afterExec=_stopped)
def reload(self, **kwargs):
"""Executes the "actionreload" command.
@ -514,6 +635,20 @@ class CommandAction(ActionBase):
"""
return self._executeOperation('<actionreload>', 'reloading')
def consistencyCheck(self, beforeRepair=None):
"""Executes the invariant check with repair if expected (conditional).
"""
ret = True
# for each started family:
if self.actioncheck:
for (family, started) in self.__started.items():
if started and not self._invariantCheck(family, beforeRepair):
# reset started flag and command of executed operation:
self.__started[family] = 0
self._operationExecuted('<actionstart>', family, None)
ret &= False
return ret
@staticmethod
def escapeTag(value):
"""Escape characters which may be used for command injection.
@ -541,7 +676,7 @@ class CommandAction(ActionBase):
return value
@classmethod
def replaceTag(cls, query, aInfo, conditional='', cache=None):
def replaceTag(cls, query, aInfo, conditional='', addrepl=None, cache=None):
"""Replaces tags in `query` with property values.
Parameters
@ -582,7 +717,8 @@ class CommandAction(ActionBase):
pass
# interpolation of dictionary:
if subInfo is None:
subInfo = substituteRecursiveTags(aInfo, conditional, ignore=cls._escapedTags)
subInfo = substituteRecursiveTags(aInfo, conditional, ignore=cls._escapedTags,
addrepl=addrepl)
# cache if possible:
if csubkey is not None:
cache[csubkey] = subInfo
@ -705,7 +841,58 @@ class CommandAction(ActionBase):
realCmd = Utils.buildShellCmd(realCmd, varsDict)
return realCmd
def _processCmd(self, cmd, aInfo=None, conditional=''):
@property
def banEpoch(self):
return getattr(self, '_banEpoch', 0)
def invalidateBanEpoch(self):
"""Increments ban epoch of jail and this action, so already banned tickets would cause
a re-ban for all tickets with previous epoch."""
if self._jail is not None:
self._banEpoch = self._jail.actions.banEpoch = self._jail.actions.banEpoch + 1
else:
self._banEpoch = self.banEpoch + 1
def _invariantCheck(self, family=None, beforeRepair=None, forceStart=True):
"""Executes a substituted `actioncheck` command.
"""
# for started action/family only (avoid check not started inet4 if inet6 gets broken):
if not forceStart and family is not None and family not in self.__started:
return 1
checkCmd = self._getOperation('<actioncheck>', family)
if not checkCmd or self.executeCmd(checkCmd, self.timeout):
return 1
# if don't need repair/restore - just return:
if beforeRepair and not beforeRepair():
return -1
self._logSys.error(
"Invariant check failed. Trying to restore a sane environment")
# increment ban epoch of jail and this action (allows re-ban on already banned):
self.invalidateBanEpoch()
# try to find repair command, if exists - exec it:
repairCmd = self._getOperation('<actionrepair>', family)
if repairCmd:
if not self.executeCmd(repairCmd, self.timeout):
self.__started[family] = 0
self._logSys.critical("Unable to restore environment")
return 0
self.__started[family] = 1
else:
# no repair command, try to restart action...
# [WARNING] TODO: be sure all banactions get a repair command, because
# otherwise stop/start will theoretically remove all the bans,
# but the tickets are still in BanManager, so in case of new failures
# it will not be banned, because "already banned" will happen.
try:
self._stop(family)
except RuntimeError: # bypass error in stop (if start/check succeeded hereafter).
pass
self._start(family, forceStart=forceStart or not self._startOnDemand)
if self.__started.get(family) and not self.executeCmd(checkCmd, self.timeout):
self._logSys.critical("Unable to restore environment")
return 0
return 1
def _processCmd(self, cmd, aInfo=None):
"""Executes a command with preliminary checks and substitutions.
Before executing any commands, executes the "check" command first
@ -730,47 +917,28 @@ class CommandAction(ActionBase):
return True
# conditional corresponding family of the given ip:
if conditional == '':
conditional = 'family=inet4'
if allowed_ipv6:
try:
ip = aInfo["ip"]
if ip and asip(ip).isIPv6:
conditional = 'family=inet6'
except KeyError:
pass
try:
family = aInfo["family"]
except (KeyError, TypeError):
family = ''
checkCmd = self.replaceTag('<actioncheck>', self._properties,
conditional=conditional, cache=self.__substCache)
if checkCmd:
if not self.executeCmd(checkCmd, self.timeout):
self._logSys.error(
"Invariant check failed. Trying to restore a sane environment")
# try to find repair command, if exists - exec it:
repairCmd = self.replaceTag('<actionrepair>', self._properties,
conditional=conditional, cache=self.__substCache)
if repairCmd:
if not self.executeCmd(repairCmd, self.timeout):
self._logSys.critical("Unable to restore environment")
return False
else:
# no repair command, try to restart action...
# [WARNING] TODO: be sure all banactions get a repair command, because
# otherwise stop/start will theoretically remove all the bans,
# but the tickets are still in BanManager, so in case of new failures
# it will not be banned, because "already banned" will happen.
try:
self.stop()
except RuntimeError: # bypass error in stop (if start/check succeeded hereafter).
pass
self.start()
if not self.executeCmd(checkCmd, self.timeout):
self._logSys.critical("Unable to restore environment")
# invariant check:
if self.actioncheck:
# don't repair/restore if unban (no matter):
def _beforeRepair():
if cmd == '<actionunban>' and not self._properties.get('actionrepair_on_unban'):
self._logSys.error("Invariant check failed. Unban is impossible.")
return False
return True
# check and repair if broken:
ret = self._invariantCheck(family, _beforeRepair, forceStart=(cmd != '<actionunban>'))
# if not sane (and not restored) return:
if ret != 1:
return False
# Replace static fields
realCmd = self.replaceTag(cmd, self._properties,
conditional=conditional, cache=self.__substCache)
conditional=('family='+family if family else ''), cache=self.__substCache)
# Replace dynamical tags, important - don't cache, no recursion and auto-escape here
if aInfo is not None:

View File

@ -81,6 +81,8 @@ class Actions(JailThread, Mapping):
self._actions = OrderedDict()
## The ban manager.
self.__banManager = BanManager()
self.banEpoch = 0
self.__lastConsistencyCheckTM = 0
## Precedence of ban (over unban), so max number of tickets banned (to call an unban check):
self.banPrecedence = 10
## Max count of outdated tickets to unban per each __checkUnBan operation:
@ -160,8 +162,8 @@ class Actions(JailThread, Mapping):
delacts = OrderedDict((name, action) for name, action in self._actions.iteritems()
if name not in self._reload_actions)
if len(delacts):
# unban all tickets using remove action only:
self.__flushBan(db=False, actions=delacts)
# unban all tickets using removed actions only:
self.__flushBan(db=False, actions=delacts, stop=True)
# stop and remove it:
self.stopActions(actions=delacts)
delattr(self, '_reload_actions')
@ -214,10 +216,10 @@ class Actions(JailThread, Mapping):
if isinstance(ip, list):
# Multiple IPs:
tickets = (BanTicket(ip if isinstance(ip, IPAddr) else IPAddr(ip), unixTime) for ip in ip)
tickets = (BanTicket(ip, unixTime) for ip in ip)
else:
# Single IP:
tickets = (BanTicket(ip if isinstance(ip, IPAddr) else IPAddr(ip), unixTime),)
tickets = (BanTicket(ip, unixTime),)
return self.__checkBan(tickets)
@ -326,7 +328,7 @@ class Actions(JailThread, Mapping):
self.__checkUnBan(bancnt if bancnt and bancnt < self.unbanMaxCount else self.unbanMaxCount)
cnt = 0
self.__flushBan()
self.__flushBan(stop=True)
self.stopActions()
return True
@ -439,6 +441,7 @@ class Actions(JailThread, Mapping):
cnt = 0
if not tickets:
tickets = self.__getFailTickets(self.banPrecedence)
rebanacts = None
for ticket in tickets:
bTicket = BanManager.createBanTicket(ticket)
ip = bTicket.getIP()
@ -461,6 +464,8 @@ class Actions(JailThread, Mapping):
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
# after all actions are processed set banned flag:
bTicket.banned = True
if self.banEpoch: # be sure tickets always have the same ban epoch (default 0):
bTicket.banEpoch = self.banEpoch
else:
bTicket = reason['ticket']
# if already banned (otherwise still process some action)
@ -475,11 +480,60 @@ class Actions(JailThread, Mapping):
else logging.NOTICE if diftm < 60 \
else logging.WARNING
logSys.log(ll, "[%s] %s already banned", self._jail.name, ip)
# if long time after ban - do consistency check (something is wrong here):
if bTicket.banEpoch == self.banEpoch and diftm > 3:
# avoid too often checks:
if not rebanacts and MyTime.time() > self.__lastConsistencyCheckTM + 3:
for action in self._actions.itervalues():
action.consistencyCheck()
self.__lastConsistencyCheckTM = MyTime.time()
# check epoch in order to reban it:
if bTicket.banEpoch < self.banEpoch:
if not rebanacts: rebanacts = dict(
(name, action) for name, action in self._actions.iteritems()
if action.banEpoch > bTicket.banEpoch)
cnt += self.__reBan(bTicket, actions=rebanacts)
else: # pragma: no cover - unexpected: ticket is not banned for some reasons - reban using all actions:
cnt += self.__reBan(bTicket)
if cnt:
logSys.debug("Banned %s / %s, %s ticket(s) in %r", cnt,
self.__banManager.getBanTotal(), self.__banManager.size(), self._jail.name)
return cnt
def __reBan(self, ticket, actions=None, log=True):
"""Repeat bans for the ticket.
Executes the actions in order to reban the host given in the
ticket.
Parameters
----------
ticket : Ticket
Ticket to reban
"""
actions = actions or self._actions
ip = ticket.getIP()
aInfo = self.__getActionInfo(ticket)
if log:
logSys.notice("[%s] Reban %s%s", self._jail.name, aInfo["ip"], (', action %r' % actions.keys()[0] if len(actions) == 1 else ''))
for name, action in actions.iteritems():
try:
logSys.debug("[%s] action %r: reban %s", self._jail.name, name, ip)
if not aInfo.immutable: aInfo.reset()
action.reban(aInfo)
except Exception as e:
logSys.error(
"Failed to execute reban jail '%s' action '%s' "
"info '%r': %s",
self._jail.name, name, aInfo, e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
return 0
# after all actions are processed set banned flag:
ticket.banned = True
if self.banEpoch: # be sure tickets always have the same ban epoch (default 0):
ticket.banEpoch = self.banEpoch
return 1
def __checkUnBan(self, maxCount=None):
"""Check for IP address to unban.
@ -494,7 +548,7 @@ class Actions(JailThread, Mapping):
cnt, self.__banManager.size(), self._jail.name)
return cnt
def __flushBan(self, db=False, actions=None):
def __flushBan(self, db=False, actions=None, stop=False):
"""Flush the ban list.
Unban all IP address which are still in the banning list.
@ -513,17 +567,33 @@ class Actions(JailThread, Mapping):
# first we'll execute flush for actions supporting this operation:
unbactions = {}
for name, action in (actions if actions is not None else self._actions).iteritems():
if hasattr(action, 'flush') and action.actionflush:
logSys.notice("[%s] Flush ticket(s) with %s", self._jail.name, name)
action.flush()
else:
unbactions[name] = action
try:
if hasattr(action, 'flush') and (not isinstance(action, CommandAction) or action.actionflush):
logSys.notice("[%s] Flush ticket(s) with %s", self._jail.name, name)
if action.flush():
continue
except Exception as e:
logSys.error("Failed to flush bans in jail '%s' action '%s': %s",
self._jail.name, name, e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
logSys.info("No flush occurred, do consistency check")
if hasattr(action, 'consistencyCheck'):
def _beforeRepair():
if stop and not getattr(action, 'actionrepair_on_unban', None): # don't need repair on stop
self._logSys.error("Invariant check failed. Flush is impossible.")
return False
return True
action.consistencyCheck(_beforeRepair)
continue
# fallback to single unbans:
logSys.debug(" Unban tickets each individualy")
unbactions[name] = action
actions = unbactions
# flush the database also:
if db and self._jail.database is not None:
logSys.debug(" Flush jail in database")
self._jail.database.delBan(self._jail)
# unban each ticket with non-flasheable actions:
# unban each ticket with non-flusheable actions:
for ticket in lst:
# unban ip:
self.__unBan(ticket, actions=actions, log=log)
@ -553,8 +623,6 @@ class Actions(JailThread, Mapping):
logSys.notice("[%s] Unban %s", self._jail.name, aInfo["ip"])
for name, action in unbactions.iteritems():
try:
if ticket.restored and getattr(action, 'norestored', False):
continue
logSys.debug("[%s] action %r: unban %s", self._jail.name, name, ip)
if not aInfo.immutable: aInfo.reset()
action.unban(aInfo)

View File

@ -202,6 +202,11 @@ class DNSUtils:
DNSUtils.CACHE_nameToIp.set(key, ips)
return ips
@staticmethod
def IPv6IsAllowed():
# return os.path.exists("/proc/net/if_inet6") || any((':' in ip) for ip in DNSUtils.getSelfIPs())
return any((':' in ip.ntoa) for ip in DNSUtils.getSelfIPs())
##
# Class for IP address handling.

View File

@ -206,6 +206,13 @@ class Ticket(object):
# return single value of data:
return self._data.get(key, default)
@property
def banEpoch(self):
return getattr(self, '_banEpoch', 0)
@banEpoch.setter
def banEpoch(self, value):
self._banEpoch = value
class FailTicket(Ticket):

View File

@ -28,11 +28,10 @@ import time
import os
import tempfile
from ..server.actions import Actions
from ..server.ticket import FailTicket
from ..server.utils import Utils
from .dummyjail import DummyJail
from .utils import LogCaptureTestCase, with_alt_time, MyTime
from .utils import LogCaptureTestCase, with_alt_time, with_tmpdir, MyTime
TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files")
@ -43,21 +42,21 @@ class ExecuteActions(LogCaptureTestCase):
"""Call before every test case."""
super(ExecuteActions, self).setUp()
self.__jail = DummyJail()
self.__actions = Actions(self.__jail)
self.__tmpfile, self.__tmpfilename = tempfile.mkstemp()
self.__actions = self.__jail.actions
def tearDown(self):
super(ExecuteActions, self).tearDown()
os.remove(self.__tmpfilename)
def defaultActions(self):
def defaultAction(self, o={}):
self.__actions.add('ip')
self.__ip = self.__actions['ip']
self.__ip.actionstart = 'echo ip start 64 >> "%s"' % self.__tmpfilename
self.__ip.actionban = 'echo ip ban <ip> >> "%s"' % self.__tmpfilename
self.__ip.actionunban = 'echo ip unban <ip> >> "%s"' % self.__tmpfilename
self.__ip.actioncheck = 'echo ip check <ip> >> "%s"' % self.__tmpfilename
self.__ip.actionstop = 'echo ip stop >> "%s"' % self.__tmpfilename
act = self.__actions['ip']
act.actionstart = 'echo ip start'+o.get('start', '')
act.actionban = 'echo ip ban <ip>'+o.get('ban', '')
act.actionunban = 'echo ip unban <ip>'+o.get('unban', '')
act.actioncheck = 'echo ip check'+o.get('check', '')
act.actionflush = 'echo ip flush'+o.get('flush', '')
act.actionstop = 'echo ip stop'+o.get('stop', '')
return act
def testActionsAddDuplicateName(self):
self.__actions.add('test')
@ -89,13 +88,12 @@ class ExecuteActions(LogCaptureTestCase):
self.assertLogged('Ban 192.0.2.3')
def testActionsOutput(self):
self.defaultActions()
self.defaultAction()
self.__actions.start()
with open(self.__tmpfilename) as f:
self.assertTrue( Utils.wait_for(lambda: (f.read() == "ip start 64\n"), 3) )
self.assertLogged("stdout: %r" % 'ip start', wait=True)
self.__actions.stop()
self.__actions.join()
self.assertLogged("stdout: %r" % 'ip flush', "stdout: %r" % 'ip stop')
self.assertEqual(self.__actions.status(),[("Currently banned", 0 ),
("Total banned", 0 ), ("Banned IP list", [] )])
@ -211,3 +209,296 @@ class ExecuteActions(LogCaptureTestCase):
self.assertLogged('Unbanned 30, 0 ticket(s)')
self.assertNotLogged('Unbanned 50, 0 ticket(s)')
def testActionsConsistencyCheck(self):
act = self.defaultAction({'check':' <family>', 'flush':' <family>'})
# flush for inet6 is intentionally "broken" here - test no unhandled except and invariant check:
act['actionflush?family=inet6'] = act.actionflush + '; exit 1'
act.actionstart_on_demand = True
self.__actions.start()
self.assertNotLogged("stdout: %r" % 'ip start')
self.assertEqual(self.__actions.addBannedIP('192.0.2.1'), 1)
self.assertEqual(self.__actions.addBannedIP('2001:db8::1'), 1)
self.assertLogged('Ban 192.0.2.1', 'Ban 2001:db8::1',
"stdout: %r" % 'ip start',
"stdout: %r" % 'ip ban 192.0.2.1',
"stdout: %r" % 'ip ban 2001:db8::1',
all=True, wait=True)
# check should fail (so cause stop/start):
self.pruneLog('[test-phase 1a] simulate inconsistent irreparable env by unban')
act['actioncheck?family=inet6'] = act.actioncheck + '; exit 1'
self.__actions.removeBannedIP('2001:db8::1')
self.assertLogged('Invariant check failed. Unban is impossible.',
wait=True)
self.pruneLog('[test-phase 1b] simulate inconsistent irreparable env by flush')
self.__actions._Actions__flushBan()
self.assertLogged(
"stdout: %r" % 'ip flush inet4',
"stdout: %r" % 'ip flush inet6',
'Failed to flush bans',
'No flush occurred, do consistency check',
'Invariant check failed. Trying to restore a sane environment',
"stdout: %r" % 'ip stop', # same for both families
'Failed to flush bans',
all=True, wait=True)
# check succeeds:
self.pruneLog('[test-phase 2] consistent env')
act['actioncheck?family=inet6'] = act.actioncheck
self.assertEqual(self.__actions.addBannedIP('2001:db8::1'), 1)
self.assertLogged('Ban 2001:db8::1',
"stdout: %r" % 'ip start', # same for both families
"stdout: %r" % 'ip ban 2001:db8::1',
all=True, wait=True)
self.assertNotLogged("stdout: %r" % 'ip check inet4',
all=True)
self.pruneLog('[test-phase 3] failed flush in consistent env')
self.__actions._Actions__flushBan()
self.assertLogged('Failed to flush bans',
'No flush occurred, do consistency check',
"stdout: %r" % 'ip flush inet6',
"stdout: %r" % 'ip check inet6',
all=True, wait=True)
self.assertNotLogged(
"stdout: %r" % 'ip flush inet4',
"stdout: %r" % 'ip stop',
"stdout: %r" % 'ip start',
'Unable to restore environment',
all=True)
# stop, flush succeeds:
self.pruneLog('[test-phase end] flush successful')
act['actionflush?family=inet6'] = act.actionflush
self.__actions.stop()
self.__actions.join()
self.assertLogged(
"stdout: %r" % 'ip flush inet6',
"stdout: %r" % 'ip stop', # same for both families
'action ip terminated',
all=True, wait=True)
# no flush for inet4 (already successfully flushed):
self.assertNotLogged("ERROR",
"stdout: %r" % 'ip flush inet4',
'Unban tickets each individualy',
all=True)
def testActionsConsistencyCheckDiffFam(self):
# same as testActionsConsistencyCheck, but different start/stop commands for both families and repair on unban
act = self.defaultAction({'start':' <family>', 'check':' <family>', 'flush':' <family>', 'stop':' <family>'})
# flush for inet6 is intentionally "broken" here - test no unhandled except and invariant check:
act['actionflush?family=inet6'] = act.actionflush + '; exit 1'
act.actionstart_on_demand = True
act.actionrepair_on_unban = True
self.__actions.start()
self.assertNotLogged("stdout: %r" % 'ip start')
self.assertEqual(self.__actions.addBannedIP('192.0.2.1'), 1)
self.assertEqual(self.__actions.addBannedIP('2001:db8::1'), 1)
self.assertLogged('Ban 192.0.2.1', 'Ban 2001:db8::1',
"stdout: %r" % 'ip start inet4',
"stdout: %r" % 'ip ban 192.0.2.1',
"stdout: %r" % 'ip start inet6',
"stdout: %r" % 'ip ban 2001:db8::1',
all=True, wait=True)
# check should fail (so cause stop/start):
act['actioncheck?family=inet6'] = act.actioncheck + '; exit 1'
self.pruneLog('[test-phase 1a] simulate inconsistent irreparable env by unban')
self.__actions.removeBannedIP('2001:db8::1')
self.assertLogged('Invariant check failed. Trying to restore a sane environment',
"stdout: %r" % 'ip stop inet6',
all=True, wait=True)
self.assertNotLogged(
"stdout: %r" % 'ip start inet6', # start on demand (not on repair)
"stdout: %r" % 'ip stop inet4', # family inet4 is not affected
"stdout: %r" % 'ip start inet4',
all=True)
self.pruneLog('[test-phase 1b] simulate inconsistent irreparable env by ban')
self.assertEqual(self.__actions.addBannedIP('2001:db8::1'), 1)
self.assertLogged('Invariant check failed. Trying to restore a sane environment',
"stdout: %r" % 'ip stop inet6',
"stdout: %r" % 'ip start inet6',
"stdout: %r" % 'ip check inet6',
'Unable to restore environment',
'Failed to execute ban',
all=True, wait=True)
self.assertNotLogged(
"stdout: %r" % 'ip stop inet4', # family inet4 is not affected
"stdout: %r" % 'ip start inet4',
all=True)
act['actioncheck?family=inet6'] = act.actioncheck
self.assertEqual(self.__actions.addBannedIP('2001:db8::2'), 1)
act['actioncheck?family=inet6'] = act.actioncheck + '; exit 1'
self.pruneLog('[test-phase 1c] simulate inconsistent irreparable env by flush')
self.__actions._Actions__flushBan()
self.assertLogged(
"stdout: %r" % 'ip flush inet4',
"stdout: %r" % 'ip flush inet6',
'Failed to flush bans',
'No flush occurred, do consistency check',
'Invariant check failed. Trying to restore a sane environment',
"stdout: %r" % 'ip stop inet6',
'Failed to flush bans in jail',
all=True, wait=True)
# start/stop should be called for inet6 only:
self.assertNotLogged(
"stdout: %r" % 'ip stop inet4',
all=True)
# check succeeds:
self.pruneLog('[test-phase 2] consistent env')
act['actioncheck?family=inet6'] = act.actioncheck
self.assertEqual(self.__actions.addBannedIP('2001:db8::1'), 1)
self.assertLogged('Ban 2001:db8::1',
"stdout: %r" % 'ip start inet6',
"stdout: %r" % 'ip ban 2001:db8::1',
all=True, wait=True)
self.assertNotLogged(
"stdout: %r" % 'ip check inet4',
"stdout: %r" % 'ip start inet4',
all=True)
self.pruneLog('[test-phase 3] failed flush in consistent env')
act['actioncheck?family=inet6'] = act.actioncheck
self.__actions._Actions__flushBan()
self.assertLogged('Failed to flush bans',
'No flush occurred, do consistency check',
"stdout: %r" % 'ip flush inet6',
"stdout: %r" % 'ip check inet6',
all=True, wait=True)
self.assertNotLogged(
"stdout: %r" % 'ip flush inet4',
"stdout: %r" % 'ip stop inet4',
"stdout: %r" % 'ip start inet4',
"stdout: %r" % 'ip stop inet6',
"stdout: %r" % 'ip start inet6',
all=True)
# stop, flush succeeds:
self.pruneLog('[test-phase end] flush successful')
act['actionflush?family=inet6'] = act.actionflush
self.__actions.stop()
self.__actions.join()
self.assertLogged(
"stdout: %r" % 'ip flush inet6',
"stdout: %r" % 'ip stop inet4',
"stdout: %r" % 'ip stop inet6',
'action ip terminated',
all=True, wait=True)
# no flush for inet4 (already successfully flushed):
self.assertNotLogged("ERROR",
"stdout: %r" % 'ip flush inet4',
'Unban tickets each individualy',
all=True)
@with_alt_time
@with_tmpdir
def testActionsRebanBrokenAfterRepair(self, tmp):
act = self.defaultAction({
'start':' <family>; touch "<FN>"',
'check':' <family>; test -f "<FN>"',
'flush':' <family>; echo -n "" > "<FN>"',
'stop': ' <family>; rm -f "<FN>"',
'ban': ' <family>; echo "<ip> <family>" >> "<FN>"',
})
act['FN'] = tmp+'/<family>'
act.actionstart_on_demand = True
act.actionrepair = 'echo ip repair <family>; touch "<FN>"'
act.actionreban = 'echo ip reban <ip> <family>; echo "<ip> <family> -- rebanned" >> "<FN>"'
self.pruneLog('[test-phase 0] initial ban')
self.assertEqual(self.__actions.addBannedIP(['192.0.2.1', '2001:db8::1']), 2)
self.assertLogged('Ban 192.0.2.1', 'Ban 2001:db8::1',
"stdout: %r" % 'ip start inet4',
"stdout: %r" % 'ip ban 192.0.2.1 inet4',
"stdout: %r" % 'ip start inet6',
"stdout: %r" % 'ip ban 2001:db8::1 inet6',
all=True)
self.pruneLog('[test-phase 1] check ban')
self.dumpFile(tmp+'/inet4')
self.assertLogged('192.0.2.1 inet4')
self.assertNotLogged('2001:db8::1 inet6')
self.pruneLog()
self.dumpFile(tmp+'/inet6')
self.assertLogged('2001:db8::1 inet6')
self.assertNotLogged('192.0.2.1 inet4')
# simulate 3 seconds past:
MyTime.setTime(MyTime.time() + 4)
# already banned produces events:
self.pruneLog('[test-phase 2] check already banned')
self.assertEqual(self.__actions.addBannedIP(['192.0.2.1', '2001:db8::1', '2001:db8::2']), 1)
self.assertLogged(
'192.0.2.1 already banned', '2001:db8::1 already banned', 'Ban 2001:db8::2',
"stdout: %r" % 'ip check inet4', # both checks occurred
"stdout: %r" % 'ip check inet6',
all=True)
self.dumpFile(tmp+'/inet4')
self.dumpFile(tmp+'/inet6')
# no reban should occur:
self.assertNotLogged('Reban 192.0.2.1', 'Reban 2001:db8::1',
"stdout: %r" % 'ip ban 192.0.2.1 inet4',
"stdout: %r" % 'ip reban 192.0.2.1 inet4',
"stdout: %r" % 'ip ban 2001:db8::1 inet6',
"stdout: %r" % 'ip reban 2001:db8::1 inet6',
'192.0.2.1 inet4 -- repaired',
'2001:db8::1 inet6 -- repaired',
all=True)
# simulate 3 seconds past:
MyTime.setTime(MyTime.time() + 4)
# break env (remove both files, so check would fail):
os.remove(tmp+'/inet4')
os.remove(tmp+'/inet6')
# test again already banned (it shall cause reban now):
self.pruneLog('[test-phase 3a] check reban after sane env repaired')
self.assertEqual(self.__actions.addBannedIP(['192.0.2.1', '2001:db8::1']), 2)
self.assertLogged(
"Invariant check failed. Trying to restore a sane environment",
"stdout: %r" % 'ip repair inet4', # both repairs occurred
"stdout: %r" % 'ip repair inet6',
"Reban 192.0.2.1, action 'ip'", "Reban 2001:db8::1, action 'ip'", # both rebans also
"stdout: %r" % 'ip reban 192.0.2.1 inet4',
"stdout: %r" % 'ip reban 2001:db8::1 inet6',
all=True)
# now last IP (2001:db8::2) - no repair, but still old epoch of ticket, so it gets rebanned:
self.pruneLog('[test-phase 3a] check reban by epoch mismatch (without repair)')
self.assertEqual(self.__actions.addBannedIP('2001:db8::2'), 1)
self.assertLogged(
"Reban 2001:db8::2, action 'ip'",
"stdout: %r" % 'ip reban 2001:db8::2 inet6',
all=True)
self.assertNotLogged(
"Invariant check failed. Trying to restore a sane environment",
"stdout: %r" % 'ip repair inet4', # both repairs occurred
"stdout: %r" % 'ip repair inet6',
"Reban 192.0.2.1, action 'ip'", "Reban 2001:db8::1, action 'ip'", # both rebans also
"stdout: %r" % 'ip reban 192.0.2.1 inet4',
"stdout: %r" % 'ip reban 2001:db8::1 inet6',
all=True)
# and bans present in files:
self.pruneLog('[test-phase 4] check reban')
self.dumpFile(tmp+'/inet4')
self.assertLogged('192.0.2.1 inet4 -- rebanned')
self.assertNotLogged('2001:db8::1 inet6 -- rebanned')
self.pruneLog()
self.dumpFile(tmp+'/inet6')
self.assertLogged(
'2001:db8::1 inet6 -- rebanned',
'2001:db8::2 inet6 -- rebanned', all=True)
self.assertNotLogged('192.0.2.1 inet4 -- rebanned')
# coverage - intended error in reban (no unhandled exception, message logged):
act.actionreban = ''
act.actionban = 'exit 1'
self.assertEqual(self.__actions._Actions__reBan(FailTicket("192.0.2.1", 0)), 0)
self.assertLogged(
'Failed to execute reban',
'Error banning 192.0.2.1', all=True)

View File

@ -34,8 +34,8 @@ from ..server.actions import OrderedDict, Actions
from ..server.utils import Utils
from .dummyjail import DummyJail
from .utils import LogCaptureTestCase
from .utils import pid_exists
from .utils import pid_exists, with_tmpdir, LogCaptureTestCase
class CommandActionTest(LogCaptureTestCase):
@ -297,18 +297,21 @@ class CommandActionTest(LogCaptureTestCase):
"Text 000-567 text 567 '567'")
self.assertTrue(len(cache) >= 3)
def testExecuteActionBan(self):
self.__action.actionstart = "touch /tmp/fail2ban.test"
self.assertEqual(self.__action.actionstart, "touch /tmp/fail2ban.test")
self.__action.actionstop = "rm -f /tmp/fail2ban.test"
self.assertEqual(self.__action.actionstop, 'rm -f /tmp/fail2ban.test')
@with_tmpdir
def testExecuteActionBan(self, tmp):
tmp += "/fail2ban.test"
self.__action.actionstart = "touch '%s'" % tmp
self.__action.actionrepair = self.__action.actionstart
self.assertEqual(self.__action.actionstart, "touch '%s'" % tmp)
self.__action.actionstop = "rm -f '%s'" % tmp
self.assertEqual(self.__action.actionstop, "rm -f '%s'" % tmp)
self.__action.actionban = "echo -n"
self.assertEqual(self.__action.actionban, 'echo -n')
self.__action.actioncheck = "[ -e /tmp/fail2ban.test ]"
self.assertEqual(self.__action.actioncheck, '[ -e /tmp/fail2ban.test ]')
self.__action.actioncheck = "[ -e '%s' ]" % tmp
self.assertEqual(self.__action.actioncheck, "[ -e '%s' ]" % tmp)
self.__action.actionunban = "true"
self.assertEqual(self.__action.actionunban, 'true')
self.pruneLog()
self.assertNotLogged('returned')
# no action was actually executed yet
@ -316,42 +319,66 @@ class CommandActionTest(LogCaptureTestCase):
self.__action.ban({'ip': None})
self.assertLogged('Invariant check failed')
self.assertLogged('returned successfully')
self.__action.stop()
self.assertLogged(self.__action.actionstop)
def testExecuteActionEmptyUnban(self):
# unban will be executed for actions with banned items only:
self.__action.actionban = ""
self.__action.actionunban = ""
self.__action.actionflush = "echo -n 'flush'"
self.__action.actionstop = "echo -n 'stop'"
self.__action.start();
self.__action.ban({});
self.pruneLog()
self.__action.unban({})
self.assertLogged('Nothing to do')
self.assertLogged('Nothing to do', wait=True)
# same as above but with interim flush, so no unban anymore:
self.__action.ban({});
self.pruneLog('[phase 2]')
self.__action.flush()
self.__action.unban({})
self.__action.stop()
self.assertLogged('stop', wait=True)
self.assertNotLogged('Nothing to do')
def testExecuteActionStartCtags(self):
@with_tmpdir
def testExecuteActionStartCtags(self, tmp):
tmp += '/fail2ban.test'
self.__action.HOST = "192.0.2.0"
self.__action.actionstart = "touch /tmp/fail2ban.test.<HOST>"
self.__action.actionstop = "rm -f /tmp/fail2ban.test.<HOST>"
self.__action.actioncheck = "[ -e /tmp/fail2ban.test.192.0.2.0 ]"
self.__action.actionstart = "touch '%s.<HOST>'" % tmp
self.__action.actionstop = "rm -f '%s.<HOST>'" % tmp
self.__action.actioncheck = "[ -e '%s.192.0.2.0' ]" % tmp
self.__action.start()
self.__action.consistencyCheck()
def testExecuteActionCheckRestoreEnvironment(self):
@with_tmpdir
def testExecuteActionCheckRestoreEnvironment(self, tmp):
tmp += '/fail2ban.test'
self.__action.actionstart = ""
self.__action.actionstop = "rm -f /tmp/fail2ban.test"
self.__action.actionban = "rm /tmp/fail2ban.test"
self.__action.actioncheck = "[ -e /tmp/fail2ban.test ]"
self.__action.actionstop = "rm -f '%s'" % tmp
self.__action.actionban = "rm '%s'" % tmp
self.__action.actioncheck = "[ -e '%s' ]" % tmp
self.assertRaises(RuntimeError, self.__action.ban, {'ip': None})
self.assertLogged('Invariant check failed', 'Unable to restore environment', all=True)
# 2nd time, try to restore with producing error in stop, but succeeded start hereafter:
self.pruneLog('[phase 2]')
self.__action.actionstart = "touch /tmp/fail2ban.test"
self.__action.actionstop = "rm /tmp/fail2ban.test"
self.__action.actionban = 'printf "%%b\n" <ip> >> /tmp/fail2ban.test'
self.__action.actioncheck = "[ -e /tmp/fail2ban.test ]"
self.__action.actionstart = "touch '%s'" % tmp
self.__action.actionstop = "rm '%s'" % tmp
self.__action.actionban = """printf "%%%%b\n" <ip> >> '%s'""" % tmp
self.__action.actioncheck = "[ -e '%s' ]" % tmp
self.__action.ban({'ip': None})
self.assertLogged('Invariant check failed')
self.assertNotLogged('Unable to restore environment')
def testExecuteActionCheckRepairEnvironment(self):
@with_tmpdir
def testExecuteActionCheckRepairEnvironment(self, tmp):
tmp += '/fail2ban.test'
self.__action.actionstart = ""
self.__action.actionstop = ""
self.__action.actionban = "rm /tmp/fail2ban.test"
self.__action.actioncheck = "[ -e /tmp/fail2ban.test ]"
self.__action.actionrepair = "echo 'repair ...'; touch /tmp/fail2ban.test"
self.__action.actionban = "rm '%s'" % tmp
self.__action.actioncheck = "[ -e '%s' ]" % tmp
self.__action.actionrepair = "echo 'repair ...'; touch '%s'" % tmp
# 1st time with success repair:
self.__action.ban({'ip': None})
self.assertLogged("Invariant check failed. Trying", "echo 'repair ...'", all=True)
@ -379,13 +406,13 @@ class CommandActionTest(LogCaptureTestCase):
'user': "tester"
}
})
self.__action.actionban = "touch /tmp/fail2ban.test.123; echo 'failure <F-ID> of <F-USER> -<F-TEST>- from <ip>:<F-PORT>'"
self.__action.actionunban = "rm /tmp/fail2ban.test.<ABC>; echo 'user <F-USER> unbanned'"
self.__action.actionban = "echo '<ABC>, failure <F-ID> of <F-USER> -<F-TEST>- from <ip>:<F-PORT>'"
self.__action.actionunban = "echo '<ABC>, user <F-USER> unbanned'"
self.__action.ban(aInfo)
self.__action.unban(aInfo)
self.assertLogged(
" -- stdout: 'failure 111 of tester -- from 192.0.2.1:222'",
" -- stdout: 'user tester unbanned'",
" -- stdout: '123, failure 111 of tester -- from 192.0.2.1:222'",
" -- stdout: '123, user tester unbanned'",
all=True
)
@ -560,6 +587,19 @@ class CommandActionTest(LogCaptureTestCase):
self.assertEqual(len(m), 3)
self.assertIn('c', m)
self.assertEqual((m['a'], m['b'], m['c']), (5, 11, 'test'))
# immutability of copy:
m['d'] = 'dddd'
m2 = m.copy()
m2['c'] = lambda self: self['a'] + 7
m2['a'] = 1
del m2['b']
del m2['d']
self.assertTrue('b' in m)
self.assertTrue('d' in m)
self.assertFalse('b' in m2)
self.assertFalse('d' in m2)
self.assertEqual((m['a'], m['b'], m['c'], m['d']), (5, 11, 'test', 'dddd'))
self.assertEqual((m2['a'], m2['c']), (1, 8))
def testCallingMapRep(self):
m = CallingMap({

View File

@ -31,7 +31,7 @@ import unittest
from ..client.configreader import ConfigReader, ConfigReaderUnshared, \
DefinitionInitConfigReader, NoSectionError
from ..client import configparserinc
from ..client.jailreader import JailReader, extractOptions
from ..client.jailreader import JailReader, extractOptions, splitWithOptions
from ..client.filterreader import FilterReader
from ..client.jailsreader import JailsReader
from ..client.actionreader import ActionReader, CommandAction
@ -778,7 +778,7 @@ class JailsReaderTest(LogCaptureTestCase):
# somewhat duplicating here what is done in JailsReader if
# the jail is enabled
for act in actions.split('\n'):
for act in splitWithOptions(actions):
actName, actOpt = extractOptions(act)
self.assertTrue(len(actName))
self.assertTrue(isinstance(actOpt, dict))

View File

@ -113,16 +113,7 @@ fail2banclient.input_command = _test_input_command
fail2bancmdline.PRODUCTION = \
fail2banserver.PRODUCTION = False
def _out_file(fn, handle=logSys.debug):
"""Helper which outputs content of the file at HEAVYDEBUG loglevels"""
if (handle != logSys.debug or logSys.getEffectiveLevel() <= logging.DEBUG):
handle('---- ' + fn + ' ----')
for line in fileinput.input(fn):
line = line.rstrip('\n')
handle(line)
handle('-'*30)
_out_file = LogCaptureTestCase.dumpFile
def _write_file(fn, mode, *lines):
f = open(fn, mode)

View File

@ -12,4 +12,9 @@ class TestAction(ActionBase):
del aInfo['ip']
self._logSys.info("%s unban deleted aInfo IP", self._name)
def flush(self):
# intended error to cover no unhandled exception occurs in flush
# as well as unbans are done individually after errored flush.
raise ValueError("intended error")
Action = TestAction

View File

@ -40,7 +40,7 @@ from ..server.jail import Jail
from ..server.filterpoll import FilterPoll
from ..server.filter import FailTicket, Filter, FileFilter, FileContainer
from ..server.failmanager import FailManagerEmpty
from ..server.ipdns import getfqdn, DNSUtils, IPAddr
from ..server.ipdns import asip, getfqdn, DNSUtils, IPAddr
from ..server.mytime import MyTime
from ..server.utils import Utils, uni_decode
from .utils import setUpMyTime, tearDownMyTime, mtimesleep, with_tmpdir, LogCaptureTestCase, \
@ -1843,11 +1843,15 @@ class DNSUtilsNetworkTests(unittest.TestCase):
def setUp(self):
"""Call before every test case."""
super(DNSUtilsNetworkTests, self).setUp()
unittest.F2B.SkipIfNoNetwork()
#unittest.F2B.SkipIfNoNetwork()
def test_IPAddr(self):
self.assertTrue(IPAddr('192.0.2.1').isIPv4)
self.assertTrue(IPAddr('2001:DB8::').isIPv6)
ip4 = IPAddr('192.0.2.1')
ip6 = IPAddr('2001:DB8::')
self.assertTrue(ip4.isIPv4)
self.assertTrue(ip6.isIPv6)
self.assertTrue(asip('192.0.2.1').isIPv4)
self.assertTrue(id(asip(ip4)) == id(ip4))
def test_IPAddr_Raw(self):
# raw string:
@ -1884,6 +1888,7 @@ class DNSUtilsNetworkTests(unittest.TestCase):
def testUseDns(self):
res = DNSUtils.textToIp('www.example.com', 'no')
self.assertSortedEqual(res, [])
unittest.F2B.SkipIfNoNetwork()
res = DNSUtils.textToIp('www.example.com', 'warn')
# sort ipaddr, IPv4 is always smaller as IPv6
self.assertSortedEqual(res, ['93.184.216.34', '2606:2800:220:1:248:1893:25c8:1946'])
@ -1892,6 +1897,7 @@ class DNSUtilsNetworkTests(unittest.TestCase):
self.assertSortedEqual(res, ['93.184.216.34', '2606:2800:220:1:248:1893:25c8:1946'])
def testTextToIp(self):
unittest.F2B.SkipIfNoNetwork()
# Test hostnames
hostnames = [
'www.example.com',
@ -1905,6 +1911,8 @@ class DNSUtilsNetworkTests(unittest.TestCase):
self.assertSortedEqual(res, ['93.184.216.34', '2606:2800:220:1:248:1893:25c8:1946'])
else:
self.assertSortedEqual(res, [])
def testIpToIp(self):
# pure ips:
for s in ('93.184.216.34', '2606:2800:220:1:248:1893:25c8:1946'):
ips = DNSUtils.textToIp(s, 'yes')
@ -2061,6 +2069,7 @@ class DNSUtilsNetworkTests(unittest.TestCase):
self.assertTrue(IPAddr("2606:2800:220:1:248:1893:25c8:1946").isInNet(ips))
def testIPAddr_wrongDNS_IP(self):
unittest.F2B.SkipIfNoNetwork()
DNSUtils.dnsToIp('`this`.dns-is-wrong.`wrong-nic`-dummy')
DNSUtils.ipToName('*')
@ -2083,6 +2092,9 @@ class DNSUtilsNetworkTests(unittest.TestCase):
self.assertEqual(getfqdn(lname), lname)
# coverage (targeting all branches): FQDN from loopback and DNS blackhole is always the same:
self.assertIn(getfqdn('localhost.'), ('localhost', 'localhost.'))
def testFQDN_DNS(self):
unittest.F2B.SkipIfNoNetwork()
self.assertIn(getfqdn('as112.arpa.'), ('as112.arpa.', 'as112.arpa'))

View File

@ -752,7 +752,7 @@ class Transmitter(TransmitterBase):
self.assertSortedEqual(
self.transm.proceed(["get", self.jailName, "actionmethods",
action])[1],
['ban', 'start', 'stop', 'testmethod', 'unban'])
['ban', 'reban', 'start', 'stop', 'testmethod', 'unban'])
self.assertEqual(
self.transm.proceed(["set", self.jailName, "action", action,
"testmethod", '{"text": "world!"}']),

View File

@ -22,6 +22,7 @@ __author__ = "Yaroslav Halchenko"
__copyright__ = "Copyright (c) 2013 Yaroslav Halchenko"
__license__ = "GPL"
import fileinput
import itertools
import logging
import optparse
@ -741,11 +742,8 @@ class LogCaptureTestCase(unittest.TestCase):
self._dirty |= 2 # records changed
def setUp(self):
# For extended testing of what gets output into logging
# system, we will redirect it to a string
logSys = getLogger("fail2ban")
# Keep old settings
self._old_level = logSys.level
self._old_handlers = logSys.handlers
@ -762,7 +760,6 @@ class LogCaptureTestCase(unittest.TestCase):
"""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
super(LogCaptureTestCase, self).tearDown()
@ -845,5 +842,15 @@ class LogCaptureTestCase(unittest.TestCase):
def getLog(self):
return self._log.getvalue()
@staticmethod
def dumpFile(fn, handle=logSys.debug):
"""Helper which outputs content of the file at HEAVYDEBUG loglevels"""
if (handle != logSys.debug or logSys.getEffectiveLevel() <= logging.DEBUG):
handle('---- ' + fn + ' ----')
for line in fileinput.input(fn):
line = line.rstrip('\n')
handle(line)
handle('-'*30)
pid_exists = Utils.pid_exists