]> git.ipfire.org Git - thirdparty/unbound.git/commitdiff
- Allow fallback to the parent side when MAX_TARGET_NX is reached.
authorGeorge Thessalonikefs <george@nlnetlabs.nl>
Wed, 29 Jun 2022 15:31:23 +0000 (17:31 +0200)
committerGeorge Thessalonikefs <george@nlnetlabs.nl>
Wed, 29 Jun 2022 15:32:29 +0000 (17:32 +0200)
  This will also allow MAX_TARGET_NX more NXDOMAINs.

doc/Changelog
iterator/iter_delegpt.c
iterator/iter_delegpt.h
iterator/iter_utils.c
iterator/iterator.c
iterator/iterator.h
testdata/iter_nxns_fallback.rpl [new file with mode: 0644]
testdata/iter_nxns_parentside.rpl [new file with mode: 0644]

index 4695802e9c8c0cb7c3e5475b5016dedc43529402..6911f6eef164ecd05cd31d8b024fd89e0a22aafa 100644 (file)
@@ -7,6 +7,8 @@
          sent; introduces 'num.query.udpout' to the 'unbound-control stats'
          command.
        - Fix to not count cached NXDOMAIN for MAX_TARGET_NX.
+       - Allow fallback to the parent side when MAX_TARGET_NX is reached.
+         This will also allow MAX_TARGET_NX more NXDOMAINs.
 
 28 June 2022: George
        - Show the output of the exact .rpl run that failed with 'make test'.
index 80148e8108907c483e2e93639aa8454c2322c70e..4bffa1b3a7d54ad5c9142417914857d399374627 100644 (file)
@@ -185,6 +185,10 @@ delegpt_add_target(struct delegpt* dp, struct regional* region,
                else    ns->got4 = 1;
                if(ns->got4 && ns->got6)
                        ns->resolved = 1;
+       } else {
+               if(addr_is_ip6(addr, addrlen))
+                       ns->done_pside6 = 1;
+               else    ns->done_pside4 = 1;
        }
        log_assert(ns->port>0);
        return delegpt_add_addr(dp, region, addr, addrlen, bogus, lame,
@@ -338,13 +342,16 @@ delegpt_count_targets(struct delegpt* dp)
 }
 
 size_t 
-delegpt_count_missing_targets(struct delegpt* dp)
+delegpt_count_missing_targets(struct delegpt* dp, int* alllame)
 {
        struct delegpt_ns* ns;
-       size_t n = 0;
-       for(ns = dp->nslist; ns; ns = ns->next)
-               if(!ns->resolved)
-                       n++;
+       size_t n = 0, nlame = 0;
+       for(ns = dp->nslist; ns; ns = ns->next) {
+               if(ns->resolved) continue;
+               n++;
+               if(ns->lame) nlame++;
+       }
+       if(alllame && n == nlame) *alllame = 1;
        return n;
 }
 
@@ -694,6 +701,10 @@ int delegpt_add_target_mlc(struct delegpt* dp, uint8_t* name, size_t namelen,
                else    ns->got4 = 1;
                if(ns->got4 && ns->got6)
                        ns->resolved = 1;
+       } else {
+               if(addr_is_ip6(addr, addrlen))
+                       ns->done_pside6 = 1;
+               else    ns->done_pside4 = 1;
        }
        log_assert(ns->port>0);
        return delegpt_add_addr_mlc(dp, addr, addrlen, bogus, lame,
index 998b98cd803ec59aa358f70c5e998f597707a5ee..62c8edc51225952d83780a79d5949806383af9e0 100644 (file)
@@ -330,9 +330,10 @@ void delegpt_add_unused_targets(struct delegpt* dp);
 /**
  * Count number of missing targets. These are ns names with no resolved flag.
  * @param dp: delegation point.
+ * @param alllame: if set, check if all the missing targets are lame.
  * @return number of missing targets (or 0).
  */
-size_t delegpt_count_missing_targets(struct delegpt* dp);
+size_t delegpt_count_missing_targets(struct delegpt* dp, int* alllame);
 
 /** count total number of targets in dp */
 size_t delegpt_count_targets(struct delegpt* dp);
index f3bea46d6c95542a80e799e2c50edf00b27fd50d..6d159157a9952ef3ffcfead5bea3bd265e974cb1 100644 (file)
@@ -367,6 +367,7 @@ iter_filter_order(struct iter_env* iter_env, struct module_env* env,
        struct sock_list* blacklist, time_t prefetch)
 {
        int got_num = 0, low_rtt = 0, swap_to_front, rtt_band = RTT_BAND, nth;
+       int alllame = 0;
        size_t num_results;
        struct delegpt_addr* a, *n, *prev=NULL;
 
@@ -376,7 +377,10 @@ iter_filter_order(struct iter_env* iter_env, struct module_env* env,
        if(got_num == 0)
                return 0;
        if(low_rtt >= USEFUL_SERVER_TOP_TIMEOUT &&
-               (delegpt_count_missing_targets(dp) > 0 || open_target > 0)) {
+               /* If all missing (or not fully resolved) targets are lame,
+                * then use the remaining lame address. */
+               ((delegpt_count_missing_targets(dp, &alllame) > 0 && !alllame) ||
+               open_target > 0)) {
                verbose(VERB_ALGO, "Bad choices, trying to get more choice");
                return 0; /* we want more choice. The best choice is a bad one.
                             return 0 to force the caller to fetch more */
index 2e64c3945201fc4913c64ed7ab7e9b18001f5dbd..ea0b37cbffe634b5b95ae23d2179e752683b25cb 100644 (file)
@@ -253,8 +253,9 @@ error_supers(struct module_qstate* qstate, int id, struct module_qstate* super)
                delegpt_mark_neg(dpns, qstate->qinfo.qtype);
                dpns->resolved = 1; /* mark as failed */
                if((dpns->got4 == 2 || !ie->supports_ipv4) &&
-                       (dpns->got6 == 2 || !ie->supports_ipv6))
+                       (dpns->got6 == 2 || !ie->supports_ipv6)) {
                        target_count_increase_nx(super_iq, 1);
+               }
        }
        if(qstate->qinfo.qtype == LDNS_RR_TYPE_NS) {
                /* prime failed to get delegation */
@@ -678,15 +679,20 @@ is_caps_whitelisted(struct iter_env* ie, struct iter_qstate* iq)
                iq->qchase.qclass) != NULL;
 }
 
-/** create target count structure for this query */
+/**
+ * Create target count structure for this query. This is always explicitly
+ * created for the parent query.
+ */
 static void
 target_count_create(struct iter_qstate* iq)
 {
        if(!iq->target_count) {
-               iq->target_count = (int*)calloc(3, sizeof(int));
+               iq->target_count = (int*)calloc(TARGET_COUNT_MAX, sizeof(int));
                /* if calloc fails we simply do not track this number */
-               if(iq->target_count)
-                       iq->target_count[0] = 1;
+               if(iq->target_count) {
+                       iq->target_count[TARGET_COUNT_REF] = 1;
+                       iq->nxns_dp = (uint8_t**)calloc(1, sizeof(uint8_t*));
+               }
        }
 }
 
@@ -695,7 +701,7 @@ target_count_increase(struct iter_qstate* iq, int num)
 {
        target_count_create(iq);
        if(iq->target_count)
-               iq->target_count[1] += num;
+               iq->target_count[TARGET_COUNT_QUERIES] += num;
        iq->dp_target_count++;
 }
 
@@ -704,7 +710,7 @@ target_count_increase_nx(struct iter_qstate* iq, int num)
 {
        target_count_create(iq);
        if(iq->target_count)
-               iq->target_count[2] += num;
+               iq->target_count[TARGET_COUNT_NX] += num;
 }
 
 /**
@@ -799,8 +805,10 @@ generate_sub_request(uint8_t* qname, size_t qnamelen, uint16_t qtype,
                subiq->num_target_queries = 0;
                target_count_create(iq);
                subiq->target_count = iq->target_count;
-               if(iq->target_count)
-                       iq->target_count[0] ++; /* extra reference */
+               if(iq->target_count) {
+                       iq->target_count[TARGET_COUNT_REF] ++; /* extra reference */
+                       subiq->nxns_dp = iq->nxns_dp;
+               }
                subiq->dp_target_count = 0;
                subiq->num_current_queries = 0;
                subiq->depth = iq->depth+1;
@@ -1832,7 +1840,7 @@ query_for_targets(struct module_qstate* qstate, struct iter_qstate* iq,
        int toget = 0;
 
        iter_mark_cycle_targets(qstate, iq->dp);
-       missing = (int)delegpt_count_missing_targets(iq->dp);
+       missing = (int)delegpt_count_missing_targets(iq->dp, NULL);
        log_assert(maxtargets != 0); /* that would not be useful */
 
        /* Generate target requests. Basically, any missing targets
@@ -1851,11 +1859,12 @@ query_for_targets(struct module_qstate* qstate, struct iter_qstate* iq,
        if(iq->depth == ie->max_dependency_depth)
                return 0;
        if(iq->depth > 0 && iq->target_count &&
-               iq->target_count[1] > MAX_TARGET_COUNT) {
+               iq->target_count[TARGET_COUNT_QUERIES] > MAX_TARGET_COUNT) {
                char s[LDNS_MAX_DOMAINLEN+1];
                dname_str(qstate->qinfo.qname, s);
                verbose(VERB_QUERY, "request %s has exceeded the maximum "
-                       "number of glue fetches %d", s, iq->target_count[1]);
+                       "number of glue fetches %d", s,
+                       iq->target_count[TARGET_COUNT_QUERIES]);
                return 0;
        }
        if(iq->dp_target_count > MAX_DP_TARGET_COUNT) {
@@ -1883,7 +1892,9 @@ query_for_targets(struct module_qstate* qstate, struct iter_qstate* iq,
                        continue;
                }
 
-               if(ie->supports_ipv6 && !ns->got6) {
+               if(ie->supports_ipv6 &&
+                       ((ns->lame && !ns->done_pside6) ||
+                       (!ns->lame && !ns->got6))) {
                        /* Send the AAAA request. */
                        if(!generate_target_query(qstate, iq, id, 
                                ns->name, ns->namelen,
@@ -1896,7 +1907,9 @@ query_for_targets(struct module_qstate* qstate, struct iter_qstate* iq,
                        query_count++;
                }
                /* Send the A request. */
-               if(ie->supports_ipv4 && !ns->got4) {
+               if(ie->supports_ipv4 &&
+                       ((ns->lame && !ns->done_pside4) ||
+                       (!ns->lame && !ns->got4))) {
                        if(!generate_target_query(qstate, iq, id, 
                                ns->name, ns->namelen, 
                                LDNS_RR_TYPE_A, iq->qchase.qclass)) {
@@ -2006,7 +2019,7 @@ processLastResort(struct module_qstate* qstate, struct iter_qstate* iq,
                return next_state(iq, QUERYTARGETS_STATE);
        }
        /* query for an extra name added by the parent-NS record */
-       if(delegpt_count_missing_targets(iq->dp) > 0) {
+       if(delegpt_count_missing_targets(iq->dp, NULL) > 0) {
                int qs = 0;
                verbose(VERB_ALGO, "try parent-side target name");
                if(!query_for_targets(qstate, iq, ie, id, 1, &qs)) {
@@ -2027,11 +2040,12 @@ processLastResort(struct module_qstate* qstate, struct iter_qstate* iq,
                return error_response_cache(qstate, id, LDNS_RCODE_SERVFAIL);
        }
        if(iq->depth > 0 && iq->target_count &&
-               iq->target_count[1] > MAX_TARGET_COUNT) {
+               iq->target_count[TARGET_COUNT_QUERIES] > MAX_TARGET_COUNT) {
                char s[LDNS_MAX_DOMAINLEN+1];
                dname_str(qstate->qinfo.qname, s);
                verbose(VERB_QUERY, "request %s has exceeded the maximum "
-                       "number of glue fetches %d", s, iq->target_count[1]);
+                       "number of glue fetches %d", s,
+                       iq->target_count[TARGET_COUNT_QUERIES]);
                errinf(qstate, "exceeded the maximum number of glue fetches");
                return error_response_cache(qstate, id, LDNS_RCODE_SERVFAIL);
        }
@@ -2211,12 +2225,112 @@ processQueryTargets(struct module_qstate* qstate, struct iter_qstate* iq,
                errinf(qstate, "exceeded the maximum number of sends");
                return error_response(qstate, id, LDNS_RCODE_SERVFAIL);
        }
-       if(iq->target_count && iq->target_count[2] > MAX_TARGET_NX) {
-               verbose(VERB_QUERY, "request has exceeded the maximum "
-                       " number of nxdomain nameserver lookups with %d",
-                       iq->target_count[2]);
-               errinf(qstate, "exceeded the maximum nameserver nxdomains");
-               return error_response(qstate, id, LDNS_RCODE_SERVFAIL);
+       /* Check if we reached MAX_TARGET_NX limit without a fallback activation. */
+       if(iq->target_count && !*iq->nxns_dp &&
+               iq->target_count[TARGET_COUNT_NX] > MAX_TARGET_NX) {
+               struct delegpt_ns* ns;
+               /* If we can wait for resolution, do so. */
+               if(iq->num_target_queries>0 || iq->num_current_queries>0) {
+                       if(iq->num_target_queries>0 && iq->num_current_queries>0) {
+                               verbose(VERB_ALGO, "waiting for %d targets to "
+                                       "resolve or %d outstanding queries to "
+                                       "respond", iq->num_target_queries,
+                                       iq->num_current_queries);
+                               qstate->ext_state[id] = module_wait_reply;
+                       } else if(iq->num_target_queries>0) {
+                               verbose(VERB_ALGO, "waiting for %d targets to "
+                                       "resolve", iq->num_target_queries);
+                               qstate->ext_state[id] = module_wait_subquery;
+                       } else {
+                               verbose(VERB_ALGO, "waiting for %d "
+                                       "outstanding queries to respond",
+                                       iq->num_current_queries);
+                               qstate->ext_state[id] = module_wait_reply;
+                       }
+                       return 0;
+               }
+               verbose(VERB_ALGO, "request has exceeded the maximum "
+                       "number of nxdomain nameserver lookups (%d) with %d",
+                       MAX_TARGET_NX, iq->target_count[TARGET_COUNT_NX]);
+               /* Check for dp because we require one below */
+               if(!iq->dp) {
+                       verbose(VERB_QUERY, "Failed to get a delegation, "
+                               "giving up");
+                       errinf(qstate, "failed to get a delegation (eg. prime "
+                               "failure)");
+                       return error_response(qstate, id, LDNS_RCODE_SERVFAIL);
+               }
+               /* We reached the limit but we already have parent side
+                * information; stop resolution */
+               if(iq->dp->has_parent_side_NS) {
+                       errinf(qstate, "exceeded the maximum nameserver nxdomains");
+                       return error_response(qstate, id, LDNS_RCODE_SERVFAIL);
+               }
+               /* Mark all the current NSes as resolved to allow for parent
+                * fallback */
+               for(ns=iq->dp->nslist; ns; ns=ns->next) {
+                       ns->resolved = 1;
+               }
+               /* Note the delegation point that triggered the NXNS fallback;
+                * no reason for shared queries to keep trying there.
+                * This also marks the fallback activation. */
+               *iq->nxns_dp = malloc(iq->dp->namelen);
+               if(!*iq->nxns_dp) {
+                       errinf(qstate, "exceeded the maximum nameserver nxdomains");
+                       return error_response(qstate, id, LDNS_RCODE_SERVFAIL);
+               }
+               memcpy(*iq->nxns_dp, iq->dp->name, iq->dp->namelen);
+       } else if(iq->target_count && *iq->nxns_dp) {
+               /* Handle the NXNS fallback case. */
+               /* If we can wait for resolution, do so. */
+               if(iq->num_target_queries>0 || iq->num_current_queries>0) {
+                       if(iq->num_target_queries>0 && iq->num_current_queries>0) {
+                               verbose(VERB_ALGO, "waiting for %d targets to "
+                                       "resolve or %d outstanding queries to "
+                                       "respond", iq->num_target_queries,
+                                       iq->num_current_queries);
+                               qstate->ext_state[id] = module_wait_reply;
+                       } else if(iq->num_target_queries>0) {
+                               verbose(VERB_ALGO, "waiting for %d targets to "
+                                       "resolve", iq->num_target_queries);
+                               qstate->ext_state[id] = module_wait_subquery;
+                       } else {
+                               verbose(VERB_ALGO, "waiting for %d "
+                                       "outstanding queries to respond",
+                                       iq->num_current_queries);
+                               qstate->ext_state[id] = module_wait_reply;
+                       }
+                       return 0;
+               }
+               /* Check for dp because we require one below */
+               if(!iq->dp) {
+                       verbose(VERB_QUERY, "Failed to get a delegation, "
+                               "giving up");
+                       errinf(qstate, "failed to get a delegation (eg. prime "
+                               "failure)");
+                       return error_response(qstate, id, LDNS_RCODE_SERVFAIL);
+               }
+
+               if(iq->target_count[TARGET_COUNT_NX] > MAX_TARGET_NX_FALLBACK) {
+                       verbose(VERB_ALGO, "request has exceeded the maximum "
+                               "number of fallback nxdomain nameserver "
+                               "lookups (%d) with %d", MAX_TARGET_NX_FALLBACK,
+                               iq->target_count[TARGET_COUNT_NX]);
+                       errinf(qstate, "exceeded the maximum nameserver nxdomains");
+                       return error_response(qstate, id, LDNS_RCODE_SERVFAIL);
+               }
+
+               if(!iq->dp->has_parent_side_NS) {
+                       struct delegpt_ns* ns;
+                       if(!dname_canonical_compare(*iq->nxns_dp, iq->dp->name)) {
+                               verbose(VERB_ALGO, "this delegation point "
+                                       "initiated the fallback, marking the "
+                                       "nslist as resolved");
+                               for(ns=iq->dp->nslist; ns; ns=ns->next) {
+                                       ns->resolved = 1;
+                               }
+                       }
+               }
        }
        
        /* Make sure we have a delegation point, otherwise priming failed
@@ -2434,7 +2548,7 @@ processQueryTargets(struct module_qstate* qstate, struct iter_qstate* iq,
         * that servfail is cached, which is not good as opportunism goes. */
        if(iq->depth < ie->max_dependency_depth
                && iq->num_target_queries == 0
-               && (!iq->target_count || iq->target_count[2]==0)
+               && (!iq->target_count || iq->target_count[TARGET_COUNT_NX]==0)
                && iq->sent_count < TARGET_FETCH_STOP) {
                tf_policy = ie->target_fetch_policy[iq->depth];
        }
@@ -2523,9 +2637,9 @@ processQueryTargets(struct module_qstate* qstate, struct iter_qstate* iq,
        }
 
        /* Select the next usable target, filtering out unsuitable targets. */
-       target = iter_server_selection(ie, qstate->env, iq->dp, 
+       target = iter_server_selection(ie, qstate->env, iq->dp,
                iq->dp->name, iq->dp->namelen, iq->qchase.qtype,
-               &iq->dnssec_lame_query, &iq->chase_to_rd, 
+               &iq->dnssec_lame_query, &iq->chase_to_rd,
                iq->num_target_queries, qstate->blacklist,
                qstate->prefetch_leeway);
 
@@ -2544,7 +2658,7 @@ processQueryTargets(struct module_qstate* qstate, struct iter_qstate* iq,
                        /* If there is nothing to wait for, then we need 
                         * to distinguish between generating (a) new target 
                         * query, or failing. */
-                       if(delegpt_count_missing_targets(iq->dp) > 0) {
+                       if(delegpt_count_missing_targets(iq->dp, NULL) > 0) {
                                int qs = 0;
                                verbose(VERB_ALGO, "querying for next "
                                        "missing target");
@@ -2556,7 +2670,7 @@ processQueryTargets(struct module_qstate* qstate, struct iter_qstate* iq,
                                                LDNS_RCODE_SERVFAIL);
                                }
                                if(qs == 0 && 
-                                  delegpt_count_missing_targets(iq->dp) == 0){
+                                  delegpt_count_missing_targets(iq->dp, NULL) == 0){
                                        /* it looked like there were missing
                                         * targets, but they did not turn up.
                                         * Try the bad choices again (if any),
@@ -4005,8 +4119,11 @@ iter_clear(struct module_qstate* qstate, int id)
        iq = (struct iter_qstate*)qstate->minfo[id];
        if(iq) {
                outbound_list_clear(&iq->outlist);
-               if(iq->target_count && --iq->target_count[0] == 0)
+               if(iq->target_count && --iq->target_count[TARGET_COUNT_REF] == 0) {
                        free(iq->target_count);
+                       if(*iq->nxns_dp) free(*iq->nxns_dp);
+                       free(iq->nxns_dp);
+               }
                iq->num_current_queries = 0;
        }
        qstate->minfo[id] = NULL;
index 8b840528d9d979592177fbc37171d7afb9064eb7..62f4768ea01d5caf73ee0188d65d268b75c03f05 100644 (file)
@@ -60,6 +60,9 @@ struct rbtree_type;
 /** max number of nxdomains allowed for target lookups for a query and
  * its subqueries */
 #define MAX_TARGET_NX          5
+/** max number of nxdomains allowed for target lookups for a query and
+ * its subqueries when fallback has kicked in */
+#define MAX_TARGET_NX_FALLBACK (MAX_TARGET_NX*2)
 /** max number of query restarts. Determines max number of CNAME chain. */
 #define MAX_RESTART_COUNT      11
 /** max number of referrals. Makes sure resolver does not run away */
@@ -217,6 +220,21 @@ enum iter_state {
        FINISHED_STATE
 };
 
+/**
+ * Shared counters for queries.
+ */
+enum target_count_variables {
+       /** Reference count for the shared iter_qstate->target_count. */
+       TARGET_COUNT_REF = 0,
+       /** Number of target queries spawned for the query and subqueries. */
+       TARGET_COUNT_QUERIES,
+       /** Number of nxdomain responses encountered. */
+       TARGET_COUNT_NX,
+
+       /** This should stay last here, it is used for the allocation */
+       TARGET_COUNT_MAX,
+};
+
 /**
  * Per query state for the iterator module.
  */
@@ -310,15 +328,20 @@ struct iter_qstate {
        /** number of queries fired off */
        int sent_count;
        
-       /** number of target queries spawned in [1], for this query and its
-        * subqueries, the malloced-array is shared, [0] refcount.
-        * in [2] the number of nxdomains is counted. */
+       /** malloced-array shared with this query and its subqueries. It keeps
+        * track of the defined enum target_count_variables counters. */
        int* target_count;
 
        /** number of target lookups per delegation point. Reset to 0 after
         * receiving referral answer. Not shared with subqueries. */
        int dp_target_count;
 
+       /** Delegation point that triggered the NXNS fallback; shared with
+        * this query and its subqueries, count-referenced by the reference
+        * counter in target_count.
+        * This also marks the fallback activation. */
+       uint8_t** nxns_dp;
+
        /** if true, already tested for ratelimiting and passed the test */
        int ratelimit_ok;
 
diff --git a/testdata/iter_nxns_fallback.rpl b/testdata/iter_nxns_fallback.rpl
new file mode 100644 (file)
index 0000000..3240686
--- /dev/null
@@ -0,0 +1,380 @@
+; Check if fallback to the parent side works when MAX_TARGET_NX is reached.
+
+server:
+       module-config: "iterator"
+       trust-anchor-signaling: no
+       target-fetch-policy: "0 0 0 0 0"
+       verbosity: 3
+       access-control: 127.0.0.1 allow_snoop
+       qname-minimisation: no
+       minimal-responses: no
+       rrset-roundrobin: no
+
+stub-zone:
+       name: "."
+       stub-addr: 193.0.14.129         # K.ROOT-SERVERS.NET.
+CONFIG_END
+
+SCENARIO_BEGIN Test the NXNS fallback
+
+; K.ROOT-SERVERS.NET.
+RANGE_BEGIN 0 100
+       ADDRESS 193.0.14.129
+       ENTRY_BEGIN
+               MATCH opcode qtype qname
+               ADJUST copy_id
+               REPLY QR NOERROR
+               SECTION QUESTION
+                       . IN NS
+               SECTION ANSWER
+                       . IN NS K.ROOT-SERVERS.NET.
+               SECTION ADDITIONAL
+                       K.ROOT-SERVERS.NET.     IN      A       193.0.14.129
+       ENTRY_END
+
+       ENTRY_BEGIN
+               MATCH opcode qtype subdomain
+               ADJUST copy_id copy_query
+               REPLY QR NOERROR
+               SECTION QUESTION
+                       example.com. IN A
+               SECTION AUTHORITY
+                       com.    IN NS   a.gtld-servers.net.
+               SECTION ADDITIONAL
+                       a.gtld-servers.net.     IN      A       192.5.6.30
+       ENTRY_END
+
+       ENTRY_BEGIN
+               MATCH opcode subdomain
+               ADJUST copy_id copy_query
+               REPLY QR NOERROR
+               SECTION QUESTION
+                       nonexistant.com. IN A
+               SECTION AUTHORITY
+                       com.    IN NS   a.gtld-servers.net.
+               SECTION ADDITIONAL
+                       a.gtld-servers.net.     IN      A       192.5.6.30
+       ENTRY_END
+RANGE_END
+
+; a.gtld-servers.net.
+RANGE_BEGIN 0 100
+       ADDRESS 192.5.6.30
+       ENTRY_BEGIN
+               MATCH opcode qtype qname
+               ADJUST copy_id
+               REPLY QR NOERROR
+               SECTION QUESTION
+                       com. IN NS
+               SECTION ANSWER
+                       com.    IN NS   a.gtld-servers.net.
+               SECTION ADDITIONAL
+                       a.gtld-servers.net.     IN      A       192.5.6.30
+       ENTRY_END
+
+       ENTRY_BEGIN
+               MATCH opcode qtype subdomain
+               ADJUST copy_id copy_query
+               REPLY QR NOERROR
+               SECTION QUESTION
+                       example.com. IN A
+               SECTION AUTHORITY
+                       example.com.    IN NS   ns.example.com.
+               SECTION ADDITIONAL
+                       ns.example.com. 10 IN A         1.2.3.4
+       ENTRY_END
+
+       ENTRY_BEGIN
+               MATCH opcode subdomain
+               ADJUST copy_id copy_query
+               REPLY QR NOERROR
+               SECTION QUESTION
+                       nonexistant.com. IN A
+               SECTION AUTHORITY
+                       nonexistant.com. IN NS  ns.example.com.
+               SECTION ADDITIONAL
+                       ns.example.com.  10 IN A        1.2.3.4
+       ENTRY_END
+RANGE_END
+
+; ns.example.com.
+RANGE_BEGIN 0 100
+       ADDRESS 1.2.3.4
+       ENTRY_BEGIN
+               MATCH opcode qtype qname
+               ADJUST copy_id
+               REPLY QR NOERROR
+               SECTION QUESTION
+                       example.com. IN NS
+               SECTION ANSWER
+                       example.com.    IN NS   ns1.nonexistant.com.
+                       example.com.    IN NS   ns2.nonexistant.com.
+                       example.com.    IN NS   ns3.nonexistant.com.
+                       example.com.    IN NS   ns4.nonexistant.com.
+                       example.com.    IN NS   ns5.nonexistant.com.
+                       example.com.    IN NS   ns6.nonexistant.com.
+                       example.com.    IN NS   ns7.nonexistant.com.
+                       example.com.    IN NS   ns8.nonexistant.com.
+                       example.com.    IN NS   ns9.nonexistant.com.
+                       example.com.    IN NS   ns10.nonexistant.com.
+                       example.com.    IN NS   ns11.nonexistant.com.
+                       example.com.    IN NS   ns12.nonexistant.com.
+       ENTRY_END
+
+       ENTRY_BEGIN
+               MATCH opcode qtype qname
+               ADJUST copy_id
+               REPLY QR NOERROR
+               SECTION QUESTION
+                       ns.example.com. IN A
+               SECTION ANSWER
+                       ns.example.com. 10 IN A 1.2.3.4
+       ENTRY_END
+
+       ENTRY_BEGIN
+               MATCH opcode qtype qname
+               ADJUST copy_id
+               REPLY QR NOERROR
+               SECTION QUESTION
+                       ns.example.com. IN AAAA
+       ENTRY_END
+
+       ENTRY_BEGIN
+               MATCH opcode subdomain
+               ADJUST copy_id copy_query
+               REPLY QR NXDOMAIN
+               SECTION QUESTION
+                       nonexistant.com. IN A
+       ENTRY_END
+
+       ENTRY_BEGIN
+               MATCH opcode qtype qname
+               ADJUST copy_id
+               REPLY QR NOERROR
+               SECTION QUESTION
+                       a.example.com. IN A
+               SECTION ANSWER
+                       a.example.com. IN A     10.20.30.40
+               SECTION AUTHORITY
+                       example.com.    IN NS   ns1.nonexistant.com.
+                       example.com.    IN NS   ns2.nonexistant.com.
+                       example.com.    IN NS   ns3.nonexistant.com.
+                       example.com.    IN NS   ns4.nonexistant.com.
+                       example.com.    IN NS   ns5.nonexistant.com.
+                       example.com.    IN NS   ns6.nonexistant.com.
+                       example.com.    IN NS   ns7.nonexistant.com.
+                       example.com.    IN NS   ns8.nonexistant.com.
+                       example.com.    IN NS   ns9.nonexistant.com.
+                       example.com.    IN NS   ns10.nonexistant.com.
+                       example.com.    IN NS   ns11.nonexistant.com.
+                       example.com.    IN NS   ns12.nonexistant.com.
+       ENTRY_END
+
+       ENTRY_BEGIN
+               MATCH opcode qtype qname
+               ADJUST copy_id
+               REPLY QR NOERROR
+               SECTION QUESTION
+                       b.example.com. IN A
+               SECTION ANSWER
+                       b.example.com. IN A     10.20.30.40
+               SECTION AUTHORITY
+                       example.com.    IN NS   ns1.nonexistant.com.
+                       example.com.    IN NS   ns2.nonexistant.com.
+                       example.com.    IN NS   ns3.nonexistant.com.
+                       example.com.    IN NS   ns4.nonexistant.com.
+                       example.com.    IN NS   ns5.nonexistant.com.
+                       example.com.    IN NS   ns6.nonexistant.com.
+                       example.com.    IN NS   ns7.nonexistant.com.
+                       example.com.    IN NS   ns8.nonexistant.com.
+                       example.com.    IN NS   ns9.nonexistant.com.
+                       example.com.    IN NS   ns10.nonexistant.com.
+                       example.com.    IN NS   ns11.nonexistant.com.
+                       example.com.    IN NS   ns12.nonexistant.com.
+       ENTRY_END
+
+       ENTRY_BEGIN
+               MATCH opcode qtype qname
+               ADJUST copy_id
+               REPLY QR NOERROR
+               SECTION QUESTION
+                       c.example.com. IN A
+               SECTION ANSWER
+                       c.example.com. IN A     10.20.30.40
+               SECTION AUTHORITY
+                       example.com.    IN NS   ns1.nonexistant.com.
+                       example.com.    IN NS   ns2.nonexistant.com.
+                       example.com.    IN NS   ns3.nonexistant.com.
+                       example.com.    IN NS   ns4.nonexistant.com.
+                       example.com.    IN NS   ns5.nonexistant.com.
+                       example.com.    IN NS   ns6.nonexistant.com.
+                       example.com.    IN NS   ns7.nonexistant.com.
+                       example.com.    IN NS   ns8.nonexistant.com.
+                       example.com.    IN NS   ns9.nonexistant.com.
+                       example.com.    IN NS   ns10.nonexistant.com.
+                       example.com.    IN NS   ns11.nonexistant.com.
+                       example.com.    IN NS   ns12.nonexistant.com.
+       ENTRY_END
+
+       ENTRY_BEGIN
+               MATCH opcode qtype qname
+               ADJUST copy_id
+               REPLY QR NOERROR
+               SECTION QUESTION
+                       d.example.com. IN A
+               SECTION ANSWER
+                       d.example.com. IN A     10.20.30.40
+               SECTION AUTHORITY
+                       example.com.    IN NS   ns1.nonexistant.com.
+                       example.com.    IN NS   ns2.nonexistant.com.
+                       example.com.    IN NS   ns3.nonexistant.com.
+                       example.com.    IN NS   ns4.nonexistant.com.
+                       example.com.    IN NS   ns5.nonexistant.com.
+                       example.com.    IN NS   ns6.nonexistant.com.
+                       example.com.    IN NS   ns7.nonexistant.com.
+                       example.com.    IN NS   ns8.nonexistant.com.
+                       example.com.    IN NS   ns9.nonexistant.com.
+                       example.com.    IN NS   ns10.nonexistant.com.
+                       example.com.    IN NS   ns11.nonexistant.com.
+                       example.com.    IN NS   ns12.nonexistant.com.
+       ENTRY_END
+RANGE_END
+
+STEP 1 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+a.example.com. IN A
+ENTRY_END
+
+; This was resolved by asking the parent side nameservers
+STEP 2 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA NOERROR
+SECTION QUESTION
+a.example.com. IN A
+SECTION ANSWER
+a.example.com. IN A    10.20.30.40
+SECTION AUTHORITY
+example.com.    IN NS   ns1.nonexistant.com.
+example.com.    IN NS   ns2.nonexistant.com.
+example.com.    IN NS   ns3.nonexistant.com.
+example.com.    IN NS   ns4.nonexistant.com.
+example.com.    IN NS   ns5.nonexistant.com.
+example.com.    IN NS   ns6.nonexistant.com.
+example.com.    IN NS   ns7.nonexistant.com.
+example.com.    IN NS   ns8.nonexistant.com.
+example.com.    IN NS   ns9.nonexistant.com.
+example.com.    IN NS   ns10.nonexistant.com.
+example.com.    IN NS   ns11.nonexistant.com.
+example.com.    IN NS   ns12.nonexistant.com.
+ENTRY_END
+
+; The child side nameservers are now known to Unbound
+
+; Query again, the child server nameservers will be asked now
+STEP 3 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+b.example.com. IN A
+ENTRY_END
+
+; This was resolved by falling back to the parent side nameservers
+STEP 4 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA NOERROR
+SECTION QUESTION
+b.example.com. IN A
+SECTION ANSWER
+b.example.com. IN A    10.20.30.40
+SECTION AUTHORITY
+example.com.    IN NS   ns1.nonexistant.com.
+example.com.    IN NS   ns2.nonexistant.com.
+example.com.    IN NS   ns3.nonexistant.com.
+example.com.    IN NS   ns4.nonexistant.com.
+example.com.    IN NS   ns5.nonexistant.com.
+example.com.    IN NS   ns6.nonexistant.com.
+example.com.    IN NS   ns7.nonexistant.com.
+example.com.    IN NS   ns8.nonexistant.com.
+example.com.    IN NS   ns9.nonexistant.com.
+example.com.    IN NS   ns10.nonexistant.com.
+example.com.    IN NS   ns11.nonexistant.com.
+example.com.    IN NS   ns12.nonexistant.com.
+ENTRY_END
+
+; Query a third time, this will get the cached NXDOMAINs (no NX counter for
+; those) and will go to the parent as a last resort. This query will test that
+; we will not have resolution for the lame(parent side) addresses that could
+; raise the NX counter because of no address addition to the delegation point
+; (the same addresses are already there).
+STEP 5 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+c.example.com. IN A
+ENTRY_END
+
+; This was resolved by going back to the parent side nameservers (child side
+; was exhausted from cache and queries < MAX_TARGET_NX).
+STEP 6 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA NOERROR
+SECTION QUESTION
+c.example.com. IN A
+SECTION ANSWER
+c.example.com. IN A    10.20.30.40
+SECTION AUTHORITY
+example.com.    IN NS   ns1.nonexistant.com.
+example.com.    IN NS   ns2.nonexistant.com.
+example.com.    IN NS   ns3.nonexistant.com.
+example.com.    IN NS   ns4.nonexistant.com.
+example.com.    IN NS   ns5.nonexistant.com.
+example.com.    IN NS   ns6.nonexistant.com.
+example.com.    IN NS   ns7.nonexistant.com.
+example.com.    IN NS   ns8.nonexistant.com.
+example.com.    IN NS   ns9.nonexistant.com.
+example.com.    IN NS   ns10.nonexistant.com.
+example.com.    IN NS   ns11.nonexistant.com.
+example.com.    IN NS   ns12.nonexistant.com.
+ENTRY_END
+
+; Allow for the nameserver glue to expire
+STEP 10 TIME_PASSES ELAPSE 11
+
+; Query again for the parent side fallback
+STEP 11 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+d.example.com. IN A
+ENTRY_END
+
+; This was resolved by falling back to the parent side nameservers
+STEP 12 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA NOERROR
+SECTION QUESTION
+d.example.com. IN A
+SECTION ANSWER
+d.example.com. IN A    10.20.30.40
+SECTION AUTHORITY
+example.com.    IN NS   ns1.nonexistant.com.
+example.com.    IN NS   ns2.nonexistant.com.
+example.com.    IN NS   ns3.nonexistant.com.
+example.com.    IN NS   ns4.nonexistant.com.
+example.com.    IN NS   ns5.nonexistant.com.
+example.com.    IN NS   ns6.nonexistant.com.
+example.com.    IN NS   ns7.nonexistant.com.
+example.com.    IN NS   ns8.nonexistant.com.
+example.com.    IN NS   ns9.nonexistant.com.
+example.com.    IN NS   ns10.nonexistant.com.
+example.com.    IN NS   ns11.nonexistant.com.
+example.com.    IN NS   ns12.nonexistant.com.
+ENTRY_END
+
+SCENARIO_END
diff --git a/testdata/iter_nxns_parentside.rpl b/testdata/iter_nxns_parentside.rpl
new file mode 100644 (file)
index 0000000..94a5a6f
--- /dev/null
@@ -0,0 +1,118 @@
+; Check if the NXNS fallback to the parent side does not mess with normal
+; parent side resolution. Parent side resolution should SERVFAIL when reaching
+; the MAX_TARGET_NX limit.
+
+server:
+       module-config: "iterator"
+       trust-anchor-signaling: no
+       target-fetch-policy: "0 0 0 0 0"
+       verbosity: 3
+       access-control: 127.0.0.1 allow_snoop
+       qname-minimisation: no
+       minimal-responses: no
+       rrset-roundrobin: no
+
+stub-zone:
+       name: "."
+       stub-addr: 193.0.14.129         # K.ROOT-SERVERS.NET.
+CONFIG_END
+
+SCENARIO_BEGIN Test that the NXNS fallback does not mess with parent side resolution
+
+; K.ROOT-SERVERS.NET.
+RANGE_BEGIN 0 100
+       ADDRESS 193.0.14.129
+       ENTRY_BEGIN
+               MATCH opcode qtype qname
+               ADJUST copy_id
+               REPLY QR NOERROR
+               SECTION QUESTION
+                       . IN NS
+               SECTION ANSWER
+                       . IN NS K.ROOT-SERVERS.NET.
+               SECTION ADDITIONAL
+                       K.ROOT-SERVERS.NET.     IN      A       193.0.14.129
+       ENTRY_END
+
+       ENTRY_BEGIN
+               MATCH opcode qtype subdomain
+               ADJUST copy_id copy_query
+               REPLY QR NOERROR
+               SECTION QUESTION
+                       example.com. IN A
+               SECTION AUTHORITY
+                       com.    IN NS   a.gtld-servers.net.
+               SECTION ADDITIONAL
+                       a.gtld-servers.net.     IN      A       192.5.6.30
+       ENTRY_END
+
+       ENTRY_BEGIN
+               MATCH opcode subdomain
+               ADJUST copy_id copy_query
+               REPLY QR NOERROR
+               SECTION QUESTION
+                       nonexistant.com. IN A
+               SECTION AUTHORITY
+                       com.    IN NS   a.gtld-servers.net.
+               SECTION ADDITIONAL
+                       a.gtld-servers.net.     IN      A       192.5.6.30
+       ENTRY_END
+RANGE_END
+
+; a.gtld-servers.net.
+RANGE_BEGIN 0 100
+       ADDRESS 192.5.6.30
+       ENTRY_BEGIN
+               MATCH opcode qtype qname
+               ADJUST copy_id
+               REPLY QR NOERROR
+               SECTION QUESTION
+                       com. IN NS
+               SECTION ANSWER
+                       com.    IN NS   a.gtld-servers.net.
+               SECTION ADDITIONAL
+                       a.gtld-servers.net.     IN      A       192.5.6.30
+       ENTRY_END
+
+       ENTRY_BEGIN
+               MATCH opcode qtype subdomain
+               ADJUST copy_id copy_query
+               REPLY QR NOERROR
+               SECTION QUESTION
+                       example.com. IN A
+               SECTION AUTHORITY
+                       example.com.    IN NS   ns1.nonexistant.com.
+                       example.com.    IN NS   ns2.nonexistant.com.
+                       example.com.    IN NS   ns3.nonexistant.com.
+                       example.com.    IN NS   ns4.nonexistant.com.
+                       example.com.    IN NS   ns5.nonexistant.com.
+                       example.com.    IN NS   ns6.nonexistant.com.
+                       example.com.    IN NS   ns7.nonexistant.com.
+                       example.com.    IN NS   ns8.nonexistant.com.
+       ENTRY_END
+
+       ENTRY_BEGIN
+               MATCH opcode subdomain
+               ADJUST copy_id copy_query
+               REPLY QR NXDOMAIN
+               SECTION QUESTION
+                       nonexistant.com. IN A
+       ENTRY_END
+RANGE_END
+
+STEP 1 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+a.example.com. IN A
+ENTRY_END
+
+STEP 2 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA SERVFAIL
+SECTION QUESTION
+a.example.com. IN A
+ENTRY_END
+
+SCENARIO_END