tdns_hacks.py - electrum - Electrum Bitcoin wallet
HTML git clone https://git.parazyd.org/electrum
DIR Log
DIR Files
DIR Refs
DIR Submodules
---
tdns_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