You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
475 lines
17 KiB
475 lines
17 KiB
# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license |
|
|
|
# Copyright (C) 2003-2017 Nominum, Inc. |
|
# |
|
# Permission to use, copy, modify, and distribute this software and its |
|
# documentation for any purpose with or without fee is hereby granted, |
|
# provided that the above copyright notice and this permission notice |
|
# appear in all copies. |
|
# |
|
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES |
|
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
|
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR |
|
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
|
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
|
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT |
|
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
|
|
|
"""Asynchronous DNS stub resolver.""" |
|
|
|
import socket |
|
import time |
|
from typing import Any, Dict, List, Optional, Union |
|
|
|
import dns._ddr |
|
import dns.asyncbackend |
|
import dns.asyncquery |
|
import dns.exception |
|
import dns.name |
|
import dns.query |
|
import dns.rdataclass |
|
import dns.rdatatype |
|
import dns.resolver # lgtm[py/import-and-import-from] |
|
|
|
# import some resolver symbols for brevity |
|
from dns.resolver import NXDOMAIN, NoAnswer, NoRootSOA, NotAbsolute |
|
|
|
# for indentation purposes below |
|
_udp = dns.asyncquery.udp |
|
_tcp = dns.asyncquery.tcp |
|
|
|
|
|
class Resolver(dns.resolver.BaseResolver): |
|
"""Asynchronous DNS stub resolver.""" |
|
|
|
async def resolve( |
|
self, |
|
qname: Union[dns.name.Name, str], |
|
rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.A, |
|
rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN, |
|
tcp: bool = False, |
|
source: Optional[str] = None, |
|
raise_on_no_answer: bool = True, |
|
source_port: int = 0, |
|
lifetime: Optional[float] = None, |
|
search: Optional[bool] = None, |
|
backend: Optional[dns.asyncbackend.Backend] = None, |
|
) -> dns.resolver.Answer: |
|
"""Query nameservers asynchronously to find the answer to the question. |
|
|
|
*backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``, |
|
the default, then dnspython will use the default backend. |
|
|
|
See :py:func:`dns.resolver.Resolver.resolve()` for the |
|
documentation of the other parameters, exceptions, and return |
|
type of this method. |
|
""" |
|
|
|
resolution = dns.resolver._Resolution( |
|
self, qname, rdtype, rdclass, tcp, raise_on_no_answer, search |
|
) |
|
if not backend: |
|
backend = dns.asyncbackend.get_default_backend() |
|
start = time.time() |
|
while True: |
|
(request, answer) = resolution.next_request() |
|
# Note we need to say "if answer is not None" and not just |
|
# "if answer" because answer implements __len__, and python |
|
# will call that. We want to return if we have an answer |
|
# object, including in cases where its length is 0. |
|
if answer is not None: |
|
# cache hit! |
|
return answer |
|
assert request is not None # needed for type checking |
|
done = False |
|
while not done: |
|
(nameserver, tcp, backoff) = resolution.next_nameserver() |
|
if backoff: |
|
await backend.sleep(backoff) |
|
timeout = self._compute_timeout(start, lifetime, resolution.errors) |
|
try: |
|
response = await nameserver.async_query( |
|
request, |
|
timeout=timeout, |
|
source=source, |
|
source_port=source_port, |
|
max_size=tcp, |
|
backend=backend, |
|
) |
|
except Exception as ex: |
|
(_, done) = resolution.query_result(None, ex) |
|
continue |
|
(answer, done) = resolution.query_result(response, None) |
|
# Note we need to say "if answer is not None" and not just |
|
# "if answer" because answer implements __len__, and python |
|
# will call that. We want to return if we have an answer |
|
# object, including in cases where its length is 0. |
|
if answer is not None: |
|
return answer |
|
|
|
async def resolve_address( |
|
self, ipaddr: str, *args: Any, **kwargs: Any |
|
) -> dns.resolver.Answer: |
|
"""Use an asynchronous resolver to run a reverse query for PTR |
|
records. |
|
|
|
This utilizes the resolve() method to perform a PTR lookup on the |
|
specified IP address. |
|
|
|
*ipaddr*, a ``str``, the IPv4 or IPv6 address you want to get |
|
the PTR record for. |
|
|
|
All other arguments that can be passed to the resolve() function |
|
except for rdtype and rdclass are also supported by this |
|
function. |
|
|
|
""" |
|
# We make a modified kwargs for type checking happiness, as otherwise |
|
# we get a legit warning about possibly having rdtype and rdclass |
|
# in the kwargs more than once. |
|
modified_kwargs: Dict[str, Any] = {} |
|
modified_kwargs.update(kwargs) |
|
modified_kwargs["rdtype"] = dns.rdatatype.PTR |
|
modified_kwargs["rdclass"] = dns.rdataclass.IN |
|
return await self.resolve( |
|
dns.reversename.from_address(ipaddr), *args, **modified_kwargs |
|
) |
|
|
|
async def resolve_name( |
|
self, |
|
name: Union[dns.name.Name, str], |
|
family: int = socket.AF_UNSPEC, |
|
**kwargs: Any, |
|
) -> dns.resolver.HostAnswers: |
|
"""Use an asynchronous resolver to query for address records. |
|
|
|
This utilizes the resolve() method to perform A and/or AAAA lookups on |
|
the specified name. |
|
|
|
*qname*, a ``dns.name.Name`` or ``str``, the name to resolve. |
|
|
|
*family*, an ``int``, the address family. If socket.AF_UNSPEC |
|
(the default), both A and AAAA records will be retrieved. |
|
|
|
All other arguments that can be passed to the resolve() function |
|
except for rdtype and rdclass are also supported by this |
|
function. |
|
""" |
|
# We make a modified kwargs for type checking happiness, as otherwise |
|
# we get a legit warning about possibly having rdtype and rdclass |
|
# in the kwargs more than once. |
|
modified_kwargs: Dict[str, Any] = {} |
|
modified_kwargs.update(kwargs) |
|
modified_kwargs.pop("rdtype", None) |
|
modified_kwargs["rdclass"] = dns.rdataclass.IN |
|
|
|
if family == socket.AF_INET: |
|
v4 = await self.resolve(name, dns.rdatatype.A, **modified_kwargs) |
|
return dns.resolver.HostAnswers.make(v4=v4) |
|
elif family == socket.AF_INET6: |
|
v6 = await self.resolve(name, dns.rdatatype.AAAA, **modified_kwargs) |
|
return dns.resolver.HostAnswers.make(v6=v6) |
|
elif family != socket.AF_UNSPEC: |
|
raise NotImplementedError(f"unknown address family {family}") |
|
|
|
raise_on_no_answer = modified_kwargs.pop("raise_on_no_answer", True) |
|
lifetime = modified_kwargs.pop("lifetime", None) |
|
start = time.time() |
|
v6 = await self.resolve( |
|
name, |
|
dns.rdatatype.AAAA, |
|
raise_on_no_answer=False, |
|
lifetime=self._compute_timeout(start, lifetime), |
|
**modified_kwargs, |
|
) |
|
# Note that setting name ensures we query the same name |
|
# for A as we did for AAAA. (This is just in case search lists |
|
# are active by default in the resolver configuration and |
|
# we might be talking to a server that says NXDOMAIN when it |
|
# wants to say NOERROR no data. |
|
name = v6.qname |
|
v4 = await self.resolve( |
|
name, |
|
dns.rdatatype.A, |
|
raise_on_no_answer=False, |
|
lifetime=self._compute_timeout(start, lifetime), |
|
**modified_kwargs, |
|
) |
|
answers = dns.resolver.HostAnswers.make( |
|
v6=v6, v4=v4, add_empty=not raise_on_no_answer |
|
) |
|
if not answers: |
|
raise NoAnswer(response=v6.response) |
|
return answers |
|
|
|
# pylint: disable=redefined-outer-name |
|
|
|
async def canonical_name(self, name: Union[dns.name.Name, str]) -> dns.name.Name: |
|
"""Determine the canonical name of *name*. |
|
|
|
The canonical name is the name the resolver uses for queries |
|
after all CNAME and DNAME renamings have been applied. |
|
|
|
*name*, a ``dns.name.Name`` or ``str``, the query name. |
|
|
|
This method can raise any exception that ``resolve()`` can |
|
raise, other than ``dns.resolver.NoAnswer`` and |
|
``dns.resolver.NXDOMAIN``. |
|
|
|
Returns a ``dns.name.Name``. |
|
""" |
|
try: |
|
answer = await self.resolve(name, raise_on_no_answer=False) |
|
canonical_name = answer.canonical_name |
|
except dns.resolver.NXDOMAIN as e: |
|
canonical_name = e.canonical_name |
|
return canonical_name |
|
|
|
async def try_ddr(self, lifetime: float = 5.0) -> None: |
|
"""Try to update the resolver's nameservers using Discovery of Designated |
|
Resolvers (DDR). If successful, the resolver will subsequently use |
|
DNS-over-HTTPS or DNS-over-TLS for future queries. |
|
|
|
*lifetime*, a float, is the maximum time to spend attempting DDR. The default |
|
is 5 seconds. |
|
|
|
If the SVCB query is successful and results in a non-empty list of nameservers, |
|
then the resolver's nameservers are set to the returned servers in priority |
|
order. |
|
|
|
The current implementation does not use any address hints from the SVCB record, |
|
nor does it resolve addresses for the SCVB target name, rather it assumes that |
|
the bootstrap nameserver will always be one of the addresses and uses it. |
|
A future revision to the code may offer fuller support. The code verifies that |
|
the bootstrap nameserver is in the Subject Alternative Name field of the |
|
TLS certficate. |
|
""" |
|
try: |
|
expiration = time.time() + lifetime |
|
answer = await self.resolve( |
|
dns._ddr._local_resolver_name, "svcb", lifetime=lifetime |
|
) |
|
timeout = dns.query._remaining(expiration) |
|
nameservers = await dns._ddr._get_nameservers_async(answer, timeout) |
|
if len(nameservers) > 0: |
|
self.nameservers = nameservers |
|
except Exception: |
|
pass |
|
|
|
|
|
default_resolver = None |
|
|
|
|
|
def get_default_resolver() -> Resolver: |
|
"""Get the default asynchronous resolver, initializing it if necessary.""" |
|
if default_resolver is None: |
|
reset_default_resolver() |
|
assert default_resolver is not None |
|
return default_resolver |
|
|
|
|
|
def reset_default_resolver() -> None: |
|
"""Re-initialize default asynchronous resolver. |
|
|
|
Note that the resolver configuration (i.e. /etc/resolv.conf on UNIX |
|
systems) will be re-read immediately. |
|
""" |
|
|
|
global default_resolver |
|
default_resolver = Resolver() |
|
|
|
|
|
async def resolve( |
|
qname: Union[dns.name.Name, str], |
|
rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.A, |
|
rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN, |
|
tcp: bool = False, |
|
source: Optional[str] = None, |
|
raise_on_no_answer: bool = True, |
|
source_port: int = 0, |
|
lifetime: Optional[float] = None, |
|
search: Optional[bool] = None, |
|
backend: Optional[dns.asyncbackend.Backend] = None, |
|
) -> dns.resolver.Answer: |
|
"""Query nameservers asynchronously to find the answer to the question. |
|
|
|
This is a convenience function that uses the default resolver |
|
object to make the query. |
|
|
|
See :py:func:`dns.asyncresolver.Resolver.resolve` for more |
|
information on the parameters. |
|
""" |
|
|
|
return await get_default_resolver().resolve( |
|
qname, |
|
rdtype, |
|
rdclass, |
|
tcp, |
|
source, |
|
raise_on_no_answer, |
|
source_port, |
|
lifetime, |
|
search, |
|
backend, |
|
) |
|
|
|
|
|
async def resolve_address( |
|
ipaddr: str, *args: Any, **kwargs: Any |
|
) -> dns.resolver.Answer: |
|
"""Use a resolver to run a reverse query for PTR records. |
|
|
|
See :py:func:`dns.asyncresolver.Resolver.resolve_address` for more |
|
information on the parameters. |
|
""" |
|
|
|
return await get_default_resolver().resolve_address(ipaddr, *args, **kwargs) |
|
|
|
|
|
async def resolve_name( |
|
name: Union[dns.name.Name, str], family: int = socket.AF_UNSPEC, **kwargs: Any |
|
) -> dns.resolver.HostAnswers: |
|
"""Use a resolver to asynchronously query for address records. |
|
|
|
See :py:func:`dns.asyncresolver.Resolver.resolve_name` for more |
|
information on the parameters. |
|
""" |
|
|
|
return await get_default_resolver().resolve_name(name, family, **kwargs) |
|
|
|
|
|
async def canonical_name(name: Union[dns.name.Name, str]) -> dns.name.Name: |
|
"""Determine the canonical name of *name*. |
|
|
|
See :py:func:`dns.resolver.Resolver.canonical_name` for more |
|
information on the parameters and possible exceptions. |
|
""" |
|
|
|
return await get_default_resolver().canonical_name(name) |
|
|
|
|
|
async def try_ddr(timeout: float = 5.0) -> None: |
|
"""Try to update the default resolver's nameservers using Discovery of Designated |
|
Resolvers (DDR). If successful, the resolver will subsequently use |
|
DNS-over-HTTPS or DNS-over-TLS for future queries. |
|
|
|
See :py:func:`dns.resolver.Resolver.try_ddr` for more information. |
|
""" |
|
return await get_default_resolver().try_ddr(timeout) |
|
|
|
|
|
async def zone_for_name( |
|
name: Union[dns.name.Name, str], |
|
rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN, |
|
tcp: bool = False, |
|
resolver: Optional[Resolver] = None, |
|
backend: Optional[dns.asyncbackend.Backend] = None, |
|
) -> dns.name.Name: |
|
"""Find the name of the zone which contains the specified name. |
|
|
|
See :py:func:`dns.resolver.Resolver.zone_for_name` for more |
|
information on the parameters and possible exceptions. |
|
""" |
|
|
|
if isinstance(name, str): |
|
name = dns.name.from_text(name, dns.name.root) |
|
if resolver is None: |
|
resolver = get_default_resolver() |
|
if not name.is_absolute(): |
|
raise NotAbsolute(name) |
|
while True: |
|
try: |
|
answer = await resolver.resolve( |
|
name, dns.rdatatype.SOA, rdclass, tcp, backend=backend |
|
) |
|
assert answer.rrset is not None |
|
if answer.rrset.name == name: |
|
return name |
|
# otherwise we were CNAMEd or DNAMEd and need to look higher |
|
except (NXDOMAIN, NoAnswer): |
|
pass |
|
try: |
|
name = name.parent() |
|
except dns.name.NoParent: # pragma: no cover |
|
raise NoRootSOA |
|
|
|
|
|
async def make_resolver_at( |
|
where: Union[dns.name.Name, str], |
|
port: int = 53, |
|
family: int = socket.AF_UNSPEC, |
|
resolver: Optional[Resolver] = None, |
|
) -> Resolver: |
|
"""Make a stub resolver using the specified destination as the full resolver. |
|
|
|
*where*, a ``dns.name.Name`` or ``str`` the domain name or IP address of the |
|
full resolver. |
|
|
|
*port*, an ``int``, the port to use. If not specified, the default is 53. |
|
|
|
*family*, an ``int``, the address family to use. This parameter is used if |
|
*where* is not an address. The default is ``socket.AF_UNSPEC`` in which case |
|
the first address returned by ``resolve_name()`` will be used, otherwise the |
|
first address of the specified family will be used. |
|
|
|
*resolver*, a ``dns.asyncresolver.Resolver`` or ``None``, the resolver to use for |
|
resolution of hostnames. If not specified, the default resolver will be used. |
|
|
|
Returns a ``dns.resolver.Resolver`` or raises an exception. |
|
""" |
|
if resolver is None: |
|
resolver = get_default_resolver() |
|
nameservers: List[Union[str, dns.nameserver.Nameserver]] = [] |
|
if isinstance(where, str) and dns.inet.is_address(where): |
|
nameservers.append(dns.nameserver.Do53Nameserver(where, port)) |
|
else: |
|
answers = await resolver.resolve_name(where, family) |
|
for address in answers.addresses(): |
|
nameservers.append(dns.nameserver.Do53Nameserver(address, port)) |
|
res = dns.asyncresolver.Resolver(configure=False) |
|
res.nameservers = nameservers |
|
return res |
|
|
|
|
|
async def resolve_at( |
|
where: Union[dns.name.Name, str], |
|
qname: Union[dns.name.Name, str], |
|
rdtype: Union[dns.rdatatype.RdataType, str] = dns.rdatatype.A, |
|
rdclass: Union[dns.rdataclass.RdataClass, str] = dns.rdataclass.IN, |
|
tcp: bool = False, |
|
source: Optional[str] = None, |
|
raise_on_no_answer: bool = True, |
|
source_port: int = 0, |
|
lifetime: Optional[float] = None, |
|
search: Optional[bool] = None, |
|
backend: Optional[dns.asyncbackend.Backend] = None, |
|
port: int = 53, |
|
family: int = socket.AF_UNSPEC, |
|
resolver: Optional[Resolver] = None, |
|
) -> dns.resolver.Answer: |
|
"""Query nameservers to find the answer to the question. |
|
|
|
This is a convenience function that calls ``dns.asyncresolver.make_resolver_at()`` |
|
to make a resolver, and then uses it to resolve the query. |
|
|
|
See ``dns.asyncresolver.Resolver.resolve`` for more information on the resolution |
|
parameters, and ``dns.asyncresolver.make_resolver_at`` for information about the |
|
resolver parameters *where*, *port*, *family*, and *resolver*. |
|
|
|
If making more than one query, it is more efficient to call |
|
``dns.asyncresolver.make_resolver_at()`` and then use that resolver for the queries |
|
instead of calling ``resolve_at()`` multiple times. |
|
""" |
|
res = await make_resolver_at(where, port, family, resolver) |
|
return await res.resolve( |
|
qname, |
|
rdtype, |
|
rdclass, |
|
tcp, |
|
source, |
|
raise_on_no_answer, |
|
source_port, |
|
lifetime, |
|
search, |
|
backend, |
|
)
|
|
|