From 75f3fbdd6563dd87c93964e48a3fb7e6c520d74e Mon Sep 17 00:00:00 2001
From: Willem Toorop <willem@nlnetlabs.nl>
Date: Wed, 28 Sep 2022 10:28:19 +0200
Subject: [PATCH] Downstream DNS Cookies a la RFC7873 and RFC9018

Create server cookies for clients that send client cookies.
Needs to be turned on in the config file with:

	answer-cookie: yes

A cookie-secret can be configured for anycast setups.
Also adds an access control list that will allow queries with
either a valid cookie or over a stateful transport.
---
 Makefile.in            |   8 +-
 daemon/acl_list.c      |   2 +
 daemon/acl_list.h      |   4 +-
 daemon/worker.c        |  44 ++++++++++-
 doc/unbound.conf.5.in  |  23 +++++-
 libunbound/libworker.c |   2 +
 services/authzone.c    |   4 +
 sldns/rrdef.h          |   4 +
 testcode/fake_event.c  |   2 +
 util/config_file.c     |  23 ++++++
 util/config_file.h     |   7 ++
 util/configlexer.lex   |   2 +
 util/configparser.y    |  35 ++++++++-
 util/data/msgparse.c   | 151 ++++++++++++++++++++++++++++++++++++-
 util/data/msgparse.h   |  14 +++-
 util/siphash.c         | 165 +++++++++++++++++++++++++++++++++++++++++
 validator/autotrust.c  |   2 +
 17 files changed, 473 insertions(+), 19 deletions(-)
 create mode 100644 util/siphash.c

diff --git a/Makefile.in b/Makefile.in
index 3189731ad..718f47f5c 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -128,7 +128,7 @@ util/config_file.c util/configlexer.c util/configparser.c \
 util/shm_side/shm_main.c services/authzone.c \
 util/fptr_wlist.c util/locks.c util/log.c util/mini_event.c util/module.c \
 util/netevent.c util/net_help.c util/random.c util/rbtree.c util/regional.c \
-util/rtt.c util/edns.c util/storage/dnstree.c util/storage/lookup3.c \
+util/rtt.c util/siphash.c util/edns.c util/storage/dnstree.c util/storage/lookup3.c \
 util/storage/lruhash.c util/storage/slabhash.c util/tcp_conn_limit.c \
 util/timehist.c util/tube.c util/proxy_protocol.c \
 util/ub_event.c util/ub_event_pluggable.c util/winsock_event.c \
@@ -145,7 +145,7 @@ as112.lo msgparse.lo msgreply.lo packed_rrset.lo iterator.lo iter_delegpt.lo \
 iter_donotq.lo iter_fwd.lo iter_hints.lo iter_priv.lo iter_resptype.lo \
 iter_scrub.lo iter_utils.lo localzone.lo mesh.lo modstack.lo view.lo \
 outbound_list.lo alloc.lo config_file.lo configlexer.lo configparser.lo \
-fptr_wlist.lo edns.lo locks.lo log.lo mini_event.lo module.lo net_help.lo \
+fptr_wlist.lo siphash.lo edns.lo locks.lo log.lo mini_event.lo module.lo net_help.lo \
 random.lo rbtree.lo regional.lo rtt.lo dnstree.lo lookup3.lo lruhash.lo \
 slabhash.lo tcp_conn_limit.lo timehist.lo tube.lo winsock_event.lo \
 autotrust.lo val_anchor.lo rpz.lo proxy_protocol.lo \
@@ -915,7 +915,8 @@ config_file.lo config_file.o: $(srcdir)/util/config_file.c config.h $(srcdir)/ut
 configlexer.lo configlexer.o: util/configlexer.c config.h $(srcdir)/util/configyyrename.h \
  $(srcdir)/util/config_file.h util/configparser.h
 configparser.lo configparser.o: util/configparser.c config.h $(srcdir)/util/configyyrename.h \
- $(srcdir)/util/config_file.h $(srcdir)/util/net_help.h $(srcdir)/util/log.h
+ $(srcdir)/util/config_file.h $(srcdir)/util/net_help.h $(srcdir)/util/log.h $(srcdir)/sldns/str2wire.h \
+ $(srcdir)/sldns/rrdef.h
 shm_main.lo shm_main.o: $(srcdir)/util/shm_side/shm_main.c config.h $(srcdir)/util/shm_side/shm_main.h \
  $(srcdir)/libunbound/unbound.h $(srcdir)/daemon/daemon.h $(srcdir)/util/locks.h $(srcdir)/util/log.h \
  $(srcdir)/util/alloc.h $(srcdir)/services/modstack.h  \
@@ -1004,6 +1005,7 @@ rtt.lo rtt.o: $(srcdir)/util/rtt.c config.h $(srcdir)/util/rtt.h $(srcdir)/itera
  $(srcdir)/services/outbound_list.h $(srcdir)/util/data/msgreply.h $(srcdir)/util/storage/lruhash.h \
  $(srcdir)/util/locks.h $(srcdir)/util/log.h $(srcdir)/util/data/packed_rrset.h $(srcdir)/util/module.h \
  $(srcdir)/util/data/msgparse.h $(srcdir)/sldns/pkthdr.h $(srcdir)/sldns/rrdef.h
+siphash.lo siphash.o: $(srcdir)/util/siphash.c
 edns.lo edns.o: $(srcdir)/util/edns.c config.h $(srcdir)/util/edns.h $(srcdir)/util/storage/dnstree.h \
  $(srcdir)/util/rbtree.h $(srcdir)/util/config_file.h $(srcdir)/util/netevent.h $(srcdir)/dnscrypt/dnscrypt.h \
   $(srcdir)/util/net_help.h $(srcdir)/util/log.h $(srcdir)/util/regional.h \
diff --git a/daemon/acl_list.c b/daemon/acl_list.c
index 8e8e1fc9b..e6d0470a1 100644
--- a/daemon/acl_list.c
+++ b/daemon/acl_list.c
@@ -109,6 +109,8 @@ parse_acl_access(const char* str, enum acl_access* control)
 		*control = acl_allow_snoop;
 	else if(strcmp(str, "allow_setrd") == 0)
 		*control = acl_allow_setrd;
+	else if (strcmp(str, "allow_cookie") == 0)
+		*control = acl_allow_cookie;
 	else {
 		log_err("access control type %s unknown", str);
 		return 0;
diff --git a/daemon/acl_list.h b/daemon/acl_list.h
index c717179ba..8aece11bf 100644
--- a/daemon/acl_list.h
+++ b/daemon/acl_list.h
@@ -65,7 +65,9 @@ enum acl_access {
 	/** allow full access for all queries, recursion and cache snooping */
 	acl_allow_snoop,
 	/** allow full access for recursion queries and set RD flag regardless of request */
-	acl_allow_setrd
+	acl_allow_setrd,
+	/** allow full access if valid cookie present or stateful transport */
+	acl_allow_cookie
 };
 
 /**
diff --git a/daemon/worker.c b/daemon/worker.c
index c2a94be79..1abf20a7b 100644
--- a/daemon/worker.c
+++ b/daemon/worker.c
@@ -1432,8 +1432,10 @@ worker_handle_request(struct comm_point* c, void* arg, int error,
 		}
 		goto send_reply;
 	}
-	if((ret=parse_edns_from_query_pkt(c->buffer, &edns, worker->env.cfg, c,
-					worker->scratchpad)) != 0) {
+	if((ret=parse_edns_from_query_pkt(
+			c->buffer, &edns, worker->env.cfg, c, repinfo,
+			(worker->env.now ? *worker->env.now : time(NULL)),
+			worker->scratchpad)) != 0) {
 		struct edns_data reply_edns;
 		verbose(VERB_ALGO, "worker parse edns: formerror.");
 		log_addr(VERB_CLIENT, "from", &repinfo->client_addr,
@@ -1466,6 +1468,44 @@ worker_handle_request(struct comm_point* c, void* arg, int error,
 			edns.udp_size = NORMAL_UDP_SIZE;
 		}
 	}
+	/* "if, else if" sequence below deals with downstream DNS Cookies */
+	if (acl != acl_allow_cookie)
+		; /* pass; No cookie downstream processing whatsoever */
+
+	else if (edns.cookie_valid)
+		; /* pass; Valid cookie is good! */
+
+	else if (c->type != comm_udp)
+		; /* pass; Stateful transport */
+
+	else if (edns.cookie_present) {
+		/* Cookie present, but not valid: Cookie was bad! */
+		extended_error_encode(c->buffer,
+			LDNS_EXT_RCODE_BADCOOKIE, &qinfo,
+			*(uint16_t*)(void *)
+			sldns_buffer_begin(c->buffer),
+			sldns_buffer_read_u16_at(c->buffer, 2),
+			0, &edns);
+		regional_free_all(worker->scratchpad);
+		goto send_reply;
+	} else {
+		/* Cookie requered, but no cookie present on UDP */
+		verbose(VERB_ALGO, "worker request: "
+			"need cookie or stateful transport");
+		log_addr(VERB_ALGO, "from",
+			&repinfo->remote_addr, repinfo->remote_addrlen);
+		EDNS_OPT_LIST_APPEND_EDE(&edns.opt_list_out,
+			worker->scratchpad, LDNS_EDE_OTHER,
+			"DNS Cookie needed for UDP replies");
+		error_encode(c->buffer,
+			(LDNS_RCODE_REFUSED|BIT_TC), &qinfo,
+			*(uint16_t*)(void *)
+			sldns_buffer_begin(c->buffer),
+			sldns_buffer_read_u16_at(c->buffer, 2),
+			&edns);
+		regional_free_all(worker->scratchpad);
+		goto send_reply;
+	}
 	if(edns.udp_size > worker->daemon->cfg->max_udp_size &&
 		c->type == comm_udp) {
 		verbose(VERB_QUERY,
diff --git a/doc/unbound.conf.5.in b/doc/unbound.conf.5.in
index 73575d93a..e187452de 100644
--- a/doc/unbound.conf.5.in
+++ b/doc/unbound.conf.5.in
@@ -673,9 +673,9 @@ This option is experimental at this time.
 .B access\-control: \fI<IP netblock> <action>
 The netblock is given as an IP4 or IP6 address with /size appended for a
 classless network block. The action can be \fIdeny\fR, \fIrefuse\fR,
-\fIallow\fR, \fIallow_setrd\fR, \fIallow_snoop\fR, \fIdeny_non_local\fR or
-\fIrefuse_non_local\fR.
-The most specific netblock match is used, if none match \fIrefuse\fR is used.
+\fIallow\fR, \fIallow_setrd\fR, \fIallow_snoop\fR, \fIallow_cookie\fR,
+\fIdeny_non_local\fR or \fIrefuse_non_local\fR.
+The most specific netblock match is used, if none match \fIdeny\fR is used.
 The order of the access\-control statements therefore does not matter.
 .IP
 The action \fIdeny\fR stops queries from hosts from that netblock.
@@ -710,6 +710,14 @@ the cache contents (for malicious acts).  However, nonrecursive queries can
 also be a valuable debugging tool (when you want to examine the cache
 contents). In that case use \fIallow_snoop\fR for your administration host.
 .IP
+When the \fBanswer\-cookie\fR option is enabled, the \fIallow_cookie\fR action
+will allow access to UDP queries that contain a valid Server Cookie as
+specified in RFC 7873 and RFC9018. UDP queries containing only a Client Cookie
+and no Server Cookie, will receive a BADCOOKIE response including a Server
+Cookie, allow clients to retry with that Server Cookie. The \fIallow_cookie\fR
+will also accept requests over statefull transports, regardless of the precence
+of a Cookie and regardless the \fBanswer\-cookie\fR setting.
+.IP
 By default only localhost is \fIallow\fRed, the rest is \fIrefuse\fRd.
 The default is \fIrefuse\fRd, because that is protocol\-friendly. The DNS
 protocol is not designed to handle dropped packets due to policy, and
@@ -1824,6 +1832,15 @@ Set the number of servers that should be used for fast server selection. Only
 use the fastest specified number of servers with the fast\-server\-permil
 option, that turns this on or off. The default is to use the fastest 3 servers.
 .TP 5
+.B answer\-cookie: \fI<yes or no>
+Enable to answer to requests containig DNS Cookies as specified in RFC7873 and
+RFC9018. Default is no.
+.TP 5
+.B cookie\-secret: \fI<128 bit hex string>
+Server's in an Anycast deployment need to be able to verify each other's
+Server Cookies. For this they need to share the secret used to construct
+and verify the Server Cookies.
+Default is a 128 bits random secret generated at startup time.
 .B edns\-client\-string: \fI<IP netblock> <string>
 Include an EDNS0 option containing configured ascii string in queries with
 destination address matching the configured IP netblock.  This configuration
diff --git a/libunbound/libworker.c b/libunbound/libworker.c
index 11bf5f9db..d5fabe6fb 100644
--- a/libunbound/libworker.c
+++ b/libunbound/libworker.c
@@ -604,6 +604,8 @@ setup_qinfo_edns(struct libworker* w, struct ctx_query* q,
 	edns->opt_list_out = NULL;
 	edns->opt_list_inplace_cb_out = NULL;
 	edns->padding_block_size = 0;
+	edns->cookie_present = 0;
+	edns->cookie_valid = 0;
 	if(sldns_buffer_capacity(w->back->udp_buff) < 65535)
 		edns->udp_size = (uint16_t)sldns_buffer_capacity(
 			w->back->udp_buff);
diff --git a/services/authzone.c b/services/authzone.c
index 6de1e4319..079a7eaf1 100644
--- a/services/authzone.c
+++ b/services/authzone.c
@@ -5419,6 +5419,8 @@ xfr_transfer_lookup_host(struct auth_xfer* xfr, struct module_env* env)
 	edns.opt_list_out = NULL;
 	edns.opt_list_inplace_cb_out = NULL;
 	edns.padding_block_size = 0;
+	edns.cookie_present = 0;
+	edns.cookie_valid = 0;
 	if(sldns_buffer_capacity(buf) < 65535)
 		edns.udp_size = (uint16_t)sldns_buffer_capacity(buf);
 	else	edns.udp_size = 65535;
@@ -6612,6 +6614,8 @@ xfr_probe_lookup_host(struct auth_xfer* xfr, struct module_env* env)
 	edns.opt_list_out = NULL;
 	edns.opt_list_inplace_cb_out = NULL;
 	edns.padding_block_size = 0;
+	edns.cookie_present = 0;
+	edns.cookie_valid = 0;
 	if(sldns_buffer_capacity(buf) < 65535)
 		edns.udp_size = (uint16_t)sldns_buffer_capacity(buf);
 	else	edns.udp_size = 65535;
diff --git a/sldns/rrdef.h b/sldns/rrdef.h
index 999c22307..d5b0585fd 100644
--- a/sldns/rrdef.h
+++ b/sldns/rrdef.h
@@ -433,6 +433,7 @@ enum sldns_enum_edns_option
 	LDNS_EDNS_DHU = 6, /* RFC6975 */
 	LDNS_EDNS_N3U = 7, /* RFC6975 */
 	LDNS_EDNS_CLIENT_SUBNET = 8, /* RFC7871 */
+	LDNS_EDNS_COOKIE = 10, /* RFC7873 */
 	LDNS_EDNS_KEEPALIVE = 11, /* draft-ietf-dnsop-edns-tcp-keepalive*/
 	LDNS_EDNS_PADDING = 12, /* RFC7830 */
 	LDNS_EDNS_EDE = 15, /* RFC8914 */
@@ -482,6 +483,9 @@ typedef enum sldns_enum_ede_code sldns_ede_code;
 #define LDNS_TSIG_ERROR_BADNAME  20
 #define LDNS_TSIG_ERROR_BADALG   21
 
+/** DNS Cookie extended rcode */
+#define LDNS_EXT_RCODE_BADCOOKIE 23
+
 /**
  * Contains all information about resource record types.
  *
diff --git a/testcode/fake_event.c b/testcode/fake_event.c
index 03e1c04f3..c93ceaa4c 100644
--- a/testcode/fake_event.c
+++ b/testcode/fake_event.c
@@ -1263,6 +1263,8 @@ struct serviced_query* outnet_serviced_query(struct outside_network* outnet,
 		if(dnssec)
 			edns.bits = EDNS_DO;
 		edns.padding_block_size = 0;
+		edns.cookie_present = 0;
+		edns.cookie_valid = 0;
 		edns.opt_list_in = NULL;
 		edns.opt_list_out = per_upstream_opt_list;
 		edns.opt_list_inplace_cb_out = NULL;
diff --git a/util/config_file.c b/util/config_file.c
index 158169c42..a92342761 100644
--- a/util/config_file.c
+++ b/util/config_file.c
@@ -55,6 +55,7 @@
 #include "util/regional.h"
 #include "util/fptr_wlist.h"
 #include "util/data/dname.h"
+#include "util/random.h"
 #include "util/rtt.h"
 #include "services/cache/infra.h"
 #include "sldns/wire2str.h"
@@ -87,6 +88,9 @@ struct config_parser_state* cfg_parser = 0;
 /** init ports possible for use */
 static void init_outgoing_availports(int* array, int num);
 
+/** init cookie with random data */
+static void init_cookie_secret(uint8_t* cookie_secret,size_t cookie_secret_len);
+
 struct config_file* 
 config_create(void)
 {
@@ -364,6 +368,10 @@ config_create(void)
 	cfg->ipsecmod_whitelist = NULL;
 	cfg->ipsecmod_strict = 0;
 #endif
+	cfg->do_answer_cookie = 0;
+	memset(cfg->cookie_secret, 0, sizeof(cfg->cookie_secret));
+	cfg->cookie_secret_len = 16;
+	init_cookie_secret(cfg->cookie_secret, cfg->cookie_secret_len);
 #ifdef USE_CACHEDB
 	if(!(cfg->cachedb_backend = strdup("testframe"))) goto error_exit;
 	if(!(cfg->cachedb_secret = strdup("default"))) goto error_exit;
@@ -1660,6 +1668,21 @@ config_delete(struct config_file* cfg)
 	free(cfg);
 }
 
+static void
+init_cookie_secret(uint8_t* cookie_secret, size_t cookie_secret_len)
+{
+	struct ub_randstate *rand = ub_initstate(NULL);
+
+	if (!rand)
+		fatal_exit("could not init random generator");
+	while (cookie_secret_len) {
+		*cookie_secret++ = (uint8_t)ub_random(rand);
+		cookie_secret_len--;
+	}
+	ub_randfree(rand);
+}
+
+
 static void 
 init_outgoing_availports(int* a, int num)
 {
diff --git a/util/config_file.h b/util/config_file.h
index bbc6d4ac1..3db4676b9 100644
--- a/util/config_file.h
+++ b/util/config_file.h
@@ -688,6 +688,13 @@ struct config_file {
 	int redis_expire_records;
 #endif
 #endif
+	/** Downstream DNS Cookies */
+	/** do answer with server cookie when request contained cookie option */
+	int do_answer_cookie;
+	/** cookie secret */
+	uint8_t cookie_secret[40];
+	/** cookie secret length */
+	size_t  cookie_secret_len;
 
 	/* ipset module */
 #ifdef USE_IPSET
diff --git a/util/configlexer.lex b/util/configlexer.lex
index fc9aa7266..4fdb2cde0 100644
--- a/util/configlexer.lex
+++ b/util/configlexer.lex
@@ -558,6 +558,8 @@ name-v4{COLON}			{ YDVAR(1, VAR_IPSET_NAME_V4) }
 name-v6{COLON}			{ YDVAR(1, VAR_IPSET_NAME_V6) }
 udp-upstream-without-downstream{COLON} { YDVAR(1, VAR_UDP_UPSTREAM_WITHOUT_DOWNSTREAM) }
 tcp-connection-limit{COLON}	{ YDVAR(2, VAR_TCP_CONNECTION_LIMIT) }
+answer-cookie{COLON}		{ YDVAR(1, VAR_ANSWER_COOKIE ) }
+cookie-secret{COLON}		{ YDVAR(1, VAR_COOKIE_SECRET) }
 edns-client-string{COLON}	{ YDVAR(2, VAR_EDNS_CLIENT_STRING) }
 edns-client-string-opcode{COLON} { YDVAR(1, VAR_EDNS_CLIENT_STRING_OPCODE) }
 nsid{COLON}			{ YDVAR(1, VAR_NSID ) }
diff --git a/util/configparser.y b/util/configparser.y
index 8f3672f5d..980201460 100644
--- a/util/configparser.y
+++ b/util/configparser.y
@@ -42,11 +42,13 @@
 #include <stdio.h>
 #include <string.h>
 #include <stdlib.h>
+#include <time.h>
 #include <assert.h>
 
 #include "util/configyyrename.h"
 #include "util/config_file.h"
 #include "util/net_help.h"
+#include "sldns/str2wire.h"
 
 int ub_c_lex(void);
 void ub_c_error(const char *message);
@@ -181,6 +183,7 @@ extern struct config_parser_state* cfg_parser;
 %token VAR_FALLBACK_ENABLED VAR_TLS_ADDITIONAL_PORT VAR_LOW_RTT VAR_LOW_RTT_PERMIL
 %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
 %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_STREAM_WAIT_SIZE VAR_TLS_CIPHERS VAR_TLS_CIPHERSUITES VAR_TLS_USE_SNI
@@ -316,6 +319,7 @@ content_server: server_num_threads | server_verbosity | server_port |
 	server_unknown_server_time_limit | server_log_tag_queryreply |
 	server_stream_wait_size | server_tls_ciphers |
 	server_tls_ciphersuites | server_tls_session_ticket_keys |
+	server_answer_cookie | server_cookie_secret |
 	server_tls_use_sni | server_edns_client_string |
 	server_edns_client_string_opcode | server_nsid |
 	server_zonemd_permissive_mode | server_max_reuse_tcp_queries |
@@ -3695,6 +3699,30 @@ server_tcp_connection_limit: VAR_TCP_CONNECTION_LIMIT STRING_ARG STRING_ARG
 		}
 	}
 	;
+server_answer_cookie: VAR_ANSWER_COOKIE STRING_ARG
+	{
+		OUTYY(("P(server_answer_cookie:%s)\n", $2));
+		if(strcmp($2, "yes") != 0 && strcmp($2, "no") != 0)
+			yyerror("expected yes or no.");
+		else cfg_parser->cfg->do_answer_cookie = (strcmp($2, "yes")==0);
+		free($2);
+	}
+	;
+server_cookie_secret: VAR_COOKIE_SECRET STRING_ARG
+	{
+		uint8_t secret[32];
+		size_t secret_len = sizeof(secret);
+
+		OUTYY(("P(server_cookie_secret:%s)\n", $2));
+		if (sldns_str2wire_hex_buf($2, secret, &secret_len)
+		|| (  secret_len != 16))
+			yyerror("expected 128 bit hex string");
+		else {
+			cfg_parser->cfg->cookie_secret_len = secret_len;
+			memcpy(cfg_parser->cfg->cookie_secret, secret, sizeof(secret));
+		}
+		free($2);
+	}
 	ipsetstart: VAR_IPSET
 		{
 			OUTYY(("\nP(ipset:)\n"));
@@ -3764,10 +3792,11 @@ validate_acl_action(const char* action)
 		strcmp(action, "refuse_non_local")!=0 &&
 		strcmp(action, "allow_setrd")!=0 &&
 		strcmp(action, "allow")!=0 &&
-		strcmp(action, "allow_snoop")!=0)
+		strcmp(action, "allow_snoop")!=0 &&
+		strcmp(action, "allow_cookie")!=0)
 	{
 		yyerror("expected deny, refuse, deny_non_local, "
-			"refuse_non_local, allow, allow_setrd or "
-			"allow_snoop as access control action");
+			"refuse_non_local, allow, allow_setrd, "
+			"allow_snoop or allow_cookie as access control action");
 	}
 }
diff --git a/util/data/msgparse.c b/util/data/msgparse.c
index 5bb69d6ed..3059d555c 100644
--- a/util/data/msgparse.c
+++ b/util/data/msgparse.c
@@ -951,11 +951,67 @@ edns_opt_list_append_keepalive(struct edns_option** list, int msec,
 			data, region);
 }
 
+int siphash(const uint8_t *in, const size_t inlen,
+		const uint8_t *k, uint8_t *out, const size_t outlen);
+
+/** RFC 1982 comparison, uses unsigned integers, and tries to avoid
+ * compiler optimization (eg. by avoiding a-b<0 comparisons),
+ * this routine matches compare_serial(), for SOA serial number checks */
+static int
+compare_1982(uint32_t a, uint32_t b)
+{
+	/* for 32 bit values */
+	const uint32_t cutoff = ((uint32_t) 1 << (32 - 1));
+
+	if (a == b) {
+		return 0;
+	} else if ((a < b && b - a < cutoff) || (a > b && a - b > cutoff)) {
+		return -1;
+	} else {
+		return 1;
+	}
+}
+
+/** if we know that b is larger than a, return the difference between them,
+ * that is the distance between them. in RFC1982 arith */
+static uint32_t
+subtract_1982(uint32_t a, uint32_t b)
+{
+	/* for 32 bit values */
+	const uint32_t cutoff = ((uint32_t) 1 << (32 - 1));
+
+	if(a == b)
+		return 0;
+	if(a < b && b - a < cutoff) {
+		return b-a;
+	}
+	if(a > b && a - b > cutoff) {
+		return ((uint32_t)0xffffffff) - (a-b-1);
+	}
+	/* wrong case, b smaller than a */
+	return 0;
+}
+
+
+static uint8_t *
+cookie_hash(uint8_t *hash, uint8_t *buf,
+		struct sockaddr_storage *addr, uint8_t *secret)
+{
+	if (addr->ss_family == AF_INET6) {
+		memcpy(buf+16, &((struct sockaddr_in6 *)addr)->sin6_addr, 16);
+		siphash(buf, 32, secret, hash, 8);
+	} else {
+		memcpy(buf+16, &((struct sockaddr_in *)addr)->sin_addr, 4);
+		siphash(buf, 20, secret, hash, 8);
+	}
+	return hash;
+}
+
 /** parse EDNS options from EDNS wireformat rdata */
 static int
 parse_edns_options_from_query(uint8_t* rdata_ptr, size_t rdata_len,
 	struct edns_data* edns, struct config_file* cfg, struct comm_point* c,
-	struct regional* region)
+	struct comm_reply* repinfo, uint32_t now, struct regional* region)
 {
 	/* To respond with a Keepalive option, the client connection must have
 	 * received one message with a TCP Keepalive EDNS option, and that
@@ -979,6 +1035,10 @@ parse_edns_options_from_query(uint8_t* rdata_ptr, size_t rdata_len,
 	while(rdata_len >= 4) {
 		uint16_t opt_code = sldns_read_uint16(rdata_ptr);
 		uint16_t opt_len = sldns_read_uint16(rdata_ptr+2);
+		uint8_t server_cookie[40], hash[8];
+		uint32_t cookie_time, subt_1982;
+		int comp_1982;
+
 		rdata_ptr += 4;
 		rdata_len -= 4;
 		if(opt_len > rdata_len)
@@ -1041,6 +1101,86 @@ parse_edns_options_from_query(uint8_t* rdata_ptr, size_t rdata_len,
 			edns->padding_block_size = cfg->pad_responses_block_size;
 			break;
 
+		case LDNS_EDNS_COOKIE:
+			if(!cfg || !cfg->do_answer_cookie)
+				break;
+			if(opt_len != 8 && (opt_len < 16 || opt_len > 40)) {
+				verbose(VERB_ALGO, "worker request: "
+						"badly formatted cookie");
+				return LDNS_RCODE_FORMERR;
+			}
+			edns->cookie_present = 1;
+
+			/* Copy client cookie, version and timestamp for
+			 * validation and creation purposes.
+			 */
+			memcpy(server_cookie, rdata_ptr, 16);
+
+			/* In the "if, if else" block below, we validate a
+			 * RFC9018 cookie. If it doesn't match the recipe, or
+			 * if it doesn't validate, or if the cookie is too old
+			 * (< 30 min), a new cookie is generated.
+			 */
+			if (opt_len != 24)
+				; /* RFC9018 cookies are 24 bytes long */
+
+			else if (cfg->cookie_secret_len != 16)
+				; /* RFC9018 cookies have 16 byte secrets */
+
+			else if (rdata_ptr[8] != 1)
+				; /* RFC9018 cookies are cookie version 1 */
+
+			else if ((comp_1982 = compare_1982(now,
+					(cookie_time = sldns_read_uint32(rdata_ptr + 12)))) > 0
+			     &&  (subt_1982 = subtract_1982(cookie_time, now)) > 3600)
+				; /* Cookie is older than 1 hour
+				   * (see RFC9018 Section 4.3.)
+				   */
+
+			else if (comp_1982 <= 0
+			     &&  subtract_1982(now, cookie_time) > 300)
+				; /* Cookie time is more than 5 minutes in the
+				   * future. (see RFC9018 Section 4.3.)
+				   */
+
+			else if (memcmp( cookie_hash( hash, server_cookie
+			                            , &repinfo->remote_addr
+			                            , cfg->cookie_secret)
+			               , rdata_ptr + 16 , 8 ) == 0) {
+
+				/* Cookie is valid! */
+				edns->cookie_valid = 1;
+				if (comp_1982 > 0 && subt_1982 > 1800)
+					; /* But older than 30 minutes,
+					   * so create a new one anyway */
+
+				else if (!edns_opt_list_append( /* Reuse cookie */
+				    &edns->opt_list_out, LDNS_EDNS_COOKIE, opt_len,
+				    rdata_ptr, region)) {
+					log_err("out of memory");
+					return LDNS_RCODE_SERVFAIL;
+				} else
+					/* Cookie to be reused added to
+					 * outgoing options. Done!
+					 */
+					break;
+			}
+			/* Add a new server cookie to outgoing cookies */
+			server_cookie[ 8] = 1;   /* Version */
+			server_cookie[ 9] = 0;   /* Reserved */
+			server_cookie[10] = 0;   /* Reserved */
+			server_cookie[11] = 0;   /* Reserved */
+			sldns_write_uint32(server_cookie + 12, now);
+			cookie_hash( hash, server_cookie
+			           , &repinfo->remote_addr, cfg->cookie_secret);
+			memcpy(server_cookie + 16, hash, 8);
+			if (!edns_opt_list_append( &edns->opt_list_out
+			                         , LDNS_EDNS_COOKIE
+						 , 24, server_cookie, region)) {
+				log_err("out of memory");
+				return LDNS_RCODE_SERVFAIL;
+			}
+			break;
 		default:
 			break;
 		}
@@ -1115,6 +1255,8 @@ parse_extract_edns_from_response_msg(struct msg_parse* msg,
 	edns->opt_list_out = NULL;
 	edns->opt_list_inplace_cb_out = NULL;
 	edns->padding_block_size = 0;
+	edns->cookie_present = 0;
+	edns->cookie_valid = 0;
 
 	/* take the options */
 	rdata_len = found->rr_first->size-2;
@@ -1170,7 +1312,8 @@ skip_pkt_rrs(sldns_buffer* pkt, int num)
 
 int 
 parse_edns_from_query_pkt(sldns_buffer* pkt, struct edns_data* edns,
-	struct config_file* cfg, struct comm_point* c, struct regional* region)
+	struct config_file* cfg, struct comm_point* c,
+	struct comm_reply* repinfo, time_t now, struct regional* region)
 {
 	size_t rdata_len;
 	uint8_t* rdata_ptr;
@@ -1206,6 +1349,8 @@ parse_edns_from_query_pkt(sldns_buffer* pkt, struct edns_data* edns,
 	edns->opt_list_out = NULL;
 	edns->opt_list_inplace_cb_out = NULL;
 	edns->padding_block_size = 0;
+	edns->cookie_present = 0;
+	edns->cookie_valid = 0;
 
 	/* take the options */
 	rdata_len = sldns_buffer_read_u16(pkt);
@@ -1214,7 +1359,7 @@ parse_edns_from_query_pkt(sldns_buffer* pkt, struct edns_data* edns,
 	rdata_ptr = sldns_buffer_current(pkt);
 	/* ignore rrsigs */
 	return parse_edns_options_from_query(rdata_ptr, rdata_len, edns, cfg,
-			c, region);
+			c, repinfo, now, region);
 }
 
 void
diff --git a/util/data/msgparse.h b/util/data/msgparse.h
index 0c458e6e8..aebeb2a88 100644
--- a/util/data/msgparse.h
+++ b/util/data/msgparse.h
@@ -72,6 +72,7 @@ struct regional;
 struct edns_option;
 struct config_file;
 struct comm_point;
+struct comm_reply;
 
 /** number of buckets in parse rrset hash table. Must be power of 2. */
 #define PARSE_TABLE_SIZE 32
@@ -217,8 +218,6 @@ struct rr_parse {
  * region.
  */
 struct edns_data {
-	/** if EDNS OPT record was present */
-	int edns_present;
 	/** Extended RCODE */
 	uint8_t ext_rcode;
 	/** The EDNS version number */
@@ -238,7 +237,13 @@ struct edns_data {
 	struct edns_option* opt_list_inplace_cb_out;
 	/** block size to pad */
 	uint16_t padding_block_size;
-};
+	/** if EDNS OPT record was present */
+	unsigned int edns_present   : 1;
+	/** if a cookie was present */
+	unsigned int cookie_present : 1;
+	/** if the cookie validated */
+	unsigned int cookie_valid   : 1;
+};	
 
 /**
  * EDNS option
@@ -315,7 +320,8 @@ int skip_pkt_rrs(struct sldns_buffer* pkt, int num);
  *	RCODE formerr if OPT is badly formatted and so on.
  */
 int parse_edns_from_query_pkt(struct sldns_buffer* pkt, struct edns_data* edns,
-	struct config_file* cfg, struct comm_point* c, struct regional* region);
+	struct config_file* cfg, struct comm_point* c,
+	struct comm_reply* repinfo, time_t now, struct regional* region);
 
 /**
  * Calculate hash value for rrset in packet.
diff --git a/util/siphash.c b/util/siphash.c
new file mode 100644
index 000000000..d69f4b579
--- /dev/null
+++ b/util/siphash.c
@@ -0,0 +1,165 @@
+/*
+   SipHash reference C implementation
+
+   Copyright (c) 2012-2016 Jean-Philippe Aumasson
+   <jeanphilippe.aumasson@gmail.com>
+   Copyright (c) 2012-2014 Daniel J. Bernstein <djb@cr.yp.to>
+
+   To the extent possible under law, the author(s) have dedicated all copyright
+   and related and neighboring rights to this software to the public domain
+   worldwide. This software is distributed without any warranty.
+
+   You should have received a copy of the CC0 Public Domain Dedication along
+   with
+   this software. If not, see
+   <http://creativecommons.org/publicdomain/zero/1.0/>.
+ */
+#include <assert.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+/* default: SipHash-2-4 */
+#define cROUNDS 2
+#define dROUNDS 4
+
+#define ROTL(x, b) (uint64_t)(((x) << (b)) | ((x) >> (64 - (b))))
+
+#define U32TO8_LE(p, v)                                                        \
+    (p)[0] = (uint8_t)((v));                                                   \
+    (p)[1] = (uint8_t)((v) >> 8);                                              \
+    (p)[2] = (uint8_t)((v) >> 16);                                             \
+    (p)[3] = (uint8_t)((v) >> 24);
+
+#define U64TO8_LE(p, v)                                                        \
+    U32TO8_LE((p), (uint32_t)((v)));                                           \
+    U32TO8_LE((p) + 4, (uint32_t)((v) >> 32));
+
+#define U8TO64_LE(p)                                                           \
+    (((uint64_t)((p)[0])) | ((uint64_t)((p)[1]) << 8) |                        \
+     ((uint64_t)((p)[2]) << 16) | ((uint64_t)((p)[3]) << 24) |                 \
+     ((uint64_t)((p)[4]) << 32) | ((uint64_t)((p)[5]) << 40) |                 \
+     ((uint64_t)((p)[6]) << 48) | ((uint64_t)((p)[7]) << 56))
+
+#define SIPROUND                                                               \
+    do {                                                                       \
+        v0 += v1;                                                              \
+        v1 = ROTL(v1, 13);                                                     \
+        v1 ^= v0;                                                              \
+        v0 = ROTL(v0, 32);                                                     \
+        v2 += v3;                                                              \
+        v3 = ROTL(v3, 16);                                                     \
+        v3 ^= v2;                                                              \
+        v0 += v3;                                                              \
+        v3 = ROTL(v3, 21);                                                     \
+        v3 ^= v0;                                                              \
+        v2 += v1;                                                              \
+        v1 = ROTL(v1, 17);                                                     \
+        v1 ^= v2;                                                              \
+        v2 = ROTL(v2, 32);                                                     \
+    } while (0)
+
+#ifdef DEBUG
+#define TRACE                                                                  \
+    do {                                                                       \
+        printf("(%3d) v0 %08x %08x\n", (int)inlen, (uint32_t)(v0 >> 32),       \
+               (uint32_t)v0);                                                  \
+        printf("(%3d) v1 %08x %08x\n", (int)inlen, (uint32_t)(v1 >> 32),       \
+               (uint32_t)v1);                                                  \
+        printf("(%3d) v2 %08x %08x\n", (int)inlen, (uint32_t)(v2 >> 32),       \
+               (uint32_t)v2);                                                  \
+        printf("(%3d) v3 %08x %08x\n", (int)inlen, (uint32_t)(v3 >> 32),       \
+               (uint32_t)v3);                                                  \
+    } while (0)
+#else
+#define TRACE
+#endif
+
+int siphash(const uint8_t *in, const size_t inlen, const uint8_t *k,
+            uint8_t *out, const size_t outlen) {
+
+    assert((outlen == 8) || (outlen == 16));
+    uint64_t v0 = 0x736f6d6570736575ULL;
+    uint64_t v1 = 0x646f72616e646f6dULL;
+    uint64_t v2 = 0x6c7967656e657261ULL;
+    uint64_t v3 = 0x7465646279746573ULL;
+    uint64_t k0 = U8TO64_LE(k);
+    uint64_t k1 = U8TO64_LE(k + 8);
+    uint64_t m;
+    int i;
+    const uint8_t *end = in + inlen - (inlen % sizeof(uint64_t));
+    const int left = inlen & 7;
+    uint64_t b = ((uint64_t)inlen) << 56;
+    v3 ^= k1;
+    v2 ^= k0;
+    v1 ^= k1;
+    v0 ^= k0;
+
+    if (outlen == 16)
+        v1 ^= 0xee;
+
+    for (; in != end; in += 8) {
+        m = U8TO64_LE(in);
+        v3 ^= m;
+
+        TRACE;
+        for (i = 0; i < cROUNDS; ++i)
+            SIPROUND;
+
+        v0 ^= m;
+    }
+
+    switch (left) {
+    case 7:
+        b |= ((uint64_t)in[6]) << 48;
+    case 6:
+        b |= ((uint64_t)in[5]) << 40;
+    case 5:
+        b |= ((uint64_t)in[4]) << 32;
+    case 4:
+        b |= ((uint64_t)in[3]) << 24;
+    case 3:
+        b |= ((uint64_t)in[2]) << 16;
+    case 2:
+        b |= ((uint64_t)in[1]) << 8;
+    case 1:
+        b |= ((uint64_t)in[0]);
+        break;
+    case 0:
+        break;
+    }
+
+    v3 ^= b;
+
+    TRACE;
+    for (i = 0; i < cROUNDS; ++i)
+        SIPROUND;
+
+    v0 ^= b;
+
+    if (outlen == 16)
+        v2 ^= 0xee;
+    else
+        v2 ^= 0xff;
+
+    TRACE;
+    for (i = 0; i < dROUNDS; ++i)
+        SIPROUND;
+
+    b = v0 ^ v1 ^ v2 ^ v3;
+    U64TO8_LE(out, b);
+
+    if (outlen == 8)
+        return 0;
+
+    v1 ^= 0xdd;
+
+    TRACE;
+    for (i = 0; i < dROUNDS; ++i)
+        SIPROUND;
+
+    b = v0 ^ v1 ^ v2 ^ v3;
+    U64TO8_LE(out + 8, b);
+
+    return 0;
+}
diff --git a/validator/autotrust.c b/validator/autotrust.c
index 3cdf9ceae..3011a0ace 100644
--- a/validator/autotrust.c
+++ b/validator/autotrust.c
@@ -2376,6 +2376,8 @@ probe_anchor(struct module_env* env, struct trust_anchor* tp)
 	edns.opt_list_out = NULL;
 	edns.opt_list_inplace_cb_out = NULL;
 	edns.padding_block_size = 0;
+	edns.cookie_present = 0;
+	edns.cookie_valid = 0;
 	if(sldns_buffer_capacity(buf) < 65535)
 		edns.udp_size = (uint16_t)sldns_buffer_capacity(buf);
 	else	edns.udp_size = 65535;
