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