"Fossies" - the Fresh Open Source Software Archive

Member "fail2ban-0.11.1/fail2ban/server/banmanager.py" (11 Jan 2020, 10987 Bytes) of package /linux/misc/fail2ban-0.11.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "banmanager.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 0.10.5_vs_0.11.1.

    1 # emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
    2 # vi: set ft=python sts=4 ts=4 sw=4 noet :
    3 
    4 # This file is part of Fail2Ban.
    5 #
    6 # Fail2Ban is free software; you can redistribute it and/or modify
    7 # it under the terms of the GNU General Public License as published by
    8 # the Free Software Foundation; either version 2 of the License, or
    9 # (at your option) any later version.
   10 #
   11 # Fail2Ban is distributed in the hope that it will be useful,
   12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
   13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   14 # GNU General Public License for more details.
   15 #
   16 # You should have received a copy of the GNU General Public License
   17 # along with Fail2Ban; if not, write to the Free Software
   18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
   19 
   20 # Author: Cyril Jaquier
   21 # 
   22 
   23 __author__ = "Cyril Jaquier"
   24 __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
   25 __license__ = "GPL"
   26 
   27 from threading import Lock
   28 
   29 from .ticket import BanTicket
   30 from .mytime import MyTime
   31 from ..helpers import getLogger, logging
   32 
   33 # Gets the instance of the logger.
   34 logSys = getLogger(__name__)
   35 
   36 
   37 ##
   38 # Banning Manager.
   39 #
   40 # Manage the banned IP addresses. Convert FailTicket to BanTicket.
   41 # This class is mainly used by the Action class.
   42 
   43 class BanManager:
   44     
   45     ##
   46     # Constructor.
   47     #
   48     # Initialize members with default values.
   49     
   50     def __init__(self):
   51         ## Mutex used to protect the ban list.
   52         self.__lock = Lock()
   53         ## The ban list.
   54         self.__banList = dict()
   55         ## The amount of time an IP address gets banned.
   56         self.__banTime = 600
   57         ## Total number of banned IP address
   58         self.__banTotal = 0
   59         ## The time for next unban process (for performance and load reasons):
   60         self.__nextUnbanTime = BanTicket.MAX_TIME
   61     
   62     ##
   63     # Set the ban time.
   64     #
   65     # Set the amount of time an IP address get banned.
   66     # @param value the time
   67     
   68     def setBanTime(self, value):
   69         with self.__lock:
   70             self.__banTime = int(value)
   71     
   72     ##
   73     # Get the ban time.
   74     #
   75     # Get the amount of time an IP address get banned.
   76     # @return the time
   77     
   78     def getBanTime(self):
   79         with self.__lock:
   80             return self.__banTime
   81     
   82     ##
   83     # Set the total number of banned address.
   84     #
   85     # @param value total number
   86     
   87     def setBanTotal(self, value):
   88         with self.__lock:
   89             self.__banTotal = value
   90     
   91     ##
   92     # Get the total number of banned address.
   93     #
   94     # @return the total number
   95     
   96     def getBanTotal(self):
   97         with self.__lock:
   98             return self.__banTotal
   99 
  100     ##
  101     # Returns a copy of the IP list.
  102     #
  103     # @return IP list
  104     
  105     def getBanList(self, ordered=False, withTime=False):
  106         with self.__lock:
  107             if not ordered:
  108                 return self.__banList.keys()
  109             lst = []
  110             for ticket in self.__banList.itervalues():
  111                 eob = ticket.getEndOfBanTime(self.__banTime)
  112                 lst.append((ticket,eob))
  113             lst.sort(key=lambda t: t[1])
  114             t2s = MyTime.time2str
  115             if withTime:
  116                 return ['%s \t%s + %d = %s' % (
  117                         t[0].getID(), 
  118                         t2s(t[0].getTime()), t[0].getBanTime(self.__banTime), t2s(t[1])
  119                     ) for t in lst]
  120             return [t[0].getID() for t in lst]
  121 
  122     ##
  123     # Returns a iterator to ban list (used in reload, so idle).
  124     #
  125     # @return ban list iterator
  126     
  127     def __iter__(self):
  128         with self.__lock:
  129             return self.__banList.itervalues()
  130 
  131     ##
  132     # Returns normalized value
  133     #
  134     # @return value or "unknown" if value is None or empty string
  135 
  136     @staticmethod
  137     def handleBlankResult(value):
  138         if value is None or len(value) == 0:
  139             return "unknown"
  140         else:
  141             return value
  142 
  143     ##
  144     # Returns Cymru DNS query information
  145     #
  146     # @return {"asn": [], "country": [], "rir": []} dict for self.__banList IPs
  147 
  148     def getBanListExtendedCymruInfo(self, timeout=10):
  149         return_dict = {"asn": [], "country": [], "rir": []}
  150         if not hasattr(self, 'dnsResolver'):
  151             global dns
  152             try:
  153                 import dns.exception
  154                 import dns.resolver
  155                 resolver = dns.resolver.Resolver()
  156                 resolver.lifetime = timeout
  157                 resolver.timeout = timeout / 2
  158                 self.dnsResolver = resolver
  159             except ImportError as e: # pragma: no cover
  160                 logSys.error("dnspython package is required but could not be imported")
  161                 return_dict["error"] = repr(e)
  162                 return_dict["asn"].append("error")
  163                 return_dict["country"].append("error")
  164                 return_dict["rir"].append("error")
  165                 return return_dict
  166         # get ips in lock:
  167         with self.__lock:
  168             banIPs = [banData.getIP() for banData in self.__banList.values()]
  169         # get cymru info:
  170         try:
  171             for ip in banIPs:
  172                 # Reference: https://www.team-cymru.com/IP-ASN-mapping.html#dns
  173                 question = ip.getPTR(
  174                     "origin.asn.cymru.com" if ip.isIPv4
  175                     else "origin6.asn.cymru.com"
  176                 )
  177                 try:
  178                     resolver = self.dnsResolver
  179                     answers = resolver.query(question, "TXT")
  180                     if not answers:
  181                         raise ValueError("No data retrieved")
  182                     asns = set()
  183                     countries = set()
  184                     rirs = set()
  185                     for rdata in answers:
  186                         asn, net, country, rir, changed =\
  187                             [answer.strip("'\" ") for answer in rdata.to_text().split("|")]
  188                         asn = self.handleBlankResult(asn)
  189                         country = self.handleBlankResult(country)
  190                         rir = self.handleBlankResult(rir)
  191                         asns.add(self.handleBlankResult(asn))
  192                         countries.add(self.handleBlankResult(country))
  193                         rirs.add(self.handleBlankResult(rir))
  194                     return_dict["asn"].append(', '.join(sorted(asns)))
  195                     return_dict["country"].append(', '.join(sorted(countries)))
  196                     return_dict["rir"].append(', '.join(sorted(rirs)))
  197                 except dns.resolver.NXDOMAIN:
  198                     return_dict["asn"].append("nxdomain")
  199                     return_dict["country"].append("nxdomain")
  200                     return_dict["rir"].append("nxdomain")
  201                 except (dns.exception.DNSException, dns.resolver.NoNameservers, dns.exception.Timeout) as dnse: # pragma: no cover
  202                     logSys.error("DNSException %r querying Cymru for %s TXT", dnse, question)
  203                     if logSys.level <= logging.DEBUG:
  204                         logSys.exception(dnse)
  205                     return_dict["error"] = repr(dnse)
  206                     break
  207                 except Exception as e: # pragma: no cover
  208                     logSys.error("Unhandled Exception %r querying Cymru for %s TXT", e, question)
  209                     if logSys.level <= logging.DEBUG:
  210                         logSys.exception(e)
  211                     return_dict["error"] = repr(e)
  212                     break
  213         except Exception as e: # pragma: no cover
  214             logSys.error("Failure looking up extended Cymru info: %s", e)
  215             if logSys.level <= logging.DEBUG:
  216                 logSys.exception(e)
  217             return_dict["error"] = repr(e)
  218         return return_dict
  219 
  220     ##
  221     # Returns list of Banned ASNs from Cymru info
  222     #
  223     # Use getBanListExtendedCymruInfo() to provide cymru_info
  224     #
  225     # @return list of Banned ASNs
  226 
  227     def geBanListExtendedASN(self, cymru_info):
  228         try:
  229             return [asn for asn in cymru_info["asn"]]
  230         except Exception as e:
  231             logSys.error("Failed to lookup ASN")
  232             logSys.exception(e)
  233             return []
  234 
  235     ##
  236     # Returns list of Banned Countries from Cymru info
  237     #
  238     # Use getBanListExtendedCymruInfo() to provide cymru_info
  239     #
  240     # @return list of Banned Countries
  241 
  242     def geBanListExtendedCountry(self, cymru_info):
  243         try:
  244             return [country for country in cymru_info["country"]]
  245         except Exception as e:
  246             logSys.error("Failed to lookup Country")
  247             logSys.exception(e)
  248             return []
  249 
  250     ##
  251     # Returns list of Banned RIRs from Cymru info
  252     #
  253     # Use getBanListExtendedCymruInfo() to provide cymru_info
  254     #
  255     # @return list of Banned RIRs
  256 
  257     def geBanListExtendedRIR(self, cymru_info):
  258         try:
  259             return [rir for rir in cymru_info["rir"]]
  260         except Exception as e:
  261             logSys.error("Failed to lookup RIR")
  262             logSys.exception(e)
  263             return []
  264 
  265     ##
  266     # Add a ban ticket.
  267     #
  268     # Add a BanTicket instance into the ban list.
  269     # @param ticket the ticket
  270     # @return True if the IP address is not in the ban list
  271     
  272     def addBanTicket(self, ticket, reason={}):
  273         eob = ticket.getEndOfBanTime(self.__banTime)
  274         if eob < MyTime.time():
  275             reason['expired'] = 1
  276             return False
  277         with self.__lock:
  278             # check already banned
  279             fid = ticket.getID()
  280             oldticket = self.__banList.get(fid)
  281             if oldticket:
  282                 reason['ticket'] = oldticket
  283                 # if new time for end of ban is larger than already banned end-time:
  284                 if eob > oldticket.getEndOfBanTime(self.__banTime):
  285                     # we have longest ban - set new (increment) ban time
  286                     reason['prolong'] = 1
  287                     btm = ticket.getBanTime(self.__banTime)
  288                     # if not permanent:
  289                     if btm != -1:
  290                         diftm = ticket.getTime() - oldticket.getTime()
  291                         if diftm > 0:
  292                             btm += diftm
  293                     oldticket.setBanTime(btm)
  294                 return False
  295             # not yet banned - add new one:
  296             self.__banList[fid] = ticket
  297             self.__banTotal += 1
  298             ticket.incrBanCount()
  299             # correct next unban time:
  300             if self.__nextUnbanTime > eob:
  301                 self.__nextUnbanTime = eob
  302             return True
  303 
  304     ##
  305     # Get the size of the ban list.
  306     #
  307     # @return the size
  308 
  309     def size(self):
  310         return len(self.__banList)
  311 
  312     ##
  313     # Check if a ticket is in the list.
  314     #
  315     # Check if a BanTicket with a given IP address is already in the
  316     # ban list.
  317     # @param ticket the ticket
  318     # @return True if a ticket already exists
  319     
  320     def _inBanList(self, ticket):
  321         return ticket.getID() in self.__banList
  322     
  323     ##
  324     # Get the list of IP address to unban.
  325     #
  326     # Return a list of BanTicket which need to be unbanned.
  327     # @param time the time
  328     # @return the list of ticket to unban
  329     
  330     def unBanList(self, time, maxCount=0x7fffffff):
  331         with self.__lock:
  332             # Permanent banning
  333             if self.__banTime < 0:
  334                 return list()
  335 
  336             # Check next unban time:
  337             nextUnbanTime = self.__nextUnbanTime
  338             if nextUnbanTime > time:
  339                 return list()
  340 
  341             # Gets the list of ticket to remove (thereby correct next unban time).
  342             unBanList = {}
  343             nextUnbanTime = BanTicket.MAX_TIME
  344             for fid,ticket in self.__banList.iteritems():
  345                 # current time greater as end of ban - timed out:
  346                 eob = ticket.getEndOfBanTime(self.__banTime)
  347                 if time > eob:
  348                     unBanList[fid] = ticket
  349                     if len(unBanList) >= maxCount: # stop search cycle, so reset back the next check time
  350                         nextUnbanTime = self.__nextUnbanTime
  351                         break
  352                 elif nextUnbanTime > eob:
  353                     nextUnbanTime = eob
  354 
  355             self.__nextUnbanTime = nextUnbanTime
  356             # Removes tickets.
  357             if len(unBanList):
  358                 if len(unBanList) / 2.0 <= len(self.__banList) / 3.0:
  359                     # few as 2/3 should be removed - remove particular items:
  360                     for fid in unBanList.iterkeys():
  361                         del self.__banList[fid]
  362                 else:
  363                     # create new dictionary without items to be deleted:
  364                     self.__banList = dict((fid,ticket) for fid,ticket in self.__banList.iteritems() \
  365                         if fid not in unBanList)
  366                         
  367             # return list of tickets:
  368             return unBanList.values()
  369 
  370     ##
  371     # Flush the ban list.
  372     #
  373     # Get the ban list and initialize it with an empty one.
  374     # @return the complete ban list
  375     
  376     def flushBanList(self):
  377         with self.__lock:
  378             uBList = self.__banList.values()
  379             self.__banList = dict()
  380             return uBList
  381 
  382     ##
  383     # Gets the ticket for the specified ID (most of the time it is IP-address).
  384     #
  385     # @return the ticket or False.
  386     def getTicketByID(self, fid):
  387         with self.__lock:
  388             try:
  389                 # Return the ticket after removing (popping)
  390                 # if from the ban list.
  391                 return self.__banList.pop(fid)
  392             except KeyError:
  393                 pass
  394         return None                       # if none found