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++;
+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.
- 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.
(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.
* 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).
#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)
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;
}
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)
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;
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);
}
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;
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;
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);
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.
{
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,
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);
}
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;
+}
*/
#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
*/
* 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;
};
/**
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;
};
/**
/** 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 */
/** 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)
{
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";
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.
*/
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);
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 {
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);
* 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
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 */
/** 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 */
--- /dev/null
+; 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.
--- /dev/null
+; 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.
--- /dev/null
+; 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.
; config options go here.
server:
- num-queries-per-thread: 1
+ num-queries-per-thread: 2
forward-zone:
name: "."
forward-addr: 216.0.0.1