dns_hacks.py (4285B)
1 # Copyright (C) 2020 The Electrum developers 2 # Distributed under the MIT software license, see the accompanying 3 # file LICENCE or http://www.opensource.org/licenses/mit-license.php 4 5 import sys 6 import socket 7 import concurrent 8 from concurrent import futures 9 import ipaddress 10 from typing import Optional 11 12 import dns 13 import dns.resolver 14 15 from .logging import get_logger 16 17 18 _logger = get_logger(__name__) 19 20 _dns_threads_executor = None # type: Optional[concurrent.futures.Executor] 21 22 23 def configure_dns_depending_on_proxy(is_proxy: bool) -> None: 24 # Store this somewhere so we can un-monkey-patch: 25 if not hasattr(socket, "_getaddrinfo"): 26 socket._getaddrinfo = socket.getaddrinfo 27 if is_proxy: 28 # prevent dns leaks, see http://stackoverflow.com/questions/13184205/dns-over-proxy 29 socket.getaddrinfo = lambda *args: [(socket.AF_INET, socket.SOCK_STREAM, 6, '', (args[0], args[1]))] 30 else: 31 if sys.platform == 'win32': 32 # On Windows, socket.getaddrinfo takes a mutex, and might hold it for up to 10 seconds 33 # when dns-resolving. To speed it up drastically, we resolve dns ourselves, outside that lock. 34 # See https://github.com/spesmilo/electrum/issues/4421 35 try: 36 _prepare_windows_dns_hack() 37 except Exception as e: 38 _logger.exception('failed to apply windows dns hack.') 39 else: 40 socket.getaddrinfo = _fast_getaddrinfo 41 else: 42 socket.getaddrinfo = socket._getaddrinfo 43 44 45 def _prepare_windows_dns_hack(): 46 # enable dns cache 47 resolver = dns.resolver.get_default_resolver() 48 if resolver.cache is None: 49 resolver.cache = dns.resolver.Cache() 50 # ensure overall timeout for requests is long enough 51 resolver.lifetime = max(resolver.lifetime or 1, 30.0) 52 # prepare threads 53 global _dns_threads_executor 54 if _dns_threads_executor is None: 55 _dns_threads_executor = concurrent.futures.ThreadPoolExecutor(max_workers=20, 56 thread_name_prefix='dns_resolver') 57 58 59 def _fast_getaddrinfo(host, *args, **kwargs): 60 def needs_dns_resolving(host): 61 try: 62 ipaddress.ip_address(host) 63 return False # already valid IP 64 except ValueError: 65 pass # not an IP 66 if str(host) in ('localhost', 'localhost.',): 67 return False 68 return True 69 70 def resolve_with_dnspython(host): 71 addrs = [] 72 expected_errors = (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, 73 concurrent.futures.CancelledError, concurrent.futures.TimeoutError) 74 ipv6_fut = _dns_threads_executor.submit(dns.resolver.resolve, host, dns.rdatatype.AAAA) 75 ipv4_fut = _dns_threads_executor.submit(dns.resolver.resolve, host, dns.rdatatype.A) 76 # try IPv6 77 try: 78 answers = ipv6_fut.result() 79 addrs += [str(answer) for answer in answers] 80 except expected_errors as e: 81 pass 82 except BaseException as e: 83 _logger.info(f'dnspython failed to resolve dns (AAAA) for {repr(host)} with error: {repr(e)}') 84 # try IPv4 85 try: 86 answers = ipv4_fut.result() 87 addrs += [str(answer) for answer in answers] 88 except expected_errors as e: 89 # dns failed for some reason, e.g. dns.resolver.NXDOMAIN this is normal. 90 # Simply report back failure; except if we already have some results. 91 if not addrs: 92 raise socket.gaierror(11001, 'getaddrinfo failed') from e 93 except BaseException as e: 94 # Possibly internal error in dnspython :( see #4483 and #5638 95 _logger.info(f'dnspython failed to resolve dns (A) for {repr(host)} with error: {repr(e)}') 96 if addrs: 97 return addrs 98 # Fall back to original socket.getaddrinfo to resolve dns. 99 return [host] 100 101 addrs = [host] 102 if needs_dns_resolving(host): 103 addrs = resolve_with_dnspython(host) 104 list_of_list_of_socketinfos = [socket._getaddrinfo(addr, *args, **kwargs) for addr in addrs] 105 list_of_socketinfos = [item for lst in list_of_list_of_socketinfos for item in lst] 106 return list_of_socketinfos