CVE-2024-33655: Insufficient Information (NVD-CWE-noinfo)
value: The DNS protocol in RFC 1035 and updates allows remote attackers to cause a denial of service (resource consumption) by arranging for DNS queries to be accumulated for seconds, such that responses are later sent in a pulsing burst (which can be considered traffic amplification in some cases), aka the "DNSBomb" issue.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-06-06T17:15Z
Repository: https://github.com/NLnetLabs/unbound
Language: Other
Files changed: doc/Changelog, doc/example.conf.in, doc/unbound.conf.5.in, services/cache/infra.c, services/cache/infra.h, services/mesh.c, testdata/cachedb_expired_client_timeout.crpl, testdata/cachedb_subnet_expired.crpl, testdata/doh_downstream.tdir/doh_downstream.conf, testdata/doh_downstream_notls.tdir/doh_downstream_notls.conf, testdata/doh_downstream_post.tdir/doh_downstream_post.conf, testdata/fwd_three_service.tdir/fwd_three_service.conf, testdata/iter_ghost_timewindow.rpl, testdata/ssl_req_order.tdir/ssl_req_order.conf, testdata/tcp_req_order.tdir/tcp_req_order.conf, testdata/tcp_sigpipe.tdir/tcp_sigpipe.conf, util/config_file.c, util/config_file.h, util/configlexer.lex, util/configparser.y
Commit date: 2024-05-01 10:10:58 +0200
Commit message: - Fix for the DNSBomb vulnerability CVE-2024-33655. Thanks to Xiang Li
  from the Network and Information Security Lab of Tsinghua University
  for reporting it.

Diff: diff --git a/doc/Changelog b/doc/Changelog
index dab04633..24fbb42c 100644
--- a/doc/Changelog
+++ b/doc/Changelog
@@ -1,5 +1,10 @@
+1 May 2024: Wouter
+	- Fix for the DNSBomb vulnerability CVE-2024-33655. Thanks to Xiang Li
+	  from the Network and Information Security Lab of Tsinghua University
+	  for reporting it.
+
 29 April 2024: Yorgos
 	- Cleanup unnecessary strdup calls for EDE strings.
 
 29 April 2024: Wouter
 	- Fix doxygen comment for errinf_to_str_bogus.
diff --git a/doc/example.conf.in b/doc/example.conf.in
index ea211e97..e0aec8ec 100644
--- a/doc/example.conf.in
+++ b/doc/example.conf.in
@@ -189,10 +189,25 @@ server:
 
 	# msec for waiting for an unknown server to reply.  Increase if you
 	# are behind a slow satellite link, to eg. 1128.
 	# unknown-server-time-limit: 376
 
+	# msec before recursion replies are dropped. The work item continues.
+	# discard-timeout: 1900
+
+	# Max number of replies waiting for recursion per IP address.
+	# wait-limit: 1000
+
+	# Max replies waiting for recursion for IP address with cookie.
+	# wait-limit-cookie: 10000
+
+	# Apart from the default, the wait limit can be set for a netblock.
+	# wait-limit-netblock: 192.0.2.0/24 50000
+
+	# Apart from the default, the wait limit with cookie can be adjusted.
+	# wait-limit-cookie-netblock: 192.0.2.0/24 50000
+
 	# the amount of memory to use for the RRset cache.
 	# plain value in bytes or you can append k, m or G. default is "4Mb".
 	# rrset-cache-size: 4m
 
 	# the number of slabs to use for the RRset cache.
diff --git a/doc/unbound.conf.5.in b/doc/unbound.conf.5.in
index 167f4a5d..6d310c11 100644
--- a/doc/unbound.conf.5.in
+++ b/doc/unbound.conf.5.in
@@ -300,10 +300,40 @@ Default is yes.
 The wait time in msec for waiting for an unknown server to reply.
 Increase this if you are behind a slow satellite link, to eg. 1128.
 That would then avoid re\-querying every initial query because it times out.
 Default is 376 msec.
 .TP
+.B discard\-timeout: \fI<msec>
+The wait time in msec where recursion requests are dropped. This is
+to stop a large number of replies from accumulating. They receive
+no reply, the work item continues to recurse. It is nice to be a bit
+larger than serve\-expired\-client\-timeout if that is enabled.
+A value of 1900 msec is suggested. The value 0 disables it.
+Default 1900 msec.
+.TP
+.B wait\-limit: \fI<number>
+The number of replies that can wait for recursion, for an IP address.
+This makes a ratelimit per IP address of waiting replies for recursion.
+It stops very large amounts of queries waiting to be returned to one
+destination. The value 0 disables wait limits. Default is 1000.
+.TP
+.B wait\-limit\-cookie: \fI<number>
+The number of replies that can wait for recursion, for an IP address
+that sent the query with a valid DNS cookie. Since the cookie validates
+the client address, the limit can be higher. Default is 10000.
+.TP
+.B wait\-limit\-netblock: \fI<netblock> <number>
+The wait limit for the netblock. If not given the wait\-limit value is
+used. The most specific netblock is used to determine the limit. Useful for
+overriding the default for a specific, group or individual, server.
+The value -1 disables wait limits for the netblock.
+.TP
+.B wait\-limit\-cookie\-netblock: \fI<netblock> <number>
+The wait limit for the netblock, when the query has a DNS cookie.
+If not given, the wait\-limit\-cookie value is used.
+The value -1 disables wait limits for the netblock.
+.TP
 .B so\-rcvbuf: \fI<number>
 If not 0, then set the SO_RCVBUF socket option to get more buffer
 space on UDP port 53 incoming queries.  So that short spikes on busy
 servers do not drop packets (see counter in netstat \-su).  Default is
 0 (use system value).  Otherwise, the number of bytes to ask for, try
diff --git a/services/cache/infra.c b/services/cache/infra.c
index 31462d13..457685ab 100644
--- a/services/cache/infra.c
+++ b/services/cache/infra.c
@@ -232,10 +232,85 @@ setup_domain_limits(struct infra_cache* infra, struct config_file* cfg)
 	}
 	name_tree_init_parents(&infra->domain_limits);
 	return 1;
 }
 
+/** find or create element in wait limit netblock tree */
+static struct wait_limit_netblock_info*
+wait_limit_netblock_findcreate(struct infra_cache* infra, char* str,
+	int cookie)
+{
+	rbtree_type* tree;
+	struct sockaddr_storage addr;
+	int net;
+	socklen_t addrlen;
+	struct wait_limit_netblock_info* d;
+
+	if(!netblockstrtoaddr(str, 0, &addr, &addrlen, &net)) {
+		log_err("cannot parse wait limit netblock '%s'", str);
+		return 0;
+	}
+
+	/* can we find it? */
+	if(cookie)
+		tree = &infra->wait_limits_cookie_netblock;
+	else
+		tree = &infra->wait_limits_netblock;
+	d = (struct wait_limit_netblock_info*)addr_tree_find(tree, &addr,
+		addrlen, net);
+	if(d)
+		return d;
+
+	/* create it */
+	d = (struct wait_limit_netblock_info*)calloc(1, sizeof(*d));
+	if(!d)
+		return NULL;
+	d->limit = -1;
+	if(!addr_tree_insert(tree, &d->node, &addr, addrlen, net)) {
+		log_err("duplicate element in domainlimit tree");
+		free(d);
+		return NULL;
+	}
+	return d;
+}
+
+
+/** insert wait limit information into lookup tree */
+static int
+infra_wait_limit_netblock_insert(struct infra_cache* infra,
+	struct config_file* cfg)
+{
+	struct config_str2list* p;
+	struct wait_limit_netblock_info* d;
+	for(p = cfg->wait_limit_netblock; p; p = p->next) {
+		d = wait_limit_netblock_findcreate(infra, p->str, 0);
+		if(!d)
+			return 0;
+		d->limit = atoi(p->str2);
+	}
+	for(p = cfg->wait_limit_cookie_netblock; p; p = p->next) {
+		d = wait_limit_netblock_findcreate(infra, p->str, 1);
+		if(!d)
+			return 0;
+		d->limit = atoi(p->str2);
+	}
+	return 1;
+}
+
+/** setup wait limits tree (0 on failure) */
+static int
+setup_wait_limits(struct infra_cache* infra, struct config_file* cfg)
+{
+	addr_tree_init(&infra->wait_limits_netblock);
+	addr_tree_init(&infra->wait_limits_cookie_netblock);
+	if(!infra_wait_limit_netblock_insert(infra, cfg))
+		return 0;
+	addr_tree_init_parents(&infra->wait_limits_netblock);
+	addr_tree_init_parents(&infra->wait_limits_cookie_netblock);
+	return 1;
+}
+
 struct infra_cache* 
 infra_create(struct config_file* cfg)
 {
 	struct infra_cache* infra = (struct infra_cache*)calloc(1, 
 		sizeof(struct infra_cache));
@@ -265,10 +340,14 @@ infra_create(struct config_file* cfg)
 	/* insert config data into ratelimits */
 	if(!setup_domain_limits(infra, cfg)) {
 		infra_delete(infra);
 		return NULL;
 	}
+	if(!setup_wait_limits(infra, cfg)) {
+		infra_delete(infra);
+		return NULL;
+	}
 	infra_ip_ratelimit = cfg->ip_ratelimit;
 	infra->client_ip_rates = slabhash_create(cfg->ip_ratelimit_slabs,
 	    INFRA_HOST_STARTSIZE, cfg->ip_ratelimit_size, &ip_rate_sizefunc,
 	    &ip_rate_compfunc, &ip_rate_delkeyfunc, &ip_rate_deldatafunc, NULL);
 	if(!infra->client_ip_rates) {
@@ -285,19 +364,29 @@ static void domain_limit_free(rbnode_type* n, void* ATTR_UNUSED(arg))
 		free(((struct domain_limit_data*)n)->node.name);
 		free(n);
 	}
 }
 
+/** delete wait_limit_netblock_info entries */
+static void wait_limit_netblock_del(rbnode_type* n, void* ATTR_UNUSED(arg))
+{
+	free(n);
+}
+
 void 
 infra_delete(struct infra_cache* infra)
 {
 	if(!infra)
 		return;
 	slabhash_delete(infra->hosts);
 	slabhash_delete(infra->domain_rates);
 	traverse_postorder(&infra->domain_limits, domain_limit_free, NULL);
 	slabhash_delete(infra->client_ip_rates);
+	traverse_postorder(&infra->wait_limits_netblock,
+		wait_limit_netblock_del, NULL);
+	traverse_postorder(&infra->wait_limits_cookie_netblock,
+		wait_limit_netblock_del, NULL);
 	free(infra);
 }
 
 struct infra_cache* 
 infra_adjust(struct infra_cache* infra, struct config_file* cfg)
@@ -878,11 +967,12 @@ static void infra_create_ratedata(struct infra_cache* infra,
 	slabhash_insert(infra->domain_rates, h, &k->entry, d, NULL);
 }
 
 /** create rate data item for ip address */
 static void infra_ip_create_ratedata(struct infra_cache* infra,
-	struct sockaddr_storage* addr, socklen_t addrlen, time_t timenow)
+	struct sockaddr_storage* addr, socklen_t addrlen, time_t timenow,
+	int mesh_wait)
 {
 	hashvalue_type h = hash_addr(addr, addrlen, 0);
 	struct ip_rate_key* k = (struct ip_rate_key*)calloc(1, sizeof(*k));
 	struct ip_rate_data* d = (struct ip_rate_data*)calloc(1, sizeof(*d));
 	if(!k || !d) {
@@ -896,10 +986,11 @@ static void infra_ip_create_ratedata(struct infra_cache* infra,
 	k->entry.hash = h;
 	k->entry.key = k;
 	k->entry.data = d;
 	d->qps[0] = 1;
 	d->timestamp[0] = timenow;
+	d->mesh_wait = mesh_wait;
 	slabhash_insert(infra->client_ip_rates, h, &k->entry, d, NULL);
 }
 
 /** Find the second and return its rate counter. If none and should_add, remove
  *  oldest to accommodate. Else return none. */
@@ -1119,8 +1210,83 @@ int infra_ip_ratelimit_inc(struct infra_cache* infra,
 		return check_ip_ratelimit(addr, addrlen, buffer, premax, max,
 			has_cookie);
 	}
 
 	/* create */
-	infra_ip_create_ratedata(infra, addr, addrlen, timenow);
+	infra_ip_create_ratedata(infra, addr, addrlen, timenow, 0);
 	return 1;
 }
+
+int infra_wait_limit_allowed(struct infra_cache* infra, struct comm_reply* rep,
+	int cookie_valid, struct config_file* cfg)
+{
+	struct lruhash_entry* entry;
+	if(cfg->wait_limit == 0)
+		return 1;
+
+	entry = infra_find_ip_ratedata(infra, &rep->client_addr,
+		rep->client_addrlen, 0);
+	if(entry) {
+		rbtree_type* tree;
+		struct wait_limit_netblock_info* w;
+		struct rate_data* d = (struct rate_data*)entry->data;
+		int mesh_wait = d->mesh_wait;
+		lock_rw_unlock(&entry->lock);
+
+		/* have the wait amount, check how much is allowed */
+		if(cookie_valid)
+			tree = &infra->wait_limits_cookie_netblock;
+		else	tree = &infra->wait_limits_netblock;
+		w = (struct wait_limit_netblock_info*)addr_tree_lookup(tree,
+			&rep->client_addr, rep->client_addrlen);
+		if(w) {
+			if(w->limit != -1 && mesh_wait > w->limit)
+				return 0;
+		} else {
+			/* if there is no IP netblock specific information,
+			 * use the configured value. */
+			if(mesh_wait > (cookie_valid?cfg->wait_limit_cookie:
+				cfg->wait_limit))
+				return 0;
+		}
+	}
+	return 1;
+}
+
+void infra_wait_limit_inc(struct infra_cache* infra, struct comm_reply* rep,
+	time_t timenow, struct config_file* cfg)
+{
+	struct lruhash_entry* entry;
+	if(cfg->wait_limit == 0)
+		return;
+
+	/* Find it */
+	entry = infra_find_ip_ratedata(infra, &rep->client_addr,
+		rep->client_addrlen, 1);
+	if(entry) {
+		struct rate_data* d = (struct rate_data*)entry->data;
+		d->mesh_wait++;
+		lock_rw_unlock(&entry->lock);
+		return;
+	}
+
+	/* Create it */
+	infra_ip_create_ratedata(infra, &rep->client_addr,
+		rep->client_addrlen, timenow, 1);
+}
+
+void infra_wait_limit_dec(struct infra_cache* infra, struct comm_reply* rep,
+	struct config_file* cfg)
+{
+	struct lruhash_entry* entry;
+	if(cfg->wait_limit == 0)
+		return;
+
+	entry = infra_find_ip_ratedata(infra, &rep->client_addr,
+		rep->client_addrlen, 1);
+	if(entry) {
+		struct rate_data* d = (struct rate_data*)entry->data;
+		if(d->mesh_wait > 0)
+			d->mesh_wait--;
+		lock_rw_unlock(&entry->lock);
+	}
+}
diff --git a/services/cache/infra.h b/services/cache/infra.h
index 525073bf..ee6f384d 100644
--- a/services/cache/infra.h
+++ b/services/cache/infra.h
@@ -120,10 +120,14 @@ struct infra_cache {
 	struct slabhash* domain_rates;
 	/** ratelimit settings for domains, struct domain_limit_data */
 	rbtree_type domain_limits;
 	/** hash table with query rates per client ip: ip_rate_key, ip_rate_data */
 	struct slabhash* client_ip_rates;
+	/** tree of addr_tree_node, with wait_limit_netblock_info information */
+	rbtree_type wait_limits_netblock;
+	/** tree of addr_tree_node, with wait_limit_netblock_info information */
+	rbtree_type wait_limits_cookie_netblock;
 };
 
 /** ratelimit, unless overridden by domain_limits, 0 is off */
 extern int infra_dp_ratelimit;
 
@@ -182,14 +186,26 @@ struct rate_data {
 	/** queries counted, for that second. 0 if not in use. */
 	int qps[RATE_WINDOW];
 	/** what the timestamp is of the qps array members, counter is
 	 * valid for that timestamp.  Usually now and now-1. */
 	time_t timestamp[RATE_WINDOW];
+	/** the number of queries waiting in the mesh */
+	int mesh_wait;
 };
 
 #define ip_rate_data rate_data
 
+/**
+ * Data to store the configuration per netblock for the wait limit
+ */
+struct wait_limit_netblock_info {
+	/** The addr tree node, this must be first. */
+	struct addr_tree_node node;
+	/** the limit on the amount */
+	int limit;
+};
+
 /** infra host cache default hash lookup size */
 #define INFRA_HOST_STARTSIZE 32
 /** bytes per zonename reserved in the hostcache, dnamelen(zonename.com.) */
 #define INFRA_BYTES_NAME 14
 
@@ -472,6 +488,18 @@ int ip_rate_compfunc(void* key1, void* key2);
 void ip_rate_delkeyfunc(void* d, void* arg);
 
 /* delete data */
 #define ip_rate_deldatafunc rate_deldatafunc
 
+/** See if the IP address can have another reply in the wait limit */
+int infra_wait_limit_allowed(struct infra_cache* infra, struct comm_reply* rep,
+	int cookie_valid, struct config_file* cfg);
+
+/** Increment number of waiting replies for IP */
+void infra_wait_limit_inc(struct infra_cache* infra, struct comm_reply* rep,
+	time_t timenow, struct config_file* cfg);
+
+/** Decrement number of waiting replies for IP */
+void infra_wait_limit_dec(struct infra_cache* infra, struct comm_reply* rep,
+	struct config_file* cfg);
+
 #endif /* SERVICES_CACHE_INFRA_H */
diff --git a/services/mesh.c b/services/mesh.c
index 55afd065..e886c4b9 100644
--- a/services/mesh.c
+++ b/services/mesh.c
@@ -45,10 +45,11 @@
 #include "config.h"
 #include "services/mesh.h"
 #include "services/outbound_list.h"
 #include "services/cache/dns.h"
 #include "services/cache/rrset.h"
+#include "services/cache/infra.h"
 #include "util/log.h"
 #include "util/net_help.h"
 #include "util/module.h"
 #include "util/regional.h"
 #include "util/data/msgencode.h"
@@ -413,10 +414,18 @@ void mesh_new_client(struct mesh_area* mesh, struct query_info* qinfo,
 		mesh->env->cfg->serve_expired_client_timeout:0;
 	struct sldns_buffer* r_buffer = rep->c->buffer;
 	if(rep->c->tcp_req_info) {
 		r_buffer = rep->c->tcp_req_info->spool_buffer;
 	}
+	if(!infra_wait_limit_allowed(mesh->env->infra_cache, rep,
+		edns->cookie_valid, mesh->env->cfg)) {
+		verbose(VERB_ALGO, "Too many queries waiting from the IP. "
+			"dropping incoming query.");
+		comm_point_drop_reply(rep);
+		mesh->stats_dropped++;
+		return;
+	}
 	if(!unique)
 		s = mesh_area_find(mesh, cinfo, qinfo, qflags&(BIT_RD|BIT_CD), 0, 0);
 	/* does this create a new reply state? */
 	if(!s || s->list_select == mesh_no_list) {
 		if(!mesh_make_new_space(mesh, rep->c->buffer)) {
@@ -520,10 +529,12 @@ void mesh_new_client(struct mesh_area* mesh, struct query_info* qinfo,
 			log_err("mesh_new_client: out of memory initializing serve expired");
 			goto servfail_mem;
 		}
 	}
 #endif
+	infra_wait_limit_inc(mesh->env->infra_cache, rep, *mesh->env->now,
+		mesh->env->cfg);
 	/* update statistics */
 	if(was_detached) {
 		log_assert(mesh->num_detached_states > 0);
 		mesh->num_detached_states--;
 	}
@@ -951,10 +962,12 @@ mesh_state_cleanup(struct mesh_state* mstate)
 		/* in tcp_req_info, the mstates linked are removed, but
 		 * the reply_list is now NULL, so the remove-from-empty-list
 		 * takes no time and also it does not do the mesh accounting */
 		mstate->reply_list = NULL;
 		for(; rep; rep=rep->next) {
+			infra_wait_limit_dec(mesh->env->infra_cache,
+				&rep->query_reply, mesh->env->cfg);
 			comm_point_drop_reply(&rep->query_reply);
 			log_assert(mesh->num_reply_addrs > 0);
 			mesh->num_reply_addrs--;
 		}
 		while((cb = mstate->cb_list)!=NULL) {
@@ -1434,10 +1447,12 @@ mesh_send_reply(struct mesh_state* m, int rcode, struct reply_info* rep,
 		}
 		m->reply_list = NULL;
 		comm_point_send_reply(&r->query_reply);
 		m->reply_list = rlist;
 	}
+	infra_wait_limit_dec(m->s.env->infra_cache, &r->query_reply,
+		m->s.env->cfg);
 	/* account */
 	log_assert(m->s.env->mesh->num_reply_addrs > 0);
 	m->s.env->mesh->num_reply_addrs--;
 	end_time = *m->s.env->now_tv;
 	timeval_subtract(&duration, &end_time, &r->start_time);
@@ -1489,10 +1504,32 @@ void mesh_query_done(struct mesh_state* mstate)
 			char* err = errinf_to_str_servfail(&mstate->s);
 			if(err) { log_err("%s", err); }
 		}
 	}
 	for(r = mstate->reply_list; r; r = r->next) {
+		struct timeval old;
+		timeval_subtract(&old, mstate->s.env->now_tv, &r->start_time);
+		if(mstate->s.env->cfg->discard_timeout != 0 &&
+			((int)old.tv_sec)*1000+((int)old.tv_usec)/1000 >
+			mstate->s.env->cfg->discard_timeout) {
+			/* Drop the reply, it is too old */
+			/* briefly set the reply_list to NULL, so that the
+			 * tcp req info cleanup routine that calls the mesh
+			 * to deregister the meshstate for it is not done
+			 * because the list is NULL and also accounting is not
+			 * done there, but instead we do that here. */
+			struct mesh_reply* reply_list = mstate->reply_list;
+			verbose(VERB_ALGO, "drop reply, it is older than discard-timeout");
+			infra_wait_limit_dec(mstate->s.env->infra_cache,
+				&r->query_reply, mstate->s.env->cfg);
+			mstate->reply_list = NULL;
+			comm_point_drop_reply(&r->query_reply);
+			mstate->reply_list = reply_list;
+			mstate->s.env->mesh->stats_dropped++;
+			continue;
+		}
+
 		i++;
 		tv = r->start_time;
 
 		/* if a response-ip address block has been stored the
 		 *  information should be logged for each client. */
@@ -1512,10 +1549,12 @@ void mesh_query_done(struct mesh_state* mstate)
 			 * tcp req info cleanup routine that calls the mesh
 			 * to deregister the meshstate for it is not done
 			 * because the list is NULL and also accounting is not
 			 * done there, but instead we do that here. */
 			struct mesh_reply* reply_list = mstate->reply_list;
+			infra_wait_limit_dec(mstate->s.env->infra_cache,
+				&r->query_reply, mstate->s.env->cfg);
 			mstate->reply_list = NULL;
 			comm_point_drop_reply(&r->query_reply);
 			mstate->reply_list = reply_list;
 		} else {
 			struct sldns_buffer* r_buffer = r->query_reply.c->buffer;
@@ -2044,10 +2083,12 @@ void mesh_state_remove_reply(struct mesh_area* mesh, struct mesh_state* m,
 			if(prev) prev->next = n->next;
 			else m->reply_list = n->next;
 			/* delete it, but allocated in m region */
 			log_assert(mesh->num_reply_addrs > 0);
 			mesh->num_reply_addrs--;
+			infra_wait_limit_dec(mesh->env->infra_cache,
+				&n->query_reply, mesh->env->cfg);
 
 			/* prev = prev; */
 			n = n->next;
 			continue;
 		}
@@ -2184,10 +2225,32 @@ mesh_serve_expired_callback(void* arg)
 
 	if(verbosity >= VERB_ALGO)
 		log_dns_msg("Serve expired lookup", &qstate->qinfo, msg->rep);
 
 	for(r = mstate->reply_list; r; r = r->next) {
+		struct timeval old;
+		timeval_subtract(&old, mstate->s.env->now_tv, &r->start_time);
+		if(mstate->s.env->cfg->discard_timeout != 0 &&
+			((int)old.tv_sec)*1000+((int)old.tv_usec)/1000 >
+			mstate->s.env->cfg->discard_timeout) {
+			/* Drop the reply, it is too old */
+			/* briefly set the reply_list to NULL, so that the
+			 * tcp req info cleanup routine that calls the mesh
+			 * to deregister the meshstate for it is not done
+			 * because the list is NULL and also accounting is not
+			 * done there, but instead we do that here. */
+			struct mesh_reply* reply_list = mstate->reply_list;
+			verbose(VERB_ALGO, "drop reply, it is older than discard-timeout");
+			infra_wait_limit_dec(mstate->s.env->infra_cache,
+				&r->query_reply, mstate->s.env->cfg);
+			mstate->reply_list = NULL;
+			comm_point_drop_reply(&r->query_reply);
+			mstate->reply_list = reply_list;
+			mstate->s.env->mesh->stats_dropped++;
+			continue;
+		}
+
 		i++;
 		tv = r->start_time;
 
 		/* If address info is returned, it means the action should be an
 		* 'inform' variant and the information should be logged. */
@@ -2211,10 +2274,12 @@ mesh_serve_expired_callback(void* arg)
 			r_buffer = r->query_reply.c->tcp_req_info->spool_buffer;
 		mesh_send_reply(mstate, LDNS_RCODE_NOERROR, msg->rep,
 			r, r_buffer, prev, prev_buffer);
 		if(r->query_reply.c->tcp_req_info)
 			tcp_req_info_remove_mesh_state(r->query_reply.c->tcp_req_info, mstate);
+		infra_wait_limit_dec(mstate->s.env->infra_cache,
+			&r->query_reply, mstate->s.env->cfg);
 		prev = r;
 		prev_buffer = r_buffer;
 	}
 	/* Account for each reply sent. */
 	if(i > 0) {
diff --git a/testdata/cachedb_expired_client_timeout.crpl b/testdata/cachedb_expired_client_timeout.crpl
index 23f6dbd7..78ddf4d8 100644
--- a/testdata/cachedb_expired_client_timeout.crpl
+++ b/testdata/cachedb_expired_client_timeout.crpl
@@ -7,10 +7,11 @@ server:
 	serve-expired-reply-ttl: 30
 	; at least one second, so we can time skip past the timer in the
 	; testbound script steps, but also reply within the time.
 	serve-expired-client-timeout: 1200
 	module-config: "cachedb iterator"
+	discard-timeout: 3000
 
 cachedb:
 	backend: "testframe"
 	secret-seed: "testvalue"
 	cachedb-check-when-serve-expired: yes
diff --git a/testdata/cachedb_subnet_expired.crpl b/testdata/cachedb_subnet_expired.crpl
index a6057b0b..eddff100 100644
--- a/testdata/cachedb_subnet_expired.crpl
+++ b/testdata/cachedb_subnet_expired.crpl
@@ -12,10 +12,11 @@ server:
 	max-client-subnet-ipv4: 17
 	; subnetcache is to the left of cachedb, because it sets no cache
 	; store for edns subnet content for modules to the right of it.
 	; this keeps subnet content out of cachedb as global content.
 	module-config: "subnetcache cachedb iterator"
+	discard-timeout: 3000
 
 cachedb:
 	backend: "testframe"
 	secret-seed: "testvalue"
 	cachedb-check-when-serve-expired: yes
diff --git a/testdata/doh_downstream.tdir/doh_downstream.conf b/testdata/doh_downstream.tdir/doh_downstream.conf
index f0857bb5..222c2159 100644
--- a/testdata/doh_downstream.tdir/doh_downstream.conf
+++ b/testdata/doh_downstream.tdir/doh_downstream.conf
@@ -9,10 +9,11 @@ server:
 	directory: .
 	pidfile: "unbound.pid"
 	chroot: ""
 	username: ""
 	do-not-query-localhost: no
+	discard-timeout: 3000  # testns uses sleep=2
 	http-query-buffer-size: 1G
 	http-response-buffer-size: 1G
 	http-max-streams: 200
 
 	local-zone: "example.net" static
diff --git a/testdata/doh_downstream_notls.tdir/doh_downstream_notls.conf b/testdata/doh_downstream_notls.tdir/doh_downstream_notls.conf
index bdca4564..161c3555 100644
--- a/testdata/doh_downstream_notls.tdir/doh_downstream_notls.conf
+++ b/testdata/doh_downstream_notls.tdir/doh_downstream_notls.conf
@@ -9,10 +9,11 @@ server:
 	directory: .
 	pidfile: "unbound.pid"
 	chroot: ""
 	username: ""
 	do-not-query-localhost: no
+	discard-timeout: 3000  # testns uses sleep=2
 	http-query-buffer-size: 1G
 	http-response-buffer-size: 1G
 	http-max-streams: 200
 	http-notls-downstream: yes
 
diff --git a/testdata/doh_downstream_post.tdir/doh_downstream_post.conf b/testdata/doh_downstream_post.tdir/doh_downstream_post.conf
index f0857bb5..222c2159 100644
--- a/testdata/doh_downstream_post.tdir/doh_downstream_post.conf
+++ b/testdata/doh_downstream_post.tdir/doh_downstream_post.conf
@@ -9,10 +9,11 @@ server:
 	directory: .
 	pidfile: "unbound.pid"
 	chroot: ""
 	username: ""
 	do-not-query-localhost: no
+	discard-timeout: 3000  # testns uses sleep=2
 	http-query-buffer-size: 1G
 	http-response-buffer-size: 1G
 	http-max-streams: 200
 
 	local-zone: "example.net" static
diff --git a/testdata/fwd_three_service.tdir/fwd_three_service.conf b/testdata/fwd_three_service.tdir/fwd_three_service.conf
index 05fafe01..d6c9a205 100644
--- a/testdata/fwd_three_service.tdir/fwd_three_service.conf
+++ b/testdata/fwd_three_service.tdir/fwd_three_service.conf
@@ -9,8 +9,9 @@ server:
 	chroot: ""
 	username: ""
 	num-queries-per-thread: 1024
 	use-syslog: no
 	do-not-query-localhost: no
+	discard-timeout: 3000  # testns uses sleep=2
 forward-zone:
 	name: "."
 	forward-addr: "127.0.0.1@@TOPORT@"
diff --git a/testdata/iter_ghost_timewindow.rpl b/testdata/iter_ghost_timewindow.rpl
index 566be82a..9e304628 100644
--- a/testdata/iter_ghost_timewindow.rpl
+++ b/testdata/iter_ghost_timewindow.rpl
@@ -1,10 +1,11 @@
 ; config options
 server:
 	target-fetch-policy: "0 0 0 0 0"
 	qname-minimisation: "no"
 	minimal-responses: no
+	discard-timeout: 86400
 
 stub-zone:
 	name: "."
 	stub-addr: 193.0.14.129 	# K.ROOT-SERVERS.NET.
 CONFIG_END
diff --git a/testdata/ssl_req_order.tdir/ssl_req_order.conf b/testdata/ssl_req_order.tdir/ssl_req_order.conf
index 3b2e2b1b..ec39d3ab 100644
--- a/testdata/ssl_req_order.tdir/ssl_req_order.conf
+++ b/testdata/ssl_req_order.tdir/ssl_req_order.conf
@@ -7,10 +7,11 @@ server:
 	directory: .
 	pidfile: "unbound.pid"
 	chroot: ""
 	username: ""
 	do-not-query-localhost: no
+	discard-timeout: 3000  # testns uses sleep=2
 	ssl-port: @PORT@
 	ssl-service-key: "unbound_server.key"
 	ssl-service-pem: "unbound_server.pem"
 
 	local-zone: "example.net" static
diff --git a/testdata/tcp_req_order.tdir/tcp_req_order.conf b/testdata/tcp_req_order.tdir/tcp_req_order.conf
index 40d6f55c..b2804e8e 100644
--- a/testdata/tcp_req_order.tdir/tcp_req_order.conf
+++ b/testdata/tcp_req_order.tdir/tcp_req_order.conf
@@ -7,10 +7,11 @@ server:
 	directory: .
 	pidfile: "unbound.pid"
 	chroot: ""
 	username: ""
 	do-not-query-localhost: no
+	discard-timeout: 3000  # testns uses sleep=2
 
 	local-zone: "example.net" static
 	local-data: "www1.example.net. IN A 1.2.3.1"
 	local-data: "www2.example.net. IN A 1.2.3.2"
 	local-data: "www3.example.net. IN A 1.2.3.3"
diff --git a/testdata/tcp_sigpipe.tdir/tcp_sigpipe.conf b/testdata/tcp_sigpipe.tdir/tcp_sigpipe.conf
index 384f16b0..4f1ff9b0 100644
--- a/testdata/tcp_sigpipe.tdir/tcp_sigpipe.conf
+++ b/testdata/tcp_sigpipe.tdir/tcp_sigpipe.conf
@@ -1,15 +1,16 @@
 server:
-	verbosity: 2
+	verbosity: 4
 	# num-threads: 1
 	interface: 127.0.0.1
 	port: @PORT@
 	use-syslog: no
 	directory: .
 	pidfile: "unbound.pid"
 	chroot: ""
 	username: ""
 	do-not-query-localhost: no
+	discard-timeout: 3000  # testns uses sleep=2
 
 forward-zone:
 	name: "."
 	forward-addr: "127.0.0.1@@TOPORT@"
diff --git a/util/config_file.c b/util/config_file.c
index e4fe3ee9..2b67d4c1 100644
--- a/util/config_file.c
+++ b/util/config_file.c
@@ -307,10 +307,15 @@ config_create(void)
 	cfg->control_port = UNBOUND_CONTROL_PORT;
 	cfg->control_use_cert = 1;
 	cfg->minimal_responses = 1;
 	cfg->rrset_roundrobin = 1;
 	cfg->unknown_server_time_limit = 376;
+	cfg->discard_timeout = 1900; /* msec */
+	cfg->wait_limit = 1000;
+	cfg->wait_limit_cookie = 10000;
+	cfg->wait_limit_netblock = NULL;
+	cfg->wait_limit_cookie_netblock = NULL;
 	cfg->max_udp_size = 1232; /* value taken from edns_buffer_size */
 	if(!(cfg->server_key_file = strdup(RUN_DIR"/unbound_server.key")))
 		goto error_exit;
 	if(!(cfg->server_cert_file = strdup(RUN_DIR"/unbound_server.pem")))
 		goto error_exit;
@@ -724,10 +729,13 @@ int config_set_option(struct config_file* cfg, const char* opt,
 	else S_POW2("key-cache-slabs:", key_cache_slabs)
 	else S_MEMSIZE("neg-cache-size:", neg_cache_size)
 	else S_YNO("minimal-responses:", minimal_responses)
 	else S_YNO("rrset-roundrobin:", rrset_roundrobin)
 	else S_NUMBER_OR_ZERO("unknown-server-time-limit:", unknown_server_time_limit)
+	else S_NUMBER_OR_ZERO("discard-timeout:", discard_timeout)
+	else S_NUMBER_OR_ZERO("wait-limit:", wait_limit)
+	else S_NUMBER_OR_ZERO("wait-limit-cookie:", wait_limit_cookie)
 	else S_STRLIST("local-data:", local_data)
 	else S_YNO("unblock-lan-zones:", unblock_lan_zones)
 	else S_YNO("insecure-lan-zones:", insecure_lan_zones)
 	else S_YNO("control-enable:", remote_control_enable)
 	else S_STRLIST_APPEND("control-interface:", control_ifs)
@@ -1205,10 +1213,15 @@ config_get_option(struct config_file* cfg, const char* opt,
 	else O_LST(opt, "domain-insecure", domain_insecure)
 	else O_UNS(opt, "val-override-date", val_date_override)
 	else O_YNO(opt, "minimal-responses", minimal_responses)
 	else O_YNO(opt, "rrset-roundrobin", rrset_roundrobin)
 	else O_DEC(opt, "unknown-server-time-limit", unknown_server_time_limit)
+	else O_DEC(opt, "discard-timeout", discard_timeout)
+	else O_DEC(opt, "wait-limit", wait_limit)
+	else O_DEC(opt, "wait-limit-cookie", wait_limit_cookie)
+	else O_LS2(opt, "wait-limit-netblock", wait_limit_netblock)
+	else O_LS2(opt, "wait-limit-cookie-netblock", wait_limit_cookie_netblock)
 #ifdef CLIENT_SUBNET
 	else O_LST(opt, "send-client-subnet", client_subnet)
 	else O_LST(opt, "client-subnet-zone", client_subnet_zone)
 	else O_DEC(opt, "max-client-subnet-ipv4", max_client_subnet_ipv4)
 	else O_DEC(opt, "max-client-subnet-ipv6", max_client_subnet_ipv6)
@@ -1676,10 +1689,12 @@ config_delete(struct config_file* cfg)
 	config_deldblstrlist(cfg->interface_view);
 	config_del_strbytelist(cfg->interface_tags);
 	config_deltrplstrlist(cfg->interface_tag_actions);
 	config_deltrplstrlist(cfg->interface_tag_datas);
 	config_delstrlist(cfg->control_ifs.first);
+	config_deldblstrlist(cfg->wait_limit_netblock);
+	config_deldblstrlist(cfg->wait_limit_cookie_netblock);
 	free(cfg->server_key_file);
 	free(cfg->server_cert_file);
 	free(cfg->control_key_file);
 	free(cfg->control_cert_file);
 	free(cfg->nat64_prefix);
diff --git a/util/config_file.h b/util/config_file.h
index 42836796..d3a2e268 100644
--- a/util/config_file.h
+++ b/util/config_file.h
@@ -535,10 +535,25 @@ struct config_file {
 	int rrset_roundrobin;
 
 	/* wait time for unknown server in msec */
 	int unknown_server_time_limit;
 
+	/** Wait time to drop recursion replies */
+	int discard_timeout;
+
+	/** Wait limit for number of replies per IP address */
+	int wait_limit;
+
+	/** Wait limit for number of replies per IP address with cookie */
+	int wait_limit_cookie;
+
+	/** wait limit per netblock */
+	struct config_str2list* wait_limit_netblock;
+
+	/** wait limit with cookie per netblock */
+	struct config_str2list* wait_limit_cookie_netblock;
+
 	/* maximum UDP response size */
 	size_t max_udp_size;
 
 	/* DNS64 prefix */
 	char* dns64_prefix;
diff --git a/util/configlexer.lex b/util/configlexer.lex
index 51bf0660..7ae1b8c3 100644
--- a/util/configlexer.lex
+++ b/util/configlexer.lex
@@ -462,10 +462,15 @@ dynlib-file{COLON}		{ YDVAR(1, VAR_DYNLIB_FILE) }
 dynlib{COLON}			{ YDVAR(0, VAR_DYNLIB) }
 domain-insecure{COLON}		{ YDVAR(1, VAR_DOMAIN_INSECURE) }
 minimal-responses{COLON}	{ YDVAR(1, VAR_MINIMAL_RESPONSES) }
 rrset-roundrobin{COLON}		{ YDVAR(1, VAR_RRSET_ROUNDROBIN) }
 unknown-server-time-limit{COLON} { YDVAR(1, VAR_UNKNOWN_SERVER_TIME_LIMIT) }
+discard-timeout{COLON}		{ YDVAR(1, VAR_DISCARD_TIMEOUT) }
+wait-limit{COLON}		{ YDVAR(1, VAR_WAIT_LIMIT) }
+wait-limit-cookie{COLON}	{ YDVAR(1, VAR_WAIT_LIMIT_COOKIE) }
+wait-limit-netblock{COLON}	{ YDVAR(1, VAR_WAIT_LIMIT_NETBLOCK) }
+wait-limit-cookie-netblock{COLON} { YDVAR(1, VAR_WAIT_LIMIT_COOKIE_NETBLOCK) }
 max-udp-size{COLON}		{ YDVAR(1, VAR_MAX_UDP_SIZE) }
 dns64-prefix{COLON}		{ YDVAR(1, VAR_DNS64_PREFIX) }
 dns64-synthall{COLON}		{ YDVAR(1, VAR_DNS64_SYNTHALL) }
 dns64-ignore-aaaa{COLON}	{ YDVAR(1, VAR_DNS64_IGNORE_AAAA) }
 nat64-prefix{COLON}		{ YDVAR(1, VAR_NAT64_PREFIX) }
diff --git a/util/configparser.y b/util/configparser.y
index 2adbf92c..0feeb61b 100644
--- a/util/configparser.y
+++ b/util/configparser.y
@@ -187,10 +187,12 @@ extern struct config_parser_state* cfg_parser;
 %token VAR_FAST_SERVER_PERMIL VAR_FAST_SERVER_NUM
 %token VAR_ALLOW_NOTIFY VAR_TLS_WIN_CERT VAR_TCP_CONNECTION_LIMIT
 %token VAR_ANSWER_COOKIE VAR_COOKIE_SECRET VAR_IP_RATELIMIT_COOKIE
 %token VAR_FORWARD_NO_CACHE VAR_STUB_NO_CACHE VAR_LOG_SERVFAIL VAR_DENY_ANY
 %token VAR_UNKNOWN_SERVER_TIME_LIMIT VAR_LOG_TAG_QUERYREPLY
+%token VAR_DISCARD_TIMEOUT VAR_WAIT_LIMIT VAR_WAIT_LIMIT_COOKIE
+%token VAR_WAIT_LIMIT_NETBLOCK VAR_WAIT_LIMIT_COOKIE_NETBLOCK
 %token VAR_STREAM_WAIT_SIZE VAR_TLS_CIPHERS VAR_TLS_CIPHERSUITES VAR_TLS_USE_SNI
 %token VAR_IPSET VAR_IPSET_NAME_V4 VAR_IPSET_NAME_V6
 %token VAR_TLS_SESSION_TICKET_KEYS VAR_RPZ VAR_TAGS VAR_RPZ_ACTION_OVERRIDE
 %token VAR_RPZ_CNAME_OVERRIDE VAR_RPZ_LOG VAR_RPZ_LOG_NAME
 %token VAR_DYNLIB VAR_DYNLIB_FILE VAR_EDNS_CLIENT_STRING
@@ -325,10 +327,12 @@ content_server: server_num_threads | server_verbosity | server_port |
 	server_udp_upstream_without_downstream | server_aggressive_nsec |
 	server_tls_cert_bundle | server_tls_additional_port | server_low_rtt |
 	server_fast_server_permil | server_fast_server_num  | server_tls_win_cert |
 	server_tcp_connection_limit | server_log_servfail | server_deny_any |
 	server_unknown_server_time_limit | server_log_tag_queryreply |
+	server_discard_timeout | server_wait_limit | server_wait_limit_cookie |
+	server_wait_limit_netblock | server_wait_limit_cookie_netblock |
 	server_stream_wait_size | server_tls_ciphers |
 	server_tls_ciphersuites | server_tls_session_ticket_keys |
 	server_answer_cookie | server_cookie_secret | server_ip_ratelimit_cookie |
 	server_tls_use_sni | server_edns_client_string |
 	server_edns_client_string_opcode | server_nsid |
@@ -2375,10 +2379,61 @@ server_unknown_server_time_limit: VAR_UNKNOWN_SERVER_TIME_LIMIT STRING_ARG
 		OUTYY(("P(server_unknown_server_time_limit:%s)\n", $2));
 		cfg_parser->cfg->unknown_server_time_limit = atoi($2);
 		free($2);
 	}
 	;
+server_discard_timeout: VAR_DISCARD_TIMEOUT STRING_ARG
+	{
+		OUTYY(("P(server_discard_timeout:%s)\n", $2));
+		cfg_parser->cfg->discard_timeout = atoi($2);
+		free($2);
+	}
+	;
+server_wait_limit: VAR_WAIT_LIMIT STRING_ARG
+	{
+		OUTYY(("P(server_wait_limit:%s)\n", $2));
+		cfg_parser->cfg->wait_limit = atoi($2);
+		free($2);
+	}
+	;
+server_wait_limit_cookie: VAR_WAIT_LIMIT_COOKIE STRING_ARG
+	{
+		OUTYY(("P(server_wait_limit_cookie:%s)\n", $2));
+		cfg_parser->cfg->wait_limit_cookie = atoi($2);
+		free($2);
+	}
+	;
+server_wait_limit_netblock: VAR_WAIT_LIMIT_NETBLOCK STRING_ARG STRING_ARG
+	{
+		OUTYY(("P(server_wait_limit_netblock:%s %s)\n", $2, $3));
+		if(atoi($3) == 0 && strcmp($3, "0") != 0) {
+			yyerror("number expected");
+			free($2);
+			free($3);
+		} else {
+			if(!cfg_str2list_insert(&cfg_parser->cfg->
+				wait_limit_netblock, $2, $3))
+				fatal_exit("out of memory adding "
+					"wait-limit-netblock");
+		}
+	}
+	;
+server_wait_limit_cookie_netblock: VAR_WAIT_LIMIT_COOKIE_NETBLOCK STRING_ARG STRING_ARG
+	{
+		OUTYY(("P(server_wait_limit_cookie_netblock:%s %s)\n", $2, $3));
+		if(atoi($3) == 0 && strcmp($3, "0") != 0) {
+			yyerror("number expected");
+			free($2);
+			free($3);
+		} else {
+			if(!cfg_str2list_insert(&cfg_parser->cfg->
+				wait_limit_cookie_netblock, $2, $3))
+				fatal_exit("out of memory adding "
+					"wait-limit-cookie-netblock");
+		}
+	}
+	;
 server_max_udp_size: VAR_MAX_UDP_SIZE STRING_ARG
 	{
 		OUTYY(("P(server_max_udp_size:%s)\n", $2));
 		cfg_parser->cfg->max_udp_size = atoi($2);
 		free($2);

Vulnerable code: 	struct sockaddr_storage* addr, socklen_t addrlen, time_t timenow)
	infra_ip_create_ratedata(infra, addr, addrlen, timenow);
	verbosity: 2
Fixed code: 1 May 2024: Wouter
	- Fix for the DNSBomb vulnerability CVE-2024-33655. Thanks to Xiang Li
	  from the Network and Information Security Lab of Tsinghua University
	  for reporting it.

	# msec before recursion replies are dropped. The work item continues.
	# discard-timeout: 1900

	# Max number of replies waiting for recursion per IP address.
	# wait-limit: 1000

	# Max replies waiting for recursion for IP address with cookie.
	# wait-limit-cookie: 10000

	# Apart from the default, the wait limit can be set for a netblock.
	# wait-limit-netblock: 192.0.2.0/24 50000

	# Apart from the default, the wait limit with cookie can be adjusted.
	# wait-limit-cookie-netblock: 192.0.2.0/24 50000

.B discard\-timeout: \fI<msec>
The wait time in msec where recursion requests are dropped. This is
to stop a large number of replies from accumulating. They receive
no reply, the work item continues to recurse. It is nice to be a bit
larger than serve\-expired\-client\-timeout if that is enabled.
A value of 1900 msec is suggested. The value 0 disables it.
Default 1900 msec.
.TP
.B wait\-limit: \fI<number>
The number of replies that can wait for recursion, for an IP address.
This makes a ratelimit per IP address of waiting replies for recursion.
It stops very large amounts of queries waiting to be returned to one
destination. The value 0 disables wait limits. Default is 1000.
.TP
.B wait\-limit\-cookie: \fI<number>
The number of replies that can wait for recursion, for an IP address
that sent the query with a valid DNS cookie. Since the cookie validates
the client address, the limit can be higher. Default is 10000.
.TP
.B wait\-limit\-netblock: \fI<netblock> <number>
The wait limit for the netblock. If not given the wait\-limit value is
used. The most specific netblock is used to determine the limit. Useful for
overriding the default for a specific, group or individual, server.
The value -1 disables wait limits for the netblock.
.TP
.B wait\-limit\-cookie\-netblock: \fI<netblock> <number>
The wait limit for the netblock, when the query has a DNS cookie.
If not given, the wait\-limit\-cookie value is used.
The value -1 disables wait limits for the netblock.
.TP
/** find or create element in wait limit netblock tree */
static struct wait_limit_netblock_info*
wait_limit_netblock_findcreate(struct infra_cache* infra, char* str,
	int cookie)
{
	rbtree_type* tree;
	struct sockaddr_storage addr;
	int net;
	socklen_t addrlen;
	struct wait_limit_netblock_info* d;

	if(!netblockstrtoaddr(str, 0, &addr, &addrlen, &net)) {
		log_err("cannot parse wait limit netblock '%s'", str);
		return 0;
	}

	/* can we find it? */
	if(cookie)
		tree = &infra->wait_limits_cookie_netblock;
	else
		tree = &infra->wait_limits_netblock;
	d = (struct wait_limit_netblock_info*)addr_tree_find(tree, &addr,
		addrlen, net);
	if(d)
		return d;

	/* create it */
	d = (struct wait_limit_netblock_info*)calloc(1, sizeof(*d));
	if(!d)
		return NULL;
	d->limit = -1;
	if(!addr_tree_insert(tree, &d->node, &addr, addrlen, net)) {
		log_err("duplicate element in domainlimit tree");
		free(d);
		return NULL;
	}
	return d;
}


/** insert wait limit information into lookup tree */
static int
infra_wait_limit_netblock_insert(struct infra_cache* infra,
	struct config_file* cfg)
{
	struct config_str2list* p;
	struct wait_limit_netblock_info* d;
	for(p = cfg->wait_limit_netblock; p; p = p->next) {
		d = wait_limit_netblock_findcreate(infra, p->str, 0);
		if(!d)
			return 0;
		d->limit = atoi(p->str2);
	}
	for(p = cfg->wait_limit_cookie_netblock; p; p = p->next) {
		d = wait_limit_netblock_findcreate(infra, p->str, 1);
		if(!d)
			return 0;
		d->limit = atoi(p->str2);
	}
	return 1;
}

/** setup wait limits tree (0 on failure) */
static int
setup_wait_limits(struct infra_cache* infra, struct config_file* cfg)
{
	addr_tree_init(&infra->wait_limits_netblock);
	addr_tree_init(&infra->wait_limits_cookie_netblock);
	if(!infra_wait_limit_netblock_insert(infra, cfg))
		return 0;
	addr_tree_init_parents(&infra->wait_limits_netblock);
	addr_tree_init_parents(&infra->wait_limits_cookie_netblock);
	return 1;
}

	if(!setup_wait_limits(infra, cfg)) {
		infra_delete(infra);
		return NULL;
	}
/** delete wait_limit_netblock_info entries */
static void wait_limit_netblock_del(rbnode_type* n, void* ATTR_UNUSED(arg))
{
	free(n);
}

	traverse_postorder(&infra->wait_limits_netblock,
		wait_limit_netblock_del, NULL);
	traverse_postorder(&infra->wait_limits_cookie_netblock,
		wait_limit_netblock_del, NULL);
	struct sockaddr_storage* addr, socklen_t addrlen, time_t timenow,
	int mesh_wait)
	d->mesh_wait = mesh_wait;
	infra_ip_create_ratedata(infra, addr, addrlen, timenow, 0);

int infra_wait_limit_allowed(struct infra_cache* infra, struct comm_reply* rep,
	int cookie_valid, struct config_file* cfg)
{
	struct lruhash_entry* entry;
	if(cfg->wait_limit == 0)
		return 1;

	entry = infra_find_ip_ratedata(infra, &rep->client_addr,
		rep->client_addrlen, 0);
	if(entry) {
		rbtree_type* tree;
		struct wait_limit_netblock_info* w;
		struct rate_data* d = (struct rate_data*)entry->data;
		int mesh_wait = d->mesh_wait;
		lock_rw_unlock(&entry->lock);

		/* have the wait amount, check how much is allowed */
		if(cookie_valid)
			tree = &infra->wait_limits_cookie_netblock;
		else	tree = &infra->wait_limits_netblock;
		w = (struct wait_limit_netblock_info*)addr_tree_lookup(tree,
			&rep->client_addr, rep->client_addrlen);
		if(w) {
			if(w->limit != -1 && mesh_wait > w->limit)
				return 0;
		} else {
			/* if there is no IP netblock specific information,
			 * use the configured value. */
			if(mesh_wait > (cookie_valid?cfg->wait_limit_cookie:
				cfg->wait_limit))
				return 0;
		}
	}
	return 1;
}

void infra_wait_limit_inc(struct infra_cache* infra, struct comm_reply* rep,
	time_t timenow, struct config_file* cfg)
{
	struct lruhash_entry* entry;
	if(cfg->wait_limit == 0)
		return;

	/* Find it */
	entry = infra_find_ip_ratedata(infra, &rep->client_addr,
		rep->client_addrlen, 1);
	if(entry) {
		struct rate_data* d = (struct rate_data*)entry->data;
		d->mesh_wait++;
		lock_rw_unlock(&entry->lock);
		return;
	}

	/* Create it */
	infra_ip_create_ratedata(infra, &rep->client_addr,
		rep->client_addrlen, timenow, 1);
}

void infra_wait_limit_dec(struct infra_cache* infra, struct comm_reply* rep,
	struct config_file* cfg)
{
	struct lruhash_entry* entry;
	if(cfg->wait_limit == 0)
		return;

	entry = infra_find_ip_ratedata(infra, &rep->client_addr,
		rep->client_addrlen, 1);
	if(entry) {
		struct rate_data* d = (struct rate_data*)entry->data;
		if(d->mesh_wait > 0)
			d->mesh_wait--;
		lock_rw_unlock(&entry->lock);
	}
}
	/** tree of addr_tree_node, with wait_limit_netblock_info information */
	rbtree_type wait_limits_netblock;
	/** tree of addr_tree_node, with wait_limit_netblock_info information */
	rbtree_type wait_limits_cookie_netblock;
	/** the number of queries waiting in the mesh */
	int mesh_wait;
/**
 * Data to store the configuration per netblock for the wait limit
 */
struct wait_limit_netblock_info {
	/** The addr tree node, this must be first. */
	struct addr_tree_node node;
	/** the limit on the amount */
	int limit;
};

/** See if the IP address can have another reply in the wait limit */
int infra_wait_limit_allowed(struct infra_cache* infra, struct comm_reply* rep,
	int cookie_valid, struct config_file* cfg);

/** Increment number of waiting replies for IP */
void infra_wait_limit_inc(struct infra_cache* infra, struct comm_reply* rep,
	time_t timenow, struct config_file* cfg);

/** Decrement number of waiting replies for IP */
void infra_wait_limit_dec(struct infra_cache* infra, struct comm_reply* rep,
	struct config_file* cfg);

#include "services/cache/infra.h"
	if(!infra_wait_limit_allowed(mesh->env->infra_cache, rep,
		edns->cookie_valid, mesh->env->cfg)) {
		verbose(VERB_ALGO, "Too many queries waiting from the IP. "
			"dropping incoming query.");
		comm_point_drop_reply(rep);
		mesh->stats_dropped++;
		return;
	}
	infra_wait_limit_inc(mesh->env->infra_cache, rep, *mesh->env->now,
		mesh->env->cfg);
			infra_wait_limit_dec(mesh->env->infra_cache,
				&rep->query_reply, mesh->env->cfg);
	infra_wait_limit_dec(m->s.env->infra_cache, &r->query_reply,
		m->s.env->cfg);
		struct timeval old;
		timeval_subtract(&old, mstate->s.env->now_tv, &r->start_time);
		if(mstate->s.env->cfg->discard_timeout != 0 &&
			((int)old.tv_sec)*1000+((int)old.tv_usec)/1000 >
			mstate->s.env->cfg->discard_timeout) {
			/* Drop the reply, it is too old */
			/* briefly set the reply_list to NULL, so that the
			 * tcp req info cleanup routine that calls the mesh
			 * to deregister the meshstate for it is not done
			 * because the list is NULL and also accounting is not
			 * done there, but instead we do that here. */
			struct mesh_reply* reply_list = mstate->reply_list;
			verbose(VERB_ALGO, "drop reply, it is older than discard-timeout");
			infra_wait_limit_dec(mstate->s.env->infra_cache,
				&r->query_reply, mstate->s.env->cfg);
			mstate->reply_list = NULL;
			comm_point_drop_reply(&r->query_reply);
			mstate->reply_list = reply_list;
			mstate->s.env->mesh->stats_dropped++;
			continue;
		}

			infra_wait_limit_dec(mstate->s.env->infra_cache,
				&r->query_reply, mstate->s.env->cfg);
			infra_wait_limit_dec(mesh->env->infra_cache,
				&n->query_reply, mesh->env->cfg);
		struct timeval old;
		timeval_subtract(&old, mstate->s.env->now_tv, &r->start_time);
		if(mstate->s.env->cfg->discard_timeout != 0 &&
			((int)old.tv_sec)*1000+((int)old.tv_usec)/1000 >
			mstate->s.env->cfg->discard_timeout) {
			/* Drop the reply, it is too old */
			/* briefly set the reply_list to NULL, so that the
			 * tcp req info cleanup routine that calls the mesh
			 * to deregister the meshstate for it is not done
			 * because the list is NULL and also accounting is not
			 * done there, but instead we do that here. */
			struct mesh_reply* reply_list = mstate->reply_list;
			verbose(VERB_ALGO, "drop reply, it is older than discard-timeout");
			infra_wait_limit_dec(mstate->s.env->infra_cache,
				&r->query_reply, mstate->s.env->cfg);
			mstate->reply_list = NULL;
			comm_point_drop_reply(&r->query_reply);
			mstate->reply_list = reply_list;
			mstate->s.env->mesh->stats_dropped++;
			continue;
		}

		infra_wait_limit_dec(mstate->s.env->infra_cache,
			&r->query_reply, mstate->s.env->cfg);
	discard-timeout: 3000
	discard-timeout: 3000
	discard-timeout: 3000  # testns uses sleep=2
	discard-timeout: 3000  # testns uses sleep=2
	discard-timeout: 3000  # testns uses sleep=2
	discard-timeout: 3000  # testns uses sleep=2
	discard-timeout: 86400
	discard-timeout: 3000  # testns uses sleep=2
	discard-timeout: 3000  # testns uses sleep=2
	verbosity: 4
	discard-timeout: 3000  # testns uses sleep=2
	cfg->discard_timeout = 1900; /* msec */
	cfg->wait_limit = 1000;
	cfg->wait_limit_cookie = 10000;
	cfg->wait_limit_netblock = NULL;
	cfg->wait_limit_cookie_netblock = NULL;
	else S_NUMBER_OR_ZERO("discard-timeout:", discard_timeout)
	else S_NUMBER_OR_ZERO("wait-limit:", wait_limit)
	else S_NUMBER_OR_ZERO("wait-limit-cookie:", wait_limit_cookie)
	else O_DEC(opt, "discard-timeout", discard_timeout)
	else O_DEC(opt, "wait-limit", wait_limit)
	else O_DEC(opt, "wait-limit-cookie", wait_limit_cookie)
	else O_LS2(opt, "wait-limit-netblock", wait_limit_netblock)
	else O_LS2(opt, "wait-limit-cookie-netblock", wait_limit_cookie_netblock)
	config_deldblstrlist(cfg->wait_limit_netblock);
	config_deldblstrlist(cfg->wait_limit_cookie_netblock);
	/** Wait time to drop recursion replies */
	int discard_timeout;

	/** Wait limit for number of replies per IP address */
	int wait_limit;

	/** Wait limit for number of replies per IP address with cookie */
	int wait_limit_cookie;

	/** wait limit per netblock */
	struct config_str2list* wait_limit_netblock;

	/** wait limit with cookie per netblock */
	struct config_str2list* wait_limit_cookie_netblock;

discard-timeout{COLON}		{ YDVAR(1, VAR_DISCARD_TIMEOUT) }
wait-limit{COLON}		{ YDVAR(1, VAR_WAIT_LIMIT) }
wait-limit-cookie{COLON}	{ YDVAR(1, VAR_WAIT_LIMIT_COOKIE) }
wait-limit-netblock{COLON}	{ YDVAR(1, VAR_WAIT_LIMIT_NETBLOCK) }
wait-limit-cookie-netblock{COLON} { YDVAR(1, VAR_WAIT_LIMIT_COOKIE_NETBLOCK) }
%token VAR_DISCARD_TIMEOUT VAR_WAIT_LIMIT VAR_WAIT_LIMIT_COOKIE
%token VAR_WAIT_LIMIT_NETBLOCK VAR_WAIT_LIMIT_COOKIE_NETBLOCK
	server_discard_timeout | server_wait_limit | server_wait_limit_cookie |
	server_wait_limit_netblock | server_wait_limit_cookie_netblock |
server_discard_timeout: VAR_DISCARD_TIMEOUT STRING_ARG
	{
		OUTYY(("P(server_discard_timeout:%s)\n", $2));
		cfg_parser->cfg->discard_timeout = atoi($2);
		free($2);
	}
	;
server_wait_limit: VAR_WAIT_LIMIT STRING_ARG
	{
		OUTYY(("P(server_wait_limit:%s)\n", $2));
		cfg_parser->cfg->wait_limit = atoi($2);
		free($2);
	}
	;
server_wait_limit_cookie: VAR_WAIT_LIMIT_COOKIE STRING_ARG
	{
		OUTYY(("P(server_wait_limit_cookie:%s)\n", $2));
		cfg_parser->cfg->wait_limit_cookie = atoi($2);
		free($2);
	}
	;
server_wait_limit_netblock: VAR_WAIT_LIMIT_NETBLOCK STRING_ARG STRING_ARG
	{
		OUTYY(("P(server_wait_limit_netblock:%s %s)\n", $2, $3));
		if(atoi($3) == 0 && strcmp($3, "0") != 0) {
			yyerror("number expected");
			free($2);
			free($3);
		} else {
			if(!cfg_str2list_insert(&cfg_parser->cfg->
				wait_limit_netblock, $2, $3))
				fatal_exit("out of memory adding "
					"wait-limit-netblock");
		}
	}
	;
server_wait_limit_cookie_netblock: VAR_WAIT_LIMIT_COOKIE_NETBLOCK STRING_ARG STRING_ARG
	{
		OUTYY(("P(server_wait_limit_cookie_netblock:%s %s)\n", $2, $3));
		if(atoi($3) == 0 && strcmp($3, "0") != 0) {
			yyerror("number expected");
			free($2);
			free($3);
		} else {
			if(!cfg_str2list_insert(&cfg_parser->cfg->
				wait_limit_cookie_netblock, $2, $3))
				fatal_exit("out of memory adding "
					"wait-limit-cookie-netblock");
		}
	}
	;