implements gh-2349: `fail2ban-client set jain banip/unbanip ip1 .. ipN` extended to ban/unban multiple tickets;

reorganized banning facilities (addBannedIP moved from filter to actions in order to ban directly without implication of fail-manager in between.
pull/2351/head
sebres 2019-02-20 14:56:00 +01:00
parent e30ebb1f3b
commit 84cec5e861
10 changed files with 84 additions and 66 deletions

View File

@ -99,8 +99,8 @@ protocol = [
["set <JAIL> bantime <TIME>", "sets the number of seconds <TIME> a host will be banned for <JAIL>"], ["set <JAIL> bantime <TIME>", "sets the number of seconds <TIME> a host will be banned for <JAIL>"],
["set <JAIL> datepattern <PATTERN>", "sets the <PATTERN> used to match date/times for <JAIL>"], ["set <JAIL> datepattern <PATTERN>", "sets the <PATTERN> used to match date/times for <JAIL>"],
["set <JAIL> usedns <VALUE>", "sets the usedns mode for <JAIL>"], ["set <JAIL> usedns <VALUE>", "sets the usedns mode for <JAIL>"],
["set <JAIL> banip <IP>", "manually Ban <IP> for <JAIL>"], ["set <JAIL> banip <IP> ... <IP>", "manually Ban <IP> for <JAIL>"],
["set <JAIL> unbanip <IP>", "manually Unban <IP> in <JAIL>"], ["set <JAIL> unbanip [--report-absent] <IP> ... <IP>", "manually Unban <IP> in <JAIL>"],
["set <JAIL> maxretry <RETRY>", "sets the number of failures <RETRY> before banning the host for <JAIL>"], ["set <JAIL> maxretry <RETRY>", "sets the number of failures <RETRY> before banning the host for <JAIL>"],
["set <JAIL> maxlines <LINES>", "sets the number of <LINES> to buffer for regex search for <JAIL>"], ["set <JAIL> maxlines <LINES>", "sets the number of <LINES> to buffer for regex search for <JAIL>"],
["set <JAIL> addaction <ACT>[ <PYTHONFILE> <JSONKWARGS>]", "adds a new action named <ACT> for <JAIL>. Optionally for a Python based action, a <PYTHONFILE> and <JSONKWARGS> can be specified, else will be a Command Action"], ["set <JAIL> addaction <ACT>[ <PYTHONFILE> <JSONKWARGS>]", "adds a new action named <ACT> for <JAIL>. Optionally for a Python based action, a <PYTHONFILE> and <JSONKWARGS> can be specified, else will be a Command Action"],

View File

@ -34,7 +34,8 @@ try:
except ImportError: except ImportError:
OrderedDict = dict OrderedDict = dict
from .banmanager import BanManager from .banmanager import BanManager, BanTicket
from .ipdns import IPAddr
from .jailthread import JailThread from .jailthread import JailThread
from .action import ActionBase, CommandAction, CallingMap from .action import ActionBase, CommandAction, CallingMap
from .mytime import MyTime from .mytime import MyTime
@ -203,6 +204,19 @@ class Actions(JailThread, Mapping):
def getBanTime(self): def getBanTime(self):
return self.__banManager.getBanTime() return self.__banManager.getBanTime()
def addBannedIP(self, ip):
"""Ban an IP or list of IPs."""
unixTime = MyTime.time()
if isinstance(ip, list):
# Multiple IPs:
tickets = (BanTicket(ip if isinstance(ip, IPAddr) else IPAddr(ip), unixTime) for ip in ip)
else:
# Single IP:
tickets = (BanTicket(ip if isinstance(ip, IPAddr) else IPAddr(ip), unixTime),)
return self.__checkBan(tickets)
def removeBannedIP(self, ip=None, db=True, ifexists=False): def removeBannedIP(self, ip=None, db=True, ifexists=False):
"""Removes banned IP calling actions' unban method """Removes banned IP calling actions' unban method
@ -211,8 +225,8 @@ class Actions(JailThread, Mapping):
Parameters Parameters
---------- ----------
ip : str or IPAddr or None ip : list, str, IPAddr or None
The IP address to unban or all IPs if None The IP address (or multiple IPs as list) to unban or all IPs if None
Raises Raises
------ ------
@ -222,6 +236,19 @@ class Actions(JailThread, Mapping):
# Unban all? # Unban all?
if ip is None: if ip is None:
return self.__flushBan(db) return self.__flushBan(db)
# Multiple IPs:
if isinstance(ip, list):
missed = []
cnt = 0
for i in ip:
try:
cnt += self.removeBannedIP(i, db, ifexists)
except ValueError:
if not ifexists:
missed.append(i)
if missed:
raise ValueError("not banned: %r" % missed)
return cnt
# Single IP: # Single IP:
# Always delete ip from database (also if currently not banned) # Always delete ip from database (also if currently not banned)
if db and self._jail.database is not None: if db and self._jail.database is not None:
@ -232,9 +259,11 @@ class Actions(JailThread, Mapping):
# Unban the IP. # Unban the IP.
self.__unBan(ticket) self.__unBan(ticket)
else: else:
msg = "%s is not banned" % ip
logSys.log(logging.MSG, msg)
if ifexists: if ifexists:
return 0 return 0
raise ValueError("%s is not banned" % ip) raise ValueError(msg)
return 1 return 1
@ -373,11 +402,20 @@ class Actions(JailThread, Mapping):
aInfo = Actions.ActionInfo(ticket, self._jail) aInfo = Actions.ActionInfo(ticket, self._jail)
return aInfo return aInfo
def __getFailTickets(self, count=100):
"""Generator to get maximal count failure tickets from fail-manager."""
cnt = 0
while cnt < count:
ticket = self._jail.getFailTicket()
if not ticket:
break
yield ticket
cnt += 1
def __checkBan(self): def __checkBan(self, tickets=None):
"""Check for IP address to ban. """Check for IP address to ban.
Look in the jail queue for FailTicket. If a ticket is available, If tickets are not specified look in the jail queue for FailTicket. If a ticket is available,
it executes the "ban" command and adds a ticket to the BanManager. it executes the "ban" command and adds a ticket to the BanManager.
Returns Returns
@ -386,10 +424,9 @@ class Actions(JailThread, Mapping):
True if an IP address get banned. True if an IP address get banned.
""" """
cnt = 0 cnt = 0
while cnt < 100: if not tickets:
ticket = self._jail.getFailTicket() tickets = self.__getFailTickets()
if not ticket: for ticket in tickets:
break
bTicket = BanManager.createBanTicket(ticket) bTicket = BanManager.createBanTicket(ticket)
ip = bTicket.getIP() ip = bTicket.getIP()
aInfo = self.__getActionInfo(bTicket) aInfo = self.__getActionInfo(bTicket)

View File

@ -427,31 +427,6 @@ class Filter(JailThread):
) )
else: else:
self.__ignoreCache = None self.__ignoreCache = None
##
# Ban an IP - http://blogs.buanzo.com.ar/2009/04/fail2ban-patch-ban-ip-address-manually.html
# Arturo 'Buanzo' Busleiman <buanzo@buanzo.com.ar>
#
# to enable banip fail2ban-client BAN command
def addBannedIP(self, ip):
if not isinstance(ip, IPAddr):
ip = IPAddr(ip)
unixTime = MyTime.time()
ticket = FailTicket(ip, unixTime)
if self._inIgnoreIPList(ip, ticket, log_ignore=False):
logSys.warning('Requested to manually ban an ignored IP %s. User knows best. Proceeding to ban it.', ip)
self.failManager.addFailure(ticket, self.failManager.getMaxRetry())
# Perform the banning of the IP now.
try: # pragma: no branch - exception is the only way out
while True:
ticket = self.failManager.toBan(ip)
self.jail.putFailTicket(ticket)
except FailManagerEmpty:
self.failManager.cleanup(MyTime.time())
return ip
## ##
# Ignore own IP/DNS. # Ignore own IP/DNS.

View File

@ -474,19 +474,20 @@ class Server:
self.__jails[name].actions.setBanTime(value) self.__jails[name].actions.setBanTime(value)
def setBanIP(self, name, value): def setBanIP(self, name, value):
return self.__jails[name].filter.addBannedIP(value) return self.__jails[name].actions.addBannedIP(value)
def setUnbanIP(self, name=None, value=None): def setUnbanIP(self, name=None, value=None, ifexists=True):
if name is not None: if name is not None:
# in all jails: # single jail:
jails = [self.__jails[name]] jails = [self.__jails[name]]
else: else:
# single jail: # in all jails:
jails = self.__jails.values() jails = self.__jails.values()
# unban given or all (if value is None): # unban given or all (if value is None):
cnt = 0 cnt = 0
ifexists |= (name is None)
for jail in jails: for jail in jails:
cnt += jail.actions.removeBannedIP(value, ifexists=(name is None)) cnt += jail.actions.removeBannedIP(value, ifexists=ifexists)
if value and not cnt: if value and not cnt:
logSys.info("%s is not banned", value) logSys.info("%s is not banned", value)
return cnt return cnt

View File

@ -109,10 +109,7 @@ class Transmitter:
# if all ips: # if all ips:
if len(value) == 1 and value[0] == "--all": if len(value) == 1 and value[0] == "--all":
return self.__server.setUnbanIP() return self.__server.setUnbanIP()
cnt = 0 return self.__server.setUnbanIP(None, value)
for value in value:
cnt += self.__server.setUnbanIP(None, value)
return cnt
elif command[0] == "echo": elif command[0] == "echo":
return command[1:] return command[1:]
elif command[0] == "server-status": elif command[0] == "server-status":
@ -286,12 +283,16 @@ class Transmitter:
self.__server.setBanTime(name, value) self.__server.setBanTime(name, value)
return self.__server.getBanTime(name) return self.__server.getBanTime(name)
elif command[1] == "banip": elif command[1] == "banip":
value = command[2] value = command[2:]
return self.__server.setBanIP(name,value) return self.__server.setBanIP(name,value)
elif command[1] == "unbanip": elif command[1] == "unbanip":
value = command[2] ifexists = True
self.__server.setUnbanIP(name, value) if command[2] != "--report-absent":
return value value = command[2:]
else:
ifexists = False
value = command[3:]
return self.__server.setUnbanIP(name, value, ifexists=ifexists)
elif command[1] == "addaction": elif command[1] == "addaction":
args = [command[2]] args = [command[2]]
if len(command) > 3: if len(command) > 3:

View File

@ -78,6 +78,16 @@ class ExecuteActions(LogCaptureTestCase):
self.assertEqual(self.__actions.getBanTime(),127) self.assertEqual(self.__actions.getBanTime(),127)
self.assertRaises(ValueError, self.__actions.removeBannedIP, '127.0.0.1') self.assertRaises(ValueError, self.__actions.removeBannedIP, '127.0.0.1')
def testAddBannedIP(self):
self.assertEqual(self.__actions.addBannedIP('192.0.2.1'), 1)
self.assertLogged('Ban 192.0.2.1')
self.pruneLog()
self.assertEqual(self.__actions.addBannedIP(['192.0.2.1', '192.0.2.2', '192.0.2.3']), 2)
self.assertLogged('192.0.2.1 already banned')
self.assertNotLogged('Ban 192.0.2.1')
self.assertLogged('Ban 192.0.2.2')
self.assertLogged('Ban 192.0.2.3')
def testActionsOutput(self): def testActionsOutput(self):
self.defaultActions() self.defaultActions()
self.__actions.start() self.__actions.start()

View File

@ -1121,7 +1121,7 @@ class Fail2banServerTest(Fail2banClientServerBase):
"--async", "unban", "192.0.2.5", "192.0.2.6") "--async", "unban", "192.0.2.5", "192.0.2.6")
self.assertLogged( self.assertLogged(
"192.0.2.5 is not banned", "192.0.2.5 is not banned",
"[test-jail1] Unban 192.0.2.6", all=True "[test-jail1] Unban 192.0.2.6", all=True, wait=MID_WAITTIME
) )
# reload all (one jail) with unban all: # reload all (one jail) with unban all:

View File

@ -394,12 +394,6 @@ class IgnoreIP(LogCaptureTestCase):
self.assertLogged('Ignore 192.168.1.32') self.assertLogged('Ignore 192.168.1.32')
tearDownMyTime() tearDownMyTime()
def testIgnoreAddBannedIP(self):
self.filter.addIgnoreIP('192.168.1.0/25')
self.filter.addBannedIP('192.168.1.32')
self.assertNotLogged('Ignore 192.168.1.32')
self.assertLogged('Requested to manually ban an ignored IP 192.168.1.32. User knows best. Proceeding to ban it.')
def testIgnoreCommand(self): def testIgnoreCommand(self):
self.filter.ignoreCommand = sys.executable + ' ' + os.path.join(TEST_FILES_DIR, "ignorecommand.py <ip>") self.filter.ignoreCommand = sys.executable + ' ' + os.path.join(TEST_FILES_DIR, "ignorecommand.py <ip>")
self.assertTrue(self.filter.inIgnoreIPList("10.0.0.1")) self.assertTrue(self.filter.inIgnoreIPList("10.0.0.1"))

View File

@ -330,22 +330,22 @@ class Transmitter(TransmitterBase):
self.server.startJail(self.jailName) # Jail must be started self.server.startJail(self.jailName) # Jail must be started
self.assertEqual( self.assertEqual(
self.transm.proceed(["set", self.jailName, "banip", "127.0.0.1"]), self.transm.proceed(["set", self.jailName, "banip", "192.0.2.1", "192.0.2.1", "192.0.2.2"]),
(0, "127.0.0.1")) (0, 2))
self.assertLogged("Ban 127.0.0.1", wait=True) # Give chance to ban self.assertLogged("Ban 192.0.2.1", "Ban 192.0.2.2", all=True, wait=True) # Give chance to ban
self.assertEqual( self.assertEqual(
self.transm.proceed(["set", self.jailName, "banip", "Badger"]), self.transm.proceed(["set", self.jailName, "banip", "Badger"]),
(0, "Badger")) #NOTE: Is IP address validated? Is DNS Lookup done? (0, 1)) #NOTE: Is IP address validated? Is DNS Lookup done?
self.assertLogged("Ban Badger", wait=True) # Give chance to ban self.assertLogged("Ban Badger", wait=True) # Give chance to ban
# Unban IP # Unban IP
self.assertEqual( self.assertEqual(
self.transm.proceed( self.transm.proceed(
["set", self.jailName, "unbanip", "127.0.0.1"]), ["set", self.jailName, "unbanip", "192.0.2.255", "192.0.2.1", "192.0.2.2"]),
(0, "127.0.0.1")) (0, 2))
# Unban IP which isn't banned # Unban IP which isn't banned
self.assertEqual( self.assertEqual(
self.transm.proceed( self.transm.proceed(
["set", self.jailName, "unbanip", "192.168.1.1"])[0],1) ["set", self.jailName, "unbanip", "--report-absent", "192.0.2.255"])[0],1)
def testJailMaxRetry(self): def testJailMaxRetry(self):
self.setGetTest("maxretry", "5", 5, jail=self.jailName) self.setGetTest("maxretry", "5", 5, jail=self.jailName)

View File

@ -272,10 +272,10 @@ date/times for <JAIL>
\fBset <JAIL> usedns <VALUE>\fR \fBset <JAIL> usedns <VALUE>\fR
sets the usedns mode for <JAIL> sets the usedns mode for <JAIL>
.TP .TP
\fBset <JAIL> banip <IP>\fR \fBset <JAIL> banip <IP> ... <IP>\fR
manually Ban <IP> for <JAIL> manually Ban <IP> for <JAIL>
.TP .TP
\fBset <JAIL> unbanip <IP>\fR \fBset <JAIL> unbanip [\-\-report\-absent] <IP> ... <IP>\fR
manually Unban <IP> in <JAIL> manually Unban <IP> in <JAIL>
.TP .TP
\fBset <JAIL> maxretry <RETRY>\fR \fBset <JAIL> maxretry <RETRY>\fR