]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
rec: Implement native DNS64 support, without Lua
authorRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 23 Mar 2020 14:47:10 +0000 (15:47 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 23 Mar 2020 14:47:10 +0000 (15:47 +0100)
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
pdns/misc.cc
pdns/misc.hh
pdns/pdns_recursor.cc
pdns/recursordist/docs/dns64.rst
pdns/recursordist/docs/settings.rst
pdns/syncres.hh
pdns/test-misc_hh.cc
regression-tests.recursor-dnssec/test_DNS64.py [new file with mode: 0644]

index 3b13bd464db95c5b0d12197a78ee53173154a90c..61c28155dc6fe563141409ecc9a3cbc1ae137949 100644 (file)
 
 RecursorLua4::RecursorLua4() { prepareContext(); }
 
-static int getFakeAAAARecords(const DNSName& qname, const std::string& prefix, vector<DNSRecord>& ret)
-{
-  int rcode=directResolve(qname, QType(QType::A), 1, ret);
-
-  ComboAddress prefixAddress(prefix);
-
-  // Remove double CNAME records
-  std::set<DNSName> seenCNAMEs;
-  ret.erase(std::remove_if(
-        ret.begin(),
-        ret.end(),
-        [&seenCNAMEs](DNSRecord& rr) {
-          if (rr.d_type == QType::CNAME) {
-            auto target = getRR<CNAMERecordContent>(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<ARecordContent>(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<AAAARecordContent>(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<DNSRecord>& 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<string> 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<CNAMERecordContent>(newquery);
-  ret.push_back(rr);
-
-  int rcode = directResolve(DNSName(newquery), QType(QType::PTR), 1, ret);
-
-  return rcode;
-
-}
-
 boost::optional<dnsheader> 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);
index 279151116a9ee6cf2d85cbd7c1d7c1f4473cc2da..af33b56e3eb0f5009c728623872d695bdadd6d5c 100644 (file)
@@ -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<const uint8_t*>(&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<const uint8_t*>(&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");
+}
index b4924c427468f4130f73956ccb58802ebc25f919..943f60e3b254590d14898f5800e796b365064ef8 100644 (file)
@@ -609,3 +609,5 @@ bool isSettingThreadCPUAffinitySupported();
 int mapThreadToCPUList(pthread_t tid, const std::set<int>& cpus);
 
 std::vector<ComboAddress> getResolvers(const std::string& resolvConfPath);
+
+DNSName reverseNameFromIP(const ComboAddress& ip);
index bfaf00ba43a7476b8caabd5c843811e22fed27d7..e76e0713581048397f4f400cfcf61acc96d70857 100644 (file)
@@ -199,6 +199,8 @@ static std::shared_ptr<SyncRes::domainmap_t> g_initialDomainMap; // new threads
 static std::shared_ptr<NetmaskGroup> g_initialAllowFrom; // new thread needs to be setup with this
 static NetmaskGroup g_XPFAcl;
 static NetmaskGroup g_proxyProtocolACL;
+static boost::optional<ComboAddress> 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<DNSRecord>& ret, const QType& qtype)
   return rcode;
 }
 
+int getFakeAAAARecords(const DNSName& qname, ComboAddress prefix, vector<DNSRecord>& ret)
+{
+  int rcode = directResolve(qname, QType(QType::A), QClass::IN, ret);
+
+  // Remove double CNAME records
+  std::set<DNSName> seenCNAMEs;
+  ret.erase(std::remove_if(
+        ret.begin(),
+        ret.end(),
+        [&seenCNAMEs](DNSRecord& rr) {
+          if (rr.d_type == QType::CNAME) {
+            auto target = getRR<CNAMERecordContent>(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<ARecordContent>(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<AAAARecordContent>(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<DNSRecord>& 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<string> 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<CNAMERecordContent>(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<DNSComboWriter>& dc, SyncRes& sr, int& res, vector<DNSRecord>& 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";
index 9c5e609d6379c5fafe07bcb48828480a1f4103cb..a9f2b0de464e34068ac17287995301b5925fb85c 100644 (file)
@@ -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 <setting-dns64-prefix>`.
+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.
index fffda3d167cbe66b615d764ebeb8f1b5ea827412..4680dfd54e9b77c9d912aa355536a0d0a51834b3 100644 (file)
@@ -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``
index eaff86341e09dbcc6c89aa55c7c1338dc6d78855..87cc541c23e024179f1be4c2adcee8cdbba46fe7 100644 (file)
@@ -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<DNSRecord>& ret);
 int followCNAMERecords(std::vector<DNSRecord>& ret, const QType& qtype);
+int getFakeAAAARecords(const DNSName& qname, ComboAddress prefix, vector<DNSRecord>& ret);
+int getFakePTRRecords(const DNSName& qname, vector<DNSRecord>& ret);
 
 template<class T> T broadcastAccFunction(const boost::function<T*()>& func);
 
index 1522850c3089a332a697415f52cee61c1efdc55c..e655ce6b52aeced4aad7aa07bc8ddd718300a42c 100644 (file)
@@ -8,10 +8,13 @@
 #include <boost/assign/list_of.hpp>
 
 #include <boost/tuple/tuple.hpp>
-#include "misc.hh"
-#include "dns.hh"
+
 #include <arpa/inet.h>
-#include <utility>
+
+#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_t>(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 (file)
index 0000000..7038fe4
--- /dev/null
@@ -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)