From ef3ee6066a496612fbda644e07c4b33b23ccd819 Mon Sep 17 00:00:00 2001 From: Remi Gacogne Date: Mon, 23 Mar 2020 15:47:10 +0100 Subject: [PATCH] rec: Implement native DNS64 support, without Lua Native support is much less flexible than Lua hooks but should satisfy most of the DNS64 setups. It is also much faster since it does not involve calling a Lua hook for all queries. --- pdns/lua-recursor4.cc | 93 +---------- pdns/misc.cc | 27 +++ pdns/misc.hh | 2 + pdns/pdns_recursor.cc | 143 ++++++++++++++-- pdns/recursordist/docs/dns64.rst | 3 +- pdns/recursordist/docs/settings.rst | 14 ++ pdns/syncres.hh | 2 + pdns/test-misc_hh.cc | 18 +- .../test_DNS64.py | 155 ++++++++++++++++++ 9 files changed, 350 insertions(+), 107 deletions(-) create mode 100644 regression-tests.recursor-dnssec/test_DNS64.py diff --git a/pdns/lua-recursor4.cc b/pdns/lua-recursor4.cc index 3b13bd464d..61c28155dc 100644 --- a/pdns/lua-recursor4.cc +++ b/pdns/lua-recursor4.cc @@ -34,95 +34,6 @@ RecursorLua4::RecursorLua4() { prepareContext(); } -static int getFakeAAAARecords(const DNSName& qname, const std::string& prefix, vector& ret) -{ - int rcode=directResolve(qname, QType(QType::A), 1, ret); - - ComboAddress prefixAddress(prefix); - - // Remove double CNAME records - std::set seenCNAMEs; - ret.erase(std::remove_if( - ret.begin(), - ret.end(), - [&seenCNAMEs](DNSRecord& rr) { - if (rr.d_type == QType::CNAME) { - auto target = getRR(rr); - if (target == nullptr) { - return false; - } - if (seenCNAMEs.count(target->getTarget()) > 0) { - // We've had this CNAME before, remove it - return true; - } - seenCNAMEs.insert(target->getTarget()); - } - return false; - }), - ret.end()); - - bool seenA = false; - for(DNSRecord& rr : ret) - { - if(rr.d_type == QType::A && rr.d_place==DNSResourceRecord::ANSWER) { - if(auto rec = getRR(rr)) { - ComboAddress ipv4(rec->getCA()); - uint32_t tmp; - memcpy((void*)&tmp, &ipv4.sin4.sin_addr.s_addr, 4); - // tmp=htonl(tmp); - memcpy(((char*)&prefixAddress.sin6.sin6_addr.s6_addr)+12, &tmp, 4); - rr.d_content = std::make_shared(prefixAddress); - rr.d_type = QType::AAAA; - } - seenA = true; - } - } - - if (seenA) { - // We've seen an A in the ANSWER section, so there is no need to keep any - // SOA in the AUTHORITY section as this is not a NODATA response. - ret.erase(std::remove_if( - ret.begin(), - ret.end(), - [](DNSRecord& rr) { - return (rr.d_type == QType::SOA && rr.d_place==DNSResourceRecord::AUTHORITY); - }), - ret.end()); - } - return rcode; -} - -static int getFakePTRRecords(const DNSName& qname, const std::string& prefix, vector& ret) -{ - /* qname has a reverse ordered IPv6 address, need to extract the underlying IPv4 address from it - and turn it into an IPv4 in-addr.arpa query */ - ret.clear(); - vector parts = qname.getRawLabels(); - - if(parts.size() < 8) - return -1; - - string newquery; - for(int n = 0; n < 4; ++n) { - newquery += - std::to_string(stoll(parts[n*2], 0, 16) + 16*stoll(parts[n*2+1], 0, 16)); - newquery.append(1,'.'); - } - newquery += "in-addr.arpa."; - - - DNSRecord rr; - rr.d_name = qname; - rr.d_type = QType::CNAME; - rr.d_content = std::make_shared(newquery); - ret.push_back(rr); - - int rcode = directResolve(DNSName(newquery), QType(QType::PTR), 1, ret); - - return rcode; - -} - boost::optional RecursorLua4::DNSQuestion::getDH() const { if (dh) @@ -662,10 +573,10 @@ loop:; ret = followCNAMERecords(dq.records, QType(dq.qtype)); } else if(dq.followupFunction=="getFakeAAAARecords") { - ret=getFakeAAAARecords(dq.followupName, dq.followupPrefix, dq.records); + ret=getFakeAAAARecords(dq.followupName, ComboAddress(dq.followupPrefix), dq.records); } else if(dq.followupFunction=="getFakePTRRecords") { - ret=getFakePTRRecords(dq.followupName, dq.followupPrefix, dq.records); + ret=getFakePTRRecords(dq.followupName, dq.records); } else if(dq.followupFunction=="udpQueryResponse") { dq.udpAnswer = GenUDPQueryResponse(dq.udpQueryDest, dq.udpQuery); diff --git a/pdns/misc.cc b/pdns/misc.cc index 279151116a..af33b56e3e 100644 --- a/pdns/misc.cc +++ b/pdns/misc.cc @@ -1613,3 +1613,30 @@ bool setPipeBufferSize(int fd, size_t size) return false; #endif /* F_SETPIPE_SZ */ } + +DNSName reverseNameFromIP(const ComboAddress& ip) +{ + if (ip.isIPv4()) { + std::string result("in-addr.arpa."); + auto ptr = reinterpret_cast(&ip.sin4.sin_addr.s_addr); + for (size_t idx = 0; idx < sizeof(ip.sin4.sin_addr.s_addr); idx++) { + result = std::to_string(ptr[idx]) + "." + result; + } + return DNSName(result); + } + else if (ip.isIPv6()) { + std::string result("ip6.arpa."); + auto ptr = reinterpret_cast(&ip.sin6.sin6_addr.s6_addr[0]); + for (size_t idx = 0; idx < sizeof(ip.sin6.sin6_addr.s6_addr); idx++) { + std::stringstream stream; + stream << std::hex << (ptr[idx] & 0x0F); + stream << '.'; + stream << std::hex << (((ptr[idx]) >> 4) & 0x0F); + stream << '.'; + result = stream.str() + result; + } + return DNSName(result); + } + + throw std::runtime_error("Calling reverseNameFromIP() for an address which is neither an IPv4 nor an IPv6"); +} diff --git a/pdns/misc.hh b/pdns/misc.hh index b4924c4274..943f60e3b2 100644 --- a/pdns/misc.hh +++ b/pdns/misc.hh @@ -609,3 +609,5 @@ bool isSettingThreadCPUAffinitySupported(); int mapThreadToCPUList(pthread_t tid, const std::set& cpus); std::vector getResolvers(const std::string& resolvConfPath); + +DNSName reverseNameFromIP(const ComboAddress& ip); diff --git a/pdns/pdns_recursor.cc b/pdns/pdns_recursor.cc index bfaf00ba43..e76e071358 100644 --- a/pdns/pdns_recursor.cc +++ b/pdns/pdns_recursor.cc @@ -199,6 +199,8 @@ static std::shared_ptr g_initialDomainMap; // new threads static std::shared_ptr g_initialAllowFrom; // new thread needs to be setup with this static NetmaskGroup g_XPFAcl; static NetmaskGroup g_proxyProtocolACL; +static boost::optional g_dns64Prefix{boost::none}; +static DNSName g_dns64PrefixReverse; static size_t g_proxyProtocolMaximumSize; static size_t g_tcpMaxQueriesPerConn; static size_t s_maxUDPQueriesPerRound; @@ -1132,6 +1134,91 @@ int followCNAMERecords(vector& ret, const QType& qtype) return rcode; } +int getFakeAAAARecords(const DNSName& qname, ComboAddress prefix, vector& ret) +{ + int rcode = directResolve(qname, QType(QType::A), QClass::IN, ret); + + // Remove double CNAME records + std::set seenCNAMEs; + ret.erase(std::remove_if( + ret.begin(), + ret.end(), + [&seenCNAMEs](DNSRecord& rr) { + if (rr.d_type == QType::CNAME) { + auto target = getRR(rr); + if (target == nullptr) { + return false; + } + if (seenCNAMEs.count(target->getTarget()) > 0) { + // We've had this CNAME before, remove it + return true; + } + seenCNAMEs.insert(target->getTarget()); + } + return false; + }), + ret.end()); + + bool seenA = false; + for (DNSRecord& rr : ret) { + if (rr.d_type == QType::A && rr.d_place == DNSResourceRecord::ANSWER) { + if (auto rec = getRR(rr)) { + ComboAddress ipv4(rec->getCA()); + uint32_t tmp; + memcpy(&tmp, &ipv4.sin4.sin_addr.s_addr, 4); + // tmp=htonl(tmp); + memcpy(((char*)&prefix.sin6.sin6_addr.s6_addr)+12, &tmp, 4); + rr.d_content = std::make_shared(prefix); + rr.d_type = QType::AAAA; + } + seenA = true; + } + } + + if (seenA) { + // We've seen an A in the ANSWER section, so there is no need to keep any + // SOA in the AUTHORITY section as this is not a NODATA response. + ret.erase(std::remove_if( + ret.begin(), + ret.end(), + [](DNSRecord& rr) { + return (rr.d_type == QType::SOA && rr.d_place == DNSResourceRecord::AUTHORITY); + }), + ret.end()); + } + return rcode; +} + +int getFakePTRRecords(const DNSName& qname, vector& ret) +{ + /* qname has a reverse ordered IPv6 address, need to extract the underlying IPv4 address from it + and turn it into an IPv4 in-addr.arpa query */ + ret.clear(); + vector parts = qname.getRawLabels(); + + if (parts.size() < 8) { + return -1; + } + + string newquery; + for (int n = 0; n < 4; ++n) { + newquery += + std::to_string(stoll(parts[n*2], 0, 16) + 16*stoll(parts[n*2+1], 0, 16)); + newquery.append(1, '.'); + } + newquery += "in-addr.arpa."; + + DNSRecord rr; + rr.d_name = qname; + rr.d_type = QType::CNAME; + rr.d_content = std::make_shared(newquery); + ret.push_back(rr); + + int rcode = directResolve(DNSName(newquery), QType(QType::PTR), QClass::IN, ret); + + return rcode; +} + enum class PolicyResult : uint8_t { NoAction, HaveAnswer, Drop }; static PolicyResult handlePolicyHit(const DNSFilterEngine::Policy& appliedPolicy, const std::unique_ptr& dc, SyncRes& sr, int& res, vector& ret, DNSPacketWriter& pw) @@ -1392,7 +1479,12 @@ static void startDoResolve(void *p) } // if there is a RecursorLua active, and it 'took' the query in preResolve, we don't launch beginResolve - if(!t_pdl || !t_pdl->preresolve(dq, res)) { + if (!t_pdl || !t_pdl->preresolve(dq, res)) { + + if (!g_dns64PrefixReverse.empty() && dq.qtype == QType::PTR && dq.qname.isPartOf(g_dns64PrefixReverse)) { + res = getFakePTRRecords(dq.qname, ret); + goto haveAnswer; + } sr.setWantsRPZ(wantsRPZ); if (wantsRPZ && appliedPolicy.d_kind != DNSFilterEngine::PolicyKind::NoAction) { @@ -1445,21 +1537,34 @@ static void startDoResolve(void *p) } } - if(t_pdl) { - if(res == RCode::NoError) { - auto i=ret.cbegin(); - for(; i!= ret.cend(); ++i) - if(i->d_type == dc->d_mdp.d_qtype && i->d_place == DNSResourceRecord::ANSWER) - break; - if(i == ret.cend() && t_pdl->nodata(dq, res)) - shouldNotValidate = true; + if (t_pdl || g_dns64Prefix) { + if (res == RCode::NoError) { + auto i = ret.cbegin(); + for(; i!= ret.cend(); ++i) { + if (i->d_type == dc->d_mdp.d_qtype && i->d_place == DNSResourceRecord::ANSWER) { + break; + } + } + + if (i == ret.cend()) { + /* no record in the answer section, NODATA */ + if (t_pdl && t_pdl->nodata(dq, res)) { + shouldNotValidate = true; + } + else if (g_dns64Prefix && dq.qtype == QType::AAAA && dq.validationState != Bogus) { + res = getFakeAAAARecords(dq.qname, *g_dns64Prefix, ret); + shouldNotValidate = true; + } + } } - else if(res == RCode::NXDomain && t_pdl->nxdomain(dq, res)) + else if(res == RCode::NXDomain && t_pdl && t_pdl->nxdomain(dq, res)) { shouldNotValidate = true; + } - if(t_pdl->postresolve(dq, res)) + if (t_pdl && t_pdl->postresolve(dq, res)) { shouldNotValidate = true; + } } if (wantsRPZ) { //XXX This block is repeated, see above @@ -4205,6 +4310,20 @@ static int serviceMain(int argc, char*argv[]) g_proxyProtocolACL.toMasks(::arg()["proxy-protocol-from"]); g_proxyProtocolMaximumSize = ::arg().asNum("proxy-protocol-maximum-size"); + if (!::arg()["dns64-prefix"].empty()) { + auto dns64Prefix = Netmask(::arg()["dns64-prefix"]); + if (dns64Prefix.getBits() != 96) { + g_log << Logger::Error << "Invalid prefix for 'dns64-prefix', the current implementation only supports /96 prefixe: " << ::arg()["dns64-prefix"] << endl; + exit(1); + } + g_dns64Prefix = dns64Prefix.getNetwork(); + g_dns64PrefixReverse = reverseNameFromIP(*g_dns64Prefix); + /* /96 is 24 nibbles + 2 for "ip6.arpa." */ + while (g_dns64PrefixReverse.countLabels() > 26) { + g_dns64PrefixReverse.chopOff(); + } + } + g_networkTimeoutMsec = ::arg().asNum("network-timeout"); g_initialDomainMap = parseAuthAndForwards(); @@ -4930,6 +5049,8 @@ int main(int argc, char **argv) ::arg().set("proxy-protocol-from", "A Proxy Protocol header is only allowed from these subnets")=""; ::arg().set("proxy-protocol-maximum-size", "The maximum size of a proxy protocol payload, including the TLV values")="512"; + ::arg().set("dns64-prefix", "DNS64 prefix")=""; + ::arg().set("udp-source-port-min", "Minimum UDP port to bind on")="1024"; ::arg().set("udp-source-port-max", "Maximum UDP port to bind on")="65535"; ::arg().set("udp-source-port-avoid", "List of comma separated UDP port number to avoid")="11211"; diff --git a/pdns/recursordist/docs/dns64.rst b/pdns/recursordist/docs/dns64.rst index 9c5e609d63..a9f2b0de46 100644 --- a/pdns/recursordist/docs/dns64.rst +++ b/pdns/recursordist/docs/dns64.rst @@ -9,7 +9,8 @@ However, if ``example.com`` does not actually have an IPv6 address, what we do i We do this by retrieving the A records for ``www.example.com``, and translating them to AAAA records. Elsewhere, a NAT64 device listens on these IPv6 addresses, and extracts the IPv4 address from each packet, and proxies it on. -For maximum flexibility, DNS64 support is included in the :doc:`lua-scripting/index`. +As of 4.4.0, an efficient implementation is built the recursor and can be enabled via the using the :ref:`dns64-prefix setting `. +On earlier versions or for maximum flexibility, DNS64 support is included in the :doc:`lua-scripting/index`. This allows for example to hand out custom IPv6 gateway ranges depending on the location of the requestor, enabling the use of NAT64 services close to the user. Apart from faking AAAA records, it is also possible to also generate the associated PTR records. diff --git a/pdns/recursordist/docs/settings.rst b/pdns/recursordist/docs/settings.rst index fffda3d167..4680dfd54e 100644 --- a/pdns/recursordist/docs/settings.rst +++ b/pdns/recursordist/docs/settings.rst @@ -372,6 +372,20 @@ If `pdns-distributes-queries`_ is set, spawn this number of distributor threads handle incoming queries and distribute them to other threads based on a hash of the query, to maximize the cache hit ratio. +.. _setting-dns64-prefix: + +``dns64-prefix`` +---------------- +.. versionadded:: 4.4.0 + +- Netmask, as a string +- Default: None + +Enable DNS64 (:rfc:`6147`) support using the supplied /96 IPv6 prefix. This will generate 'fake' AAAA records for names +with only `A` records, as well as 'fake' PTR records to make sure that reverse lookup of DNS64-generated IPv6 addresses +generate the right name. +See :doc:`dns64` for more flexible but slower alternatives using Lua. + .. _setting-dnssec: ``dnssec`` diff --git a/pdns/syncres.hh b/pdns/syncres.hh index eaff86341e..87cc541c23 100644 --- a/pdns/syncres.hh +++ b/pdns/syncres.hh @@ -1092,6 +1092,8 @@ void distributeAsyncFunction(const std::string& question, const pipefunc_t& func int directResolve(const DNSName& qname, const QType& qtype, int qclass, vector& ret); int followCNAMERecords(std::vector& ret, const QType& qtype); +int getFakeAAAARecords(const DNSName& qname, ComboAddress prefix, vector& ret); +int getFakePTRRecords(const DNSName& qname, vector& ret); template T broadcastAccFunction(const boost::function& func); diff --git a/pdns/test-misc_hh.cc b/pdns/test-misc_hh.cc index 1522850c30..e655ce6b52 100644 --- a/pdns/test-misc_hh.cc +++ b/pdns/test-misc_hh.cc @@ -8,10 +8,13 @@ #include #include -#include "misc.hh" -#include "dns.hh" + #include -#include + +#include "dns.hh" +#include "iputils.hh" +#include "misc.hh" +#include "utility.hh" using std::string; @@ -202,5 +205,12 @@ BOOST_AUTO_TEST_CASE(test_rfc1982LessThan) { BOOST_CHECK(rfc1982check(UINT64_MAX/2, UINT64_MAX-10)); } -BOOST_AUTO_TEST_SUITE_END() +BOOST_AUTO_TEST_CASE(test_reverse_name_to_ip) +{ + static const ComboAddress v4("192.0.2.1"); + static const ComboAddress v6("2001:DB8::42"); + BOOST_CHECK_EQUAL(reverseNameFromIP(v4).toString(), "1.2.0.192.in-addr.arpa."); + BOOST_CHECK_EQUAL(reverseNameFromIP(v6).toString(), "2.4.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa."); +} +BOOST_AUTO_TEST_SUITE_END() diff --git a/regression-tests.recursor-dnssec/test_DNS64.py b/regression-tests.recursor-dnssec/test_DNS64.py new file mode 100644 index 0000000000..7038fe48f6 --- /dev/null +++ b/regression-tests.recursor-dnssec/test_DNS64.py @@ -0,0 +1,155 @@ +import dns +import os + +from recursortests import RecursorTest + +class DNS64RecursorTest(RecursorTest): + + _confdir = 'DNS64' + _config_template = """ + auth-zones=example.dns64=configs/%s/example.dns64.zone + auth-zones+=in-addr.arpa=configs/%s/in-addr.arpa.zone + auth-zones+=ip6.arpa=configs/%s/ip6.arpa.zone + + dns64-prefix=64:ff9b::/96 + """ % (_confdir, _confdir, _confdir) + + @classmethod + def setUpClass(cls): + + # we don't need all the auth stuff + cls.setUpSockets() + cls.startResponders() + + confdir = os.path.join('configs', cls._confdir) + cls.createConfigDir(confdir) + + cls.generateRecursorConfig(confdir) + cls.startRecursor(confdir, cls._recursorPort) + + @classmethod + def tearDownClass(cls): + cls.tearDownRecursor() + + @classmethod + def generateRecursorConfig(cls, confdir): + authzonepath = os.path.join(confdir, 'example.dns64.zone') + with open(authzonepath, 'w') as authzone: + authzone.write("""$ORIGIN example.dns64 +@ 3600 IN SOA {soa} +www 3600 IN A 192.0.2.42 +www 3600 IN TXT "does exist" +aaaa 3600 IN AAAA 2001:db8::1 +""".format(soa=cls._SOA)) + + authzonepath = os.path.join(confdir, 'in-addr.arpa.zone') + with open(authzonepath, 'w') as authzone: + authzone.write("""$ORIGIN in-addr.arpa +@ 3600 IN SOA {soa} +42.2.0.192 IN PTR www.example.dns64. +""".format(soa=cls._SOA)) + + authzonepath = os.path.join(confdir, 'ip6.arpa.zone') + with open(authzonepath, 'w') as authzone: + authzone.write("""$ORIGIN ip6.arpa +@ 3600 IN SOA {soa} +1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2 IN PTR aaaa.example.dns64. +""".format(soa=cls._SOA)) + + super(DNS64RecursorTest, cls).generateRecursorConfig(confdir) + + # this type (A) exists for this name + def testExistingA(self): + qname = 'www.example.dns64.' + expected = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'A', '192.0.2.42') + + query = dns.message.make_query(qname, 'A', want_dnssec=True) + for method in ("sendUDPQuery", "sendTCPQuery"): + sender = getattr(self, method) + res = sender(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertRRsetInAnswer(res, expected) + + # there is no A record, we should get a NODATA + def testNonExistingA(self): + qname = 'aaaa.example.dns64.' + + query = dns.message.make_query(qname, 'A', want_dnssec=True) + for method in ("sendUDPQuery", "sendTCPQuery"): + sender = getattr(self, method) + res = sender(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertEquals(len(res.answer), 0) + + # this type (AAAA) does not exist for this name but there is an A record, we should get a DNS64-wrapped AAAA + def testNonExistingAAAA(self): + qname = 'www.example.dns64.' + expected = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'AAAA', '64:ff9b::c000:22a') + + query = dns.message.make_query(qname, 'AAAA', want_dnssec=True) + for method in ("sendUDPQuery", "sendTCPQuery"): + sender = getattr(self, method) + res = sender(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertRRsetInAnswer(res, expected) + + # this type (AAAA) does not exist for this name and there is no A record either, we should get a NXDomain + def testNonExistingAAAA(self): + qname = 'nxd.example.dns64.' + + query = dns.message.make_query(qname, 'AAAA', want_dnssec=True) + for method in ("sendUDPQuery", "sendTCPQuery"): + sender = getattr(self, method) + res = sender(query) + self.assertRcodeEqual(res, dns.rcode.NXDOMAIN) + + # there is an AAAA record, we should get it + def testExistingAAAA(self): + qname = 'aaaa.example.dns64.' + expected = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'AAAA', '2001:db8::1') + + query = dns.message.make_query(qname, 'AAAA', want_dnssec=True) + for method in ("sendUDPQuery", "sendTCPQuery"): + sender = getattr(self, method) + res = sender(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertRRsetInAnswer(res, expected) + + # there is a TXT record, we should get it + def testExistingTXT(self): + qname = 'www.example.dns64.' + expected = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'TXT', '"does exist"') + + query = dns.message.make_query(qname, 'TXT', want_dnssec=True) + for method in ("sendUDPQuery", "sendTCPQuery"): + sender = getattr(self, method) + res = sender(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertRRsetInAnswer(res, expected) + + # the PTR records for the DNS64 prefix should be generated + def testNonExistingPTR(self): + qname = 'a.2.2.0.0.0.0.c.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.b.9.f.f.4.6.0.0.ip6.arpa.' + expectedCNAME = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'CNAME', '42.2.0.192.in-addr.arpa.') + expected = dns.rrset.from_text('42.2.0.192.in-addr.arpa.', 0, dns.rdataclass.IN, 'PTR', 'www.example.dns64.') + + query = dns.message.make_query(qname, 'PTR', want_dnssec=True) + for method in ("sendUDPQuery", "sendTCPQuery"): + sender = getattr(self, method) + res = sender(query) + print(res) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertRRsetInAnswer(res, expectedCNAME) + self.assertRRsetInAnswer(res, expected) + + # but not for other prefixes + def testExistingPTR(self): + qname = '1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.' + expected = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'PTR', 'aaaa.example.dns64.') + + query = dns.message.make_query(qname, 'PTR', want_dnssec=True) + for method in ("sendUDPQuery", "sendTCPQuery"): + sender = getattr(self, method) + res = sender(query) + self.assertRcodeEqual(res, dns.rcode.NOERROR) + self.assertRRsetInAnswer(res, expected) -- 2.47.2