mirror of https://github.com/fail2ban/fail2ban
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
parent
e30ebb1f3b
commit
84cec5e861
|
@ -99,8 +99,8 @@ protocol = [
|
|||
["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> usedns <VALUE>", "sets the usedns mode for <JAIL>"],
|
||||
["set <JAIL> banip <IP>", "manually Ban <IP> for <JAIL>"],
|
||||
["set <JAIL> unbanip <IP>", "manually Unban <IP> in <JAIL>"],
|
||||
["set <JAIL> banip <IP> ... <IP>", "manually Ban <IP> for <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> 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"],
|
||||
|
|
|
@ -34,7 +34,8 @@ try:
|
|||
except ImportError:
|
||||
OrderedDict = dict
|
||||
|
||||
from .banmanager import BanManager
|
||||
from .banmanager import BanManager, BanTicket
|
||||
from .ipdns import IPAddr
|
||||
from .jailthread import JailThread
|
||||
from .action import ActionBase, CommandAction, CallingMap
|
||||
from .mytime import MyTime
|
||||
|
@ -203,6 +204,19 @@ class Actions(JailThread, Mapping):
|
|||
def getBanTime(self):
|
||||
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):
|
||||
"""Removes banned IP calling actions' unban method
|
||||
|
||||
|
@ -211,8 +225,8 @@ class Actions(JailThread, Mapping):
|
|||
|
||||
Parameters
|
||||
----------
|
||||
ip : str or IPAddr or None
|
||||
The IP address to unban or all IPs if None
|
||||
ip : list, str, IPAddr or None
|
||||
The IP address (or multiple IPs as list) to unban or all IPs if None
|
||||
|
||||
Raises
|
||||
------
|
||||
|
@ -222,6 +236,19 @@ class Actions(JailThread, Mapping):
|
|||
# Unban all?
|
||||
if ip is None:
|
||||
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:
|
||||
# Always delete ip from database (also if currently not banned)
|
||||
if db and self._jail.database is not None:
|
||||
|
@ -232,9 +259,11 @@ class Actions(JailThread, Mapping):
|
|||
# Unban the IP.
|
||||
self.__unBan(ticket)
|
||||
else:
|
||||
msg = "%s is not banned" % ip
|
||||
logSys.log(logging.MSG, msg)
|
||||
if ifexists:
|
||||
return 0
|
||||
raise ValueError("%s is not banned" % ip)
|
||||
raise ValueError(msg)
|
||||
return 1
|
||||
|
||||
|
||||
|
@ -373,11 +402,20 @@ class Actions(JailThread, Mapping):
|
|||
aInfo = Actions.ActionInfo(ticket, self._jail)
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Returns
|
||||
|
@ -386,10 +424,9 @@ class Actions(JailThread, Mapping):
|
|||
True if an IP address get banned.
|
||||
"""
|
||||
cnt = 0
|
||||
while cnt < 100:
|
||||
ticket = self._jail.getFailTicket()
|
||||
if not ticket:
|
||||
break
|
||||
if not tickets:
|
||||
tickets = self.__getFailTickets()
|
||||
for ticket in tickets:
|
||||
bTicket = BanManager.createBanTicket(ticket)
|
||||
ip = bTicket.getIP()
|
||||
aInfo = self.__getActionInfo(bTicket)
|
||||
|
|
|
@ -427,31 +427,6 @@ class Filter(JailThread):
|
|||
)
|
||||
else:
|
||||
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.
|
||||
|
|
|
@ -474,19 +474,20 @@ class Server:
|
|||
self.__jails[name].actions.setBanTime(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:
|
||||
# in all jails:
|
||||
# single jail:
|
||||
jails = [self.__jails[name]]
|
||||
else:
|
||||
# single jail:
|
||||
# in all jails:
|
||||
jails = self.__jails.values()
|
||||
# unban given or all (if value is None):
|
||||
cnt = 0
|
||||
ifexists |= (name is None)
|
||||
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:
|
||||
logSys.info("%s is not banned", value)
|
||||
return cnt
|
||||
|
|
|
@ -109,10 +109,7 @@ class Transmitter:
|
|||
# if all ips:
|
||||
if len(value) == 1 and value[0] == "--all":
|
||||
return self.__server.setUnbanIP()
|
||||
cnt = 0
|
||||
for value in value:
|
||||
cnt += self.__server.setUnbanIP(None, value)
|
||||
return cnt
|
||||
return self.__server.setUnbanIP(None, value)
|
||||
elif command[0] == "echo":
|
||||
return command[1:]
|
||||
elif command[0] == "server-status":
|
||||
|
@ -286,12 +283,16 @@ class Transmitter:
|
|||
self.__server.setBanTime(name, value)
|
||||
return self.__server.getBanTime(name)
|
||||
elif command[1] == "banip":
|
||||
value = command[2]
|
||||
value = command[2:]
|
||||
return self.__server.setBanIP(name,value)
|
||||
elif command[1] == "unbanip":
|
||||
value = command[2]
|
||||
self.__server.setUnbanIP(name, value)
|
||||
return value
|
||||
ifexists = True
|
||||
if command[2] != "--report-absent":
|
||||
value = command[2:]
|
||||
else:
|
||||
ifexists = False
|
||||
value = command[3:]
|
||||
return self.__server.setUnbanIP(name, value, ifexists=ifexists)
|
||||
elif command[1] == "addaction":
|
||||
args = [command[2]]
|
||||
if len(command) > 3:
|
||||
|
|
|
@ -78,6 +78,16 @@ class ExecuteActions(LogCaptureTestCase):
|
|||
self.assertEqual(self.__actions.getBanTime(),127)
|
||||
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):
|
||||
self.defaultActions()
|
||||
self.__actions.start()
|
||||
|
|
|
@ -1121,7 +1121,7 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
|||
"--async", "unban", "192.0.2.5", "192.0.2.6")
|
||||
self.assertLogged(
|
||||
"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:
|
||||
|
|
|
@ -394,12 +394,6 @@ class IgnoreIP(LogCaptureTestCase):
|
|||
self.assertLogged('Ignore 192.168.1.32')
|
||||
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):
|
||||
self.filter.ignoreCommand = sys.executable + ' ' + os.path.join(TEST_FILES_DIR, "ignorecommand.py <ip>")
|
||||
self.assertTrue(self.filter.inIgnoreIPList("10.0.0.1"))
|
||||
|
|
|
@ -330,22 +330,22 @@ class Transmitter(TransmitterBase):
|
|||
self.server.startJail(self.jailName) # Jail must be started
|
||||
|
||||
self.assertEqual(
|
||||
self.transm.proceed(["set", self.jailName, "banip", "127.0.0.1"]),
|
||||
(0, "127.0.0.1"))
|
||||
self.assertLogged("Ban 127.0.0.1", wait=True) # Give chance to ban
|
||||
self.transm.proceed(["set", self.jailName, "banip", "192.0.2.1", "192.0.2.1", "192.0.2.2"]),
|
||||
(0, 2))
|
||||
self.assertLogged("Ban 192.0.2.1", "Ban 192.0.2.2", all=True, wait=True) # Give chance to ban
|
||||
self.assertEqual(
|
||||
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
|
||||
# Unban IP
|
||||
self.assertEqual(
|
||||
self.transm.proceed(
|
||||
["set", self.jailName, "unbanip", "127.0.0.1"]),
|
||||
(0, "127.0.0.1"))
|
||||
["set", self.jailName, "unbanip", "192.0.2.255", "192.0.2.1", "192.0.2.2"]),
|
||||
(0, 2))
|
||||
# Unban IP which isn't banned
|
||||
self.assertEqual(
|
||||
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):
|
||||
self.setGetTest("maxretry", "5", 5, jail=self.jailName)
|
||||
|
|
|
@ -272,10 +272,10 @@ date/times for <JAIL>
|
|||
\fBset <JAIL> usedns <VALUE>\fR
|
||||
sets the usedns mode for <JAIL>
|
||||
.TP
|
||||
\fBset <JAIL> banip <IP>\fR
|
||||
\fBset <JAIL> banip <IP> ... <IP>\fR
|
||||
manually Ban <IP> for <JAIL>
|
||||
.TP
|
||||
\fBset <JAIL> unbanip <IP>\fR
|
||||
\fBset <JAIL> unbanip [\-\-report\-absent] <IP> ... <IP>\fR
|
||||
manually Unban <IP> in <JAIL>
|
||||
.TP
|
||||
\fBset <JAIL> maxretry <RETRY>\fR
|
||||
|
|
Loading…
Reference in New Issue