mirror of https://github.com/fail2ban/fail2ban
tests fixed, prepared for other conditional operations (for subnet usage), operations like repair/flush/stop considering started families (executed for started only)
parent
3c42c7b9ef
commit
165b7d6643
|
@ -36,7 +36,8 @@ from .failregex import mapTag2Opt
|
|||
from .ipdns import asip, 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__)
|
||||
|
@ -51,7 +52,7 @@ allowed_ipv6 = True
|
|||
FCUSTAG_CRE = re.compile(r'<F-([A-Z0-9_\-]+)>'); # currently uppercase only
|
||||
|
||||
COND_FAMILIES = ('inet4', 'inet6')
|
||||
CONDITIONAL_FAM_RE = re.compile(r"^(\w+)\?(family)=")
|
||||
CONDITIONAL_FAM_RE = re.compile(r"^(\w+)\?(family)=(.*)$")
|
||||
|
||||
# Special tags:
|
||||
DYN_REPL_TAGS = {
|
||||
|
@ -382,7 +383,33 @@ class CommandAction(ActionBase):
|
|||
addrepl=(lambda tag:family if tag == 'family' else None),
|
||||
cache=self.__substCache)
|
||||
|
||||
def _executeOperation(self, tag, operation, family=[], forceExec=False, afterExec=None):
|
||||
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
|
||||
|
@ -390,26 +417,24 @@ 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:
|
||||
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('inet4' if 'inet4' in family else '', ret)
|
||||
# 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')
|
||||
forceExec |= (family and 'inet6' in family and 'inet4' not in family)
|
||||
if cmd6 and (forceExec or cmd6 != cmd): # - avoid double execution of same command
|
||||
ret = self.executeCmd(cmd6, self.timeout)
|
||||
res &= ret
|
||||
if afterExec: afterExec('inet6' if 'inet6' in family else '', ret)
|
||||
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))
|
||||
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
|
||||
|
||||
@property
|
||||
|
@ -417,13 +442,33 @@ class CommandAction(ActionBase):
|
|||
v = self._properties.get('__hasCondSection')
|
||||
if v is None:
|
||||
v = False
|
||||
famset = set()
|
||||
for n in self._properties:
|
||||
if CONDITIONAL_FAM_RE.match(n):
|
||||
v = True
|
||||
break
|
||||
grp = CONDITIONAL_FAM_RE.match(n)
|
||||
if grp:
|
||||
self._properties['__hasCondSection'] = v = True
|
||||
if self._properties.get('families') or self._startOnDemand:
|
||||
break
|
||||
famset.add(grp.group(2))
|
||||
self._properties['__families'] = famset
|
||||
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):
|
||||
"""Checks the action depends on family (conditional)"""
|
||||
|
@ -435,7 +480,15 @@ class CommandAction(ActionBase):
|
|||
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='', forceStart=False):
|
||||
"""Executes the "actionstart" command.
|
||||
|
||||
Replace the tags in the action command with actions properties
|
||||
|
@ -447,9 +500,12 @@ class CommandAction(ActionBase):
|
|||
return True
|
||||
elif not forceStart and self.__started.get(family): # pragma: no cover - normally unreachable
|
||||
return True
|
||||
ret = self._executeOperation('<actionstart>', 'starting', family=[family], forceExec=forceStart)
|
||||
if ret:
|
||||
self.__started[family] = 1
|
||||
family = [family] if family != '' 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):
|
||||
|
@ -468,11 +524,11 @@ class CommandAction(ActionBase):
|
|||
family = aInfo.get('family', '')
|
||||
if self._startOnDemand:
|
||||
if not self.__started.get(family):
|
||||
self.start(family, forceStart=True)
|
||||
self._start(family, forceStart=True)
|
||||
# ban:
|
||||
if not self._processCmd('<actionban>', aInfo):
|
||||
raise RuntimeError("Error banning %(ip)s" % aInfo)
|
||||
self.__started[family] |= 2; # contains items
|
||||
self.__started[family] = self.__started.get(family, 0) | 3; # started and contains items
|
||||
|
||||
def unban(self, aInfo):
|
||||
"""Executes the "actionunban" command.
|
||||
|
@ -487,7 +543,7 @@ class CommandAction(ActionBase):
|
|||
the ban.
|
||||
"""
|
||||
family = aInfo.get('family', '')
|
||||
if self.__started.get(family) & 2: # contains items
|
||||
if self.__started.get(family, 0) & 2: # contains items
|
||||
if not self._processCmd('<actionunban>', aInfo):
|
||||
raise RuntimeError("Error unbanning %(ip)s" % aInfo)
|
||||
|
||||
|
@ -510,14 +566,22 @@ class CommandAction(ActionBase):
|
|||
self.__started[family] &= ~2; # no items anymore
|
||||
return self._executeOperation('<actionflush>', 'flushing', family=family, afterExec=_afterFlush)
|
||||
|
||||
def stop(self, family=None):
|
||||
def stop(self):
|
||||
"""Executes the "actionstop" command.
|
||||
|
||||
Replaces the tags in the action command with actions properties
|
||||
and executes the resulting command.
|
||||
"""
|
||||
return self._stop()
|
||||
|
||||
def _stop(self, family=''):
|
||||
"""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 family is None:
|
||||
if not family:
|
||||
family = [f for (f,v) in self.__started.iteritems() if v]
|
||||
# if no started (on demand) actions:
|
||||
if not family: return True
|
||||
|
@ -528,7 +592,10 @@ class CommandAction(ActionBase):
|
|||
family = [family]
|
||||
except KeyError: # pragma: no cover
|
||||
return True
|
||||
return self._executeOperation('<actionstop>', 'stopping', family=family)
|
||||
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.
|
||||
|
@ -551,7 +618,9 @@ class CommandAction(ActionBase):
|
|||
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
|
||||
|
||||
|
@ -767,6 +836,7 @@ class CommandAction(ActionBase):
|
|||
if not self.executeCmd(repairCmd, self.timeout):
|
||||
self._logSys.critical("Unable to restore environment")
|
||||
return False
|
||||
self.__started[family] = 1
|
||||
else:
|
||||
# no repair command, try to restart action...
|
||||
# [WARNING] TODO: be sure all banactions get a repair command, because
|
||||
|
@ -774,10 +844,10 @@ class CommandAction(ActionBase):
|
|||
# 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)
|
||||
self._stop(family)
|
||||
except RuntimeError: # bypass error in stop (if start/check succeeded hereafter).
|
||||
pass
|
||||
self.start(family)
|
||||
self._start(family)
|
||||
if not self.executeCmd(checkCmd, self.timeout):
|
||||
self._logSys.critical("Unable to restore environment")
|
||||
return False
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in New Issue