dns: try unqualified queries as top level

`resolv.conf` docs say unqualified names must resolve from search (or local) domain.
However, common OS `getaddrinfo()` implementations append trailing dot (e.g. `db -> db.`)
and ask nameservers, as if top-level domain was queried.
Eventlet now supports this behavior.
https://github.com/eventlet/eventlet/issues/363
This commit is contained in:
Sergey Shepelev
2016-12-22 16:25:47 +03:00
parent 3a8115c560
commit e1bb2fee1d
2 changed files with 65 additions and 10 deletions

View File

@@ -303,17 +303,58 @@ class ResolverProxy(object):
self._resolver.cache = dns.resolver.LRUCache() self._resolver.cache = dns.resolver.LRUCache()
def query(self, qname, rdtype=dns.rdatatype.A, rdclass=dns.rdataclass.IN, def query(self, qname, rdtype=dns.rdatatype.A, rdclass=dns.rdataclass.IN,
tcp=False, source=None, raise_on_no_answer=True): tcp=False, source=None, raise_on_no_answer=True,
"""Query the resolver, using /etc/hosts if enabled""" _hosts_rdtypes=(dns.rdatatype.A, dns.rdatatype.AAAA)):
"""Query the resolver, using /etc/hosts if enabled.
"""
result = [None, None, 0]
if qname is None: if qname is None:
qname = '0.0.0.0' qname = '0.0.0.0'
if rdclass == dns.rdataclass.IN and self._hosts: if isinstance(qname, six.string_types):
qname = dns.name.from_text(qname, None)
def step(fun, *args, **kwargs):
try: try:
return self._hosts.query(qname, rdtype) a = fun(*args, **kwargs)
except dns.resolver.NoAnswer: except Exception as e:
pass result[1] = e
return self._resolver.query(qname, rdtype, rdclass, return False
tcp, source, raise_on_no_answer) if a.rrset is not None and len(a.rrset):
if result[0] is None:
result[0] = a
else:
result[0].rrset.union_update(a.rrset)
result[2] += len(a.rrset)
return True
# Main query
step(self._resolver.query, qname, rdtype, rdclass, tcp, source, raise_on_no_answer=False)
# `resolv.conf` docs say unqualified names must resolve from search (or local) domain.
# However, common OS `getaddrinfo()` implementations append trailing dot (e.g. `db -> db.`)
# and ask nameservers, as if top-level domain was queried.
# This step follows established practice.
# https://github.com/nameko/nameko/issues/392
# https://github.com/eventlet/eventlet/issues/363
if len(qname) == 1:
step(self._resolver.query, qname.concatenate(dns.name.root),
rdtype, rdclass, tcp, source, raise_on_no_answer=False)
# Return answers from /etc/hosts despite nameserver errors
# https://github.com/eventlet/eventlet/pull/354
if ((result[2] == 0) and self._hosts and
(rdclass == dns.rdataclass.IN) and (rdtype in _hosts_rdtypes)):
step(self._hosts.query, qname, rdtype, raise_on_no_answer=False)
if result[0] is not None:
if raise_on_no_answer and result[2] == 0:
raise dns.resolver.NoAnswer
return result[0]
if result[1] is not None:
if raise_on_no_answer or not isinstance(result[1], dns.resolver.NoAnswer):
raise result[1]
raise dns.resolver.NXDOMAIN(qnames=(qname,))
def getaliases(self, hostname): def getaliases(self, hostname):
"""Return a list of all the aliases of a given hostname""" """Return a list of all the aliases of a given hostname"""

View File

@@ -6,10 +6,10 @@ import socket
import tempfile import tempfile
import time import time
import tests
from tests import mock
from eventlet.support import greendns from eventlet.support import greendns
from eventlet.support.greendns import dns from eventlet.support.greendns import dns
import tests
import tests.mock
class TestHostsResolver(tests.LimitedTestCase): class TestHostsResolver(tests.LimitedTestCase):
@@ -777,3 +777,17 @@ class TestGethostbyname_ex(tests.LimitedTestCase):
def test_reverse_name(): def test_reverse_name():
tests.run_isolated('greendns_from_address_203.py') tests.run_isolated('greendns_from_address_203.py')
def test_proxy_resolve_unqualified():
# https://github.com/eventlet/eventlet/issues/363
rp = greendns.ResolverProxy(filename=None)
rp._resolver.search.append(dns.name.from_text('example.com'))
with tests.mock.patch('dns.resolver.Resolver.query', side_effect=dns.resolver.NoAnswer) as m:
try:
rp.query('machine')
assert False, 'Expected NoAnswer exception'
except dns.resolver.NoAnswer:
pass
assert any(call[0][0] == dns.name.from_text('machine') for call in m.call_args_list)
assert any(call[0][0] == dns.name.from_text('machine.') for call in m.call_args_list)