]> git.ipfire.org Git - thirdparty/unbound.git/commitdiff
DoS protection.
authorWouter Wijngaards <wouter@nlnetlabs.nl>
Tue, 2 Sep 2008 13:04:47 +0000 (13:04 +0000)
committerWouter Wijngaards <wouter@nlnetlabs.nl>
Tue, 2 Sep 2008 13:04:47 +0000 (13:04 +0000)
git-svn-id: file:///svn/unbound/trunk@1221 be551aaa-1e26-0410-a405-d3ace91eadb9

12 files changed:
daemon/worker.c
doc/Changelog
doc/plan
services/mesh.c
services/mesh.h
testcode/fake_event.c
testcode/replay.c
testcode/replay.h
testdata/fwd_droptoomany.rpl [new file with mode: 0644]
testdata/fwd_jostle.rpl [new file with mode: 0644]
testdata/fwd_jostle_out.rpl [new file with mode: 0644]
testdata/fwd_two.rpl

index e90508bc3ebbfb2f65dd65c1220fef7c755482c6..a2f91a2187bc64d66a74eacfb45589ebf187c771 100644 (file)
@@ -826,13 +826,8 @@ worker_handle_request(struct comm_point* c, void* arg, int error,
        server_stats_querymiss(&worker->stats, worker);
 
        /* grab a work request structure for this new request */
-       if(worker->env.mesh->all.count > worker->request_size) {
-               verbose(VERB_ALGO, "Too many requests active. "
-                       "dropping incoming query.");
-               worker->stats.num_query_list_exceeded++;
-               comm_point_drop_reply(repinfo);
-               return 0;
-       } else if(worker->env.mesh->num_reply_addrs>worker->request_size*16) {
+       if(worker->env.mesh->num_reply_addrs>worker->request_size*16) {
+               /* protect our memory usage from storing reply addresses */
                verbose(VERB_ALGO, "Too many requests queued. "
                        "dropping incoming query.");
                worker->stats.num_query_list_exceeded++;
index 874b19b598b82c22a7a3ca49729cc0afeb03fa11..61645438f558c7514b487137f80e5fc640c64d40 100644 (file)
@@ -1,3 +1,7 @@
+2 September 2008: Wouter
+       - DoS protection features. Queries are jostled out to make room.
+       - testbound can pass time, increasing the internal timer.
+
 1 September 2008: Wouter
        - disallow nonrecursive queries for cache snooping by default.
          You can allow is using access-control: <subnet> allow_snoop.
index a8d3b679f175e628b2c8bd768aa73eb761f8ce9f..f7aae9057dcf9c130ebb95358d70d1da203b66b9 100644 (file)
--- a/doc/plan
+++ b/doc/plan
@@ -4,7 +4,7 @@ Plan for Unbound 1.1.
 - immediate attention: done
 - security issues:      1 week.
 - remote control:      2 week
-- requested:           1 week
+- improvements:                1 week
 - draft-mitigation:    2 week
 total 6 of 8 weeks; 2 weeks for maintenance activities.
 
@@ -27,13 +27,13 @@ total 6 of 8 weeks; 2 weeks for maintenance activities.
 (done)
 
 *** Security issues
-* current NS query retry is an option, default off, experimental on,
++ current NS query retry is an option, default off, experimental on,
   because of the added load to 3rd parties.
-* block nonRD queries, acl like.
++ block nonRD queries, acl like.
        what about our authority features, those are allowed.
-* DoS vector, flush more.
++ DoS vector, flush more.
        50% of max is for run-to-completion
-       50% rest is for lifo queue with 100 msec timeout.
+       50% rest is for lifo queue with 100-200 msec timeout.
 * records in the additional section should not be marked bogus
 if they have no signer or a different signed. Validate if you can,
 otherwise leave unchecked.
@@ -61,7 +61,7 @@ like dnswall does.  Allow certain subdomains to do it, config options.
 * remote control to see delegation; what servers would be used to get 
   data for a name.
 
-*** Requested
+*** Improvements
 * fallback to noEDNS if all queries are dropped.
 * dnssec lameness fixen. Check to make sure.
 * negative caching to avoid DS queries, NSEC, NSEC3 (w params).
index d7ed4d9afd2d579303abe481d70e3eea7b15653d..f9e304764839fa83c86d22d15c44d10f7b698cc2 100644 (file)
 #include "util/timehist.h"
 #include "util/fptr_wlist.h"
 #include "util/alloc.h"
+#include "util/config_file.h"
+
+/** subtract timers and the values do not overflow or become negative */
+static void
+timeval_subtract(struct timeval* d, struct timeval* end, struct timeval* start)
+{
+#ifndef S_SPLINT_S
+       d->tv_sec = end->tv_sec - start->tv_sec;
+       while(end->tv_usec < start->tv_usec) {
+               end->tv_usec += 1000000;
+               d->tv_sec--;
+       }
+       d->tv_usec = end->tv_usec - start->tv_usec;
+#endif
+}
+
+/** add timers and the values do not overflow or become negative */
+static void
+timeval_add(struct timeval* d, struct timeval* add)
+{
+#ifndef S_SPLINT_S
+       d->tv_sec += add->tv_sec;
+       d->tv_usec += add->tv_usec;
+       while(d->tv_usec > 1000000 ) {
+               d->tv_usec -= 1000000;
+               d->tv_sec++;
+       }
+#endif
+}
+
+/** divide sum of timers to get average */
+static void
+timeval_divide(struct timeval* avg, struct timeval* sum, size_t d)
+{
+#ifndef S_SPLINT_S
+       size_t leftover;
+       if(d == 0) {
+               avg->tv_sec = 0;
+               avg->tv_usec = 0;
+               return;
+       }
+       avg->tv_sec = sum->tv_sec / d;
+       avg->tv_usec = sum->tv_usec / d;
+       /* handle fraction from seconds divide */
+       leftover = sum->tv_sec - avg->tv_sec*d;
+       avg->tv_usec += (leftover*1000000)/d;
+#endif
+}
 
 int
 mesh_state_compare(const void* ap, const void* bp)
@@ -108,6 +156,11 @@ mesh_create(struct module_stack* stack, struct module_env* env)
        mesh->num_reply_addrs = 0;
        mesh->num_reply_states = 0;
        mesh->num_detached_states = 0;
+       mesh->num_forever_states = 0;
+       mesh->stats_jostled = 0;
+       mesh->stats_dropped = 0;
+       mesh->max_reply_states = env->cfg->num_queries_per_thread;
+       mesh->max_forever_states = (mesh->max_reply_states+1)/2;
        return mesh;
 }
 
@@ -130,6 +183,42 @@ mesh_delete(struct mesh_area* mesh)
        free(mesh);
 }
 
+int mesh_make_new_space(struct mesh_area* mesh)
+{
+       struct mesh_state* m = mesh->jostle_last;
+       /* free space is available */
+       if(mesh->num_reply_states < mesh->max_reply_states)
+               return 1;
+       /* try to kick out a jostle-list item */
+       if(m && m->reply_list && m->list_select == mesh_jostle_list) {
+               /* how old is it? */
+               struct timeval age;
+               timeval_subtract(&age, mesh->env->now_tv, 
+                       &m->reply_list->start_time);
+#ifndef S_SPLINT_S
+               if(age.tv_sec > 0 || age.tv_usec > MESH_JOSTLE_USEC) {
+                       /* its a goner */
+                       log_nametypeclass(VERB_ALGO, "query jostled out to "
+                               "make space for a new one",
+                               m->s.qinfo.qname, m->s.qinfo.qtype,
+                               m->s.qinfo.qclass);
+                       /* notify supers */
+                       if(m->super_set.count > 0) {
+                               verbose(VERB_ALGO, "notify supers of failure");
+                               m->s.return_msg = NULL;
+                               m->s.return_rcode = LDNS_RCODE_SERVFAIL;
+                               mesh_walk_supers(mesh, m);
+                       }
+                       mesh->stats_jostled ++;
+                       mesh_state_delete(&m->s);
+                       return 1;
+               }
+#endif
+       }
+       /* no space for new item */
+       return 0;
+}
+
 void mesh_new_client(struct mesh_area* mesh, struct query_info* qinfo,
         uint16_t qflags, struct edns_data* edns, struct comm_reply* rep,
         uint16_t qid)
@@ -138,6 +227,16 @@ void mesh_new_client(struct mesh_area* mesh, struct query_info* qinfo,
        int was_detached = 0;
        int was_noreply = 0;
        int added = 0;
+       /* does this create a new reply state? */
+       if(!s || s->list_select == mesh_no_list) {
+               if(!mesh_make_new_space(mesh)) {
+                       verbose(VERB_ALGO, "Too many queries. dropping "
+                               "incoming query.");
+                       comm_point_drop_reply(rep);
+                       mesh->stats_dropped ++;
+                       return;
+               }
+       }
        /* see if it already exists, if not, create one */
        if(!s) {
                struct rbnode_t* n;
@@ -178,6 +277,19 @@ void mesh_new_client(struct mesh_area* mesh, struct query_info* qinfo,
                mesh->num_reply_states ++;
        }
        mesh->num_reply_addrs++;
+       if(s->list_select == mesh_no_list) {
+               /* move to either the forever or the jostle_list */
+               if(mesh->num_forever_states < mesh->max_forever_states) {
+                       mesh->num_forever_states ++;
+                       mesh_list_insert(s, &mesh->forever_first, 
+                               &mesh->forever_last);
+                       s->list_select = mesh_forever_list;
+               } else {
+                       mesh_list_insert(s, &mesh->jostle_first, 
+                               &mesh->jostle_last);
+                       s->list_select = mesh_jostle_list;
+               }
+       }
        if(added)
                mesh_run(mesh, s, module_event_new, NULL);
 }
@@ -191,6 +303,8 @@ mesh_new_callback(struct mesh_area* mesh, struct query_info* qinfo,
        int was_detached = 0;
        int was_noreply = 0;
        int added = 0;
+       /* there are no limits on the number of callbacks */
+
        /* see if it already exists, if not, create one */
        if(!s) {
                struct rbnode_t* n;
@@ -257,6 +371,7 @@ mesh_state_create(struct module_env* env, struct query_info* qinfo,
        mstate->node.key = mstate;
        mstate->run_node.key = mstate;
        mstate->reply_list = NULL;
+       mstate->list_select = mesh_no_list;
        rbtree_init(&mstate->super_set, &mesh_state_ref_compare);
        rbtree_init(&mstate->sub_set, &mesh_state_ref_compare);
        mstate->num_activated = 0;
@@ -317,6 +432,14 @@ mesh_state_delete(struct module_qstate* qstate)
        mstate = qstate->mesh_info;
        mesh = mstate->s.env->mesh;
        mesh_detach_subs(&mstate->s);
+       if(mstate->list_select == mesh_forever_list) {
+               mesh->num_forever_states --;
+               mesh_list_remove(mstate, &mesh->forever_first, 
+                       &mesh->forever_last);
+       } else if(mstate->list_select == mesh_jostle_list) {
+               mesh_list_remove(mstate, &mesh->jostle_first, 
+                       &mesh->jostle_last);
+       }
        if(!mstate->reply_list && !mstate->cb_list
                && mstate->super_set.count == 0) {
                log_assert(mesh->num_detached_states > 0);
@@ -414,53 +537,6 @@ int mesh_state_attachment(struct mesh_state* super, struct mesh_state* sub)
        return 1;
 }
 
-/** subtract timers and the values do not overflow or become negative */
-static void
-timeval_subtract(struct timeval* d, struct timeval* end, struct timeval* start)
-{
-#ifndef S_SPLINT_S
-       d->tv_sec = end->tv_sec - start->tv_sec;
-       while(end->tv_usec < start->tv_usec) {
-               end->tv_usec += 1000000;
-               d->tv_sec--;
-       }
-       d->tv_usec = end->tv_usec - start->tv_usec;
-#endif
-}
-
-/** add timers and the values do not overflow or become negative */
-static void
-timeval_add(struct timeval* d, struct timeval* add)
-{
-#ifndef S_SPLINT_S
-       d->tv_sec += add->tv_sec;
-       d->tv_usec += add->tv_usec;
-       while(d->tv_usec > 1000000 ) {
-               d->tv_usec -= 1000000;
-               d->tv_sec++;
-       }
-#endif
-}
-
-/** divide sum of timers to get average */
-static void
-timeval_divide(struct timeval* avg, struct timeval* sum, size_t d)
-{
-#ifndef S_SPLINT_S
-       size_t leftover;
-       if(d == 0) {
-               avg->tv_sec = 0;
-               avg->tv_usec = 0;
-               return;
-       }
-       avg->tv_sec = sum->tv_sec / d;
-       avg->tv_usec = sum->tv_usec / d;
-       /* handle fraction from seconds divide */
-       leftover = sum->tv_sec - avg->tv_sec*d;
-       avg->tv_usec += (leftover*1000000)/d;
-#endif
-}
-
 /**
  * callback results to mesh cb entry
  * @param m: mesh state to send it for.
@@ -785,11 +861,14 @@ mesh_stats(struct mesh_area* mesh, const char* str)
 {
        verbose(VERB_DETAIL, "%s %u recursion states (%u with reply, "
                "%u detached), %u waiting replies, %u recursion replies "
-               "sent", str, (unsigned)mesh->all.count, 
+               "sent, %d replies dropped, %d states jostled out", 
+               str, (unsigned)mesh->all.count, 
                (unsigned)mesh->num_reply_states,
                (unsigned)mesh->num_detached_states,
                (unsigned)mesh->num_reply_addrs,
-               (unsigned)mesh->replies_sent);
+               (unsigned)mesh->replies_sent,
+               (unsigned)mesh->stats_dropped,
+               (unsigned)mesh->stats_jostled);
        if(mesh->replies_sent > 0) {
                struct timeval avg;
                timeval_divide(&avg, &mesh->replies_sum_wait, 
@@ -809,6 +888,8 @@ mesh_stats_clear(struct mesh_area* mesh)
        mesh->replies_sent = 0;
        mesh->replies_sum_wait.tv_sec = 0;
        mesh->replies_sum_wait.tv_usec = 0;
+       mesh->stats_jostled = 0;
+       mesh->stats_dropped = 0;
        timehist_clear(mesh->histogram);
 }
 
@@ -850,3 +931,26 @@ mesh_detect_cycle(struct module_qstate* qstate, struct query_info* qinfo,
                return 1;
        return 0;
 }
+
+void mesh_list_insert(struct mesh_state* m, struct mesh_state** fp,
+        struct mesh_state** lp)
+{
+       /* insert as last element */
+       m->prev = *lp;
+       m->next = NULL;
+       if(*lp)
+               (*lp)->next = m;
+       else    *fp = m;
+       *lp = m;
+}
+
+void mesh_list_remove(struct mesh_state* m, struct mesh_state** fp,
+        struct mesh_state** lp)
+{
+       if(m->next)
+               m->next->prev = m->prev;
+       else    *lp = m->prev;
+       if(m->prev)
+               m->prev->next = m->next;
+       else    *fp = m->next;
+}
index aded72703c5c6561eb60f587b946a0ce48c86086..cb3c9179efbbdd2777fd27d2a805cb9cf43a97e3 100644 (file)
@@ -65,6 +65,12 @@ struct timehist;
  */
 #define MESH_MAX_ACTIVATION 1000
 
+/**
+ * Maximum time to live in the jostle list. usec.
+ * The entries older than this could be removed to make space for new ones.
+ */
+#define MESH_JOSTLE_USEC 200000 /* 0.200000 sec */
+
 /** 
  * Mesh of query states
  */
@@ -89,13 +95,36 @@ struct mesh_area {
         * an empty set of super-states, thus are 'toplevel' or detached
         * internal opportunistic queries */
        size_t num_detached_states;
-
+       /** number of reply states in the forever list */
+       size_t num_forever_states;
+
+       /** max total number of reply states to have */
+       size_t max_reply_states;
+       /** max forever number of reply states to have */
+       size_t max_forever_states;
+
+       /** stats, cumulative number of reply states jostled out */
+       size_t stats_jostled;
+       /** stats, cumulative number of incoming client msgs dropped */
+       size_t stats_dropped;
        /** number of replies sent */
        size_t replies_sent;
        /** sum of waiting times for the replies */
        struct timeval replies_sum_wait;
        /** histogram of time values */
        struct timehist* histogram;
+
+       /** double linked list of the run-to-completion query states.
+        * These are query states with a reply */
+       struct mesh_state* forever_first;
+       /** last entry in run forever list */
+       struct mesh_state* forever_last;
+
+       /** double linked list of the query states that can be jostled out
+        * by new queries if too old.  These are query states with a reply */
+       struct mesh_state* jostle_first;
+       /** last entry in jostle list - this is the entry that is newest */
+       struct mesh_state* jostle_last;
 };
 
 /**
@@ -127,6 +156,14 @@ struct mesh_state {
        rbtree_t sub_set;
        /** number of activations for the mesh state */
        size_t num_activated;
+
+       /** previous in linked list for reply states */
+       struct mesh_state* prev;
+       /** next in linked list for reply states */
+       struct mesh_state* next;
+       /** if this state is in the forever list, jostle list, or neither */
+       enum mesh_list_select { mesh_no_list, mesh_forever_list, 
+               mesh_jostle_list } list_select;
 };
 
 /**
@@ -457,4 +494,29 @@ int mesh_state_compare(const void* ap, const void* bp);
 /** compare two mesh references */
 int mesh_state_ref_compare(const void* ap, const void* bp);
 
+/**
+ * Make space for another recursion state for a reply in the mesh
+ * @param mesh: mesh area
+ * @return false if no space is available.
+ */
+int mesh_make_new_space(struct mesh_area* mesh);
+
+/**
+ * Insert mesh state into a double linked list.  Inserted at end.
+ * @param m: mesh state.
+ * @param fp: pointer to the first-elem-pointer of the list.
+ * @param lp: pointer to the last-elem-pointer of the list.
+ */
+void mesh_list_insert(struct mesh_state* m, struct mesh_state** fp,
+       struct mesh_state** lp);
+
+/**
+ * Remove mesh state from a double linked list.  Remove from any position.
+ * @param m: mesh state.
+ * @param fp: pointer to the first-elem-pointer of the list.
+ * @param lp: pointer to the last-elem-pointer of the list.
+ */
+void mesh_list_remove(struct mesh_state* m, struct mesh_state** fp,
+       struct mesh_state** lp);
+
 #endif /* SERVICES_MESH_H */
index 70471c9287ba707430179e8fd00edf5017da1526..d6afae9d5aeac774079d5e4c7114f7d5ef56d5bd 100644 (file)
 /** Global variable: the scenario. Saved here for when event_init is done. */
 static struct replay_scenario* saved_scenario = NULL;
 
+/** add timers and the values do not overflow or become negative */
+static void
+timeval_add(struct timeval* d, struct timeval* add)
+{
+#ifndef S_SPLINT_S
+       d->tv_sec += add->tv_sec;
+       d->tv_usec += add->tv_usec;
+       while(d->tv_usec > 1000000 ) {
+               d->tv_usec -= 1000000;
+               d->tv_sec++;
+       }
+#endif
+}
+
 void 
 fake_event_init(struct replay_scenario* scen)
 {
@@ -98,6 +112,7 @@ repevt_string(enum replay_event_type t)
        case repevt_front_query: return "QUERY";
        case repevt_front_reply: return "CHECK_ANSWER";
        case repevt_timeout:     return "TIMEOUT";
+       case repevt_time_passes: return "TIME_PASSES";
        case repevt_back_reply:  return "REPLY";
        case repevt_back_query:  return "CHECK_OUT_QUERY";
        case repevt_error:       return "ERROR";
@@ -408,6 +423,19 @@ fake_pending_callback(struct replay_runtime* runtime,
        ldns_buffer_free(c.buffer);
 }
 
+/** pass time */
+static void
+time_passes(struct replay_runtime* runtime, struct replay_moment* mom)
+{
+       timeval_add(&runtime->now_tv, &mom->elapse);
+       runtime->now_secs = (uint32_t)runtime->now_tv.tv_sec;
+#ifndef S_SPLINT_S
+       log_info("elapsed %d.%6.6d  now %d.%6.6d", 
+               (int)mom->elapse.tv_sec, (int)mom->elapse.tv_usec,
+               (int)runtime->now_tv.tv_sec, (int)runtime->now_tv.tv_usec);
+#endif
+}
+
 /**
  * Advance to the next moment.
  */
@@ -471,6 +499,10 @@ do_moment_and_advance(struct replay_runtime* runtime)
                advance_moment(runtime);
                fake_pending_callback(runtime, mom, NETEVENT_CLOSED);
                break;
+       case repevt_time_passes:
+               time_passes(runtime, runtime->now);
+               advance_moment(runtime);
+               break;
        default:
                fatal_exit("testbound: unknown event type %d", 
                        runtime->now->evt_type);
index 1de5b132e5db763adac0374ac6d219a5f184ffa1..8e3e036534f423030065ca0f6feecee09f92f429 100644 (file)
@@ -221,6 +221,8 @@ replay_moment_read(char* remain, FILE* in, const char* name, int* lineno,
                readentry = 1;
        } else if(parse_keyword(&remain, "TIMEOUT")) {
                mom->evt_type = repevt_timeout;
+       } else if(parse_keyword(&remain, "TIME_PASSES")) {
+               mom->evt_type = repevt_time_passes;
        } else if(parse_keyword(&remain, "ERROR")) {
                mom->evt_type = repevt_error;
        } else {
@@ -242,6 +244,22 @@ replay_moment_read(char* remain, FILE* in, const char* name, int* lineno,
                        return NULL;
                }
        } 
+       if(parse_keyword(&remain, "ELAPSE")) {
+               double sec;
+               errno = 0;
+               sec = strtod(remain, &remain);
+               if(sec == 0. && errno != 0) {
+                       log_err("line %d: could not parse ELAPSE: %s (%s)", 
+                               *lineno, remain, strerror(errno));
+                       free(mom);
+                       return NULL;
+               }
+#ifndef S_SPLINT_S
+               mom->elapse.tv_sec = (int)sec;
+               mom->elapse.tv_usec = (int)((sec - (double)mom->elapse.tv_sec)
+                       *1000000. + 0.5);
+#endif
+       } 
 
        if(readentry) {
                mom->match = read_entry(in, name, lineno, ttl, or, prev);
index 5873ad0292dd83cb2ad3ac1bf0bbaeb1782d2b2d..1c0478831abf745b80a6992b5b387362309164cb 100644 (file)
@@ -58,6 +58,8 @@
  *     o CHECK_OUT_QUERY - followed by entry (if copy-id it is also reply).
  *     o REPLY - followed by entry
  *      o TIMEOUT
+ *      o TIME_PASSES ELAPSE [seconds] - increase 'now' time counter, can be 
+ *                             a floating point number.
  *      o ERROR
  * ; following entry starts on the next line, ENTRY_BEGIN.
  * ; more STEP items
@@ -149,6 +151,8 @@ struct replay_moment {
                repevt_front_reply,
                /** timeout */
                repevt_timeout,
+               /** time passes */
+               repevt_time_passes,
                /** reply arrives from the network */
                repevt_back_reply,
                /** test fails if query to the network does not match */
@@ -162,6 +166,9 @@ struct replay_moment {
        /** The sent packet must match this. Incoming events, the data. */
        struct entry* match;
 
+       /** the amount of time that passes */
+       struct timeval elapse;
+
        /** address that must be matched, or packet remote host address. */
        struct sockaddr_storage addr;
        /** length of addr, if 0, then any address will do */
diff --git a/testdata/fwd_droptoomany.rpl b/testdata/fwd_droptoomany.rpl
new file mode 100644 (file)
index 0000000..26af5f3
--- /dev/null
@@ -0,0 +1,90 @@
+; config options go here.
+server:
+       num-queries-per-thread: 1
+forward-zone: 
+       name: "." 
+       forward-addr: 216.0.0.1
+CONFIG_END
+SCENARIO_BEGIN Test too many queries asked, last is dropped.
+
+; query responses from authority servers.
+RANGE_BEGIN 0 100
+ENTRY_BEGIN
+       MATCH opcode qtype qname
+       ADJUST copy_id
+       REPLY QR RD RA NOERROR
+       SECTION QUESTION
+www.example.net. IN A
+       SECTION ANSWER
+www.example.net. IN A 10.20.30.40
+       SECTION AUTHORITY
+www.example.net. IN NS ns.example.net.
+       SECTION ADDITIONAL
+ns.example.net. IN A 10.20.30.50
+ENTRY_END
+RANGE_END
+
+STEP 1 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 2 CHECK_OUT_QUERY
+ENTRY_BEGIN
+MATCH qname qtype opcode
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+; NO REPLY (this step is not needed)
+STEP 3 NOTHING
+
+; another query
+STEP 4 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+www.example.net. IN A
+ENTRY_END
+
+; reply from first query returns
+STEP 5 REPLY
+ENTRY_BEGIN
+       MATCH opcode qtype qname
+       ADJUST copy_id
+       REPLY QR RD RA NOERROR
+       SECTION QUESTION
+www.example.com. IN A
+       SECTION ANSWER
+www.example.com. IN A 10.20.30.40
+       SECTION AUTHORITY
+www.example.com. IN NS ns.example.com.
+       SECTION ADDITIONAL
+ns.example.com. IN A 10.20.30.50
+ENTRY_END
+
+STEP 10 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qname qtype
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 10.20.30.40
+ENTRY_END
+
+; This answer does not arrive, the query was dropped
+;STEP 11 CHECK_ANSWER
+;ENTRY_BEGIN
+;MATCH opcode qname qtype
+;SECTION QUESTION
+;www.example.net. IN A
+;SECTION ANSWER
+;www.example.net. IN A 10.20.30.40
+;ENTRY_END
+SCENARIO_END
+
+; testbound checks before exit: 
+;  * no more pending queries outstanding.
+;  * and no answers that have not been checked.
diff --git a/testdata/fwd_jostle.rpl b/testdata/fwd_jostle.rpl
new file mode 100644 (file)
index 0000000..14e5e86
--- /dev/null
@@ -0,0 +1,110 @@
+; config options go here.
+; This is one forever, one jostle.
+server:
+       num-queries-per-thread: 2
+forward-zone: 
+       name: "." 
+       forward-addr: 216.0.0.1
+CONFIG_END
+SCENARIO_BEGIN Test too many queries asked, last is too recent to be jostled
+
+; fill the forever slot.
+STEP 1 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 2 CHECK_OUT_QUERY
+ENTRY_BEGIN
+MATCH qname qtype opcode
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+; NO REPLY (this step is not needed)
+STEP 3 NOTHING
+
+;something enters the jostle slot.
+STEP 4 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+www.example.net. IN A
+ENTRY_END
+
+STEP 5 CHECK_OUT_QUERY
+ENTRY_BEGIN
+MATCH qname qtype opcode
+SECTION QUESTION
+www.example.net. IN A
+ENTRY_END
+
+; something else tries to replace the entry in the jostle slot.
+; but the entry in the jostle slot is too recent.
+STEP 6 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+www.example.org. IN A
+ENTRY_END
+
+; reply from latest query returns
+STEP 7 REPLY
+ENTRY_BEGIN
+       MATCH opcode qtype qname
+       ADJUST copy_id
+       REPLY QR RD RA NOERROR
+       SECTION QUESTION
+www.example.net. IN A
+       SECTION ANSWER
+www.example.net. IN A 10.20.30.42
+       SECTION AUTHORITY
+www.example.net. IN NS ns.example.net.
+       SECTION ADDITIONAL
+ns.example.net. IN A 10.20.30.50
+ENTRY_END
+
+; answer to last query
+STEP 8 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qname qtype
+SECTION QUESTION
+www.example.net. IN A
+SECTION ANSWER
+www.example.net. IN A 10.20.30.42
+ENTRY_END
+
+
+; reply from first query returns
+STEP 10 REPLY
+ENTRY_BEGIN
+       MATCH opcode qtype qname
+       ADJUST copy_id
+       REPLY QR RD RA NOERROR
+       SECTION QUESTION
+www.example.com. IN A
+       SECTION ANSWER
+www.example.com. IN A 10.20.30.40
+       SECTION AUTHORITY
+www.example.com. IN NS ns.example.com.
+       SECTION ADDITIONAL
+ns.example.com. IN A 10.20.30.50
+ENTRY_END
+
+; answer to first query
+STEP 11 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qname qtype
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 10.20.30.40
+ENTRY_END
+
+SCENARIO_END
+
+; testbound checks before exit: 
+;  * no more pending queries outstanding.
+;  * and no answers that have not been checked.
diff --git a/testdata/fwd_jostle_out.rpl b/testdata/fwd_jostle_out.rpl
new file mode 100644 (file)
index 0000000..7e01d9e
--- /dev/null
@@ -0,0 +1,113 @@
+; config options go here.
+; This is one forever, one jostle.
+server:
+       num-queries-per-thread: 2
+forward-zone: 
+       name: "." 
+       forward-addr: 216.0.0.1
+CONFIG_END
+SCENARIO_BEGIN Test too many queries asked, last one jostled out to make space
+
+; fill the forever slot.
+STEP 1 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+STEP 2 CHECK_OUT_QUERY
+ENTRY_BEGIN
+MATCH qname qtype opcode
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+; NO REPLY (this step is not needed)
+STEP 3 NOTHING
+
+;something enters the jostle slot.
+STEP 4 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+www.example.net. IN A
+ENTRY_END
+
+STEP 5 CHECK_OUT_QUERY
+ENTRY_BEGIN
+MATCH qname qtype opcode
+SECTION QUESTION
+www.example.net. IN A
+ENTRY_END
+
+; 300 msec passes
+STEP 6 TIME_PASSES ELAPSE 0.300
+
+; something else tries to replace the entry in the jostle slot.
+; and it works because the entry is now too old.
+STEP 8 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+www.example.org. IN A
+ENTRY_END
+
+; reply from latest query returns
+STEP 9 REPLY
+ENTRY_BEGIN
+       MATCH opcode qtype qname
+       ADJUST copy_id
+       REPLY QR RD RA NOERROR
+       SECTION QUESTION
+www.example.org. IN A
+       SECTION ANSWER
+www.example.org. IN A 10.20.30.42
+       SECTION AUTHORITY
+www.example.org. IN NS ns.example.org.
+       SECTION ADDITIONAL
+ns.example.org. IN A 10.20.30.50
+ENTRY_END
+
+; answer to last query
+STEP 10 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qname qtype
+SECTION QUESTION
+www.example.org. IN A
+SECTION ANSWER
+www.example.org. IN A 10.20.30.42
+ENTRY_END
+
+
+; reply from first query returns
+STEP 11 REPLY
+ENTRY_BEGIN
+       MATCH opcode qtype qname
+       ADJUST copy_id
+       REPLY QR RD RA NOERROR
+       SECTION QUESTION
+www.example.com. IN A
+       SECTION ANSWER
+www.example.com. IN A 10.20.30.40
+       SECTION AUTHORITY
+www.example.com. IN NS ns.example.com.
+       SECTION ADDITIONAL
+ns.example.com. IN A 10.20.30.50
+ENTRY_END
+
+; answer to first query
+STEP 12 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qname qtype
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+www.example.com. IN A 10.20.30.40
+ENTRY_END
+
+SCENARIO_END
+
+; testbound checks before exit: 
+;  * no more pending queries outstanding.
+;  * and no answers that have not been checked.
index 5243e331fa4dfa0442ddb58f7eeb90596eb5c1bd..ca4d0658e022a0bc5426051216461fcf4f92c2a9 100644 (file)
@@ -1,6 +1,6 @@
 ; config options go here.
 server:
-       num-queries-per-thread: 1
+       num-queries-per-thread: 2
 forward-zone: 
        name: "." 
        forward-addr: 216.0.0.1