Merge branch '0.10-incr-dynamic-bantime' into 0.10-full

pull/1460/head
sebres 2017-05-17 21:30:20 +02:00
commit 8403eab2c4
22 changed files with 380 additions and 158 deletions

View File

@ -69,9 +69,14 @@ TODO: implementing of options resp. other tasks from PR #1346
- `<fid>` - failure identifier (if raw resp. failures without IP address)
- `<ip-rev>` - PTR reversed representation of IP address
- `<ip-host>` - host name of the IP address
- `<bancount>` - ban count of this offender if known as bad (started by 1 for unknown)
- `<bantime>` - current ban-time of the ticket (prolongation can be retarded up to 10 sec.)
- `<F-...>` - interpolates to the corresponding filter group capture `...`
- `<fq-hostname>` - fully-qualified name of host (the same as `$(hostname -f)`)
- `<sh-hostname>` - short hostname (the same as `$(uname -n)`)
* Introduced new action command `actionprolong` to prolong ban-time (e. g. set new timeout if expected);
Several actions (like ipset, etc.) rewritten using net logic with `actionprolong`.
Note: because ban-time is dynamic, it was removed from jail.conf as timeout argument (check jail.local).
* Allow to use filter options by `fail2ban-regex`, example:
fail2ban-regex text.log "sshd[mode=aggressive]"
* Samples test case factory extended with filter options - dict in JSON to control

View File

@ -18,7 +18,7 @@ before = firewallcmd-common.conf
[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>
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
actionprolong = %(actionban)s
actionunban = ipset del <ipmset> <ip> -exist
[Init]
@ -38,12 +40,6 @@ actionunban = ipset del <ipmset> <ip> -exist
#
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>
[Init?family=inet6]

View File

@ -26,7 +26,7 @@ before = iptables-common.conf
# Notes.: command executed once at the start of Fail2Ban.
# 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>
# 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
actionprolong = %(actionban)s
# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights.
@ -61,12 +63,6 @@ actionunban = ipset del <ipmset> <ip> -exist
[Init]
# Option: bantime
# Notes: specifies the bantime in seconds (handled internally rather than by fail2ban)
# Values: [ NUM ] Default: 600
#
bantime = 600
ipmset = f2b-<name>
familyopt =

View File

@ -26,7 +26,7 @@ before = iptables-common.conf
# Notes.: command executed once at the start of Fail2Ban.
# 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>
# 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
actionprolong = %(actionban)s
# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights.
@ -61,12 +63,6 @@ actionunban = ipset del <ipmset> <ip> -exist
[Init]
# Option: bantime
# Notes: specifies the bantime in seconds (handled internally rather than by fail2ban)
# Values: [ NUM ] Default: 600
#
bantime = 600
ipmset = f2b-<name>
familyopt =

View File

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

View File

@ -51,7 +51,7 @@
# Values: CMD
#
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
# Option: actionstop
@ -68,6 +68,8 @@ actionstop = ipset flush f2b-<name>
#
actionban = ipset add f2b-<name> <ip> timeout <bantime> -exist
actionprolong = %(actionban)s
# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
# 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
[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
# 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.
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"]
# ban & send an e-mail with whois report and relevant log lines
# 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"]
# 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
# 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"]
# 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],
"actionrepair": ["string", None],
"actionban": ["string", None],
"actionprolong": ["string", None],
"actionunban": ["string", None],
"norestored": ["string", None],
}

View File

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

View File

@ -34,7 +34,7 @@ try:
except ImportError:
OrderedDict = dict
from .banmanager import BanManager
from .banmanager import BanManager, BanTicket
from .ipdns import DNSUtils
from .jailthread import JailThread
from .action import ActionBase, CommandAction, CallingMap
@ -298,6 +298,8 @@ class Actions(JailThread, Mapping):
"fid": lambda self: self.__ticket.getID(),
"failures": lambda self: self.__ticket.getAttempt(),
"time": lambda self: self.__ticket.getTime(),
"bantime": lambda self: self._getBanTime(),
"bancount": lambda self: self.__ticket.getBanCount(),
"matches": lambda self: "\n".join(self.__ticket.getMatches()),
# to bypass actions, that should not be executed for restored tickets
"restored": lambda self: (1 if self.__ticket.restored else 0),
@ -322,9 +324,14 @@ class Actions(JailThread, Mapping):
self.immutable = immutable
self.data = data
def copy(self): # pargma: no cover
def copy(self): # pragma: no cover
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):
"""Gets bans merged once, a helper for lambda(s), prevents stop of executing action by any exception inside.
@ -390,15 +397,9 @@ class Actions(JailThread, Mapping):
ticket = self._jail.getFailTicket()
if not ticket:
break
bTicket = BanManager.createBanTicket(ticket)
btime = ticket.getBanTime()
if btime is not None:
bTicket.setBanTime(btime)
bTicket.setBanCount(ticket.getBanCount())
else:
btime = self.__banManager.getBanTime()
if ticket.restored:
bTicket.restored = True
bTicket = BanTicket.wrap(ticket)
btime = ticket.getBanTime(self.__banManager.getBanTime())
ip = bTicket.getIP()
aInfo = self.__getActionInfo(bTicket)
reason = {}
@ -445,6 +446,29 @@ class Actions(JailThread, Mapping):
self.__banManager.getBanTotal(), self.__banManager.size(), self._jail.name)
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):
"""Check for IP address to unban.

View File

@ -243,21 +243,6 @@ class BanManager:
logSys.exception(e)
return []
##
# Create a ban ticket.
#
# Create a BanTicket from a FailTicket. The timestamp of the BanTicket
# is the current time. This is a static method.
# @param ticket the FailTicket
# @return a BanTicket
@staticmethod
def createBanTicket(ticket):
# we should always use correct time to calculate correct end time (ban time is variable now,
# + possible double banning by restore from database and from log file)
# so use as lastTime always time from ticket.
return BanTicket(ticket=ticket)
##
# Add a ban ticket.
#
@ -291,6 +276,7 @@ class BanManager:
# not yet banned - add new one:
self.__banList[fid] = ticket
self.__banTotal += 1
ticket.incrBanCount()
# correct next unban time:
if self.__nextUnbanTime > eob:
self.__nextUnbanTime = eob

View File

@ -27,7 +27,7 @@ __license__ = "GPL"
from threading import Lock
import logging
from .ticket import FailTicket
from .ticket import FailTicket, BanTicket
from ..helpers import getLogger, BgService
# Gets the instance of the logger.
@ -103,13 +103,13 @@ class FailManager:
fData.setMatches(matches[-self.maxEntries:])
except KeyError:
# not found - already banned - prevent to add failure if comes from observer:
if observed:
if observed or isinstance(ticket, BanTicket):
return
# if already FailTicket - add it direct, otherwise create (using copy all ticket data):
if isinstance(ticket, FailTicket):
fData = ticket;
else:
fData = FailTicket(ticket=ticket)
fData = FailTicket.wrap(ticket)
if count > ticket.getAttempt():
fData.setRetry(count)
self.__failList[fid] = fData

View File

@ -226,7 +226,7 @@ class Jail(object):
if opt == 'increment':
if isinstance(value, str):
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")
if opt in ['maxtime', 'rndtime']:
if not value is None:

View File

@ -121,9 +121,28 @@ class ObserverThread(JailThread):
def add_timer(self, starttime, *event):
"""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.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):
"""Notify wakeup (sets /and resets/ notify event)
"""
@ -164,8 +183,6 @@ class ObserverThread(JailThread):
self.add_named_timer('DB_PURGE', self.__db_purge_interval, 'db_purge')
## Mapping of all possible event types of observer:
__meth = {
'failureFound': self.failureFound,
'banFound': self.banFound,
# universal lambda:
'call': self.call_lambda,
# system and service events:
@ -196,7 +213,8 @@ class ObserverThread(JailThread):
if ev is None:
break
## retrieve method by name
meth = __meth[ev[0]]
meth = ev[0]
if not callable(ev[0]): meth = __meth.get(meth) or getattr(self, meth)
## execute it with rest of event as variable arguments
meth(*ev[1:])
except Exception as e:
@ -359,6 +377,7 @@ class ObserverThread(JailThread):
db = jail.database
if db is not None:
for banCount, timeOfBan, lastBanTime in db.getBan(ip, jail):
banCount = max(banCount, ticket.getBanCount())
retryCount = ((1 << (banCount if banCount < 20 else 20))/2 + 1)
# if lastBanTime == -1 or timeOfBan + lastBanTime * 2 > MyTime.time():
# retryCount = maxRetry
@ -378,8 +397,8 @@ class ObserverThread(JailThread):
(', Ban' if retryCount >= maxRetry else ''))
# retryCount-1, because a ticket was already once incremented by filter self
retryCount = failManager.addFailure(ticket, retryCount - 1, True)
# after observe we have increased count >= maxretry ...
ticket.setBanCount(banCount)
# after observe we have increased attempt count, compare it >= maxretry ...
if retryCount >= maxRetry:
# perform the banning of the IP now (again)
# [todo]: this code part will be used multiple times - optimize it later.
@ -424,12 +443,14 @@ class ObserverThread(JailThread):
for banCount, timeOfBan, lastBanTime in \
jail.database.getBan(ip, jail, overalljails=be.get('overalljails', False)) \
:
# increment count in ticket (if still not increased from banmanager, test-cases?):
if banCount >= ticket.getBanCount():
ticket.setBanCount(banCount+1)
logSys.debug('IP %s was already banned: %s #, %s', ip, banCount, timeOfBan);
ticket.setBanCount(banCount);
# calculate new ban time
if banCount > 0:
banTime = be['evformula'](self.BanTimeIncr(banTime, banCount))
ticket.setBanTime(banTime);
ticket.setBanTime(banTime)
# check current ticket time to prevent increasing for twice read tickets (restored from log file besides database after restart)
if ticket.getTime() > timeOfBan:
logSys.info('[%s] IP %s is bad: %s # last %s - incr %s to %s' % (jail.name, ip, banCount,
@ -448,12 +469,14 @@ class ObserverThread(JailThread):
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
"""
oldbtime = btime
ip = ticket.getIP()
logSys.debug("[%s] Observer: ban found %s, %s", jail.name, ip, btime)
if ticket.restored: # pragma: no cover (normally not resored tickets only)
return
try:
# 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:
oldbtime = btime
ip = ticket.getIP()
logSys.debug("[%s] Observer: ban found %s, %s", jail.name, ip, btime)
# if not permanent and ban time was not set - check time should be increased:
if btime != -1 and ticket.getBanTime() is None:
btime = self.incrBanTime(jail, btime, ticket)
# if we should prolong ban time:
if btime == -1 or btime > oldbtime:
@ -469,12 +492,13 @@ class ObserverThread(JailThread):
return False
else:
logtime = ('permanent', 'infinite')
# increment count:
ticket.incrBanCount()
# if ban time was prolonged - log again with new ban time:
if btime != oldbtime:
logSys.notice("[%s] Increase Ban %s (%d # %s -> %s)", jail.name,
ip, ticket.getBanCount(), *logtime)
# delayed prolonging ticket via actions that expected this (not later than 10 sec):
logSys.log(5, "[%s] Observer: prolong %s in %s", jail.name, ip, (btime, oldbtime))
self.add_timer(min(10, max(0, btime - oldbtime - 5)), self.prolongBan, ticket, jail)
# 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:
# add to database always only after ban time was calculated an not yet already banned:
@ -482,6 +506,21 @@ class ObserverThread(JailThread):
except Exception as e:
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)
class _Observers:
def __init__(self):

View File

@ -33,6 +33,7 @@ logSys = getLogger(__name__)
class Ticket(object):
__slots__ = ('_ip', '_flags', '_banCount', '_banTime', '_time', '_data', '_retry', '_lastReset')
MAX_TIME = 0X7FFFFFFFFFFF ;# 4461763-th year
@ -59,36 +60,44 @@ class Ticket(object):
self._data[k] = v
if ticket:
# ticket available - copy whole information from ticket:
self.__dict__.update(i for i in ticket.__dict__.iteritems() if i[0] in self.__dict__)
self.update(ticket)
#self.__dict__.update(i for i in ticket.__dict__.iteritems() if i[0] in self.__dict__)
def __str__(self):
return "%s: ip=%s time=%s bantime=%s bancount=%s #attempts=%d matches=%r" % \
(self.__class__.__name__.split('.')[-1], self.__ip, self._time,
self._banTime, self._banCount,
self._data['failures'], self._data.get('matches', []))
(self.__class__.__name__.split('.')[-1], self._ip, self._time,
self._banTime, self._banCount,
self._data['failures'], self._data.get('matches', []))
def __repr__(self):
return str(self)
def __eq__(self, other):
try:
return self.__ip == other.__ip and \
return self._ip == other._ip and \
round(self._time, 2) == round(other._time, 2) and \
self._data == other._data
except AttributeError:
return False
def update(self, ticket):
for n in ticket.__slots__:
v = getattr(ticket, n, None)
if v is not None:
setattr(self, n, v)
def setIP(self, value):
# guarantee using IPAddr instead of unicode, str for the IP
if isinstance(value, basestring):
value = IPAddr(value)
self.__ip = value
self._ip = value
def getID(self):
return self._data.get('fid', self.__ip)
return self._data.get('fid', self._ip)
def getIP(self):
return self.__ip
return self._ip
def setTime(self, value):
self._time = value
@ -97,16 +106,17 @@ class Ticket(object):
return self._time
def setBanTime(self, value):
self._banTime = value;
self._banTime = value
def getBanTime(self, defaultBT=None):
return (self._banTime if self._banTime is not None else defaultBT)
def setBanCount(self, value):
self._banCount = value;
def setBanCount(self, value, always=False):
if always or value > self._banCount:
self._banCount = value
def incrBanCount(self, value = 1):
self._banCount += value;
def incrBanCount(self, value=1):
self._banCount += value
def getBanCount(self):
return self._banCount;
@ -204,21 +214,21 @@ class FailTicket(Ticket):
def __init__(self, ip=None, time=None, matches=None, data={}, ticket=None):
# this class variables:
self.__retry = 0
self.__lastReset = None
self._retry = 0
self._lastReset = None
# create/copy using default ticket constructor:
Ticket.__init__(self, ip, time, matches, data, ticket)
# init:
if ticket is None:
self.__lastReset = time if time is not None else self.getTime()
if not self.__retry:
self.__retry = self._data['failures'];
self._lastReset = time if time is not None else self.getTime()
if not self._retry:
self._retry = self._data['failures'];
def setRetry(self, value):
""" Set artificial retry count, normally equal failures / attempt,
used in incremental features (BanTimeIncr) to increase retry count for bad IPs
"""
self.__retry = value
self._retry = value
if not self._data['failures']:
self._data['failures'] = 1
if not value:
@ -229,10 +239,10 @@ class FailTicket(Ticket):
""" Returns failures / attempt count or
artificial retry count increased for bad IPs
"""
return max(self.__retry, self._data['failures'])
return max(self._retry, self._data['failures'])
def inc(self, matches=None, attempt=1, count=1):
self.__retry += count
self._retry += count
self._data['failures'] += attempt
if matches:
# we should duplicate "matches", because possibly referenced to multiple tickets:
@ -249,15 +259,24 @@ class FailTicket(Ticket):
return self._time
def getLastReset(self):
return self.__lastReset
return self._lastReset
def setLastReset(self, value):
self.__lastReset = value
self._lastReset = value
@staticmethod
def wrap(o):
o.__class__ = FailTicket
return o
##
# Ban Ticket.
#
# This class extends the Ticket class. It is mainly used by the BanManager.
class BanTicket(Ticket):
pass
class BanTicket(FailTicket):
@staticmethod
def wrap(o):
o.__class__ = BanTicket
return o

View File

@ -102,7 +102,7 @@ class Utils():
def unset(self, k):
try:
del self._cache[k]
except KeyError: # pragme: no cover
except KeyError: # pragma: no cover
pass
@ -330,7 +330,7 @@ class Utils():
return e.errno == errno.EPERM
else:
return True
else: # pragma : no cover (no windows currently supported)
else: # pragma: no cover (no windows currently supported)
@staticmethod
def pid_exists(pid):
import ctypes

View File

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

View File

@ -100,21 +100,22 @@ class AddFailure(unittest.TestCase):
def testBanTimeIncr(self):
ticket = BanTicket(self.__ticket.getIP(), self.__ticket.getTime())
## increase twice and at end permanent:
## increase twice and at end permanent, check time/count increase:
c = 0
for i in (1000, 2000, -1):
self.__banManager.addBanTicket(self.__ticket)
self.__banManager.addBanTicket(self.__ticket); c += 1
ticket.setBanTime(i)
self.assertFalse(self.__banManager.addBanTicket(ticket))
self.assertFalse(self.__banManager.addBanTicket(ticket)); # no incr of c (already banned)
self.assertEqual(str(self.__banManager.getTicketByID(ticket.getIP())),
"BanTicket: ip=%s time=%s bantime=%s bancount=0 #attempts=0 matches=[]" % (ticket.getIP(), ticket.getTime(), i))
"BanTicket: ip=%s time=%s bantime=%s bancount=%s #attempts=0 matches=[]" % (ticket.getIP(), ticket.getTime(), i, c))
## after permanent, it should remain permanent ban time (-1):
self.__banManager.addBanTicket(self.__ticket)
self.__banManager.addBanTicket(self.__ticket); c += 1
ticket.setBanTime(-1)
self.assertFalse(self.__banManager.addBanTicket(ticket))
self.assertFalse(self.__banManager.addBanTicket(ticket)); # no incr of c (already banned)
ticket.setBanTime(1000)
self.assertFalse(self.__banManager.addBanTicket(ticket))
self.assertFalse(self.__banManager.addBanTicket(ticket)); # no incr of c (already banned)
self.assertEqual(str(self.__banManager.getTicketByID(ticket.getIP())),
"BanTicket: ip=%s time=%s bantime=%s bancount=0 #attempts=0 matches=[]" % (ticket.getIP(), ticket.getTime(), -1))
"BanTicket: ip=%s time=%s bantime=%s bancount=%s #attempts=0 matches=[]" % (ticket.getIP(), ticket.getTime(), -1, c))
def testUnban(self):
btime = self.__banManager.getBanTime()

View File

@ -566,8 +566,6 @@ class JailsReaderTest(LogCaptureTestCase):
# all must have some actionban defined
self.assertTrue(actionReader._opts.get('actionban', '').strip(),
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):
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.mytime import MyTime
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
@ -80,6 +81,11 @@ fail2banclient.output = \
fail2banserver.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
@ -89,6 +95,22 @@ def _observer_wait_idle():
Observers.Main.wait_empty(MID_WAITTIME)
Observers.Main.wait_idle(MID_WAITTIME / 5)
def _observer_wait_before_incrban(cond, timeout=MID_WAITTIME):
"""Helper to block observer before increase bantime until some condition gets true"""
if Observers.Main is not None:
# switch ban handler:
_obs_banFound = Observers.Main.banFound
def _banFound(*args, **kwargs):
# restore original handler:
Observers.Main.banFound = _obs_banFound
# wait for:
logSys.debug(' [Observer::banFound] *** observer blocked for test')
Utils.wait_for(cond, timeout)
logSys.debug(' [Observer::banFound] +++ observer runs again')
# original banFound:
_obs_banFound(*args, **kwargs)
Observers.Main.banFound = _banFound
#
# Mocking .exit so we could test its correct operation.
# Two custom exceptions will be assessed to be raised in the tests
@ -317,6 +339,7 @@ def with_foreground_server_thread(startextra={}):
# so don't kill (same process) - if success, just wait for end of worker:
if phase.get('end', None):
th.join()
tearDownMyTime()
return wrapper
return _deco_wrapper
@ -343,6 +366,7 @@ class Fail2banClientServerBase(LogCaptureTestCase):
server.DEF_LOGTARGET = SRV_DEF_LOGTARGET
server.DEF_LOGLEVEL = SRV_DEF_LOGLEVEL
LogCaptureTestCase.tearDown(self)
tearDownMyTime()
@staticmethod
def _test_exit(code=0):
@ -1158,3 +1182,106 @@ class Fail2banServerTest(Fail2banClientServerBase):
self.assertLogged(
"Jail 'test-jail1' stopped",
"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 %%s \"[%(name)s] %(actname)s: ++ ban <ip> -c <bancount> -t <bantime> : <F-MSG>\"", \
"actionprolong = printf %%s \"[%(name)s] %(actname)s: ++ prolong <ip> -c <bancount> -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:
_observer_wait_idle()
self.assertLogged(
"stdout: '[test-jail1] test-action1: ++ ban 192.0.2.11 -c 1 -t 300 : ",
"stdout: '[test-jail1] test-action2: ++ ban 192.0.2.11 -c 1 -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)
_observer_wait_idle()
self.assertLogged(
"stdout: '[test-jail1] test-action1: -- unban 192.0.2.11",
"stdout: '[test-jail1] test-action2: -- unban 192.0.2.11",
"0 ticket(s) in 'test-jail1'",
all=True, wait=MID_WAITTIME)
_observer_wait_idle()
self.pruneLog("[test-phase 2) time+10m]")
# following tests are time-related - observer can prolong ticket (increase ban-time)
# before banning, so block it here before banFound called, prolong case later:
wakeObs = False
_observer_wait_before_incrban(lambda: wakeObs)
# 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 -c 2 -t 300 : ",
"stdout: '[test-jail1] test-action2: ++ ban 192.0.2.11 -c 2 -t 300 : ",
all=True, wait=MID_WAITTIME)
# unblock observer here and wait it is done:
wakeObs = True
_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 -c 2 -t 600 : ",
all=True, wait=MID_WAITTIME)

View File

@ -31,9 +31,8 @@ import tempfile
import time
from ..server.mytime import MyTime
from ..server.ticket import FailTicket
from ..server.ticket import FailTicket, BanTicket
from ..server.failmanager import FailManager
from ..server.banmanager import BanManager
from ..server.observer import Observers, ObserverThread
from ..server.utils import Utils
from .utils import LogCaptureTestCase
@ -246,7 +245,6 @@ class BanTimeIncrDB(unittest.TestCase):
# incr time and ban a ticket again :
ticket.setTime(stime + 15)
self.assertEqual(self.incrBanTime(ticket, 10), 20)
ticket.incrBanCount()
self.db.addBan(jail, ticket)
# get a ticket already banned in this jail:
self.assertEqual(
@ -292,7 +290,6 @@ class BanTimeIncrDB(unittest.TestCase):
ticket.setTime(stime + lastBanTime + 5)
banTime = self.incrBanTime(ticket, 10)
self.assertEqual(banTime, lastBanTime * 2)
ticket.incrBanCount()
self.db.addBan(jail, ticket)
lastBanTime = banTime
# increase again, but the last multiplier reached (time not increased):
@ -300,7 +297,6 @@ class BanTimeIncrDB(unittest.TestCase):
banTime = self.incrBanTime(ticket, 10)
self.assertNotEqual(banTime, lastBanTime * 2)
self.assertEqual(banTime, lastBanTime)
ticket.incrBanCount()
self.db.addBan(jail, ticket)
lastBanTime = banTime
# add two tickets from yesterday: one unbanned (bantime already out-dated):
@ -500,7 +496,7 @@ class BanTimeIncrDB(unittest.TestCase):
# wrap FailTicket to BanTicket:
failticket2 = ticket2
ticket2 = BanManager.createBanTicket(failticket2)
ticket2 = BanTicket.wrap(failticket2)
self.assertEqual(ticket2, failticket2)
# add this ticket to ban (use observer only without ban manager):
obs.add('banFound', ticket2, jail, 10)

View File

@ -1065,8 +1065,20 @@ class ServerConfigReaderTests(LogCaptureTestCase):
logSys.debug(l)
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):
jails = server._Server__jails
aInfos = self._testActionInfos()
for jail in jails:
# print(jail, jails[jail])
for a in jails[jail].actions:
@ -1083,16 +1095,16 @@ class ServerConfigReaderTests(LogCaptureTestCase):
action.start()
# test ban ip4 :
logSys.debug('# === ban-ipv4 ==='); self.pruneLog()
action.ban({'ip': IPAddr('192.0.2.1'), 'family': 'inet4'})
action.ban(aInfos['ipv4'])
# test unban ip4 :
logSys.debug('# === unban ipv4 ==='); self.pruneLog()
action.unban({'ip': IPAddr('192.0.2.1'), 'family': 'inet4'})
action.unban(aInfos['ipv4'])
# test ban ip6 :
logSys.debug('# === ban ipv6 ==='); self.pruneLog()
action.ban({'ip': IPAddr('2001:DB8::'), 'family': 'inet6'})
action.ban(aInfos['ipv6'])
# test unban ip6 :
logSys.debug('# === unban ipv6 ==='); self.pruneLog()
action.unban({'ip': IPAddr('2001:DB8::'), 'family': 'inet6'})
action.unban(aInfos['ipv6'])
# test stop :
logSys.debug('# === stop ==='); self.pruneLog()
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"]', {
'ip4': (' f2b-j-w-iptables-ipset ',), 'ip6': (' f2b-j-w-iptables-ipset6 ',),
'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`",
),
'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`",
),
'flush': (
@ -1348,11 +1360,11 @@ class ServerConfigReaderTests(LogCaptureTestCase):
('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-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`",
),
'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`",
),
'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"]', {
'ip4': (' f2b-j-w-fwcmd-ipset ',), 'ip6': (' f2b-j-w-fwcmd-ipset6 ',),
'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`",
),
'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`",
),
'stop': (
@ -1695,10 +1707,7 @@ class ServerConfigReaderTests(LogCaptureTestCase):
jails = server._Server__jails
tickets = {
'ip4': BanTicket('192.0.2.1'),
'ip6': BanTicket('2001:DB8::'),
}
aInfos = self._testActionInfos()
for jail, act, tests in testJailsActions:
# print(jail, jails[jail])
for a in jails[jail].actions:
@ -1716,32 +1725,28 @@ class ServerConfigReaderTests(LogCaptureTestCase):
self.assertLogged(*tests['start'], all=True)
else:
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 :
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('ip6-start'): self.assertNotLogged(*tests['ip6-start'], all=True)
self.assertLogged(*tests['ip4-check']+tests['ip4-ban'], all=True)
self.assertNotLogged(*tests['ip6'], all=True)
# test unban ip4 :
self.pruneLog('# === unban ipv4 ===')
action.unban(ainfo['ip4'])
action.unban(aInfos['ipv4'])
self.assertLogged(*tests['ip4-check']+tests['ip4-unban'], all=True)
self.assertNotLogged(*tests['ip6'], all=True)
# test ban ip6 :
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('ip4-start'): self.assertNotLogged(*tests['ip4-start'], all=True)
self.assertLogged(*tests['ip6-check']+tests['ip6-ban'], all=True)
self.assertNotLogged(*tests['ip4'], all=True)
# test unban ip6 :
self.pruneLog('# === unban ipv6 ===')
action.unban(ainfo['ip6'])
action.unban(aInfos['ipv6'])
self.assertLogged(*tests['ip6-check']+tests['ip6-unban'], all=True)
self.assertNotLogged(*tests['ip4'], all=True)
# test flush for actions should supported this: