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__ = "Fail2Ban Developers, Alexander Koeppe, Serg G. Brester, Yaroslav Halchenko" 21__copyright__ = "Copyright (c) 2004-2016 Fail2ban Developers" 22__license__ = "GPL" 23 24import socket 25import struct 26import re 27 28from .utils import Utils 29from ..helpers import getLogger 30 31# Gets the instance of the logger. 32logSys = getLogger(__name__) 33 34 35## 36# Helper functions 37# 38# 39def asip(ip): 40 """A little helper to guarantee ip being an IPAddr instance""" 41 if isinstance(ip, IPAddr): 42 return ip 43 return IPAddr(ip) 44 45def getfqdn(name=''): 46 """Get fully-qualified hostname of given host, thereby resolve of an external 47 IPs and name will be preferred before the local domain (or a loopback), see gh-2438 48 """ 49 try: 50 name = name or socket.gethostname() 51 names = ( 52 ai[3] for ai in socket.getaddrinfo( 53 name, None, 0, socket.SOCK_DGRAM, 0, socket.AI_CANONNAME 54 ) if ai[3] 55 ) 56 if names: 57 # first try to find a fqdn starting with the host name like www.domain.tld for www: 58 pref = name+'.' 59 first = None 60 for ai in names: 61 if ai.startswith(pref): 62 return ai 63 if not first: first = ai 64 # not found - simply use first known fqdn: 65 return first 66 except socket.error: 67 pass 68 # fallback to python's own getfqdn routine: 69 return socket.getfqdn(name) 70 71 72## 73# Utils class for DNS handling. 74# 75# This class contains only static methods used to handle DNS 76# 77class DNSUtils: 78 79 # todo: make configurable the expired time and max count of cache entries: 80 CACHE_nameToIp = Utils.Cache(maxCount=1000, maxTime=5*60) 81 CACHE_ipToName = Utils.Cache(maxCount=1000, maxTime=5*60) 82 83 @staticmethod 84 def dnsToIp(dns): 85 """ Convert a DNS into an IP address using the Python socket module. 86 Thanks to Kevin Drapel. 87 """ 88 # cache, also prevent long wait during retrieving of ip for wrong dns or lazy dns-system: 89 ips = DNSUtils.CACHE_nameToIp.get(dns) 90 if ips is not None: 91 return ips 92 # retrieve ips 93 ips = set() 94 saveerr = None 95 for fam, ipfam in ((socket.AF_INET, IPAddr.FAM_IPv4), (socket.AF_INET6, IPAddr.FAM_IPv6)): 96 try: 97 for result in socket.getaddrinfo(dns, None, fam, 0, socket.IPPROTO_TCP): 98 # if getaddrinfo returns something unexpected: 99 if len(result) < 4 or not len(result[4]): continue 100 # get ip from `(2, 1, 6, '', ('127.0.0.1', 0))`,be sure we've an ip-string 101 # (some python-versions resp. host configurations causes returning of integer there): 102 ip = IPAddr(str(result[4][0]), ipfam) 103 if ip.isValid: 104 ips.add(ip) 105 except Exception as e: 106 saveerr = e 107 if not ips and saveerr: 108 logSys.warning("Unable to find a corresponding IP address for %s: %s", dns, saveerr) 109 110 DNSUtils.CACHE_nameToIp.set(dns, ips) 111 return ips 112 113 @staticmethod 114 def ipToName(ip): 115 # cache, also prevent long wait during retrieving of name for wrong addresses, lazy dns: 116 v = DNSUtils.CACHE_ipToName.get(ip, ()) 117 if v != (): 118 return v 119 # retrieve name 120 try: 121 v = socket.gethostbyaddr(ip)[0] 122 except socket.error as e: 123 logSys.debug("Unable to find a name for the IP %s: %s", ip, e) 124 v = None 125 DNSUtils.CACHE_ipToName.set(ip, v) 126 return v 127 128 @staticmethod 129 def textToIp(text, useDns): 130 """ Return the IP of DNS found in a given text. 131 """ 132 ipList = set() 133 # Search for plain IP 134 plainIP = IPAddr.searchIP(text) 135 if plainIP is not None: 136 ip = IPAddr(plainIP) 137 if ip.isValid: 138 ipList.add(ip) 139 140 # If we are allowed to resolve -- give it a try if nothing was found 141 if useDns in ("yes", "warn") and not ipList: 142 # Try to get IP from possible DNS 143 ip = DNSUtils.dnsToIp(text) 144 ipList.update(ip) 145 if ip and useDns == "warn": 146 logSys.warning("Determined IP using DNS Lookup: %s = %s", 147 text, ipList) 148 149 return ipList 150 151 @staticmethod 152 def getHostname(fqdn=True): 153 """Get short hostname or fully-qualified hostname of host self""" 154 # try find cached own hostnames (this tuple-key cannot be used elsewhere): 155 key = ('self','hostname', fqdn) 156 name = DNSUtils.CACHE_ipToName.get(key) 157 # get it using different ways (hostname, fully-qualified or vice versa): 158 if name is None: 159 name = '' 160 for hostname in ( 161 (getfqdn, socket.gethostname) if fqdn else (socket.gethostname, getfqdn) 162 ): 163 try: 164 name = hostname() 165 break 166 except Exception as e: # pragma: no cover 167 logSys.warning("Retrieving own hostnames failed: %s", e) 168 # cache and return : 169 DNSUtils.CACHE_ipToName.set(key, name) 170 return name 171 172 @staticmethod 173 def getSelfNames(): 174 """Get own host names of self""" 175 # try find cached own hostnames (this tuple-key cannot be used elsewhere): 176 key = ('self','dns') 177 names = DNSUtils.CACHE_ipToName.get(key) 178 # get it using different ways (a set with names of localhost, hostname, fully qualified): 179 if names is None: 180 names = set([ 181 'localhost', DNSUtils.getHostname(False), DNSUtils.getHostname(True) 182 ]) - set(['']) # getHostname can return '' 183 # cache and return : 184 DNSUtils.CACHE_ipToName.set(key, names) 185 return names 186 187 @staticmethod 188 def getSelfIPs(): 189 """Get own IP addresses of self""" 190 # try find cached own IPs (this tuple-key cannot be used elsewhere): 191 key = ('self','ips') 192 ips = DNSUtils.CACHE_nameToIp.get(key) 193 # get it using different ways (a set with IPs of localhost, hostname, fully qualified): 194 if ips is None: 195 ips = set() 196 for hostname in DNSUtils.getSelfNames(): 197 try: 198 ips |= set(DNSUtils.textToIp(hostname, 'yes')) 199 except Exception as e: # pragma: no cover 200 logSys.warning("Retrieving own IPs of %s failed: %s", hostname, e) 201 # cache and return : 202 DNSUtils.CACHE_nameToIp.set(key, ips) 203 return ips 204 205 @staticmethod 206 def IPv6IsAllowed(): 207 # return os.path.exists("/proc/net/if_inet6") || any((':' in ip) for ip in DNSUtils.getSelfIPs()) 208 return any((':' in ip.ntoa) for ip in DNSUtils.getSelfIPs()) 209 210 211## 212# Class for IP address handling. 213# 214# This class contains methods for handling IPv4 and IPv6 addresses. 215# 216class IPAddr(object): 217 """Encapsulate functionality for IPv4 and IPv6 addresses 218 """ 219 220 IP_4_RE = r"""(?:\d{1,3}\.){3}\d{1,3}""" 221 IP_6_RE = r"""(?:[0-9a-fA-F]{1,4}::?|::){1,7}(?:[0-9a-fA-F]{1,4}|(?<=:):)""" 222 IP_4_6_CRE = re.compile( 223 r"""^(?:(?P<IPv4>%s)|\[?(?P<IPv6>%s)\]?)$""" % (IP_4_RE, IP_6_RE)) 224 # An IPv4 compatible IPv6 to be reused (see below) 225 IP6_4COMPAT = None 226 227 # object attributes 228 __slots__ = '_family','_addr','_plen','_maskplen','_raw' 229 230 # todo: make configurable the expired time and max count of cache entries: 231 CACHE_OBJ = Utils.Cache(maxCount=10000, maxTime=5*60) 232 233 CIDR_RAW = -2 234 CIDR_UNSPEC = -1 235 FAM_IPv4 = CIDR_RAW - socket.AF_INET 236 FAM_IPv6 = CIDR_RAW - socket.AF_INET6 237 238 def __new__(cls, ipstr, cidr=CIDR_UNSPEC): 239 if cidr == IPAddr.CIDR_RAW: # don't cache raw 240 ip = super(IPAddr, cls).__new__(cls) 241 ip.__init(ipstr, cidr) 242 return ip 243 # check already cached as IPAddr 244 args = (ipstr, cidr) 245 ip = IPAddr.CACHE_OBJ.get(args) 246 if ip is not None: 247 return ip 248 # wrap mask to cidr (correct plen): 249 if cidr == IPAddr.CIDR_UNSPEC: 250 ipstr, cidr = IPAddr.__wrap_ipstr(ipstr) 251 args = (ipstr, cidr) 252 # check cache again: 253 if cidr != IPAddr.CIDR_UNSPEC: 254 ip = IPAddr.CACHE_OBJ.get(args) 255 if ip is not None: 256 return ip 257 ip = super(IPAddr, cls).__new__(cls) 258 ip.__init(ipstr, cidr) 259 if ip._family != IPAddr.CIDR_RAW: 260 IPAddr.CACHE_OBJ.set(args, ip) 261 return ip 262 263 @staticmethod 264 def __wrap_ipstr(ipstr): 265 # because of standard spelling of IPv6 (with port) enclosed in brackets ([ipv6]:port), 266 # remove they now (be sure the <HOST> inside failregex uses this for IPv6 (has \[?...\]?) 267 if len(ipstr) > 2 and ipstr[0] == '[' and ipstr[-1] == ']': 268 ipstr = ipstr[1:-1] 269 # test mask: 270 if "/" not in ipstr: 271 return ipstr, IPAddr.CIDR_UNSPEC 272 s = ipstr.split('/', 1) 273 # IP address without CIDR mask 274 if len(s) > 2: 275 raise ValueError("invalid ipstr %r, too many plen representation" % (ipstr,)) 276 if "." in s[1] or ":" in s[1]: # 255.255.255.0 resp. ffff:: style mask 277 s[1] = IPAddr.masktoplen(s[1]) 278 s[1] = int(s[1]) 279 return s 280 281 def __init(self, ipstr, cidr=CIDR_UNSPEC): 282 """ initialize IP object by converting IP address string 283 to binary to integer 284 """ 285 self._family = socket.AF_UNSPEC 286 self._addr = 0 287 self._plen = 0 288 self._maskplen = None 289 # always save raw value (normally used if really raw or not valid only): 290 self._raw = ipstr 291 # if not raw - recognize family, set addr, etc.: 292 if cidr != IPAddr.CIDR_RAW: 293 if cidr is not None and cidr < IPAddr.CIDR_RAW: 294 family = [IPAddr.CIDR_RAW - cidr] 295 else: 296 family = [socket.AF_INET, socket.AF_INET6] 297 for family in family: 298 try: 299 binary = socket.inet_pton(family, ipstr) 300 self._family = family 301 break 302 except socket.error: 303 continue 304 305 if self._family == socket.AF_INET: 306 # convert host to network byte order 307 self._addr, = struct.unpack("!L", binary) 308 self._plen = 32 309 310 # mask out host portion if prefix length is supplied 311 if cidr is not None and cidr >= 0: 312 mask = ~(0xFFFFFFFF >> cidr) 313 self._addr &= mask 314 self._plen = cidr 315 316 elif self._family == socket.AF_INET6: 317 # convert host to network byte order 318 hi, lo = struct.unpack("!QQ", binary) 319 self._addr = (hi << 64) | lo 320 self._plen = 128 321 322 # mask out host portion if prefix length is supplied 323 if cidr is not None and cidr >= 0: 324 mask = ~(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF >> cidr) 325 self._addr &= mask 326 self._plen = cidr 327 328 # if IPv6 address is a IPv4-compatible, make instance a IPv4 329 elif self.isInNet(IPAddr.IP6_4COMPAT): 330 self._addr = lo & 0xFFFFFFFF 331 self._family = socket.AF_INET 332 self._plen = 32 333 else: 334 self._family = IPAddr.CIDR_RAW 335 336 def __repr__(self): 337 return repr(self.ntoa) 338 339 def __str__(self): 340 return self.ntoa if isinstance(self.ntoa, str) else str(self.ntoa) 341 342 def __reduce__(self): 343 """IPAddr pickle-handler, that simply wraps IPAddr to the str 344 345 Returns a string as instance to be pickled, because fail2ban-client can't 346 unserialize IPAddr objects 347 """ 348 return (str, (self.ntoa,)) 349 350 @property 351 def addr(self): 352 return self._addr 353 354 @property 355 def family(self): 356 return self._family 357 358 FAM2STR = {socket.AF_INET: 'inet4', socket.AF_INET6: 'inet6'} 359 @property 360 def familyStr(self): 361 return IPAddr.FAM2STR.get(self._family) 362 363 @property 364 def plen(self): 365 return self._plen 366 367 @property 368 def raw(self): 369 """The raw address 370 371 Should only be set to a non-empty string if prior address 372 conversion wasn't possible 373 """ 374 return self._raw 375 376 @property 377 def isValid(self): 378 """Either the object corresponds to a valid IP address 379 """ 380 return self._family != socket.AF_UNSPEC 381 382 @property 383 def isSingle(self): 384 """Returns whether the object is a single IP address (not DNS and subnet) 385 """ 386 return self._plen == {socket.AF_INET: 32, socket.AF_INET6: 128}.get(self._family, -1000) 387 388 def __eq__(self, other): 389 if self._family == IPAddr.CIDR_RAW and not isinstance(other, IPAddr): 390 return self._raw == other 391 if not isinstance(other, IPAddr): 392 if other is None: return False 393 other = IPAddr(other) 394 if self._family != other._family: return False 395 if self._family == socket.AF_UNSPEC: 396 return self._raw == other._raw 397 return ( 398 (self._addr == other._addr) and 399 (self._plen == other._plen) 400 ) 401 402 def __ne__(self, other): 403 return not (self == other) 404 405 def __lt__(self, other): 406 if self._family == IPAddr.CIDR_RAW and not isinstance(other, IPAddr): 407 return self._raw < other 408 if not isinstance(other, IPAddr): 409 if other is None: return False 410 other = IPAddr(other) 411 return self._family < other._family or self._addr < other._addr 412 413 def __add__(self, other): 414 if not isinstance(other, IPAddr): 415 other = IPAddr(other) 416 return "%s%s" % (self, other) 417 418 def __radd__(self, other): 419 if not isinstance(other, IPAddr): 420 other = IPAddr(other) 421 return "%s%s" % (other, self) 422 423 def __hash__(self): 424 # should be the same as by string (because of possible compare with string): 425 return hash(self.ntoa) 426 #return hash(self._addr)^hash((self._plen<<16)|self._family) 427 428 @property 429 def hexdump(self): 430 """Hex representation of the IP address (for debug purposes) 431 """ 432 if self._family == socket.AF_INET: 433 return "%08x" % self._addr 434 elif self._family == socket.AF_INET6: 435 return "%032x" % self._addr 436 else: 437 return "" 438 439 # TODO: could be lazily evaluated 440 @property 441 def ntoa(self): 442 """ represent IP object as text like the deprecated 443 C pendant inet.ntoa but address family independent 444 """ 445 add = '' 446 if self.isIPv4: 447 # convert network to host byte order 448 binary = struct.pack("!L", self._addr) 449 if self._plen and self._plen < 32: 450 add = "/%d" % self._plen 451 elif self.isIPv6: 452 # convert network to host byte order 453 hi = self._addr >> 64 454 lo = self._addr & 0xFFFFFFFFFFFFFFFF 455 binary = struct.pack("!QQ", hi, lo) 456 if self._plen and self._plen < 128: 457 add = "/%d" % self._plen 458 else: 459 return self._raw 460 461 return socket.inet_ntop(self._family, binary) + add 462 463 def getPTR(self, suffix=None): 464 """ return the DNS PTR string of the provided IP address object 465 466 If "suffix" is provided it will be appended as the second and top 467 level reverse domain. 468 If omitted it is implicitly set to the second and top level reverse 469 domain of the according IP address family 470 """ 471 if self.isIPv4: 472 exploded_ip = self.ntoa.split(".") 473 if suffix is None: 474 suffix = "in-addr.arpa." 475 elif self.isIPv6: 476 exploded_ip = self.hexdump 477 if suffix is None: 478 suffix = "ip6.arpa." 479 else: 480 return "" 481 482 return "%s.%s" % (".".join(reversed(exploded_ip)), suffix) 483 484 def getHost(self): 485 """Return the host name (DNS) of the provided IP address object 486 """ 487 return DNSUtils.ipToName(self.ntoa) 488 489 @property 490 def isIPv4(self): 491 """Either the IP object is of address family AF_INET 492 """ 493 return self.family == socket.AF_INET 494 495 @property 496 def isIPv6(self): 497 """Either the IP object is of address family AF_INET6 498 """ 499 return self.family == socket.AF_INET6 500 501 def isInNet(self, net): 502 """Return either the IP object is in the provided network 503 """ 504 # if it isn't a valid IP address, try DNS resolution 505 if not net.isValid and net.raw != "": 506 # Check if IP in DNS 507 return self in DNSUtils.dnsToIp(net.raw) 508 509 if self.family != net.family: 510 return False 511 if self.isIPv4: 512 mask = ~(0xFFFFFFFF >> net.plen) 513 elif self.isIPv6: 514 mask = ~(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF >> net.plen) 515 else: 516 return False 517 518 return (self.addr & mask) == net.addr 519 520 def contains(self, ip): 521 """Return whether the object (as network) contains given IP 522 """ 523 return isinstance(ip, IPAddr) and (ip == self or ip.isInNet(self)) 524 525 # Pre-calculated map: addr to maskplen 526 def __getMaskMap(): 527 m6 = (1 << 128)-1 528 m4 = (1 << 32)-1 529 mmap = {m6: 128, m4: 32, 0: 0} 530 m = 0 531 for i in range(0, 128): 532 m |= 1 << i 533 if i < 32: 534 mmap[m ^ m4] = 32-1-i 535 mmap[m ^ m6] = 128-1-i 536 return mmap 537 538 MAP_ADDR2MASKPLEN = __getMaskMap() 539 540 @property 541 def maskplen(self): 542 mplen = 0 543 if self._maskplen is not None: 544 return self._maskplen 545 mplen = IPAddr.MAP_ADDR2MASKPLEN.get(self._addr) 546 if mplen is None: 547 raise ValueError("invalid mask %r, no plen representation" % (str(self),)) 548 self._maskplen = mplen 549 return mplen 550 551 @staticmethod 552 def masktoplen(mask): 553 """Convert mask string to prefix length 554 555 To be used only for IPv4 masks 556 """ 557 return IPAddr(mask).maskplen 558 559 @staticmethod 560 def searchIP(text): 561 """Search if text is an IP address, and return it if so, else None 562 """ 563 match = IPAddr.IP_4_6_CRE.match(text) 564 if not match: 565 return None 566 ipstr = match.group('IPv4') 567 if ipstr != '': 568 return ipstr 569 return match.group('IPv6') 570 571 572# An IPv4 compatible IPv6 to be reused 573IPAddr.IP6_4COMPAT = IPAddr("::ffff:0:0", 96) 574