[ban-time-incr] prolong ban, dynamic bantime, etc.:

- dynamic bantime: introduces new action-tag `<bantime>` corresponds to the current ban-time of the ticket;
  Note: because it is dynamic, it should be normally removed from `jail.conf` (resp. `jail.local`).
- introduced new action command `actionprolong`, used for prolongation of the timeout (ban-time of the ticket);
- removed default `timeout` from `actionstart` of several actions;
- faster and safer function escapeTag (replacement at once in one run, '\n' and '\r' escaped also);
pull/1460/head
sebres 2017-03-15 10:15:48 +01:00
parent 54729f9ef3
commit c21b4e4d56
15 changed files with 271 additions and 77 deletions

View File

@ -18,7 +18,7 @@ before = firewallcmd-common.conf
[Definition] [Definition]
actionstart = ipset create <ipmset> hash:ip timeout <bantime> actionstart = ipset create <ipmset> hash:ip
firewall-cmd --direct --add-rule <family> filter <chain> 0 -p <protocol> -m multiport --dports <port> -m set --match-set <ipmset> src -j <blocktype> firewall-cmd --direct --add-rule <family> filter <chain> 0 -p <protocol> -m multiport --dports <port> -m set --match-set <ipmset> src -j <blocktype>
actionstop = firewall-cmd --direct --remove-rule <family> filter <chain> 0 -p <protocol> -m multiport --dports <port> -m set --match-set <ipmset> src -j <blocktype> actionstop = firewall-cmd --direct --remove-rule <family> filter <chain> 0 -p <protocol> -m multiport --dports <port> -m set --match-set <ipmset> src -j <blocktype>
@ -27,6 +27,8 @@ actionstop = firewall-cmd --direct --remove-rule <family> filter <chain> 0 -p <p
actionban = ipset add <ipmset> <ip> timeout <bantime> -exist actionban = ipset add <ipmset> <ip> timeout <bantime> -exist
actionprolong = %(actionban)s
actionunban = ipset del <ipmset> <ip> -exist actionunban = ipset del <ipmset> <ip> -exist
[Init] [Init]
@ -38,12 +40,6 @@ actionunban = ipset del <ipmset> <ip> -exist
# #
chain = INPUT_direct chain = INPUT_direct
# Option: bantime
# Notes: specifies the bantime in seconds (handled internally rather than by fail2ban)
# Values: [ NUM ] Default: 600
bantime = 600
ipmset = f2b-<name> ipmset = f2b-<name>
[Init?family=inet6] [Init?family=inet6]

View File

@ -26,7 +26,7 @@ before = iptables-common.conf
# Notes.: command executed once at the start of Fail2Ban. # Notes.: command executed once at the start of Fail2Ban.
# Values: CMD # Values: CMD
# #
actionstart = ipset create <ipmset> hash:ip timeout <bantime><familyopt> actionstart = ipset create <ipmset> hash:ip<familyopt>
<iptables> -I <chain> -m set --match-set <ipmset> src -j <blocktype> <iptables> -I <chain> -m set --match-set <ipmset> src -j <blocktype>
# Option: actionflush # Option: actionflush
@ -51,6 +51,8 @@ actionstop = <iptables> -D <chain> -m set --match-set <ipmset> src -j <blocktype
# #
actionban = ipset add <ipmset> <ip> timeout <bantime> -exist actionban = ipset add <ipmset> <ip> timeout <bantime> -exist
actionprolong = %(actionban)s
# Option: actionunban # Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the # Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights. # command is executed with Fail2Ban user rights.
@ -61,12 +63,6 @@ actionunban = ipset del <ipmset> <ip> -exist
[Init] [Init]
# Option: bantime
# Notes: specifies the bantime in seconds (handled internally rather than by fail2ban)
# Values: [ NUM ] Default: 600
#
bantime = 600
ipmset = f2b-<name> ipmset = f2b-<name>
familyopt = familyopt =

View File

@ -26,7 +26,7 @@ before = iptables-common.conf
# Notes.: command executed once at the start of Fail2Ban. # Notes.: command executed once at the start of Fail2Ban.
# Values: CMD # Values: CMD
# #
actionstart = ipset create <ipmset> hash:ip timeout <bantime><familyopt> actionstart = ipset create <ipmset> hash:ip<familyopt>
<iptables> -I <chain> -p <protocol> -m multiport --dports <port> -m set --match-set <ipmset> src -j <blocktype> <iptables> -I <chain> -p <protocol> -m multiport --dports <port> -m set --match-set <ipmset> src -j <blocktype>
# Option: actionflush # Option: actionflush
@ -51,6 +51,8 @@ actionstop = <iptables> -D <chain> -p <protocol> -m multiport --dports <port> -m
# #
actionban = ipset add <ipmset> <ip> timeout <bantime> -exist actionban = ipset add <ipmset> <ip> timeout <bantime> -exist
actionprolong = %(actionban)s
# Option: actionunban # Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the # Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights. # command is executed with Fail2Ban user rights.
@ -61,12 +63,6 @@ actionunban = ipset del <ipmset> <ip> -exist
[Init] [Init]
# Option: bantime
# Notes: specifies the bantime in seconds (handled internally rather than by fail2ban)
# Values: [ NUM ] Default: 600
#
bantime = 600
ipmset = f2b-<name> ipmset = f2b-<name>
familyopt = familyopt =

View File

@ -12,5 +12,5 @@ actioncheck =
actionban = /usr/libexec/afctl -a <ip> -t <bantime> actionban = /usr/libexec/afctl -a <ip> -t <bantime>
actionunban = /usr/libexec/afctl -r <ip> actionunban = /usr/libexec/afctl -r <ip>
[Init] actionprolong = %(actionunban)s && %(actionban)s
bantime = 2880

View File

@ -51,7 +51,7 @@
# Values: CMD # Values: CMD
# #
actionstart = if ! ipset -quiet -name list f2b-<name> >/dev/null; actionstart = if ! ipset -quiet -name list f2b-<name> >/dev/null;
then ipset -quiet -exist create f2b-<name> hash:ip timeout <bantime>; then ipset -quiet -exist create f2b-<name> hash:ip;
fi fi
# Option: actionstop # Option: actionstop
@ -68,6 +68,8 @@ actionstop = ipset flush f2b-<name>
# #
actionban = ipset add f2b-<name> <ip> timeout <bantime> -exist actionban = ipset add f2b-<name> <ip> timeout <bantime> -exist
actionprolong = %(actionban)s
# Option: actionunban # Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the # Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights. # command is executed with Fail2Ban user rights.
@ -76,10 +78,3 @@ actionban = ipset add f2b-<name> <ip> timeout <bantime> -exist
# #
actionunban = ipset del f2b-<name> <ip> -exist actionunban = ipset del f2b-<name> <ip> -exist
[Init]
# Option: bantime
# Notes: specifies the bantime in seconds (handled internally rather than by fail2ban)
# Values: [ NUM ] Default: 600
#
bantime = 600

View File

@ -202,22 +202,22 @@ banaction = iptables-multiport
banaction_allports = iptables-allports banaction_allports = iptables-allports
# The simplest action to take: ban only # The simplest action to take: ban only
action_ = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] action_ = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
# ban & send an e-mail with whois report to the destemail. # ban & send an e-mail with whois report to the destemail.
action_mw = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] action_mw = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
%(mta)s-whois[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s"] %(mta)s-whois[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s"]
# ban & send an e-mail with whois report and relevant log lines # ban & send an e-mail with whois report and relevant log lines
# to the destemail. # to the destemail.
action_mwl = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] action_mwl = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
%(mta)s-whois-lines[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"] %(mta)s-whois-lines[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"]
# See the IMPORTANT note in action.d/xarf-login-attack for when to use this action # See the IMPORTANT note in action.d/xarf-login-attack for when to use this action
# #
# ban & send a xarf e-mail to abuse contact of IP address and include relevant log lines # ban & send a xarf e-mail to abuse contact of IP address and include relevant log lines
# to the destemail. # to the destemail.
action_xarf = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] action_xarf = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
xarf-login-attack[service=%(__name__)s, sender="%(sender)s", logpath=%(logpath)s, port="%(port)s"] xarf-login-attack[service=%(__name__)s, sender="%(sender)s", logpath=%(logpath)s, port="%(port)s"]
# ban IP on CloudFlare & send an e-mail with whois report and relevant log lines # ban IP on CloudFlare & send an e-mail with whois report and relevant log lines

View File

@ -45,6 +45,7 @@ class ActionReader(DefinitionInitConfigReader):
"actioncheck": ["string", None], "actioncheck": ["string", None],
"actionrepair": ["string", None], "actionrepair": ["string", None],
"actionban": ["string", None], "actionban": ["string", None],
"actionprolong": ["string", None],
"actionunban": ["string", None], "actionunban": ["string", None],
"norestored": ["string", None], "norestored": ["string", None],
} }

View File

@ -224,6 +224,10 @@ class ActionBase(object):
""" """
pass pass
@property
def _prolongable(self): # pragma: no cover - abstract
return False
def unban(self, aInfo): # pragma: no cover - abstract def unban(self, aInfo): # pragma: no cover - abstract
"""Executed when a ban expires. """Executed when a ban expires.
@ -236,6 +240,11 @@ class ActionBase(object):
pass pass
WRAP_CMD_PARAMS = {
'timeout': 'str2seconds',
'bantime': 'ignore',
}
class CommandAction(ActionBase): class CommandAction(ActionBase):
"""A action which executes OS shell commands. """A action which executes OS shell commands.
@ -306,7 +315,10 @@ class CommandAction(ActionBase):
def __setattr__(self, name, value): def __setattr__(self, name, value):
if not name.startswith('_') and not self.__init and not callable(value): if not name.startswith('_') and not self.__init and not callable(value):
# special case for some pasrameters: # special case for some pasrameters:
if name in ('timeout', 'bantime'): wrp = WRAP_CMD_PARAMS.get(name)
if wrp == 'ignore': # ignore (filter) dynamic parameters
return
elif wrp == 'str2seconds':
value = str(MyTime.str2seconds(value)) value = str(MyTime.str2seconds(value))
# parameters changed - clear properties and substitution cache: # parameters changed - clear properties and substitution cache:
self.__properties = None self.__properties = None
@ -434,6 +446,26 @@ class CommandAction(ActionBase):
if not self._processCmd('<actionban>', aInfo): if not self._processCmd('<actionban>', aInfo):
raise RuntimeError("Error banning %(ip)s" % aInfo) raise RuntimeError("Error banning %(ip)s" % aInfo)
@property
def _prolongable(self):
return (hasattr(self, 'actionprolong') and self.actionprolong
and not str(self.actionprolong).isspace())
def prolong(self, aInfo):
"""Executes the "actionprolong" command.
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.
"""
if not self._processCmd('<actionprolong>', aInfo):
raise RuntimeError("Error prolonging %(ip)s" % aInfo)
def unban(self, aInfo): def unban(self, aInfo):
"""Executes the "actionunban" command. """Executes the "actionunban" command.
@ -498,8 +530,10 @@ class CommandAction(ActionBase):
""" """
return self._executeOperation('<actionreload>', 'reloading') return self._executeOperation('<actionreload>', 'reloading')
@staticmethod ESCAPE_CRE = re.compile(r"""[\\#&;`|*?~<>^()\[\]{}$'"\n\r]""")
def escapeTag(value):
@classmethod
def escapeTag(cls, value):
"""Escape characters which may be used for command injection. """Escape characters which may be used for command injection.
Parameters Parameters
@ -516,12 +550,15 @@ class CommandAction(ActionBase):
----- -----
The following characters are escaped:: The following characters are escaped::
\\#&;`|*?~<>^()[]{}$'" \\#&;`|*?~<>^()[]{}$'"\n\r
""" """
for c in '\\#&;`|*?~<>^()[]{}$\'"': _map2c = {'\n': 'n', '\r': 'r'}
if c in value: def substChar(m):
value = value.replace(c, '\\' + c) c = m.group()
return '\\' + _map2c.get(c, c)
value = cls.ESCAPE_CRE.sub(substChar, value)
return value return value
@classmethod @classmethod
@ -780,7 +817,8 @@ class CommandAction(ActionBase):
RuntimeError RuntimeError
If command execution times out. If command execution times out.
""" """
logSys.debug(realCmd) if logSys.getEffectiveLevel() < logging.DEBUG: # pragma: no cover
logSys.log(9, realCmd)
if not realCmd: if not realCmd:
logSys.debug("Nothing to do") logSys.debug("Nothing to do")
return True return True

View File

@ -298,6 +298,7 @@ class Actions(JailThread, Mapping):
"fid": lambda self: self.__ticket.getID(), "fid": lambda self: self.__ticket.getID(),
"failures": lambda self: self.__ticket.getAttempt(), "failures": lambda self: self.__ticket.getAttempt(),
"time": lambda self: self.__ticket.getTime(), "time": lambda self: self.__ticket.getTime(),
"bantime": lambda self: self._getBanTime(),
"matches": lambda self: "\n".join(self.__ticket.getMatches()), "matches": lambda self: "\n".join(self.__ticket.getMatches()),
# to bypass actions, that should not be executed for restored tickets # to bypass actions, that should not be executed for restored tickets
"restored": lambda self: (1 if self.__ticket.restored else 0), "restored": lambda self: (1 if self.__ticket.restored else 0),
@ -325,6 +326,11 @@ class Actions(JailThread, Mapping):
def copy(self): # pargma: no cover def copy(self): # pargma: no cover
return self.__class__(self.__ticket, self.__jail, self.immutable, self.data.copy()) return self.__class__(self.__ticket, self.__jail, self.immutable, self.data.copy())
def _getBanTime(self):
btime = self.__ticket.getBanTime()
if btime is None: btime = self.__jail.actions.getBanTime()
return btime
def _mi4ip(self, overalljails=False): def _mi4ip(self, overalljails=False):
"""Gets bans merged once, a helper for lambda(s), prevents stop of executing action by any exception inside. """Gets bans merged once, a helper for lambda(s), prevents stop of executing action by any exception inside.
@ -445,6 +451,29 @@ class Actions(JailThread, Mapping):
self.__banManager.getBanTotal(), self.__banManager.size(), self._jail.name) self.__banManager.getBanTotal(), self.__banManager.size(), self._jail.name)
return cnt return cnt
def _prolongBan(self, ticket):
# prevent to prolong ticket that was removed in-between,
# if it in ban list - ban time already prolonged (and it stays there):
if not self.__banManager._inBanList(ticket): return
# do actions :
aInfo = None
for name, action in self._actions.iteritems():
try:
if ticket.restored and getattr(action, 'norestored', False):
continue
if not action._prolongable:
continue
if aInfo is None:
aInfo = self.__getActionInfo(ticket)
if not aInfo.immutable: aInfo.reset()
action.prolong(aInfo)
except Exception as e:
logSys.error(
"Failed to execute ban jail '%s' action '%s' "
"info '%r': %s",
self._jail.name, name, aInfo, e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
def __checkUnBan(self): def __checkUnBan(self):
"""Check for IP address to unban. """Check for IP address to unban.

View File

@ -226,7 +226,7 @@ class Jail(object):
if opt == 'increment': if opt == 'increment':
if isinstance(value, str): if isinstance(value, str):
be[opt] = value.lower() in ("yes", "true", "ok", "1") be[opt] = value.lower() in ("yes", "true", "ok", "1")
if be[opt] and self.database is None: if be.get(opt) and self.database is None:
logSys.warning("ban time increment is not available as long jail database is not set") logSys.warning("ban time increment is not available as long jail database is not set")
if opt in ['maxtime', 'rndtime']: if opt in ['maxtime', 'rndtime']:
if not value is None: if not value is None:

View File

@ -121,9 +121,28 @@ class ObserverThread(JailThread):
def add_timer(self, starttime, *event): def add_timer(self, starttime, *event):
"""Add a timer event to queue will start (and wake) in 'starttime' seconds """Add a timer event to queue will start (and wake) in 'starttime' seconds
""" """
# in testing we should wait (looping) for the possible time drifts:
if MyTime.myTime is not None and starttime:
# test time after short sleep:
t = threading.Timer(Utils.DEFAULT_SLEEP_INTERVAL, self._delayedEvent,
(MyTime.time() + starttime, time.time() + starttime, event)
)
t.start()
return
# add timer event:
t = threading.Timer(starttime, self.add, event) t = threading.Timer(starttime, self.add, event)
t.start() t.start()
def _delayedEvent(self, endMyTime, endTime, event):
if MyTime.time() >= endMyTime or time.time() >= endTime:
self.add_timer(0, *event)
return
# repeat after short sleep:
t = threading.Timer(Utils.DEFAULT_SLEEP_INTERVAL, self._delayedEvent,
(endMyTime, endTime, event)
)
t.start()
def pulse_notify(self): def pulse_notify(self):
"""Notify wakeup (sets /and resets/ notify event) """Notify wakeup (sets /and resets/ notify event)
""" """
@ -196,7 +215,8 @@ class ObserverThread(JailThread):
if ev is None: if ev is None:
break break
## retrieve method by name ## retrieve method by name
meth = __meth[ev[0]] meth = ev[0]
if not callable(ev[0]): meth = __meth[meth]
## execute it with rest of event as variable arguments ## execute it with rest of event as variable arguments
meth(*ev[1:]) meth(*ev[1:])
except Exception as e: except Exception as e:
@ -448,10 +468,10 @@ class ObserverThread(JailThread):
Observer will check ip was known (bad) and possibly increase/prolong a ban time Observer will check ip was known (bad) and possibly increase/prolong a ban time
Secondary we will actualize the bans and bips (bad ip) in database Secondary we will actualize the bans and bips (bad ip) in database
""" """
try:
oldbtime = btime oldbtime = btime
ip = ticket.getIP() ip = ticket.getIP()
logSys.debug("[%s] Observer: ban found %s, %s", jail.name, ip, btime) logSys.debug("[%s] Observer: ban found %s, %s", jail.name, ip, btime)
try:
# if not permanent, not restored and ban time was not set - check time should be increased: # if not permanent, not restored and ban time was not set - check time should be increased:
if btime != -1 and not ticket.restored and ticket.getBanTime() is None: if btime != -1 and not ticket.restored and ticket.getBanTime() is None:
btime = self.incrBanTime(jail, btime, ticket) btime = self.incrBanTime(jail, btime, ticket)
@ -475,6 +495,9 @@ class ObserverThread(JailThread):
if btime != oldbtime: if btime != oldbtime:
logSys.notice("[%s] Increase Ban %s (%d # %s -> %s)", jail.name, logSys.notice("[%s] Increase Ban %s (%d # %s -> %s)", jail.name,
ip, ticket.getBanCount(), *logtime) ip, ticket.getBanCount(), *logtime)
# delayed prolonging ticket via actions that expected this:
logSys.log(5, "[%s] Observer: prolong %s in %s", jail.name, ip, (btime, oldbtime))
self.add_timer(min(10, btime - oldbtime - 5), self.prolongBan, ticket, jail)
# add ticket to database, but only if was not restored (not already read from database): # add ticket to database, but only if was not restored (not already read from database):
if jail.database is not None and not ticket.restored: if jail.database is not None and not ticket.restored:
# add to database always only after ban time was calculated an not yet already banned: # add to database always only after ban time was calculated an not yet already banned:
@ -482,6 +505,21 @@ class ObserverThread(JailThread):
except Exception as e: except Exception as e:
logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG) logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
def prolongBan(self, ticket, jail):
""" Notify observer a ban occured for ip
Observer will check ip was known (bad) and possibly increase/prolong a ban time
Secondary we will actualize the bans and bips (bad ip) in database
"""
try:
btime = ticket.getBanTime()
ip = ticket.getIP()
logSys.debug("[%s] Observer: prolong %s, %s", jail.name, ip, btime)
# prolong ticket via actions that expected this:
jail.actions._prolongBan(ticket)
except Exception as e:
logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
# Global observer initial created in server (could be later rewriten via singleton) # Global observer initial created in server (could be later rewriten via singleton)
class _Observers: class _Observers:
def __init__(self): def __init__(self):

View File

@ -206,15 +206,15 @@ class CommandActionTest(LogCaptureTestCase):
self.assertEqual( self.assertEqual(
self.__action.replaceTag("<matches>", self.__action.replaceTag("<matches>",
{'matches': "some >char< should \< be[ escap}ed&\n"}), {'matches': "some >char< should \< be[ escap}ed&\n"}),
"some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\n") "some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\\n")
self.assertEqual( self.assertEqual(
self.__action.replaceTag("<ipmatches>", self.__action.replaceTag("<ipmatches>",
{'ipmatches': "some >char< should \< be[ escap}ed&\n"}), {'ipmatches': "some >char< should \< be[ escap}ed&\n"}),
"some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\n") "some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\\n")
self.assertEqual( self.assertEqual(
self.__action.replaceTag("<ipjailmatches>", self.__action.replaceTag("<ipjailmatches>",
{'ipjailmatches': "some >char< should \< be[ escap}ed&\n"}), {'ipjailmatches': "some >char< should \< be[ escap}ed&\r\n"}),
"some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\n") "some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\\r\\n")
# Recursive # Recursive
aInfo["ABC"] = "<xyz>" aInfo["ABC"] = "<xyz>"

View File

@ -566,8 +566,6 @@ class JailsReaderTest(LogCaptureTestCase):
# all must have some actionban defined # all must have some actionban defined
self.assertTrue(actionReader._opts.get('actionban', '').strip(), self.assertTrue(actionReader._opts.get('actionban', '').strip(),
msg="Action file %r is lacking actionban" % actionConfig) msg="Action file %r is lacking actionban" % actionConfig)
self.assertIn('Init', actionReader.sections(),
msg="Action file %r is lacking [Init] section" % actionConfig)
def testReadStockJailConf(self): def testReadStockJailConf(self):
jails = JailsReader(basedir=CONFIG_DIR, share_config=CONFIG_DIR_SHARE_CFG) # we are running tests from root project dir atm jails = JailsReader(basedir=CONFIG_DIR, share_config=CONFIG_DIR_SHARE_CFG) # we are running tests from root project dir atm

View File

@ -43,7 +43,8 @@ from .. import protocol
from ..server import server from ..server import server
from ..server.mytime import MyTime from ..server.mytime import MyTime
from ..server.utils import Utils from ..server.utils import Utils
from .utils import LogCaptureTestCase, logSys as DefLogSys, with_tmpdir, shutil, logging from .utils import LogCaptureTestCase, logSys as DefLogSys, with_tmpdir, shutil, logging, \
TEST_NOW, tearDownMyTime
from ..helpers import getLogger from ..helpers import getLogger
@ -80,6 +81,11 @@ fail2banclient.output = \
fail2banserver.output = \ fail2banserver.output = \
protocol.output = _test_output protocol.output = _test_output
def _time_shift(shift):
# jump to the future (+shift minutes):
logSys.debug("===>>> time shift + %s min", shift)
MyTime.setTime(MyTime.time() + shift*60)
Observers = server.Observers Observers = server.Observers
@ -317,6 +323,7 @@ def with_foreground_server_thread(startextra={}):
# so don't kill (same process) - if success, just wait for end of worker: # so don't kill (same process) - if success, just wait for end of worker:
if phase.get('end', None): if phase.get('end', None):
th.join() th.join()
tearDownMyTime()
return wrapper return wrapper
return _deco_wrapper return _deco_wrapper
@ -343,6 +350,7 @@ class Fail2banClientServerBase(LogCaptureTestCase):
server.DEF_LOGTARGET = SRV_DEF_LOGTARGET server.DEF_LOGTARGET = SRV_DEF_LOGTARGET
server.DEF_LOGLEVEL = SRV_DEF_LOGLEVEL server.DEF_LOGLEVEL = SRV_DEF_LOGLEVEL
LogCaptureTestCase.tearDown(self) LogCaptureTestCase.tearDown(self)
tearDownMyTime()
@staticmethod @staticmethod
def _test_exit(code=0): def _test_exit(code=0):
@ -1158,3 +1166,97 @@ class Fail2banServerTest(Fail2banClientServerBase):
self.assertLogged( self.assertLogged(
"Jail 'test-jail1' stopped", "Jail 'test-jail1' stopped",
"Jail 'test-jail1' started", all=True) "Jail 'test-jail1' started", all=True)
@with_foreground_server_thread()
def testServerObserver(self, tmp, startparams):
cfg = pjoin(tmp, "config")
test1log = pjoin(tmp, "test1.log")
os.mkdir(pjoin(cfg, "action.d"))
def _write_action_cfg(actname="test-action1", prolong=True):
fn = pjoin(cfg, "action.d", "%s.conf" % actname)
_write_file(fn, "w",
"[DEFAULT]",
"",
"[Definition]",
"actionban = printf %%b '[%(name)s] %(actname)s: ++ ban <ip> -t <bantime> : <F-MSG>'",
"actionprolong = printf %%b '[%(name)s] %(actname)s: ++ prolong <ip> -t <bantime> : <F-MSG>'" \
if prolong else "",
"actionunban = printf %%b '[%(name)s] %(actname)s: -- unban <ip>'",
)
if unittest.F2B.log_level <= logging.DEBUG: # pragma: no cover
_out_file(fn)
def _write_jail_cfg(backend="polling"):
_write_file(pjoin(cfg, "jail.conf"), "w",
"[INCLUDES]", "",
"[DEFAULT]", "",
"usedns = no",
"maxretry = 3",
"findtime = 1m",
"bantime = 5m",
"bantime.increment = true",
"datepattern = {^LN-BEG}EPOCH",
"",
"[test-jail1]", "backend = " + backend, "filter =",
"action = test-action1[name='%(__name__)s']",
" test-action2[name='%(__name__)s']",
"logpath = " + test1log,
"failregex = ^\s*failure <F-ERRCODE>401|403</F-ERRCODE> from <HOST>:\s*<F-MSG>.*</F-MSG>$",
"enabled = true",
"",
)
if unittest.F2B.log_level <= logging.DEBUG: # pragma: no cover
_out_file(pjoin(cfg, "jail.conf"))
# create test config:
_write_action_cfg(actname="test-action1", prolong=False)
_write_action_cfg(actname="test-action2", prolong=True)
_write_jail_cfg()
_write_file(test1log, "w")
# initial start:
self.pruneLog("[test-phase 0) time-0]")
self.execSuccess(startparams, "reload")
# generate bad ip:
_write_file(test1log, "w+", *(
(str(int(MyTime.time())) + " failure 401 from 192.0.2.11: I'm bad \"hacker\" `` $(echo test)",) * 3
))
# wait for ban:
self.assertLogged(
"stdout: '[test-jail1] test-action1: ++ ban 192.0.2.11 -t 300 : ",
"stdout: '[test-jail1] test-action2: ++ ban 192.0.2.11 -t 300 : ",
all=True, wait=MID_WAITTIME)
# wait for observer idle (write all tickets to db):
_observer_wait_idle()
self.pruneLog("[test-phase 1) time+10m]")
# jump to the future (+10 minutes):
_time_shift(10)
self.assertLogged(
"stdout: '[test-jail1] test-action1: -- unban 192.0.2.11",
"stdout: '[test-jail1] test-action2: -- unban 192.0.2.11",
all=True, wait=MID_WAITTIME)
self.pruneLog("[test-phase 2) time+10m]")
# write again (IP already bad):
_write_file(test1log, "w+", *(
(str(int(MyTime.time())) + " failure 401 from 192.0.2.11: I'm very bad \"hacker\" `` $(echo test)",) * 2
))
# wait for ban:
self.assertLogged(
"stdout: '[test-jail1] test-action1: ++ ban 192.0.2.11 -t 300 : ",
"stdout: '[test-jail1] test-action2: ++ ban 192.0.2.11 -t 300 : ",
all=True, wait=MID_WAITTIME)
# wait for observer idle (write all tickets to db):
_observer_wait_idle()
self.pruneLog("[test-phase 2) time+11m]")
# jump to the future (+1 minute):
_time_shift(1)
# wait for observer idle (write all tickets to db):
_observer_wait_idle()
# wait for prolong:
self.assertLogged(
"stdout: '[test-jail1] test-action2: ++ prolong 192.0.2.11 -t 600 : ",
all=True, wait=MID_WAITTIME)

View File

@ -1065,8 +1065,20 @@ class ServerConfigReaderTests(LogCaptureTestCase):
logSys.debug(l) logSys.debug(l)
return True return True
def _testActionInfos(self):
if not hasattr(self, '__aInfos'):
dmyjail = DummyJail()
self.__aInfos = {}
for t, ip in (('ipv4', '192.0.2.1'), ('ipv6', '2001:DB8::')):
ticket = BanTicket(ip)
ticket.setBanTime(600)
self.__aInfos[t] = _actions.Actions.ActionInfo(ticket, dmyjail)
return self.__aInfos
def _testExecActions(self, server): def _testExecActions(self, server):
jails = server._Server__jails jails = server._Server__jails
aInfos = self._testActionInfos()
for jail in jails: for jail in jails:
# print(jail, jails[jail]) # print(jail, jails[jail])
for a in jails[jail].actions: for a in jails[jail].actions:
@ -1083,16 +1095,16 @@ class ServerConfigReaderTests(LogCaptureTestCase):
action.start() action.start()
# test ban ip4 : # test ban ip4 :
logSys.debug('# === ban-ipv4 ==='); self.pruneLog() logSys.debug('# === ban-ipv4 ==='); self.pruneLog()
action.ban({'ip': IPAddr('192.0.2.1'), 'family': 'inet4'}) action.ban(aInfos['ipv4'])
# test unban ip4 : # test unban ip4 :
logSys.debug('# === unban ipv4 ==='); self.pruneLog() logSys.debug('# === unban ipv4 ==='); self.pruneLog()
action.unban({'ip': IPAddr('192.0.2.1'), 'family': 'inet4'}) action.unban(aInfos['ipv4'])
# test ban ip6 : # test ban ip6 :
logSys.debug('# === ban ipv6 ==='); self.pruneLog() logSys.debug('# === ban ipv6 ==='); self.pruneLog()
action.ban({'ip': IPAddr('2001:DB8::'), 'family': 'inet6'}) action.ban(aInfos['ipv6'])
# test unban ip6 : # test unban ip6 :
logSys.debug('# === unban ipv6 ==='); self.pruneLog() logSys.debug('# === unban ipv6 ==='); self.pruneLog()
action.unban({'ip': IPAddr('2001:DB8::'), 'family': 'inet6'}) action.unban(aInfos['ipv6'])
# test stop : # test stop :
logSys.debug('# === stop ==='); self.pruneLog() logSys.debug('# === stop ==='); self.pruneLog()
action.stop() action.stop()
@ -1310,11 +1322,11 @@ class ServerConfigReaderTests(LogCaptureTestCase):
('j-w-iptables-ipset', 'iptables-ipset-proto6[name=%(__name__)s, bantime="10m", port="http", protocol="tcp", chain="INPUT"]', { ('j-w-iptables-ipset', 'iptables-ipset-proto6[name=%(__name__)s, bantime="10m", port="http", protocol="tcp", chain="INPUT"]', {
'ip4': (' f2b-j-w-iptables-ipset ',), 'ip6': (' f2b-j-w-iptables-ipset6 ',), 'ip4': (' f2b-j-w-iptables-ipset ',), 'ip6': (' f2b-j-w-iptables-ipset6 ',),
'ip4-start': ( 'ip4-start': (
"`ipset create f2b-j-w-iptables-ipset hash:ip timeout 600`", "`ipset create f2b-j-w-iptables-ipset hash:ip`",
"`iptables -w -I INPUT -p tcp -m multiport --dports http -m set --match-set f2b-j-w-iptables-ipset src -j REJECT --reject-with icmp-port-unreachable`", "`iptables -w -I INPUT -p tcp -m multiport --dports http -m set --match-set f2b-j-w-iptables-ipset src -j REJECT --reject-with icmp-port-unreachable`",
), ),
'ip6-start': ( 'ip6-start': (
"`ipset create f2b-j-w-iptables-ipset6 hash:ip timeout 600 family inet6`", "`ipset create f2b-j-w-iptables-ipset6 hash:ip family inet6`",
"`ip6tables -w -I INPUT -p tcp -m multiport --dports http -m set --match-set f2b-j-w-iptables-ipset6 src -j REJECT --reject-with icmp6-port-unreachable`", "`ip6tables -w -I INPUT -p tcp -m multiport --dports http -m set --match-set f2b-j-w-iptables-ipset6 src -j REJECT --reject-with icmp6-port-unreachable`",
), ),
'flush': ( 'flush': (
@ -1348,11 +1360,11 @@ class ServerConfigReaderTests(LogCaptureTestCase):
('j-w-iptables-ipset-ap', 'iptables-ipset-proto6-allports[name=%(__name__)s, bantime="10m", chain="INPUT"]', { ('j-w-iptables-ipset-ap', 'iptables-ipset-proto6-allports[name=%(__name__)s, bantime="10m", chain="INPUT"]', {
'ip4': (' f2b-j-w-iptables-ipset-ap ',), 'ip6': (' f2b-j-w-iptables-ipset-ap6 ',), 'ip4': (' f2b-j-w-iptables-ipset-ap ',), 'ip6': (' f2b-j-w-iptables-ipset-ap6 ',),
'ip4-start': ( 'ip4-start': (
"`ipset create f2b-j-w-iptables-ipset-ap hash:ip timeout 600`", "`ipset create f2b-j-w-iptables-ipset-ap hash:ip`",
"`iptables -w -I INPUT -m set --match-set f2b-j-w-iptables-ipset-ap src -j REJECT --reject-with icmp-port-unreachable`", "`iptables -w -I INPUT -m set --match-set f2b-j-w-iptables-ipset-ap src -j REJECT --reject-with icmp-port-unreachable`",
), ),
'ip6-start': ( 'ip6-start': (
"`ipset create f2b-j-w-iptables-ipset-ap6 hash:ip timeout 600 family inet6`", "`ipset create f2b-j-w-iptables-ipset-ap6 hash:ip family inet6`",
"`ip6tables -w -I INPUT -m set --match-set f2b-j-w-iptables-ipset-ap6 src -j REJECT --reject-with icmp6-port-unreachable`", "`ip6tables -w -I INPUT -m set --match-set f2b-j-w-iptables-ipset-ap6 src -j REJECT --reject-with icmp6-port-unreachable`",
), ),
'flush': ( 'flush': (
@ -1646,11 +1658,11 @@ class ServerConfigReaderTests(LogCaptureTestCase):
('j-w-fwcmd-ipset', 'firewallcmd-ipset[name=%(__name__)s, bantime="10m", port="http", protocol="tcp", chain="INPUT"]', { ('j-w-fwcmd-ipset', 'firewallcmd-ipset[name=%(__name__)s, bantime="10m", port="http", protocol="tcp", chain="INPUT"]', {
'ip4': (' f2b-j-w-fwcmd-ipset ',), 'ip6': (' f2b-j-w-fwcmd-ipset6 ',), 'ip4': (' f2b-j-w-fwcmd-ipset ',), 'ip6': (' f2b-j-w-fwcmd-ipset6 ',),
'ip4-start': ( 'ip4-start': (
"`ipset create f2b-j-w-fwcmd-ipset hash:ip timeout 600`", "`ipset create f2b-j-w-fwcmd-ipset hash:ip`",
"`firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -p tcp -m multiport --dports http -m set --match-set f2b-j-w-fwcmd-ipset src -j REJECT --reject-with icmp-port-unreachable`", "`firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -p tcp -m multiport --dports http -m set --match-set f2b-j-w-fwcmd-ipset src -j REJECT --reject-with icmp-port-unreachable`",
), ),
'ip6-start': ( 'ip6-start': (
"`ipset create f2b-j-w-fwcmd-ipset6 hash:ip timeout 600`", "`ipset create f2b-j-w-fwcmd-ipset6 hash:ip`",
"`firewall-cmd --direct --add-rule ipv6 filter INPUT 0 -p tcp -m multiport --dports http -m set --match-set f2b-j-w-fwcmd-ipset6 src -j REJECT --reject-with icmp6-port-unreachable`", "`firewall-cmd --direct --add-rule ipv6 filter INPUT 0 -p tcp -m multiport --dports http -m set --match-set f2b-j-w-fwcmd-ipset6 src -j REJECT --reject-with icmp6-port-unreachable`",
), ),
'stop': ( 'stop': (
@ -1695,10 +1707,7 @@ class ServerConfigReaderTests(LogCaptureTestCase):
jails = server._Server__jails jails = server._Server__jails
tickets = { aInfos = self._testActionInfos()
'ip4': BanTicket('192.0.2.1'),
'ip6': BanTicket('2001:DB8::'),
}
for jail, act, tests in testJailsActions: for jail, act, tests in testJailsActions:
# print(jail, jails[jail]) # print(jail, jails[jail])
for a in jails[jail].actions: for a in jails[jail].actions:
@ -1716,32 +1725,28 @@ class ServerConfigReaderTests(LogCaptureTestCase):
self.assertLogged(*tests['start'], all=True) self.assertLogged(*tests['start'], all=True)
else: else:
self.assertNotLogged(*tests['ip4-start']+tests['ip6-start'], all=True) self.assertNotLogged(*tests['ip4-start']+tests['ip6-start'], all=True)
ainfo = {
'ip4': _actions.Actions.ActionInfo(tickets['ip4'], jails[jail]),
'ip6': _actions.Actions.ActionInfo(tickets['ip6'], jails[jail]),
}
# test ban ip4 : # test ban ip4 :
self.pruneLog('# === ban-ipv4 ===') self.pruneLog('# === ban-ipv4 ===')
action.ban(ainfo['ip4']) action.ban(aInfos['ipv4'])
if tests.get('ip4-start'): self.assertLogged(*tests['ip4-start'], all=True) if tests.get('ip4-start'): self.assertLogged(*tests['ip4-start'], all=True)
if tests.get('ip6-start'): self.assertNotLogged(*tests['ip6-start'], all=True) if tests.get('ip6-start'): self.assertNotLogged(*tests['ip6-start'], all=True)
self.assertLogged(*tests['ip4-check']+tests['ip4-ban'], all=True) self.assertLogged(*tests['ip4-check']+tests['ip4-ban'], all=True)
self.assertNotLogged(*tests['ip6'], all=True) self.assertNotLogged(*tests['ip6'], all=True)
# test unban ip4 : # test unban ip4 :
self.pruneLog('# === unban ipv4 ===') self.pruneLog('# === unban ipv4 ===')
action.unban(ainfo['ip4']) action.unban(aInfos['ipv4'])
self.assertLogged(*tests['ip4-check']+tests['ip4-unban'], all=True) self.assertLogged(*tests['ip4-check']+tests['ip4-unban'], all=True)
self.assertNotLogged(*tests['ip6'], all=True) self.assertNotLogged(*tests['ip6'], all=True)
# test ban ip6 : # test ban ip6 :
self.pruneLog('# === ban ipv6 ===') self.pruneLog('# === ban ipv6 ===')
action.ban(ainfo['ip6']) action.ban(aInfos['ipv6'])
if tests.get('ip6-start'): self.assertLogged(*tests['ip6-start'], all=True) if tests.get('ip6-start'): self.assertLogged(*tests['ip6-start'], all=True)
if tests.get('ip4-start'): self.assertNotLogged(*tests['ip4-start'], all=True) if tests.get('ip4-start'): self.assertNotLogged(*tests['ip4-start'], all=True)
self.assertLogged(*tests['ip6-check']+tests['ip6-ban'], all=True) self.assertLogged(*tests['ip6-check']+tests['ip6-ban'], all=True)
self.assertNotLogged(*tests['ip4'], all=True) self.assertNotLogged(*tests['ip4'], all=True)
# test unban ip6 : # test unban ip6 :
self.pruneLog('# === unban ipv6 ===') self.pruneLog('# === unban ipv6 ===')
action.unban(ainfo['ip6']) action.unban(aInfos['ipv6'])
self.assertLogged(*tests['ip6-check']+tests['ip6-unban'], all=True) self.assertLogged(*tests['ip6-check']+tests['ip6-unban'], all=True)
self.assertNotLogged(*tests['ip4'], all=True) self.assertNotLogged(*tests['ip4'], all=True)
# test flush for actions should supported this: # test flush for actions should supported this: