]> git.ipfire.org Git - thirdparty/unbound.git/commitdiff
- Fix RPZ removal of client-ip, nsip, nsdname triggers from IXFR.
authorW.C.A. Wijngaards <wouter@nlnetlabs.nl>
Fri, 19 May 2023 12:38:41 +0000 (14:38 +0200)
committerW.C.A. Wijngaards <wouter@nlnetlabs.nl>
Fri, 19 May 2023 12:38:41 +0000 (14:38 +0200)
doc/Changelog
services/authzone.c
services/rpz.c
services/rpz.h
testdata/rpz_ixfr.rpl

index 8923aa857e0162dad50f7e645514c99ef470ab65..8d07f417d0e979b1181e9ccc164fbf3999a1248a 100644 (file)
@@ -1,3 +1,6 @@
+19 May 2023: Wouter
+       - Fix RPZ removal of client-ip, nsip, nsdname triggers from IXFR.
+
 16 May 2023: Wouter
        - Fix #888: [FR] Use kernel timestamps for dnstap.
        - Fix to print debug log for ancillary data with correct IP address.
index 3898767c7e057eb6fd9fec2c4c420587b756647b..31ab510e25ba30a96b79cbd3d892ab7459185319 100644 (file)
@@ -1306,8 +1306,8 @@ az_remove_rr(struct auth_zone* z, uint8_t* rr, size_t rr_len,
                auth_data_delete(node);
        }
        if(z->rpz) {
-               rpz_remove_rr(z->rpz, z->namelen, dname, dname_len, rr_type,
-                       rr_class, rdata, rdatalen);
+               rpz_remove_rr(z->rpz, z->name, z->namelen, dname, dname_len,
+                       rr_type, rr_class, rdata, rdatalen);
        }
        return 1;
 }
index e876f3f94834d32af40389adab3f891270f414aa..d0931f44b1d3af9a9148ad3d60f8aeb827dd819a 100644 (file)
@@ -1188,6 +1188,22 @@ rpz_find_zone(struct local_zones* zones, uint8_t* qname, size_t qname_len, uint1
        return z;
 }
 
+/** Find entry for RR type in the list of rrsets for the clientip. */
+static struct local_rrset*
+rpz_find_synthesized_rrset(uint16_t qtype,
+       struct clientip_synthesized_rr* data)
+{
+       struct local_rrset* cursor = data->data;
+       while( cursor != NULL) {
+               struct packed_rrset_key* packed_rrset = &cursor->rrset->rk;
+               if(htons(qtype) == packed_rrset->type) {
+                       return cursor;
+               }
+               cursor = cursor->next;
+       }
+       return NULL;
+}
+
 /**
  * Remove RR from RPZ's local-data
  * @param z: local-zone for RPZ, holding write lock
@@ -1270,15 +1286,15 @@ rpz_rrset_delete_rr(struct resp_addr* raddr, uint16_t rr_type, uint8_t* rdata,
 
 }
 
-/** Remove RR from RPZ's local-zone */
+/** Remove RR from rpz localzones structure */
 static void
-rpz_remove_qname_trigger(struct rpz* r, uint8_t* dname, size_t dnamelen,
-       enum rpz_action a, uint16_t rr_type, uint16_t rr_class,
-       uint8_t* rdatawl, size_t rdatalen)
+rpz_remove_local_zones_trigger(struct local_zones* zones, uint8_t* dname,
+       size_t dnamelen, enum rpz_action a, uint16_t rr_type,
+       uint16_t rr_class, uint8_t* rdatawl, size_t rdatalen)
 {
        struct local_zone* z;
        int delete_zone = 1;
-       z = rpz_find_zone(r->local_zones, dname, dnamelen, rr_class,
+       z = rpz_find_zone(zones, dname, dnamelen, rr_class,
                1 /* only exact */, 1 /* wr lock */, 1 /* keep lock*/);
        if(!z) {
                verbose(VERB_ALGO, "rpz: cannot remove RR from IXFR, "
@@ -1290,15 +1306,24 @@ rpz_remove_qname_trigger(struct rpz* r, uint8_t* dname, size_t dnamelen,
                        dnamelen, rr_type, rdatawl, rdatalen);
        else if(a != localzone_type_to_rpz_action(z->type)) {
                lock_rw_unlock(&z->lock);
-               lock_rw_unlock(&r->local_zones->lock);
+               lock_rw_unlock(&zones->lock);
                return;
        }
        lock_rw_unlock(&z->lock); 
        if(delete_zone) {
-               local_zones_del_zone(r->local_zones, z);
+               local_zones_del_zone(zones, z);
        }
-       lock_rw_unlock(&r->local_zones->lock); 
-       return;
+       lock_rw_unlock(&zones->lock);
+}
+
+/** Remove RR from RPZ's local-zone */
+static void
+rpz_remove_qname_trigger(struct rpz* r, uint8_t* dname, size_t dnamelen,
+       enum rpz_action a, uint16_t rr_type, uint16_t rr_class,
+       uint8_t* rdatawl, size_t rdatalen)
+{
+       rpz_remove_local_zones_trigger(r->local_zones, dname, dnamelen,
+               a, rr_type, rr_class, rdatawl, rdatalen);
 }
 
 static void
@@ -1335,15 +1360,159 @@ rpz_remove_response_ip_trigger(struct rpz* r, uint8_t* dname, size_t dnamelen,
        lock_rw_unlock(&r->respip_set->lock);
 }
 
+/** find and remove type from list of local_rrset entries*/
+static void
+del_local_rrset_from_list(struct local_rrset** list_head, uint16_t dtype)
+{
+       struct local_rrset* prev=NULL, *p=*list_head;
+       while(p && ntohs(p->rrset->rk.type) != dtype) {
+               prev = p;
+               p = p->next;
+       }
+       if(!p)
+               return; /* rrset type not found */
+       /* unlink it */
+       if(prev) prev->next = p->next;
+       else *list_head = p->next;
+       /* no memory recycling for zone deletions ... */
+}
+
+/** Delete client-ip trigger RR from its RRset and perhaps also the rrset
+ * from the linked list. Returns if the local data is empty and the node can
+ * be deleted too, or not. */
+static int rpz_remove_clientip_rr(struct clientip_synthesized_rr* node,
+       uint16_t rr_type, uint8_t* rdatawl, size_t rdatalen)
+{
+       struct local_rrset* rrset;
+       struct packed_rrset_data* d;
+       size_t index;
+       rrset = rpz_find_synthesized_rrset(rr_type, node);
+       if(rrset == NULL)
+               return 0; /* type not found, ignore */
+       d = (struct packed_rrset_data*)rrset->rrset->entry.data;
+       if(!packed_rrset_find_rr(d, rdatawl, rdatalen, &index))
+               return 0; /* RR not found, ignore */
+       if(d->count == 1) {
+               /* regional alloc'd */
+               /* delete the type entry from the list */
+               del_local_rrset_from_list(&node->data, rr_type);
+               /* if the list is empty, the node can be removed too */
+               if(node->data == NULL)
+                       return 1;
+       } else if (d->count > 1) {
+               if(!local_rrset_remove_rr(d, index))
+                       return 0;
+       }
+       return 0;
+}
+
+/** remove trigger RR from clientip_syntheized set tree. */
+static void
+rpz_clientip_remove_trigger_rr(struct clientip_synthesized_rrset* set,
+       struct sockaddr_storage* addr, socklen_t addrlen, int net,
+       enum rpz_action a, uint16_t rr_type, uint8_t* rdatawl, size_t rdatalen)
+{
+       struct clientip_synthesized_rr* node;
+       int delete_node = 1;
+
+       lock_rw_wrlock(&set->lock);
+       node = (struct clientip_synthesized_rr*)addr_tree_find(&set->entries,
+               addr, addrlen, net);
+       if(node == NULL) {
+               /* netblock not found */
+               verbose(VERB_ALGO, "rpz: cannot remove RR from IXFR, "
+                       "RPZ address, netblock not found");
+               lock_rw_unlock(&set->lock);
+               return;
+       }
+       lock_rw_wrlock(&node->lock);
+       if(a == RPZ_LOCAL_DATA_ACTION) {
+               /* remove RR, signal whether entry can be removed */
+               delete_node = rpz_remove_clientip_rr(node, rr_type, rdatawl,
+                       rdatalen);
+       } else if(a != node->action) {
+               /* ignore the RR with different action specification */
+               delete_node = 0;
+       }
+       if(delete_node) {
+               rbtree_delete(&set->entries, node->node.node.key);
+       }
+       lock_rw_unlock(&set->lock);
+       lock_rw_unlock(&node->lock);
+       if(delete_node) {
+               lock_rw_destroy(&node->lock);
+       }
+}
+
+/** Remove clientip trigger RR from RPZ. */
+static void
+rpz_remove_clientip_trigger(struct rpz* r, uint8_t* dname, size_t dnamelen,
+       enum rpz_action a, uint16_t rr_type, uint8_t* rdatawl, size_t rdatalen)
+{
+       struct sockaddr_storage addr;
+       socklen_t addrlen;
+       int net, af;
+       if(a == RPZ_INVALID_ACTION)
+               return;
+       if(!netblockdnametoaddr(dname, dnamelen, &addr, &addrlen, &net, &af))
+               return;
+       rpz_clientip_remove_trigger_rr(r->client_set, &addr, addrlen, net,
+               a, rr_type, rdatawl, rdatalen);
+}
+
+/** Remove nsip trigger RR from RPZ. */
+static void
+rpz_remove_nsip_trigger(struct rpz* r, uint8_t* dname, size_t dnamelen,
+       enum rpz_action a, uint16_t rr_type, uint8_t* rdatawl, size_t rdatalen)
+{
+       struct sockaddr_storage addr;
+       socklen_t addrlen;
+       int net, af;
+       if(a == RPZ_INVALID_ACTION)
+               return;
+       if(!netblockdnametoaddr(dname, dnamelen, &addr, &addrlen, &net, &af))
+               return;
+       rpz_clientip_remove_trigger_rr(r->ns_set, &addr, addrlen, net,
+               a, rr_type, rdatawl, rdatalen);
+}
+
+/** Remove nsdname trigger RR from RPZ. */
+static void
+rpz_remove_nsdname_trigger(struct rpz* r, uint8_t* dname, size_t dnamelen,
+       enum rpz_action a, uint16_t rr_type, uint16_t rr_class,
+       uint8_t* rdatawl, size_t rdatalen)
+{
+       uint8_t* dname_stripped = NULL;
+       size_t dnamelen_stripped = 0;
+       if(a == RPZ_INVALID_ACTION)
+               return;
+       if(!rpz_strip_nsdname_suffix(dname, dnamelen, &dname_stripped,
+               &dnamelen_stripped))
+               return;
+       rpz_remove_local_zones_trigger(r->nsdname_zones, dname_stripped,
+               dnamelen_stripped, a, rr_type, rr_class, rdatawl, rdatalen);
+       free(dname_stripped);
+}
+
 void
-rpz_remove_rr(struct rpz* r, size_t aznamelen, uint8_t* dname, size_t dnamelen,
-       uint16_t rr_type, uint16_t rr_class, uint8_t* rdatawl, size_t rdatalen)
+rpz_remove_rr(struct rpz* r, uint8_t* azname, size_t aznamelen, uint8_t* dname,
+       size_t dnamelen, uint16_t rr_type, uint16_t rr_class, uint8_t* rdatawl,
+       size_t rdatalen)
 {
        size_t policydnamelen;
        enum rpz_trigger t;
        enum rpz_action a;
        uint8_t* policydname;
 
+       if(rpz_type_ignored(rr_type)) {
+               /* this rpz action is not valid, eg. this is the SOA or NS RR */
+               return;
+       }
+       if(!dname_subdomain_c(dname, azname)) {
+               /* not subdomain of the RPZ zone. */
+               return;
+       }
+
        if(!(policydname = calloc(1, LDNS_MAX_DOMAINLEN + 1)))
                return;
 
@@ -1358,13 +1527,28 @@ rpz_remove_rr(struct rpz* r, size_t aznamelen, uint8_t* dname, size_t dnamelen,
                return;
        }
        t = rpz_dname_to_trigger(policydname, policydnamelen);
+       if(t == RPZ_INVALID_TRIGGER) {
+               /* skipping invalid trigger */
+               free(policydname);
+               return;
+       }
        if(t == RPZ_QNAME_TRIGGER) {
                rpz_remove_qname_trigger(r, policydname, policydnamelen, a,
                        rr_type, rr_class, rdatawl, rdatalen);
        } else if(t == RPZ_RESPONSE_IP_TRIGGER) {
                rpz_remove_response_ip_trigger(r, policydname, policydnamelen,
                        a, rr_type, rdatawl, rdatalen);
+       } else if(t == RPZ_CLIENT_IP_TRIGGER) {
+               rpz_remove_clientip_trigger(r, policydname, policydnamelen, a,
+                       rr_type, rdatawl, rdatalen);
+       } else if(t == RPZ_NSIP_TRIGGER) {
+               rpz_remove_nsip_trigger(r, policydname, policydnamelen, a,
+                       rr_type, rdatawl, rdatalen);
+       } else if(t == RPZ_NSDNAME_TRIGGER) {
+               rpz_remove_nsdname_trigger(r, policydname, policydnamelen, a,
+                       rr_type, rr_class, rdatawl, rdatalen);
        }
+       /* else it was an unsupported trigger, also skipped. */
        free(policydname);
 }
 
@@ -1563,21 +1747,6 @@ rpz_local_encode(struct module_env* env, struct query_info* qinfo,
        return 1;
 }
 
-static struct local_rrset*
-rpz_find_synthesized_rrset(uint16_t qtype,
-       struct clientip_synthesized_rr* data)
-{
-       struct local_rrset* cursor = data->data;
-       while( cursor != NULL) {
-               struct packed_rrset_key* packed_rrset = &cursor->rrset->rk;
-               if(htons(qtype) == packed_rrset->type) {
-                       return cursor;
-               }
-               cursor = cursor->next;
-       }
-       return NULL;
-}
-
 /** allocate SOA record ubrrsetkey in region */
 static struct ub_packed_rrset_key*
 make_soa_ubrrset(struct auth_zone* auth_zone, struct auth_rrset* soa,
index 53781197aeec4c4b4955bd2f3cecee90cba10123..ae93af9a8223e3d548fe6d2b7f02a7488272e85e 100644 (file)
@@ -152,6 +152,7 @@ int rpz_insert_rr(struct rpz* r, uint8_t* azname, size_t aznamelen, uint8_t* dna
 /**
  * Delete policy matching RR, used for IXFR.
  * @param r: the rpz to add the policy to.
+ * @param azname: dname of the auth-zone
  * @param aznamelen: the length of the auth-zone name
  * @param dname: dname of the RR
  * @param dnamelen: length of the dname
@@ -160,9 +161,9 @@ int rpz_insert_rr(struct rpz* r, uint8_t* azname, size_t aznamelen, uint8_t* dna
  * @param rdatawl: rdata of the RR, prepended with the rdata size
  * @param rdatalen: length if the RR, including the prepended rdata size
  */
-void rpz_remove_rr(struct rpz* r, size_t aznamelen, uint8_t* dname,
-       size_t dnamelen, uint16_t rr_type, uint16_t rr_class, uint8_t* rdatawl,
-       size_t rdatalen);
+void rpz_remove_rr(struct rpz* r, uint8_t* azname, size_t aznamelen,
+       uint8_t* dname, size_t dnamelen, uint16_t rr_type, uint16_t rr_class,
+       uint8_t* rdatawl, size_t rdatalen);
 
 /**
  * Walk over the RPZ zones to find and apply a QNAME trigger policy.
index ca2b6233562fc13f9b4e9c8c9a1b6544ac6bdc72..3566631571a380a90097673be60a68aefb87665a 100644 (file)
@@ -4,6 +4,7 @@ server:
        target-fetch-policy: "0 0 0 0 0"
        qname-minimisation: no
        rrset-roundrobin: no
+       access-control: 192.0.0.0/8 allow
 
 rpz:
        name: "rpz.example.com."
@@ -22,6 +23,11 @@ d.rpz.example.com.   IN      CNAME .
 32.3.123.0.10.rpz-ip.rpz.example.com.  A 10.66.0.3
 32.3.123.0.10.rpz-ip.rpz.example.com.  A 10.66.0.4
 32.4.123.0.10.rpz-ip.rpz.example.com.  CNAME .
+; also test client-ip, and remove it later with an IXFR.
+24.0.5.0.192.rpz-client-ip A 127.0.0.5
+24.0.6.0.192.rpz-client-ip CNAME *.
+32.41.30.20.10.rpz-nsip A 127.0.0.1
+ns.gotham.com.rpz-nsdname A 127.0.0.1
 TEMPFILE_END
 
 stub-zone:
@@ -97,6 +103,42 @@ SECTION ANSWER
 d.rpz-ip.      IN      A       10.0.123.4
 ENTRY_END
 
+ENTRY_BEGIN
+MATCH opcode qname qtype
+ADJUST copy_id
+REPLY QR NOERROR AA
+SECTION QUESTION
+a.a.   IN      A
+SECTION ANSWER
+a.a.   IN      A       10.0.123.5
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode subdomain
+ADJUST copy_id copy_query
+REPLY QR NOERROR
+SECTION QUESTION
+foo.com. IN NS
+SECTION ANSWER
+SECTION AUTHORITY
+foo.com. 10 IN NS ns.foo.com.
+SECTION ADDITIONAL
+ns.foo.com. 10 IN A 10.20.30.41
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode subdomain
+ADJUST copy_id copy_query
+REPLY QR NOERROR
+SECTION QUESTION
+gotham.com. IN NS
+SECTION ANSWER
+SECTION AUTHORITY
+gotham.com. 10 IN NS ns.gotham.com.
+SECTION ADDITIONAL
+ns.gotham.com. 10 IN A 10.20.30.42
+ENTRY_END
+
 ENTRY_BEGIN
 MATCH opcode qname qtype
 ADJUST copy_id
@@ -124,6 +166,10 @@ d.rpz.example.com. IN      CNAME .
 32.3.123.0.10.rpz-ip.rpz.example.com.  A 10.66.0.3
 32.3.123.0.10.rpz-ip.rpz.example.com.  A 10.66.0.4
 32.4.123.0.10.rpz-ip.rpz.example.com.  CNAME .
+24.0.5.0.192.rpz-client-ip.rpz.example.com. A 127.0.0.5
+24.0.6.0.192.rpz-client-ip.rpz.example.com. CNAME *.
+32.41.30.20.10.rpz-nsip.rpz.example.com. A 127.0.0.1
+ns.gotham.com.rpz-nsdname.rpz.example.com. A 127.0.0.1
 rpz.example.com. IN SOA ns.rpz.example.com. hostmaster.rpz.example.com. 2 3600 900 86400 3600
 b.rpz.example.com. TXT "hello from RPZ"
 c.rpz.example.com. TXT "hello from RPZ"
@@ -136,6 +182,78 @@ ENTRY_END
 
 RANGE_END
 
+; ns.foo.com
+RANGE_BEGIN 0 100
+       ADDRESS 10.20.30.41
+ENTRY_BEGIN
+MATCH opcode qname qtype
+ADJUST copy_id
+REPLY QR NOERROR AA
+SECTION QUESTION
+ns.foo.com. IN A
+SECTION ANSWER
+ns.foo.com. 10 IN A 10.20.30.41
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qname qtype
+ADJUST copy_id
+REPLY QR NOERROR AA
+SECTION QUESTION
+ns.foo.com. IN AAAA
+SECTION ANSWER
+SECTION AUTHORITY
+foo.com. 10 IN SOA ns.foo.com. root.foo.com. 1 2 3 4 10
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qname qtype
+ADJUST copy_id
+REPLY QR NOERROR AA
+SECTION QUESTION
+www.foo.com. IN A
+SECTION ANSWER
+www.foo.com. 10 IN A 10.20.30.42
+ENTRY_END
+
+RANGE_END
+
+; ns.gotham.com
+RANGE_BEGIN 0 100
+       ADDRESS 10.20.30.42
+ENTRY_BEGIN
+MATCH opcode qname qtype
+ADJUST copy_id
+REPLY QR NOERROR AA
+SECTION QUESTION
+ns.gotham.com. IN A
+SECTION ANSWER
+ns.gotham.com. 10 IN A 10.20.30.42
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qname qtype
+ADJUST copy_id
+REPLY QR NOERROR AA
+SECTION QUESTION
+ns.gotham.com. IN AAAA
+SECTION ANSWER
+SECTION AUTHORITY
+gotham.com. 10 IN SOA ns.gotham.com. root.gotham.com. 1 2 3 4 10
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qname qtype
+ADJUST copy_id
+REPLY QR NOERROR AA
+SECTION QUESTION
+www.gotham.com. IN A
+SECTION ANSWER
+www.gotham.com. 10 IN A 10.20.30.43
+ENTRY_END
+
+RANGE_END
+
 STEP 1 QUERY
 ENTRY_BEGIN
 REPLY RD
@@ -244,7 +362,6 @@ SECTION QUESTION
 d.rpz-ip.      IN      A
 ENTRY_END
 
-
 STEP 15 CHECK_ANSWER
 ENTRY_BEGIN
 MATCH all
@@ -253,7 +370,74 @@ SECTION QUESTION
 d.rpz-ip.      IN      A
 ENTRY_END
 
-STEP 16 TIME_PASSES ELAPSE 1
+STEP 16 QUERY ADDRESS 192.0.5.1
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+a.a. IN A
+ENTRY_END
+
+STEP 17 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA AA NOERROR
+SECTION QUESTION
+a.a. IN A
+SECTION ANSWER
+a.a. IN A 127.0.0.5
+ENTRY_END
+
+STEP 18 QUERY ADDRESS 192.0.6.1
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+a.a. IN A
+ENTRY_END
+
+STEP 19 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA AA NOERROR
+SECTION QUESTION
+a.a. IN A
+SECTION ANSWER
+ENTRY_END
+
+STEP 20 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+www.foo.com. IN A
+ENTRY_END
+
+STEP 21 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA AA NOERROR
+SECTION QUESTION
+www.foo.com. IN A
+SECTION ANSWER
+www.foo.com. IN A 127.0.0.1
+ENTRY_END
+
+STEP 22 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+www.gotham.com. IN A
+ENTRY_END
+
+STEP 23 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA AA NOERROR
+SECTION QUESTION
+www.gotham.com. IN A
+SECTION ANSWER
+www.gotham.com. IN A 127.0.0.1
+ENTRY_END
+
+STEP 24 TIME_PASSES ELAPSE 1
 STEP 30 TIME_PASSES ELAPSE 3600
 STEP 40 TRAFFIC
 
@@ -376,4 +560,72 @@ SECTION ANSWER
 d.rpz-ip.      IN      A 10.0.123.4
 ENTRY_END
 
+STEP 64 QUERY ADDRESS 192.0.5.1
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+a.a. IN A
+ENTRY_END
+
+STEP 65 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA NOERROR
+SECTION QUESTION
+a.a. IN A
+SECTION ANSWER
+a.a. IN A 10.0.123.5
+ENTRY_END
+
+STEP 66 QUERY ADDRESS 192.0.6.1
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+a.a. IN A
+ENTRY_END
+
+STEP 67 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA NOERROR
+SECTION QUESTION
+a.a. IN A
+SECTION ANSWER
+a.a. IN A 10.0.123.5
+ENTRY_END
+
+STEP 68 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+www.foo.com. IN A
+ENTRY_END
+
+STEP 69 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA NOERROR
+SECTION QUESTION
+www.foo.com. IN A
+SECTION ANSWER
+www.foo.com. 10 IN A 10.20.30.42
+ENTRY_END
+
+STEP 70 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+www.gotham.com. IN A
+ENTRY_END
+
+STEP 71 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA NOERROR
+SECTION QUESTION
+www.gotham.com. IN A
+SECTION ANSWER
+www.gotham.com. 10 IN A 10.20.30.43
+ENTRY_END
+
 SCENARIO_END