diff --git a/ChangeLog b/ChangeLog index b9efbc10..20a37d4a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -56,6 +56,7 @@ ver. 0.11.0-dev-0 (20??/??/??) - development nightly edition end of ban) of the ticket with ban-time of jail (as maximum), for all tickets with ban-time greater (or persistent); not affected if ban-time of the jail is unchanged between stop/start. * added new setup-option `--without-tests` to skip building and installing of tests files (gh-2287). +* added new command `fail2ban-client get banip ?sep-char|--with-time?` to get the banned ip addresses (gh-1916). ver. 0.10.4-dev-1 (20??/??/??) - development edition diff --git a/fail2ban/client/beautifier.py b/fail2ban/client/beautifier.py index 4d9e549f..97cd38b2 100644 --- a/fail2ban/client/beautifier.py +++ b/fail2ban/client/beautifier.py @@ -180,6 +180,12 @@ class Beautifier: msg = "The jail %s action %s has the following " \ "methods:\n" % (inC[1], inC[3]) msg += ", ".join(response) + elif inC[2] == "banip" and inC[0] == "get": + if isinstance(response, list): + sep = " " if len(inC) <= 3 else inC[3] + if sep == "--with-time": + sep = "\n" + msg = sep.join(response) except Exception: logSys.warning("Beautifier error. Please report the error") logSys.error("Beautify %r with %r failed", response, self.__inputCmd, diff --git a/fail2ban/protocol.py b/fail2ban/protocol.py index b21ab848..f9ec5b71 100644 --- a/fail2ban/protocol.py +++ b/fail2ban/protocol.py @@ -128,6 +128,7 @@ protocol = [ ["get bantime", "gets the time a host is banned for "], ["get datepattern", "gets the patern used to match date/times for "], ["get usedns", "gets the usedns setting for "], +["get banip [|--with-time]", "gets the list of of banned IP addresses for . Optionally the separator character ('', default is space) or the option '--with-time' (printing the times of ban) may be specified. The IPs are ordered by end of ban."], ["get maxretry", "gets the number of failures allowed for "], ["get maxlines", "gets the number of lines to buffer for "], ["get actions", "gets a list of actions for "], diff --git a/fail2ban/server/actions.py b/fail2ban/server/actions.py index 3d862275..3a92dcda 100644 --- a/fail2ban/server/actions.py +++ b/fail2ban/server/actions.py @@ -204,6 +204,16 @@ class Actions(JailThread, Mapping): def getBanTime(self): return self.__banManager.getBanTime() + def getBanList(self, withTime=False): + """Returns the list of banned IP addresses. + + Returns + ------- + list + The list of banned IP addresses. + """ + return self.__banManager.getBanList(ordered=True, withTime=withTime) + def removeBannedIP(self, ip=None, db=True, ifexists=False): """Removes banned IP calling actions' unban method diff --git a/fail2ban/server/banmanager.py b/fail2ban/server/banmanager.py index 1340fb52..ffbcf766 100644 --- a/fail2ban/server/banmanager.py +++ b/fail2ban/server/banmanager.py @@ -102,9 +102,22 @@ class BanManager: # # @return IP list - def getBanList(self): + def getBanList(self, ordered=False, withTime=False): with self.__lock: - return self.__banList.keys() + if not ordered: + return self.__banList.keys() + lst = [] + for ticket in self.__banList.itervalues(): + eob = ticket.getEndOfBanTime(self.__banTime) + lst.append((ticket,eob)) + lst.sort(key=lambda t: t[1]) + t2s = MyTime.time2str + if withTime: + return ['%s \t%s + %d = %s' % ( + t[0].getID(), + t2s(t[0].getTime()), t[0].getBanTime(self.__banTime), t2s(t[1]) + ) for t in lst] + return [t[0].getID() for t in lst] ## # Returns a iterator to ban list (used in reload, so idle). diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index 9265a58b..001b35b7 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -602,7 +602,7 @@ class Filter(JailThread): if self._inIgnoreIPList(ip, tick): continue logSys.info( - "[%s] Found %s - %s", self.jailName, ip, datetime.datetime.fromtimestamp(unixTime).strftime("%Y-%m-%d %H:%M:%S") + "[%s] Found %s - %s", self.jailName, ip, MyTime.time2str(unixTime) ) self.failManager.addFailure(tick) # report to observer - failure was found, for possibly increasing of it retry counter (asynchronous) @@ -1092,7 +1092,7 @@ class FileFilter(Filter): fs = container.getFileSize() if logSys.getEffectiveLevel() <= logging.DEBUG: logSys.debug("Seek to find time %s (%s), file size %s", date, - datetime.datetime.fromtimestamp(date).strftime("%Y-%m-%d %H:%M:%S"), fs) + MyTime.time2str(date), fs) minp = container.getPos() maxp = fs tryPos = minp @@ -1171,7 +1171,7 @@ class FileFilter(Filter): container.setPos(foundPos) if logSys.getEffectiveLevel() <= logging.DEBUG: logSys.debug("Position %s from %s, found time %s (%s) within %s seeks", lastPos, fs, foundTime, - (datetime.datetime.fromtimestamp(foundTime).strftime("%Y-%m-%d %H:%M:%S") if foundTime is not None else ''), cntr) + (MyTime.time2str(foundTime) if foundTime is not None else ''), cntr) def status(self, flavor="basic"): """Status of Filter plus files being monitored. diff --git a/fail2ban/server/mytime.py b/fail2ban/server/mytime.py index 49199887..98b69bd4 100644 --- a/fail2ban/server/mytime.py +++ b/fail2ban/server/mytime.py @@ -113,6 +113,16 @@ class MyTime: return time.localtime(x) else: return time.localtime(MyTime.myTime) + + @staticmethod + def time2str(unixTime, format="%Y-%m-%d %H:%M:%S"): + """Convert time to a string representing as date and time using given format. + Default format is ISO 8601, YYYY-MM-DD HH:MM:SS without microseconds. + + @return ISO-capable string representation of given unixTime + """ + return datetime.datetime.fromtimestamp( + unixTime).replace(microsecond=0).strftime(format) ## precreate/precompile primitives used in str2seconds: diff --git a/fail2ban/server/observer.py b/fail2ban/server/observer.py index c3fa7d54..ffeeec71 100644 --- a/fail2ban/server/observer.py +++ b/fail2ban/server/observer.py @@ -393,7 +393,7 @@ class ObserverThread(JailThread): return # retry counter was increased - add it again: logSys.info("[%s] Found %s, bad - %s, %s # -> %s%s", jail.name, ip, - datetime.datetime.fromtimestamp(unixTime).strftime("%Y-%m-%d %H:%M:%S"), banCount, retryCount, + MyTime.time2str(unixTime), banCount, retryCount, (', Ban' if retryCount >= maxRetry else '')) # retryCount-1, because a ticket was already once incremented by filter self retryCount = failManager.addFailure(ticket, retryCount - 1, True) @@ -454,7 +454,7 @@ class ObserverThread(JailThread): # 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, - datetime.datetime.fromtimestamp(timeOfBan).strftime("%Y-%m-%d %H:%M:%S"), + MyTime.time2str(timeOfBan), datetime.timedelta(seconds=int(orgBanTime)), datetime.timedelta(seconds=int(banTime)))); else: ticket.restored = True @@ -485,7 +485,7 @@ class ObserverThread(JailThread): if btime != -1: bendtime = ticket.getTime() + btime logtime = (datetime.timedelta(seconds=int(btime)), - datetime.datetime.fromtimestamp(bendtime).strftime("%Y-%m-%d %H:%M:%S")) + MyTime.time2str(bendtime)) # check ban is not too old : if bendtime < MyTime.time(): logSys.debug('Ignore old bantime %s', logtime[1]) diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py index dfbbd5d7..9cc17b5b 100644 --- a/fail2ban/server/server.py +++ b/fail2ban/server/server.py @@ -510,6 +510,21 @@ class Server: def getBanTime(self, name): return self.__jails[name].actions.getBanTime() + def getBanList(self, name, withTime=False): + """Returns the list of banned IP addresses for a jail. + + Parameters + ---------- + name : str + The name of a jail. + + Returns + ------- + list + The list of banned IP addresses. + """ + return self.__jails[name].actions.getBanList(withTime) + def setBanTimeExtra(self, name, opt, value): self.__jails[name].setBanTimeExtra(opt, value) diff --git a/fail2ban/server/transmitter.py b/fail2ban/server/transmitter.py index c24408c4..0c0cfba8 100644 --- a/fail2ban/server/transmitter.py +++ b/fail2ban/server/transmitter.py @@ -390,6 +390,9 @@ class Transmitter: # Action elif command[1] == "bantime": return self.__server.getBanTime(name) + elif command[1] == "banip": + return self.__server.getBanList(name, + withTime=len(command) > 2 and command[2] == "--with-time") elif command[1].startswith("bantime."): opt = command[1][len("bantime."):] return self.__server.getBanTimeExtra(name, opt) diff --git a/fail2ban/tests/fail2banclienttestcase.py b/fail2ban/tests/fail2banclienttestcase.py index c120128b..4480c71c 100644 --- a/fail2ban/tests/fail2banclienttestcase.py +++ b/fail2ban/tests/fail2banclienttestcase.py @@ -1064,6 +1064,17 @@ class Fail2banServerTest(Fail2banClientServerBase): "stdout: '[test-jail2] test-action3: ++ ban 192.0.2.22", "stdout: '[test-jail2] test-action3: ++ ban 192.0.2.22 ", all=True, wait=MID_WAITTIME) + # get banned ips: + _observer_wait_idle() + self.pruneLog("[test-phase 2d.1]") + self.execCmd(SUCCESS, startparams, "get", "test-jail2", "banip", "\n") + self.assertLogged( + "192.0.2.4", "192.0.2.8", "192.0.2.21", "192.0.2.22", all=True, wait=MID_WAITTIME) + self.pruneLog("[test-phase 2d.2]") + self.execCmd(SUCCESS, startparams, "get", "test-jail1", "banip") + self.assertLogged( + "192.0.2.1", "192.0.2.2", "192.0.2.3", "192.0.2.4", "192.0.2.8", all=True, wait=MID_WAITTIME) + # restart jail with unban all: self.pruneLog("[test-phase 2e]") self.execCmd(SUCCESS, startparams, @@ -1397,6 +1408,11 @@ class Fail2banServerTest(Fail2banClientServerBase): "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) + # get banned ips with time: + self.pruneLog("[test-phase 2) time+10m - get-ips]") + self.execCmd(SUCCESS, startparams, "get", "test-jail1", "banip", "--with-time") + self.assertLogged( + "192.0.2.11", "+ 300 =", all=True, wait=MID_WAITTIME) # unblock observer here and wait it is done: wakeObs = True _observer_wait_idle() @@ -1411,6 +1427,13 @@ class Fail2banServerTest(Fail2banClientServerBase): "stdout: '[test-jail1] test-action2: ++ prolong 192.0.2.11 -c 2 -t 600 : ", all=True, wait=MID_WAITTIME) + # get banned ips with time: + _observer_wait_idle() + self.pruneLog("[test-phase 2) time+11m - get-ips]") + self.execCmd(SUCCESS, startparams, "get", "test-jail1", "banip", "--with-time") + self.assertLogged( + "192.0.2.11", "+ 600 =", all=True, wait=MID_WAITTIME) + # test multiple start/stop of the server (threaded in foreground) -- if False: # pragma: no cover @with_foreground_server_thread() diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index b22cd0f8..cae1e173 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -94,7 +94,7 @@ class _tmSerial(): @staticmethod def _tm(time): # ## strftime it too slow for large time serializer : - # return datetime.datetime.fromtimestamp(time).strftime("%Y-%m-%d %H:%M:%S") + # return MyTime.time2str(time) c = _tmSerial sec = (time % 60) if c._last_s == time - sec: @@ -306,7 +306,7 @@ class BasicFilter(unittest.TestCase): unittest.F2B.SkipIfFast() ## test function "_tm" works correct (returns the same as slow strftime): for i in xrange(1417512352, (1417512352 // 3600 + 3) * 3600): - tm = datetime.datetime.fromtimestamp(i).strftime("%Y-%m-%d %H:%M:%S") + tm = MyTime.time2str(i) if _tm(i) != tm: # pragma: no cover - never reachable self.assertEqual((_tm(i), i), (tm, i)) diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index 8b616abc..166cd438 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -347,6 +347,46 @@ class Transmitter(TransmitterBase): self.transm.proceed( ["set", self.jailName, "unbanip", "192.168.1.1"])[0],1) + def testJailBanList(self): + jail = "TestJailBanList" + self.server.addJail(jail, FAST_BACKEND) + self.server.startJail(jail) + + # Helper to process set banip/set unbanip commands and compare the list of + # banned IP addresses with outList. + def _getBanListTest(jail, banip=None, unbanip=None, outList=[]): + # Ban IP address + if banip is not None: + self.assertEqual( + self.transm.proceed(["set", jail, "banip", banip]), + (0, banip)) + self.assertLogged("Ban %s" % banip, wait=True) # Give chance to ban + # Unban IP address + if unbanip is not None: + self.assertEqual( + self.transm.proceed(["set", jail, "unbanip", unbanip]), + (0, unbanip)) + self.assertLogged("Unban %s" % unbanip, wait=True) # Give chance to unban + # Compare the list of banned IP addresses with outList + self.assertSortedEqual( + self.transm.proceed(["get", jail, "banip"]), + (0, outList)) + + _getBanListTest(jail, + outList=[]) + _getBanListTest(jail, banip="127.0.0.1", + outList=["127.0.0.1"]) + _getBanListTest(jail, banip="192.168.0.1", + outList=["127.0.0.1", "192.168.0.1"]) + _getBanListTest(jail, banip="192.168.1.10", + outList=["127.0.0.1", "192.168.0.1", "192.168.1.10"]) + _getBanListTest(jail, unbanip="127.0.0.1", + outList=["192.168.0.1", "192.168.1.10"]) + _getBanListTest(jail, unbanip="192.168.1.10", + outList=["192.168.0.1"]) + _getBanListTest(jail, unbanip="192.168.0.1", + outList=[]) + def testJailMaxRetry(self): self.setGetTest("maxretry", "5", 5, jail=self.jailName) self.setGetTest("maxretry", "2", 2, jail=self.jailName) diff --git a/man/fail2ban-client.1 b/man/fail2ban-client.1 index d1226d56..af2af054 100644 --- a/man/fail2ban-client.1 +++ b/man/fail2ban-client.1 @@ -1,5 +1,5 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. -.TH FAIL2BAN-CLIENT "1" "October 2018" "fail2ban-client v0.11.0.dev3" "User Commands" +.TH FAIL2BAN-CLIENT "1" "January 2019" "fail2ban-client v0.11.0.dev3" "User Commands" .SH NAME fail2ban-client \- configure and control the server .SH SYNOPSIS @@ -386,6 +386,15 @@ date/times for \fBget usedns\fR gets the usedns setting for .TP +\fBget banip [|\-\-with\-time]\fR +gets the list of of banned IP +addresses for . Optionally +the separator character ('', +default is space) or the option +\&'\-\-with\-time' (printing the times +of ban) may be specified. The IPs +are ordered by end of ban. +.TP \fBget maxretry\fR gets the number of failures allowed for