]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
4742. [func] Synthesis of responses from DNSSEC-verified records.
authorMark Andrews <marka@isc.org>
Thu, 28 Sep 2017 05:16:26 +0000 (15:16 +1000)
committerMark Andrews <marka@isc.org>
Thu, 28 Sep 2017 05:16:26 +0000 (15:16 +1000)
                        Stage 2 - synthesis of records from wildcard data.
                        If the dns64 or filter-aaaa* is configured then the
                        involved lookups are currently excluded. [RT #40138]

CHANGES
bin/tests/system/synthfromdnssec/tests.sh
doc/arm/Bv9ARM-book.xml
doc/arm/notes.xml
doc/misc/options
lib/ns/query.c

diff --git a/CHANGES b/CHANGES
index 801c656160227326bf1c51007c33ee070574316b..f707525b9349b0a4c9a541ffeb2fb72938a89685 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,8 @@
+4742.  [func]          Synthesis of responses from DNSSEC-verified records.
+                       Stage 2 - synthesis of records from wildcard data.
+                       If the dns64 or filter-aaaa* is configured then the
+                       involved lookups are currently excluded. [RT #40138]
+
 4741.  [bug]           Make isc_refcount_current() atomically read the
                        counter value. [RT #46074]
 
index 645f63939f580e77c96b74346ee5f218e6ff6c24..4d9856378756af1dcd53cdfff32c75dcb00b2a17 100644 (file)
@@ -102,8 +102,8 @@ grep "flags:[^;]* ad[ ;]" dig.out.ns2.test$n > /dev/null || ret=1
 grep "status: NOERROR," dig.out.ns2.test$n > /dev/null || ret=1
 grep "b\.wild-a\.example\..*3600.IN.A" dig.out.ns2.test$n > /dev/null && ret=1
 n=`expr $n + 1`
-if [ $ret != 0 ]; then echo "I:failed (ignored - to be supported in the future)"; fi
-status=`expr $status + $ret`
+if [ $ret != 0 ]; then echo "I:failed"; fi
+status=`expr $status + $ret`
 
 echo "I:check synthesized wildcard CNAME response ($n)"
 ret=0
@@ -111,9 +111,10 @@ $DIG $DIGOPTS b.wild-cname.example. @10.53.0.2 a > dig.out.ns2.test$n || ret=1
 grep "flags:[^;]* ad[ ;]" dig.out.ns2.test$n > /dev/null || ret=1
 grep "status: NOERROR," dig.out.ns2.test$n > /dev/null || ret=1
 grep "b.wild-cname.example.*3600.IN.CNAME" dig.out.ns2.test$n > /dev/null && ret=1
+grep "ns1.example.*.IN.A" dig.out.ns2.test$n > /dev/null || ret=1
 n=`expr $n + 1`
-if [ $ret != 0 ]; then echo "I:failed (ignored - to be supported in the future)"; fi
-status=`expr $status + $ret`
+if [ $ret != 0 ]; then echo "I:failed"; fi
+status=`expr $status + $ret`
 
 echo "I:check redirect response (+dnssec) ($n)"
 ret=0
index e580be44cf52bbc518dd38026a06dfcdd5d9896c..4ef7013b9d56216d04713cb40470316203f79f78 100644 (file)
@@ -7207,16 +7207,6 @@ options {
                        option to be effective.
                      </para>
                    </listitem>
-                   <listitem>
-                     <para>
-                       This initial implementation only covers
-                       NXDOMAIN synthesis from NSEC records.
-                       Synthesis of NODATA and wildcard responses
-                       is also planned, as is synthesis from NSEC3
-                       records.  All of these will be controlled
-                       by <command>synth-from-dnssec</command>.
-                     </para>
-                   </listitem>
                  </itemizedlist>
                </para>
              </listitem>
index 2f17789b9a1d2b20dde6e49208c3f3e20fb3627c..105e5b1270658201f20c39862e33dd62bd91b637 100644 (file)
          default.
        </para>
        <para>
-         Note: This initial implementation can only synthesize NXDOMAIN
-         responses, from NSEC records.  Support for NODATA responses,
-         wilcard responses, and NSEC3 records will be added soon.
          Thanks to APNIC for sponsoring this work.
        </para>
       </listitem>
index 8d8cce89980adb83c92a842dd2e5b7a9ab0b9b65..ea2fdcc7826548ef467c23c4496d3e526e53b337 100644 (file)
@@ -168,9 +168,9 @@ options {
         fetches-per-server <integer> [ ( drop | fail ) ];
         fetches-per-zone <integer> [ ( drop | fail ) ];
         files ( default | unlimited | <sizeval> );
-        filter-aaaa { <address_match_element>; ... }; // not configured
-        filter-aaaa-on-v4 ( break-dnssec | <boolean> ); // not configured
-        filter-aaaa-on-v6 ( break-dnssec | <boolean> ); // not configured
+        filter-aaaa { <address_match_element>; ... };
+        filter-aaaa-on-v4 ( break-dnssec | <boolean> );
+        filter-aaaa-on-v6 ( break-dnssec | <boolean> );
         flush-zones-on-shutdown <boolean>;
         forward ( first | only );
         forwarders [ port <integer> ] [ dscp <integer> ] { ( <ipv4_address>
@@ -182,8 +182,8 @@ options {
         fstrm-set-output-queue-model ( mpsc | spsc ); // not configured
         fstrm-set-output-queue-size <integer>; // not configured
         fstrm-set-reopen-interval <integer>; // not configured
-        geoip-directory ( <quoted_string> | none ); // not configured
-        geoip-use-ecs <boolean>; // not configured
+        geoip-directory ( <quoted_string> | none );
+        geoip-use-ecs <boolean>;
         glue-cache <boolean>;
         has-old-clients <boolean>; // obsolete
         heartbeat-interval <integer>;
@@ -202,7 +202,7 @@ options {
         listen-on-v6 [ port <integer> ] [ dscp
             <integer> ] {
             <address_match_element>; ... }; // may occur multiple times
-        lmdb-mapsize <sizeval>; // non-operational
+        lmdb-mapsize <sizeval>;
         lock-file ( <quoted_string> | none );
         maintain-ixfr-base <boolean>; // obsolete
         managed-keys-directory <quoted_string>;
@@ -521,9 +521,9 @@ view <string> [ <class> ] {
         fetch-quota-params <integer> <fixedpoint> <fixedpoint> <fixedpoint>;
         fetches-per-server <integer> [ ( drop | fail ) ];
         fetches-per-zone <integer> [ ( drop | fail ) ];
-        filter-aaaa { <address_match_element>; ... }; // not configured
-        filter-aaaa-on-v4 ( break-dnssec | <boolean> ); // not configured
-        filter-aaaa-on-v6 ( break-dnssec | <boolean> ); // not configured
+        filter-aaaa { <address_match_element>; ... };
+        filter-aaaa-on-v4 ( break-dnssec | <boolean> );
+        filter-aaaa-on-v6 ( break-dnssec | <boolean> );
         forward ( first | only );
         forwarders [ port <integer> ] [ dscp <integer> ] { ( <ipv4_address>
             | <ipv6_address> ) [ port <integer> ] [ dscp <integer> ]; ... };
@@ -536,7 +536,7 @@ view <string> [ <class> ] {
         }; // may occur multiple times
         key-directory <quoted_string>;
         lame-ttl <ttlval>;
-        lmdb-mapsize <sizeval>; // non-operational
+        lmdb-mapsize <sizeval>;
         maintain-ixfr-base <boolean>; // obsolete
         managed-keys { <string> <string>
             <integer> <integer> <integer>
index 561e2730a483b24ed0a50679789b8b4b7a500069..a9c2486232011c2d3e24320734b39fd5320937bc 100644 (file)
@@ -240,7 +240,7 @@ rpz_ck_dnssec(ns_client_t *client, isc_result_t qresult,
 
 static void
 log_noexistnodata(void *val, int level, const char *fmt, ...)
-     ISC_FORMAT_PRINTF(3, 4);
+       ISC_FORMAT_PRINTF(3, 4);
 
 /*%
  * The structure and functions defined below implement the query logic
@@ -885,9 +885,8 @@ query_newrdataset(ns_client_t *client) {
        rdataset = NULL;
        result = dns_message_gettemprdataset(client->message, &rdataset);
        if (result != ISC_R_SUCCESS) {
-         CTRACE(ISC_LOG_DEBUG(3),
-                "query_newrdataset: "
-                "dns_message_gettemprdataset failed: done");
+               CTRACE(ISC_LOG_DEBUG(3), "query_newrdataset: "
+                      "dns_message_gettemprdataset failed: done");
                return (NULL);
        }
 
@@ -1952,7 +1951,7 @@ query_addadditional(void *arg, const dns_name_t *name, dns_rdatatype_t qtype) {
                                        dns_rdataset_disassociate(sigrdataset);
                        }
                }
 aaaa_lookup:
+ aaaa_lookup:
                if (query_isduplicate(client, fname, dns_rdatatype_aaaa, NULL))
                        goto addname;
                result = dns_db_findrdataset(db, node, version,
@@ -7811,7 +7810,7 @@ query_addds(query_ctx_t *qctx) {
        sigrdataset = NULL;
        return;
 
  addnsec3:
+ addnsec3:
        if (!dns_db_iszone(qctx->db))
                goto cleanup;
        /*
@@ -8340,6 +8339,272 @@ log_noexistnodata(void *val, int level, const char *fmt, ...) {
        va_end(ap);
 }
 
+/*
+ * Synthesize a wildcard answer using the contents of 'rdataset'.
+ * qctx contains the NODATA proof.
+ */
+static isc_result_t
+query_synthwildcard(query_ctx_t *qctx, dns_rdataset_t *rdataset,
+                   dns_rdataset_t *sigrdataset)
+{
+       dns_name_t *name = NULL;
+       isc_buffer_t *dbuf, b;
+       isc_result_t result;
+       dns_rdataset_t *clone = NULL, *sigclone = NULL;
+       dns_rdataset_t **sigrdatasetp;
+
+       /*
+        * We want the answer to be first, so save the
+        * NOQNAME proof's name now or else discard it.
+        */
+       if (WANTDNSSEC(qctx->client)) {
+               query_keepname(qctx->client, qctx->fname, qctx->dbuf);
+       } else {
+               query_releasename(qctx->client, &qctx->fname);
+       }
+
+       dbuf = query_getnamebuf(qctx->client);
+       if (dbuf == NULL) {
+               result = ISC_R_NOMEMORY;
+               goto cleanup;
+       }
+
+       name = query_newname(qctx->client, dbuf, &b);
+       if (name == NULL) {
+               result = ISC_R_NOMEMORY;
+               goto cleanup;
+       }
+       dns_name_clone(qctx->client->query.qname, name);
+
+       clone = query_newrdataset(qctx->client);
+       if (clone == NULL) {
+               result = ISC_R_NOMEMORY;
+               goto cleanup;
+       }
+       dns_rdataset_clone(rdataset, clone);
+
+       /*
+        * Add answer RRset. Omit the RRSIG if DNSSEC was not requested.
+        */
+       if (WANTDNSSEC(qctx->client)) {
+               sigclone = query_newrdataset(qctx->client);
+               if (sigclone == NULL) {
+                       result = ISC_R_NOMEMORY;
+                       goto cleanup;
+               }
+               dns_rdataset_clone(sigrdataset, sigclone);
+               sigrdatasetp = &sigclone;
+       } else {
+               sigrdatasetp = NULL;
+       }
+
+       query_addrrset(qctx->client, &name, &clone, sigrdatasetp,
+                      dbuf, DNS_SECTION_ANSWER);
+
+       if (WANTDNSSEC(qctx->client)) {
+               /*
+                * Add NODATA proof.
+                */
+               query_addrrset(qctx->client, &qctx->fname,
+                              &qctx->rdataset, &qctx->sigrdataset,
+                              NULL, DNS_SECTION_AUTHORITY);
+       }
+
+       result = ISC_R_SUCCESS;
+       inc_stats(qctx->client, ns_statscounter_wildcardsynth);
+
+cleanup:
+       if (name != NULL) {
+               query_releasename(qctx->client, &name);
+       }
+       if (clone != NULL) {
+               query_putrdataset(qctx->client, &clone);
+       }
+       if (sigclone != NULL) {
+               query_putrdataset(qctx->client, &sigclone);
+       }
+       return (result);
+}
+
+/*
+ * Add a synthesised CNAME record from the wildard RRset (rdataset)
+ * and NODATA proof by calling query_synthwildcard then setup to
+ * follow the CNAME.
+ */
+static isc_result_t
+query_synthcnamewildcard(query_ctx_t *qctx, dns_rdataset_t *rdataset,
+                        dns_rdataset_t *sigrdataset)
+{
+       isc_result_t result;
+       dns_name_t *tname = NULL;
+       dns_rdata_t rdata = DNS_RDATA_INIT;
+       dns_rdata_cname_t cname;
+
+       result = query_synthwildcard(qctx, rdataset, sigrdataset);
+       qctx->client->query.attributes |= NS_QUERYATTR_PARTIALANSWER;
+
+       /*
+        * Reset qname to be the target name of the CNAME and restart
+        * the query.
+        */
+       result = dns_message_gettempname(qctx->client->message, &tname);
+       if (result != ISC_R_SUCCESS) {
+               return (result);
+       }
+
+       result = dns_rdataset_first(rdataset);
+       if (result != ISC_R_SUCCESS) {
+               dns_message_puttempname(qctx->client->message, &tname);
+               return (result);
+       }
+
+       dns_rdataset_current(rdataset, &rdata);
+       result = dns_rdata_tostruct(&rdata, &cname, NULL);
+       dns_rdata_reset(&rdata);
+       if (result != ISC_R_SUCCESS) {
+               dns_message_puttempname(qctx->client->message, &tname);
+               return (result);
+       }
+
+       dns_name_init(tname, NULL);
+       result = dns_name_dup(&cname.cname, qctx->client->mctx, tname);
+       if (result != ISC_R_SUCCESS) {
+               dns_message_puttempname(qctx->client->message, &tname);
+               dns_rdata_freestruct(&cname);
+               return (result);
+       }
+
+       dns_rdata_freestruct(&cname);
+       ns_client_qnamereplace(qctx->client, tname);
+       qctx->want_restart = ISC_TRUE;
+       if (!WANTRECURSION(qctx->client)) {
+               qctx->options |= DNS_GETDB_NOLOG;
+       }
+
+       return (result);
+}
+
+/*
+ * Synthesise a NXDOMAIN response from qctx (which contains the
+ * NODATA proof), nowild + rdataset + sigrdataset (which contains
+ * the NOWILDCARD proof) and signer + soardatasetp + sigsoardatasetp
+ * which contain the SOA record + RRSIG for the negative answer.
+ */
+static isc_result_t
+query_synthnxdomain(query_ctx_t *qctx,
+                   dns_name_t *nowild,
+                   dns_rdataset_t *rdataset,
+                   dns_rdataset_t *sigrdataset,
+                   dns_name_t *signer,
+                   dns_rdataset_t **soardatasetp,
+                   dns_rdataset_t **sigsoardatasetp)
+{
+       dns_name_t *name = NULL;
+       dns_ttl_t ttl;
+       isc_buffer_t *dbuf, b;
+       isc_result_t result;
+       dns_rdataset_t *clone = NULL, *sigclone = NULL;
+
+       /*
+        * Detemine the correct TTL to use for the SOA and RRSIG
+        */
+       ttl = ISC_MIN(qctx->rdataset->ttl, qctx->sigrdataset->ttl);
+       ttl = ISC_MIN(ttl, rdataset->ttl);
+       ttl = ISC_MIN(ttl, sigrdataset->ttl);
+       ttl = ISC_MIN(ttl, (*soardatasetp)->ttl);
+       ttl = ISC_MIN(ttl, (*sigsoardatasetp)->ttl);
+
+       (*soardatasetp)->ttl = (*sigsoardatasetp)->ttl = ttl;
+
+       /*
+        * We want the SOA record to be first, so save the
+        * NOQNAME proof's name now or else discard it.
+        */
+       if (WANTDNSSEC(qctx->client)) {
+               query_keepname(qctx->client, qctx->fname, qctx->dbuf);
+       } else {
+               query_releasename(qctx->client, &qctx->fname);
+       }
+
+       dbuf = query_getnamebuf(qctx->client);
+       if (dbuf == NULL) {
+               result = ISC_R_NOMEMORY;
+               goto cleanup;
+       }
+
+       name = query_newname(qctx->client, dbuf, &b);
+       if (name == NULL) {
+               result = ISC_R_NOMEMORY;
+               goto cleanup;
+       }
+
+       dns_name_clone(signer, name);
+
+       /*
+        * Add SOA record. Omit the RRSIG if DNSSEC was not requested.
+        */
+       if (!WANTDNSSEC(qctx->client)) {
+               sigsoardatasetp = NULL;
+       }
+       query_addrrset(qctx->client, &name, soardatasetp, sigsoardatasetp,
+                      dbuf, DNS_SECTION_AUTHORITY);
+
+       if (WANTDNSSEC(qctx->client)) {
+               /*
+                * Add NODATA proof.
+                */
+               query_addrrset(qctx->client, &qctx->fname,
+                              &qctx->rdataset, &qctx->sigrdataset,
+                              NULL, DNS_SECTION_AUTHORITY);
+
+               dbuf = query_getnamebuf(qctx->client);
+               if (dbuf == NULL) {
+                       result = ISC_R_NOMEMORY;
+                       goto cleanup;
+               }
+
+               name = query_newname(qctx->client, dbuf, &b);
+               if (name == NULL) {
+                       result = ISC_R_NOMEMORY;
+                       goto cleanup;
+               }
+
+               dns_name_clone(nowild, name);
+
+               clone = query_newrdataset(qctx->client);
+               sigclone = query_newrdataset(qctx->client);
+               if (clone == NULL || sigclone == NULL) {
+                       result = ISC_R_NOMEMORY;
+                       goto cleanup;
+               }
+
+               dns_rdataset_clone(rdataset, clone);
+               dns_rdataset_clone(sigrdataset, sigclone);
+
+               /*
+                * Add NOWILDCARD proof.
+                */
+               query_addrrset(qctx->client, &name, &clone, &sigclone,
+                              dbuf, DNS_SECTION_AUTHORITY);
+       }
+
+       qctx->client->message->rcode = dns_rcode_nxdomain;
+       result = ISC_R_SUCCESS;
+       inc_stats(qctx->client, ns_statscounter_nxdomainsynth);
+
+cleanup:
+       if (name != NULL) {
+               query_releasename(qctx->client, &name);
+       }
+       if (clone != NULL) {
+               query_putrdataset(qctx->client, &clone);
+       }
+       if (sigclone != NULL) {
+               query_putrdataset(qctx->client, &sigclone);
+       }
+       return (result);
+}
+
 /*%
  * Handle covering NSEC responses.
  *
@@ -8373,15 +8638,11 @@ query_coveringnsec(query_ctx_t *qctx) {
        dns_name_t *nowild = NULL;
        dns_name_t *signer = NULL;
        dns_name_t *wild = NULL;
-       dns_rdataset_t **sigsoardatasetp = NULL;
-       dns_rdataset_t *clone = NULL, *sigclone = NULL;
        dns_rdataset_t *soardataset = NULL, *sigsoardataset = NULL;
        dns_rdataset_t rdataset, sigrdataset;
-       dns_ttl_t ttl;
        isc_boolean_t done = ISC_FALSE;
        isc_boolean_t exists = ISC_TRUE, data = ISC_TRUE;
        isc_boolean_t redirected = ISC_FALSE;
-       isc_buffer_t *dbuf = NULL, b;
        isc_result_t result = ISC_R_SUCCESS;
        unsigned int dboptions = qctx->client->query.dboptions;
 
@@ -8454,17 +8715,61 @@ query_coveringnsec(query_ctx_t *qctx) {
                goto cleanup;
        }
 
+       /*
+        * Zero TTL handling of wildcard record.
+        *
+        * We don't yet have code to handle synthesis and type ANY,
+        * AAAA filtering or dns64 processing so we abort the
+        * synthesis here if there would be a interaction.
+        */
+       switch (result) {
+       case ISC_R_SUCCESS:
+               if (qctx->type == dns_rdatatype_any) {  /* XXX not yet */
+                       goto cleanup;
+               }
+#ifdef ALLOW_FILTER_AAAA
+               if (qctx->client->filter_aaaa != dns_aaaa_ok &&
+                   (qctx->type == dns_rdatatype_a ||
+                    qctx->type == dns_rdatatype_aaaa)) /* XXX not yet */
+               {
+                       goto cleanup;
+               }
+#endif
+               if (!ISC_LIST_EMPTY(qctx->client->view->dns64) &&
+                   (qctx->type == dns_rdatatype_a ||
+                    qctx->type == dns_rdatatype_aaaa)) /* XXX not yet */
+               {
+                       goto cleanup;
+               }
+       case DNS_R_CNAME:
+               if (!qctx->resuming && !STALE(&rdataset) &&
+                   rdataset.ttl == 0 && RECURSIONOK(qctx->client))
+               {
+                       goto cleanup;
+               }
+       default:
+               break;
+       }
+
        switch (result) {
        case DNS_R_COVERINGNSEC:
                result = dns_nsec_noexistnodata(qctx->qtype, wild,
                                                nowild, &rdataset,
                                                &exists, &data, NULL,
                                                log_noexistnodata, qctx);
-               if (result != ISC_R_SUCCESS || exists)
+               if (result != ISC_R_SUCCESS || exists) {
                        goto cleanup;
+               }
                break;
        case ISC_R_SUCCESS:             /* wild card match */
+               (void)query_synthwildcard(qctx, &rdataset, &sigrdataset);
+               done = ISC_TRUE;
+               goto cleanup;
        case DNS_R_CNAME:               /* wild card cname */
+               (void)query_synthcnamewildcard(qctx, &rdataset,
+                                                 &sigrdataset);
+               done = ISC_TRUE;
+               goto cleanup;
        case DNS_R_NCACHENXRRSET:       /* wild card nodata */
        case DNS_R_NCACHENXDOMAIN:      /* direct nxdomain */
        default:
@@ -8522,101 +8827,16 @@ query_coveringnsec(query_ctx_t *qctx) {
                                qctx->client->now, &node,
                                fname, &cm, &ci, soardataset,
                                sigsoardataset);
-       if (result != ISC_R_SUCCESS) {
-               goto cleanup;
-       }
-
-       qctx->client->message->rcode = dns_rcode_nxdomain;
-
-       /*
-        * Detemine the correct TTL to use for the SOA and RRSIG
-        */
-       ttl = ISC_MIN(qctx->rdataset->ttl, qctx->sigrdataset->ttl);
-       ttl = ISC_MIN(ttl, rdataset.ttl);
-       ttl = ISC_MIN(ttl, sigrdataset.ttl);
-       ttl = ISC_MIN(ttl, soardataset->ttl);
-       ttl = ISC_MIN(ttl, sigsoardataset->ttl);
-
-       soardataset->ttl = sigsoardataset->ttl = ttl;
-
-       /*
-        * We want the SOA record to be first, so save the
-        * NOQNAME proof's name now or else discard it.
-        */
-       if (WANTDNSSEC(qctx->client)) {
-               query_keepname(qctx->client, qctx->fname, qctx->dbuf);
-       } else {
-               query_releasename(qctx->client, &qctx->fname);
-       }
 
-       dbuf = query_getnamebuf(qctx->client);
-       if (dbuf == NULL) {
-               goto cleanup;
-       }
-
-       name = query_newname(qctx->client, dbuf, &b);
-       if (name == NULL) {
+       if (result != ISC_R_SUCCESS) {
                goto cleanup;
        }
 
-       dns_name_clone(signer, name);
-
-       /*
-        * Add SOA record. Omit the RRSIG if DNSSEC was not requested.
-        */
-       if (WANTDNSSEC(qctx->client)) {
-               sigsoardatasetp = &sigsoardataset;
-       }
-       query_addrrset(qctx->client, &name, &soardataset, sigsoardatasetp,
-                      dbuf, DNS_SECTION_AUTHORITY);
-
-       if (WANTDNSSEC(qctx->client)) {
-               /*
-                * Add NODATA proof.
-                */
-               query_addrrset(qctx->client, &qctx->fname,
-                              &qctx->rdataset, &qctx->sigrdataset,
-                              NULL, DNS_SECTION_AUTHORITY);
-
-               dbuf = query_getnamebuf(qctx->client);
-               if (dbuf == NULL) {
-                       goto cleanup;
-               }
-
-               name = query_newname(qctx->client, dbuf, &b);
-               if (name == NULL) {
-                       goto cleanup;
-               }
-
-               dns_name_clone(nowild, name);
-
-               clone = query_newrdataset(qctx->client);
-               sigclone = query_newrdataset(qctx->client);
-               if (clone == NULL || sigclone == NULL) {
-                       goto cleanup;
-               }
-
-               dns_rdataset_clone(&rdataset, clone);
-               dns_rdataset_clone(&sigrdataset, sigclone);
-
-               /*
-                * Add NOWILDCARD proof.
-                */
-               query_addrrset(qctx->client, &name, &clone, &sigclone,
-                              dbuf, DNS_SECTION_AUTHORITY);
-       }
-
-       inc_stats(qctx->client, ns_statscounter_nxdomainsynth);
-
+       (void)query_synthnxdomain(qctx, nowild, &rdataset, &sigrdataset,
+                                 signer, &soardataset, &sigsoardataset);
        done = ISC_TRUE;
 
  cleanup:
-       if (clone != NULL) {
-               query_putrdataset(qctx->client, &clone);
-       }
-       if (sigclone != NULL) {
-               query_putrdataset(qctx->client, &sigclone);
-       }
        if (dns_rdataset_isassociated(&rdataset)) {
                dns_rdataset_disassociate(&rdataset);
        }