diff --git a/backport-CVE-2024-33655.patch b/backport-CVE-2024-33655.patch new file mode 100644 index 0000000000000000000000000000000000000000..92387ce4bce3df3fb04a5cb56d2c6ad0d8dffc84 --- /dev/null +++ b/backport-CVE-2024-33655.patch @@ -0,0 +1,798 @@ +From c3206f4568f60c486be6d165b1f2b5b254fea3de Mon Sep 17 00:00:00 2001 +From: "W.C.A. Wijngaards" +Date: Wed, 1 May 2024 10:10:58 +0200 +Subject: [PATCH] - 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. + +--- + doc/Changelog | 5 + + doc/example.conf.in | 15 ++ + doc/unbound.conf.5.in | 30 ++++ + services/cache/infra.c | 170 +++++++++++++++++- + services/cache/infra.h | 28 +++ + services/mesh.c | 65 +++++++ + testdata/cachedb_expired_client_timeout.crpl | 1 + + testdata/cachedb_subnet_expired.crpl | 1 + + .../doh_downstream.tdir/doh_downstream.conf | 1 + + .../doh_downstream_notls.conf | 1 + + .../doh_downstream_post.conf | 1 + + .../fwd_three_service.conf | 1 + + testdata/iter_ghost_timewindow.rpl | 1 + + .../ssl_req_order.tdir/ssl_req_order.conf | 1 + + .../tcp_req_order.tdir/tcp_req_order.conf | 1 + + testdata/tcp_sigpipe.tdir/tcp_sigpipe.conf | 3 +- + util/config_file.c | 15 ++ + util/config_file.h | 15 ++ + util/configlexer.lex | 5 + + util/configparser.y | 55 ++++++ + 20 files changed, 412 insertions(+), 3 deletions(-) + +diff --git a/doc/Changelog b/doc/Changelog +index dab04633d..24fbb42c7 100644 +--- a/doc/Changelog ++++ b/doc/Changelog +@@ -1,3 +1,8 @@ ++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. ++ + 5 January 2023: Wouter + - Tag for 1.17.1 release. + +diff --git a/doc/example.conf.in b/doc/example.conf.in +index ea211e97d..e0aec8ec7 100644 +--- a/doc/example.conf.in ++++ b/doc/example.conf.in +@@ -191,6 +191,21 @@ server: + # 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 +diff --git a/doc/unbound.conf.5.in b/doc/unbound.conf.5.in +index 167f4a5de..6d310c116 100644 +--- a/doc/unbound.conf.5.in ++++ b/doc/unbound.conf.5.in +@@ -302,6 +302,36 @@ 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 ++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 ++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 ++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 ++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 ++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 + 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 +diff --git a/services/cache/infra.c b/services/cache/infra.c +index 31462d13a..457685ab5 100644 +--- a/services/cache/infra.c ++++ b/services/cache/infra.c +@@ -234,6 +234,81 @@ setup_domain_limits(struct infra_cache* infra, struct config_file* cfg) + 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) + { +@@ -267,6 +342,10 @@ infra_create(struct config_file* 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, +@@ -287,6 +366,12 @@ static void domain_limit_free(rbnode_type* n, void* ATTR_UNUSED(arg)) + } + } + ++/** 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) + { +@@ -296,6 +381,10 @@ infra_delete(struct infra_cache* infra) + 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); + } + +@@ -880,7 +969,8 @@ static void infra_create_ratedata(struct infra_cache* infra, + + /** 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)); +@@ -898,6 +988,7 @@ static void infra_ip_create_ratedata(struct infra_cache* infra, + 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); + } + +@@ -1121,6 +1212,81 @@ int infra_ip_ratelimit_inc(struct infra_cache* infra, + } + + /* 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 525073bf3..ee6f384de 100644 +--- a/services/cache/infra.h ++++ b/services/cache/infra.h +@@ -122,6 +122,10 @@ struct infra_cache { + 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 */ +@@ -184,10 +188,22 @@ struct rate_data { + /** 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.) */ +@@ -474,4 +490,16 @@ 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 55afd065f..e886c4b92 100644 +--- a/services/mesh.c ++++ b/services/mesh.c +@@ -47,6 +47,7 @@ + #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" +@@ -415,6 +416,14 @@ void mesh_new_client(struct mesh_area* mesh, struct query_info* qinfo, + 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? */ +@@ -522,6 +531,8 @@ 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; + } ++ 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); +@@ -953,6 +964,8 @@ mesh_state_cleanup(struct mesh_state* mstate) + * 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--; +@@ -1436,6 +1449,8 @@ mesh_send_reply(struct mesh_state* m, int rcode, struct reply_info* rep, + 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--; +@@ -1491,6 +1506,28 @@ void mesh_query_done(struct mesh_state* mstate) + } + } + 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; ++ } ++ + tv = r->start_time; + + /* if a response-ip address block has been stored the +@@ -1514,6 +1551,8 @@ void mesh_query_done(struct mesh_state* mstate) + * 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; +@@ -2046,6 +2085,8 @@ void mesh_state_remove_reply(struct mesh_area* mesh, struct mesh_state* m, + /* 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; +@@ -2186,6 +2227,28 @@ mesh_serve_expired_callback(void* arg) + 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; ++ } ++ + tv = r->start_time; + + /* If address info is returned, it means the action should be an +@@ -2213,6 +2276,8 @@ mesh_serve_expired_callback(void* arg) + 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; + +diff --git a/testdata/doh_downstream.tdir/doh_downstream.conf b/testdata/doh_downstream.tdir/doh_downstream.conf +index f0857bb58..222c2159d 100644 +--- a/testdata/doh_downstream.tdir/doh_downstream.conf ++++ b/testdata/doh_downstream.tdir/doh_downstream.conf +@@ -11,6 +11,7 @@ server: + 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 +diff --git a/testdata/doh_downstream_notls.tdir/doh_downstream_notls.conf b/testdata/doh_downstream_notls.tdir/doh_downstream_notls.conf +index bdca45645..161c35559 100644 +--- a/testdata/doh_downstream_notls.tdir/doh_downstream_notls.conf ++++ b/testdata/doh_downstream_notls.tdir/doh_downstream_notls.conf +@@ -11,6 +11,7 @@ server: + 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 +diff --git a/testdata/doh_downstream_post.tdir/doh_downstream_post.conf b/testdata/doh_downstream_post.tdir/doh_downstream_post.conf +index f0857bb58..222c2159d 100644 +--- a/testdata/doh_downstream_post.tdir/doh_downstream_post.conf ++++ b/testdata/doh_downstream_post.tdir/doh_downstream_post.conf +@@ -11,6 +11,7 @@ server: + 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 +diff --git a/testdata/fwd_three_service.tdir/fwd_three_service.conf b/testdata/fwd_three_service.tdir/fwd_three_service.conf +index 05fafe015..d6c9a205f 100644 +--- a/testdata/fwd_three_service.tdir/fwd_three_service.conf ++++ b/testdata/fwd_three_service.tdir/fwd_three_service.conf +@@ -11,6 +11,7 @@ server: + 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 566be82a9..9e304628c 100644 +--- a/testdata/iter_ghost_timewindow.rpl ++++ b/testdata/iter_ghost_timewindow.rpl +@@ -3,6 +3,7 @@ server: + target-fetch-policy: "0 0 0 0 0" + qname-minimisation: "no" + minimal-responses: no ++ discard-timeout: 86400 + + stub-zone: + name: "." +diff --git a/testdata/ssl_req_order.tdir/ssl_req_order.conf b/testdata/ssl_req_order.tdir/ssl_req_order.conf +index 3b2e2b1b4..ec39d3ab2 100644 +--- a/testdata/ssl_req_order.tdir/ssl_req_order.conf ++++ b/testdata/ssl_req_order.tdir/ssl_req_order.conf +@@ -9,6 +9,7 @@ server: + 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" +diff --git a/testdata/tcp_req_order.tdir/tcp_req_order.conf b/testdata/tcp_req_order.tdir/tcp_req_order.conf +index 40d6f55c8..b2804e8e2 100644 +--- a/testdata/tcp_req_order.tdir/tcp_req_order.conf ++++ b/testdata/tcp_req_order.tdir/tcp_req_order.conf +@@ -9,6 +9,7 @@ server: + 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" +diff --git a/testdata/tcp_sigpipe.tdir/tcp_sigpipe.conf b/testdata/tcp_sigpipe.tdir/tcp_sigpipe.conf +index 384f16b07..4f1ff9b08 100644 +--- a/testdata/tcp_sigpipe.tdir/tcp_sigpipe.conf ++++ b/testdata/tcp_sigpipe.tdir/tcp_sigpipe.conf +@@ -1,5 +1,5 @@ + server: +- verbosity: 2 ++ verbosity: 4 + # num-threads: 1 + interface: 127.0.0.1 + port: @PORT@ +@@ -9,6 +9,7 @@ server: + chroot: "" + username: "" + do-not-query-localhost: no ++ discard-timeout: 3000 # testns uses sleep=2 + + forward-zone: + name: "." +diff --git a/util/config_file.c b/util/config_file.c +index e4fe3ee9f..2b67d4c19 100644 +--- a/util/config_file.c ++++ b/util/config_file.c +@@ -309,6 +309,11 @@ config_create(void) + 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 = 4096; + if(!(cfg->server_key_file = strdup(RUN_DIR"/unbound_server.key"))) + goto error_exit; +@@ -726,6 +731,9 @@ int config_set_option(struct config_file* cfg, const char* opt, + 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) +@@ -1207,6 +1215,11 @@ config_get_option(struct config_file* cfg, const char* opt, + 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) +@@ -1678,6 +1691,8 @@ config_delete(struct config_file* cfg) + 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); +diff --git a/util/config_file.h b/util/config_file.h +index 42836796e..d3a2e268c 100644 +--- a/util/config_file.h ++++ b/util/config_file.h +@@ -537,6 +537,21 @@ struct config_file { + /* 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; + +diff --git a/util/configlexer.lex b/util/configlexer.lex +index 51bf06601..7ae1b8c38 100644 +--- a/util/configlexer.lex ++++ b/util/configlexer.lex +@@ -464,6 +464,11 @@ 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) } +diff --git a/util/configparser.y b/util/configparser.y +index 2adbf92cc..0feeb61b1 100644 +--- a/util/configparser.y ++++ b/util/configparser.y +@@ -189,6 +189,8 @@ extern struct config_parser_state* cfg_parser; + %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_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 +@@ -327,6 +329,8 @@ content_server: server_num_threads | server_verbosity | server_port | + 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 | +@@ -2377,6 +2381,57 @@ server_unknown_server_time_limit: VAR_UNKNOWN_SERVER_TIME_LIMIT STRING_ARG + 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)); diff --git a/backport-pre-CVE-2024-33655-Downstream-DNS-Cookies-a-la-RFC7873-and-RFC9018.patch b/backport-pre-CVE-2024-33655-Downstream-DNS-Cookies-a-la-RFC7873-and-RFC9018.patch new file mode 100644 index 0000000000000000000000000000000000000000..472918a58216ab68f511270b9070a06f4639f4a1 --- /dev/null +++ b/backport-pre-CVE-2024-33655-Downstream-DNS-Cookies-a-la-RFC7873-and-RFC9018.patch @@ -0,0 +1,886 @@ +From 75f3fbdd6563dd87c93964e48a3fb7e6c520d74e Mon Sep 17 00:00:00 2001 +From: Willem Toorop +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 + 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 ++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 + 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 + #include + #include ++#include + #include + + #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 ++ ++ Copyright (c) 2012-2014 Daniel J. Bernstein ++ ++ 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 ++ . ++ */ ++#include ++#include ++#include ++#include ++ ++/* 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; diff --git a/backport-pre-CVE-2024-33655-Fix-out-of-bounds-read-in-parse_edns_options_from_query.patch b/backport-pre-CVE-2024-33655-Fix-out-of-bounds-read-in-parse_edns_options_from_query.patch new file mode 100644 index 0000000000000000000000000000000000000000..2ad2c2331a774f093153382f55dd601f5905793f --- /dev/null +++ b/backport-pre-CVE-2024-33655-Fix-out-of-bounds-read-in-parse_edns_options_from_query.patch @@ -0,0 +1,30 @@ +From 1c85901cc4c0f7693183f4b280604619e56cba00 Mon Sep 17 00:00:00 2001 +From: "W.C.A. Wijngaards" +Date: Wed, 16 Aug 2023 16:58:49 +0200 +Subject: [PATCH] - Fix out of bounds read in parse_edns_options_from_query, it + would read 8 bytes after a client option of length 8, and then ignore them + to recreate a 24 byte response. The fixup does not read out of bounds, + and puts zeroes in the buffer at that point, that then are ignored. + +--- + util/data/msgparse.c | 7 ++++++- + 1 file changed, 6 insertions(+), 1 deletion(-) + +diff --git a/util/data/msgparse.c b/util/data/msgparse.c +index 40189d613..b5414c6d0 100644 +--- a/util/data/msgparse.c ++++ b/util/data/msgparse.c +@@ -1049,7 +1049,12 @@ parse_edns_options_from_query(uint8_t* rdata_ptr, size_t rdata_len, + /* Copy client cookie, version and timestamp for + * validation and creation purposes. + */ +- memmove(server_cookie, rdata_ptr, 16); ++ if(opt_len >= 16) { ++ memmove(server_cookie, rdata_ptr, 16); ++ } else { ++ memset(server_cookie, 0, 16); ++ memmove(server_cookie, rdata_ptr, opt_len); ++ } + + /* In the "if, if else" block below, we validate a + * RFC9018 cookie. If it doesn't match the recipe, or diff --git a/backport-pre-CVE-2024-33655-Fix-possibly-unaligned-memory-access-in-parse_edns_options_from_query.patch b/backport-pre-CVE-2024-33655-Fix-possibly-unaligned-memory-access-in-parse_edns_options_from_query.patch new file mode 100644 index 0000000000000000000000000000000000000000..fd49490d46d1266e3262d431df8f97b16c4fcb39 --- /dev/null +++ b/backport-pre-CVE-2024-33655-Fix-possibly-unaligned-memory-access-in-parse_edns_options_from_query.patch @@ -0,0 +1,22 @@ +From 2b1028bdad6da4d42f0571d0182322c3554e0bcc Mon Sep 17 00:00:00 2001 +From: "W.C.A. Wijngaards" +Date: Wed, 16 Aug 2023 10:06:06 +0200 +Subject: [PATCH] - Fix possibly unaligned memory access. + +--- + util/data/msgparse.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/util/data/msgparse.c b/util/data/msgparse.c +index 8cef37a17..40189d613 100644 +--- a/util/data/msgparse.c ++++ b/util/data/msgparse.c +@@ -1049,7 +1049,7 @@ parse_edns_options_from_query(uint8_t* rdata_ptr, size_t rdata_len, + /* Copy client cookie, version and timestamp for + * validation and creation purposes. + */ +- memcpy(server_cookie, rdata_ptr, 16); ++ memmove(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 diff --git a/backport-pre-CVE-2024-33655-extended_error_encode-for-extended-errors.patch b/backport-pre-CVE-2024-33655-extended_error_encode-for-extended-errors.patch new file mode 100644 index 0000000000000000000000000000000000000000..e0bee0fb906680198f389731b4ea26a9f9cbf26b --- /dev/null +++ b/backport-pre-CVE-2024-33655-extended_error_encode-for-extended-errors.patch @@ -0,0 +1,130 @@ +From 71f23ef354fae12e99963fe43200d38dfe796222 Mon Sep 17 00:00:00 2001 +From: Willem Toorop +Date: Wed, 28 Sep 2022 09:57:56 +0200 +Subject: [PATCH] extended_error_encode() for extended errors + +--- + daemon/worker.c | 14 ++------------ + util/data/msgencode.c | 18 ++++++++++++++---- + util/data/msgencode.h | 19 ++++++++++++++++++- + 3 files changed, 34 insertions(+), 17 deletions(-) + +diff --git a/daemon/worker.c b/daemon/worker.c +index 29c1961ed..c2a94be79 100644 +--- a/daemon/worker.c ++++ b/daemon/worker.c +@@ -1439,8 +1439,6 @@ worker_handle_request(struct comm_point* c, void* arg, int error, + repinfo->client_addrlen); + memset(&reply_edns, 0, sizeof(reply_edns)); + reply_edns.edns_present = 1; +- reply_edns.udp_size = EDNS_ADVERTISED_SIZE; +- LDNS_RCODE_SET(sldns_buffer_begin(c->buffer), ret); + error_encode(c->buffer, ret, &qinfo, + *(uint16_t*)(void *)sldns_buffer_begin(c->buffer), + sldns_buffer_read_u16_at(c->buffer, 2), &reply_edns); +@@ -1449,23 +1447,15 @@ worker_handle_request(struct comm_point* c, void* arg, int error, + } + if(edns.edns_present) { + if(edns.edns_version != 0) { +- edns.ext_rcode = (uint8_t)(EDNS_RCODE_BADVERS>>4); +- edns.edns_version = EDNS_ADVERTISED_VERSION; +- edns.udp_size = EDNS_ADVERTISED_SIZE; +- edns.bits &= EDNS_DO; + edns.opt_list_in = NULL; + edns.opt_list_out = NULL; + edns.opt_list_inplace_cb_out = NULL; +- edns.padding_block_size = 0; + verbose(VERB_ALGO, "query with bad edns version."); + log_addr(VERB_CLIENT, "from", &repinfo->client_addr, + repinfo->client_addrlen); +- error_encode(c->buffer, EDNS_RCODE_BADVERS&0xf, &qinfo, ++ extended_error_encode(c->buffer, EDNS_RCODE_BADVERS, &qinfo, + *(uint16_t*)(void *)sldns_buffer_begin(c->buffer), +- sldns_buffer_read_u16_at(c->buffer, 2), NULL); +- if(sldns_buffer_capacity(c->buffer) >= +- sldns_buffer_limit(c->buffer)+calc_edns_field_size(&edns)) +- attach_edns_record(c->buffer, &edns); ++ sldns_buffer_read_u16_at(c->buffer, 2), 0, &edns); + regional_free_all(worker->scratchpad); + goto send_reply; + } +diff --git a/util/data/msgencode.c b/util/data/msgencode.c +index fe21cfb86..d832ddc91 100644 +--- a/util/data/msgencode.c ++++ b/util/data/msgencode.c +@@ -959,14 +959,15 @@ qinfo_query_encode(sldns_buffer* pkt, struct query_info* qinfo) + } + + void +-error_encode(sldns_buffer* buf, int r, struct query_info* qinfo, +- uint16_t qid, uint16_t qflags, struct edns_data* edns) ++extended_error_encode(sldns_buffer* buf, uint16_t rcode, struct query_info* qinfo, ++ uint16_t qid, uint16_t qflags, uint16_t xflags, struct edns_data* edns) + { + uint16_t flags; + + sldns_buffer_clear(buf); + sldns_buffer_write(buf, &qid, sizeof(uint16_t)); +- flags = (uint16_t)(BIT_QR | BIT_RA | r); /* QR and retcode*/ ++ flags = (uint16_t)(BIT_QR | BIT_RA | (rcode & 0xF)); /* QR and retcode*/ ++ flags |= xflags; + flags |= (qflags & (BIT_RD|BIT_CD)); /* copy RD and CD bit */ + sldns_buffer_write_u16(buf, flags); + if(qinfo) flags = 1; +@@ -993,7 +994,7 @@ error_encode(sldns_buffer* buf, int r, struct query_info* qinfo, + struct edns_data es = *edns; + es.edns_version = EDNS_ADVERTISED_VERSION; + es.udp_size = EDNS_ADVERTISED_SIZE; +- es.ext_rcode = 0; ++ es.ext_rcode = (uint8_t)(rcode >> 4); + es.bits &= EDNS_DO; + if(sldns_buffer_limit(buf) + calc_edns_field_size(&es) > + edns->udp_size) +@@ -1001,3 +1002,12 @@ error_encode(sldns_buffer* buf, int r, struct query_info* qinfo, + attach_edns_record(buf, &es); + } + } ++ ++void ++error_encode(sldns_buffer* buf, int r, struct query_info* qinfo, ++ uint16_t qid, uint16_t qflags, struct edns_data* edns) ++{ ++ extended_error_encode(buf, (r & 0x000F), qinfo, qid, qflags ++ , (r & 0xFFF0), edns); ++} ++ +diff --git a/util/data/msgencode.h b/util/data/msgencode.h +index 30dc515cb..1b37ca870 100644 +--- a/util/data/msgencode.h ++++ b/util/data/msgencode.h +@@ -120,7 +120,7 @@ void attach_edns_record(struct sldns_buffer* pkt, struct edns_data* edns); + * Encode an error. With QR and RA set. + * + * @param pkt: where to store the packet. +- * @param r: RCODE value to encode. ++ * @param r: RCODE value to encode (may contain extra flags). + * @param qinfo: if not NULL, the query is included. + * @param qid: query ID to set in packet. network order. + * @param qflags: original query flags (to copy RD and CD bits). host order. +@@ -130,4 +130,21 @@ void attach_edns_record(struct sldns_buffer* pkt, struct edns_data* edns); + void error_encode(struct sldns_buffer* pkt, int r, struct query_info* qinfo, + uint16_t qid, uint16_t qflags, struct edns_data* edns); + ++/** ++ * Encode an extended error. With QR and RA set. ++ * ++ * @param pkt: where to store the packet. ++ * @param rcode: Extended RCODE value to encode. ++ * @param qinfo: if not NULL, the query is included. ++ * @param qid: query ID to set in packet. network order. ++ * @param qflags: original query flags (to copy RD and CD bits). host order. ++ * @param xflags: extra flags to set (such as for example BIT_AA and/or BIT_TC) ++ * @param edns: if not NULL, this is the query edns info, ++ * and an edns reply is attached. Only attached if EDNS record fits reply. ++ * Without edns extended errors (i.e. > 15 )will not be conveyed. ++ */ ++void extended_error_encode(struct sldns_buffer* pkt, uint16_t rcode, ++ struct query_info* qinfo, uint16_t qid, uint16_t qflags, ++ uint16_t xflags, struct edns_data* edns); ++ + #endif /* UTIL_DATA_MSGENCODE_H */ diff --git a/unbound.spec b/unbound.spec index 3c0c4254fa1a2bfffaca44f92c2cd5142e9dce3e..5a7e807f5a4a4a6783817db4d0dd7120e7622bb9 100644 --- a/unbound.spec +++ b/unbound.spec @@ -2,7 +2,7 @@ Name: unbound Version: 1.17.1 -Release: 4 +Release: 5 Summary: Unbound is a validating, recursive, caching DNS resolver License: BSD-3-Clause Url: https://nlnetlabs.nl/projects/unbound/about/ @@ -23,10 +23,15 @@ Source13: unbound-anchor.service Patch1: unbound-remove-buildin-key.patch Patch2: backport-CVE-2023-50387_CVE-2023-50868.patch +Patch3: backport-pre-CVE-2024-33655-extended_error_encode-for-extended-errors.patch +Patch4: backport-pre-CVE-2024-33655-Downstream-DNS-Cookies-a-la-RFC7873-and-RFC9018.patch +Patch5: backport-pre-CVE-2024-33655-Fix-possibly-unaligned-memory-access-in-parse_edns_options_from_query.patch +Patch6: backport-pre-CVE-2024-33655-Fix-out-of-bounds-read-in-parse_edns_options_from_query.patch +Patch7: backport-CVE-2024-33655.patch BuildRequires: make flex swig pkgconfig systemd BuildRequires: libevent-devel expat-devel openssl-devel python3-devel -BuildRequires: gcc +BuildRequires: gcc byacc %{?systemd_requires} Requires: %{name}-libs = %{version}-%{release} @@ -235,6 +240,12 @@ popd %{_mandir}/man* %changelog +* Fri May 17 2024 gaihuiying - 1.17.1-5 +- Type:cves +- CVE:CVE-2024-33655 +- SUG:NA +- DESC:fix CVE-2024-33655 + * Tue Mar 05 2024 gaihuiying - 1.17.1-4 - Type:bugfix - CVE:NA