diff --git a/Makefile.in b/Makefile.in index 74e45bc5e..0a2e7f9b6 100644 --- a/Makefile.in +++ b/Makefile.in @@ -122,13 +122,13 @@ iterator/iter_delegpt.c iterator/iter_donotq.c iterator/iter_fwd.c \ iterator/iter_hints.c iterator/iter_priv.c iterator/iter_resptype.c \ iterator/iter_scrub.c iterator/iter_utils.c services/listen_dnsport.c \ services/localzone.c services/mesh.c services/modstack.c services/view.c \ -services/rpz.c \ +services/rpz.c util/rfc_1982.c \ services/outbound_list.c services/outside_network.c util/alloc.c \ 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/timeval_func.c \ util/ub_event.c util/ub_event_pluggable.c util/winsock_event.c \ @@ -145,10 +145,10 @@ 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 \ +autotrust.lo val_anchor.lo rpz.lo rfc_1982.lo proxy_protocol.lo \ validator.lo val_kcache.lo val_kentry.lo val_neg.lo val_nsec3.lo val_nsec.lo \ val_secalgo.lo val_sigcrypt.lo val_utils.lo dns64.lo $(CACHEDB_OBJ) authzone.lo \ $(SUBNET_OBJ) $(PYTHONMOD_OBJ) $(CHECKLOCK_OBJ) $(DNSTAP_OBJ) $(DNSCRYPT_OBJ) \ @@ -917,7 +917,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 \ @@ -1008,6 +1009,8 @@ 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 +rfc_1982.lo rfc_1982.o: $(srcdir)/util/rfc_1982.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 f3961dbbb..83cfd7ddf 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..9da43bef3 100644 --- a/daemon/acl_list.h +++ b/daemon/acl_list.h @@ -64,8 +64,12 @@ enum acl_access { acl_allow, /** 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 + /** allow full access for recursion queries and set RD flag regardless + * of request */ + acl_allow_setrd, + /** allow full access for recursion (+RD) queries if valid cookie + * present or stateful transport */ + acl_allow_cookie }; /** diff --git a/daemon/remote.c b/daemon/remote.c index c7bfa4e12..4990fc8e9 100644 --- a/daemon/remote.c +++ b/daemon/remote.c @@ -672,6 +672,12 @@ print_stats(RES* ssl, const char* nm, struct ub_stats_info* s) (unsigned long)s->svr.num_queries)) return 0; if(!ssl_printf(ssl, "%s.num.queries_ip_ratelimited"SQ"%lu\n", nm, (unsigned long)s->svr.num_queries_ip_ratelimited)) return 0; + if(!ssl_printf(ssl, "%s.num.queries_cookie_valid"SQ"%lu\n", nm, + (unsigned long)s->svr.num_queries_cookie_valid)) return 0; + if(!ssl_printf(ssl, "%s.num.queries_cookie_client"SQ"%lu\n", nm, + (unsigned long)s->svr.num_queries_cookie_client)) return 0; + if(!ssl_printf(ssl, "%s.num.queries_cookie_invalid"SQ"%lu\n", nm, + (unsigned long)s->svr.num_queries_cookie_invalid)) return 0; if(!ssl_printf(ssl, "%s.num.cachehits"SQ"%lu\n", nm, (unsigned long)(s->svr.num_queries - s->svr.num_queries_missed_cache))) return 0; diff --git a/daemon/stats.c b/daemon/stats.c index fabbd9f60..4855bf1c1 100644 --- a/daemon/stats.c +++ b/daemon/stats.c @@ -435,6 +435,9 @@ void server_stats_add(struct ub_stats_info* total, struct ub_stats_info* a) { total->svr.num_queries += a->svr.num_queries; total->svr.num_queries_ip_ratelimited += a->svr.num_queries_ip_ratelimited; + total->svr.num_queries_cookie_valid += a->svr.num_queries_cookie_valid; + total->svr.num_queries_cookie_client += a->svr.num_queries_cookie_client; + total->svr.num_queries_cookie_invalid += a->svr.num_queries_cookie_invalid; total->svr.num_queries_missed_cache += a->svr.num_queries_missed_cache; total->svr.num_queries_prefetch += a->svr.num_queries_prefetch; total->svr.num_queries_timed_out += a->svr.num_queries_timed_out; @@ -568,3 +571,16 @@ void server_stats_insrcode(struct ub_server_stats* stats, sldns_buffer* buf) stats->ans_rcode_nodata ++; } } + +void server_stats_downstream_cookie(struct ub_server_stats* stats, + struct edns_data* edns) +{ + if(!(edns->edns_present && edns->cookie_present)) return; + if(edns->cookie_valid) { + stats->num_queries_cookie_valid++; + } else if(edns->cookie_client) { + stats->num_queries_cookie_client++; + } else { + stats->num_queries_cookie_invalid++; + } +} diff --git a/daemon/stats.h b/daemon/stats.h index 4e5e6cf8a..47bb20d7f 100644 --- a/daemon/stats.h +++ b/daemon/stats.h @@ -126,4 +126,11 @@ void server_stats_insquery(struct ub_server_stats* stats, struct comm_point* c, */ void server_stats_insrcode(struct ub_server_stats* stats, struct sldns_buffer* buf); +/** + * Add DNS Cookie stats for this query + * @param stats: the stats + * @param edns: edns record + */ +void server_stats_downstream_cookie(struct ub_server_stats* stats, + struct edns_data* edns); #endif /* DAEMON_STATS_H */ diff --git a/daemon/worker.c b/daemon/worker.c index ce214380d..8c6fa3b9a 100644 --- a/daemon/worker.c +++ b/daemon/worker.c @@ -1319,6 +1319,40 @@ deny_refuse_non_local(struct comm_point* c, enum acl_access acl, worker, repinfo, acladdr, ede, check_result); } +/* Returns 1 if the ip rate limit check can happen before EDNS parsing, + * else 0 */ +static int +pre_edns_ip_ratelimit_check(enum acl_access acl) +{ + if(acl == acl_allow_cookie) return 0; + return 1; +} + +/* Check if the query is blocked by source IP rate limiting. + * Returns 1 if it passes the check, 0 otherwise. */ +static int +check_ip_ratelimit(struct worker* worker, struct sockaddr_storage* addr, + socklen_t addrlen, int has_cookie, sldns_buffer* pkt) +{ + if(!infra_ip_ratelimit_inc(worker->env.infra_cache, addr, addrlen, + *worker->env.now, has_cookie, + worker->env.cfg->ip_ratelimit_backoff, pkt)) { + /* See if we can pass through with slip factor */ + if(!has_cookie && worker->env.cfg->ip_ratelimit_factor != 0 && + ub_random_max(worker->env.rnd, + worker->env.cfg->ip_ratelimit_factor) == 0) { + char addrbuf[128]; + addr_to_str(addr, addrlen, addrbuf, sizeof(addrbuf)); + verbose(VERB_QUERY, "ip_ratelimit allowed through for " + "ip address %s because of slip in " + "ip_ratelimit_factor", addrbuf); + return 1; + } + return 0; + } + return 1; +} + int worker_handle_request(struct comm_point* c, void* arg, int error, struct comm_reply* repinfo) @@ -1332,6 +1366,7 @@ worker_handle_request(struct comm_point* c, void* arg, int error, struct edns_option* original_edns_list = NULL; enum acl_access acl; struct acl_addr* acladdr; + int pre_edns_ip_ratelimit = 1; int rc = 0; int need_drop = 0; int is_expired_answer = 0; @@ -1456,33 +1491,21 @@ worker_handle_request(struct comm_point* c, void* arg, int error, } worker->stats.num_queries++; - - /* check if this query should be dropped based on source ip rate limiting - * NOTE: we always check the repinfo->client_address. IP ratelimiting is - * implicitly disabled for proxies. */ - if(!infra_ip_ratelimit_inc(worker->env.infra_cache, - &repinfo->client_addr, repinfo->client_addrlen, - *worker->env.now, - worker->env.cfg->ip_ratelimit_backoff, c->buffer)) { - /* See if we are passed through with slip factor */ - if(worker->env.cfg->ip_ratelimit_factor != 0 && - ub_random_max(worker->env.rnd, - worker->env.cfg->ip_ratelimit_factor) == 0) { - char addrbuf[128]; - addr_to_str(&repinfo->client_addr, - repinfo->client_addrlen, addrbuf, - sizeof(addrbuf)); - verbose(VERB_QUERY, "ip_ratelimit allowed through for " - "ip address %s because of slip in " - "ip_ratelimit_factor", addrbuf); - } else { + pre_edns_ip_ratelimit = pre_edns_ip_ratelimit_check(acl); + + /* If the IP rate limiting check needs extra EDNS information (e.g., + * DNS Cookies) postpone the check until after EDNS is parsed. */ + if(pre_edns_ip_ratelimit) { + /* NOTE: we always check the repinfo->client_address. + * IP ratelimiting is implicitly disabled for proxies. */ + if(!check_ip_ratelimit(worker, &repinfo->client_addr, + repinfo->client_addrlen, 0, c->buffer)) { worker->stats.num_queries_ip_ratelimited++; comm_point_drop_reply(repinfo); return 0; } } - /* see if query is in the cache */ if(!query_info_parse(&qinfo, c->buffer)) { verbose(VERB_ALGO, "worker parse request: formerror."); log_addr(VERB_CLIENT, "from", &repinfo->client_addr, @@ -1539,16 +1562,16 @@ 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, 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); @@ -1557,23 +1580,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; } @@ -1586,6 +1601,62 @@ worker_handle_request(struct comm_point* c, void* arg, int error, edns.udp_size = NORMAL_UDP_SIZE; } } + + /* Get stats for cookies */ + server_stats_downstream_cookie(&worker->stats, &edns); + + /* If the IP rate limiting check was postponed, check now. */ + if(!pre_edns_ip_ratelimit) { + /* NOTE: we always check the repinfo->client_address. + * IP ratelimiting is implicitly disabled for proxies. */ + if(!check_ip_ratelimit(worker, &repinfo->client_addr, + repinfo->client_addrlen, edns.cookie_valid, + c->buffer)) { + worker->stats.num_queries_ip_ratelimited++; + comm_point_drop_reply(repinfo); + return 0; + } + } + + /* "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 required, 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-control.8.in b/doc/unbound-control.8.in index acbc89abe..7823de3aa 100644 --- a/doc/unbound-control.8.in +++ b/doc/unbound-control.8.in @@ -369,6 +369,15 @@ number of queries received by thread .I threadX.num.queries_ip_ratelimited number of queries rate limited by thread .TP +.I threadX.num.queries_cookie_valid +number of queries with a valid DNS Cookie by thread +.TP +.I threadX.num.queries_cookie_client +number of queries with a client part only DNS Cookie by thread +.TP +.I threadX.num.queries_cookie_invalid +number of queries with an invalid DNS Cookie by thread +.TP .I threadX.num.cachehits number of queries that were successfully answered using a cache lookup .TP @@ -446,6 +455,18 @@ buffers are full. .I total.num.queries summed over threads. .TP +.I total.num.queries_ip_ratelimited +summed over threads. +.TP +.I total.num.queries_cookie_valid +summed over threads. +.TP +.I total.num.queries_cookie_client +summed over threads. +.TP +.I total.num.queries_cookie_invalid +summed over threads. +.TP .I total.num.cachehits summed over threads. .TP @@ -611,7 +632,7 @@ ratelimiting. .TP .I num.query.dnscrypt.shared_secret.cachemiss The number of dnscrypt queries that did not find a shared secret in the cache. -The can be use to compute the shared secret hitrate. +This can be used to compute the shared secret hitrate. .TP .I num.query.dnscrypt.replay The number of dnscrypt queries that found a nonce hit in the nonce cache and diff --git a/doc/unbound.conf.5.in b/doc/unbound.conf.5.in index cc554985d..84b903f49 100644 --- a/doc/unbound.conf.5.in +++ b/doc/unbound.conf.5.in @@ -701,17 +701,17 @@ 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. +\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 \fIrefuse\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. +The \fIdeny\fR action stops queries from hosts from that netblock. .IP -The action \fIrefuse\fR stops queries too, but sends a DNS rcode REFUSED +The \fIrefuse\fR action stops queries too, but sends a DNS rcode REFUSED error message back. .IP -The action \fIallow\fR gives access to clients from that netblock. +The \fIallow\fR action gives access to clients from that netblock. It gives only access for recursion clients (which is what almost all clients need). Nonrecursive queries are refused. .IP @@ -731,13 +731,27 @@ may be useful if another DNS server must forward requests for specific zones to a resolver DNS server, but only supports stub domains and sends queries to the resolver DNS server with the RD bit cleared. .IP -The action \fIallow_snoop\fR gives nonrecursive access too. This give +The \fIallow_snoop\fR action gives nonrecursive access too. This give both recursive and non recursive access. The name \fIallow_snoop\fR refers to cache snooping, a technique to use nonrecursive queries to examine 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 +The \fIallow_cookie\fR action allows access to UDP queries that contain a +valid DNS Cookie as specified in RFC 7873 and RFC 9018, when the +\fBanswer\-cookie\fR option is enabled. +UDP queries containing only a DNS Client Cookie and no Server Cookie, or an +invalid DNS Cookie, will receive a BADCOOKIE response including a newly +generated DNS Cookie, allowing clients to retry with that DNS Cookie. +The \fIallow_cookie\fR action will also accept requests over stateful +transports, regardless of the presence of an DNS Cookie and regardless of the +\fBanswer\-cookie\fR setting. +If \fBip\-ratelimit\fR is used, clients with a valid DNS Cookie will bypass the +ratelimit. +If a ratelimit for such clients is still needed, \fBip\-ratelimit\-cookie\fR +can be used instead. +.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 @@ -1806,11 +1820,27 @@ A value of 0 will disable ratelimiting for domain names that end in this name. .TP 5 .B ip\-ratelimit: \fI Enable global ratelimiting of queries accepted per IP address. -If 0, the default, it is disabled. This option is experimental at this time. +This option is experimental at this time. The ratelimit is in queries per second that are allowed. More queries are completely dropped and will not receive a reply, SERVFAIL or otherwise. IP ratelimiting happens before looking in the cache. This may be useful for mitigating amplification attacks. +Default is 0 (disabled). +.TP 5 +.B ip\-ratelimit\-cookie: \fI +Enable global ratelimiting of queries accepted per IP address with a valid DNS +Cookie. +This option is experimental at this time. +The ratelimit is in queries per second that are allowed. +More queries are completely dropped and will not receive a reply, SERVFAIL or +otherwise. +IP ratelimiting happens before looking in the cache. +This option could be useful in combination with \fIallow_cookie\fR in an +attempt to mitigate other amplification attacks than UDP reflections (e.g., +attacks targeting Unbound itself) which are already handled with DNS Cookies. +If used, the value is suggested to be higher than \fBip\-ratelimit\fR e.g., +tenfold. +Default is 0 (disabled). .TP 5 .B ip\-ratelimit\-size: \fI Give the size of the data structure in which the current ongoing rates are @@ -1879,6 +1909,18 @@ 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 +If enabled, Unbound will answer to requests containing DNS Cookies as +specified in RFC 7873 and RFC 9018. +Default is no. +.TP 5 +.B cookie\-secret: \fI<128 bit hex string> +Server's secret for DNS Cookie generation. +Useful to explicitly set for servers in an anycast deployment that need to +share the secret in order to verify each other's Server Cookies. +An example hex string would be "000102030405060708090a0b0c0d0e0f". +Default is a 128 bits random secret generated at startup time. +.TP 5 .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 ebc1df2e5..104244937 100644 --- a/libunbound/libworker.c +++ b/libunbound/libworker.c @@ -603,6 +603,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/libunbound/unbound.h b/libunbound/unbound.h index 97be66a88..bb8e8acf0 100644 --- a/libunbound/unbound.h +++ b/libunbound/unbound.h @@ -695,6 +695,12 @@ struct ub_server_stats { long long num_queries; /** number of queries that have been dropped/ratelimited by ip. */ long long num_queries_ip_ratelimited; + /** number of queries with a valid DNS Cookie. */ + long long num_queries_cookie_valid; + /** number of queries with only the client part of the DNS Cookie. */ + long long num_queries_cookie_client; + /** number of queries with invalid DNS Cookie. */ + long long num_queries_cookie_invalid; /** number of queries that had a cache-miss. */ long long num_queries_missed_cache; /** number of prefetch queries - cachehits with prefetch */ diff --git a/services/authzone.c b/services/authzone.c index 1bba3b157..cd3ef8dbb 100644 --- a/services/authzone.c +++ b/services/authzone.c @@ -5420,6 +5420,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; @@ -6613,6 +6615,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/services/cache/infra.c b/services/cache/infra.c index 537cb949c..31462d13a 100644 --- a/services/cache/infra.c +++ b/services/cache/infra.c @@ -67,6 +67,11 @@ int infra_dp_ratelimit = 0; * in queries per second. */ int infra_ip_ratelimit = 0; +/** ratelimit value for client ip addresses, + * in queries per second. + * For clients with a valid DNS Cookie. */ +int infra_ip_ratelimit_cookie = 0; + size_t infra_sizefunc(void* k, void* ATTR_UNUSED(d)) { @@ -1051,9 +1056,50 @@ infra_get_mem(struct infra_cache* infra) return s; } +/* Returns 1 if the limit has not been exceeded, 0 otherwise. */ +static int +check_ip_ratelimit(struct sockaddr_storage* addr, socklen_t addrlen, + struct sldns_buffer* buffer, int premax, int max, int has_cookie) +{ + int limit; + + if(has_cookie) limit = infra_ip_ratelimit_cookie; + else limit = infra_ip_ratelimit; + + /* Disabled */ + if(limit == 0) return 1; + + if(premax <= limit && max > limit) { + char client_ip[128], qnm[LDNS_MAX_DOMAINLEN+1+12+12]; + addr_to_str(addr, addrlen, client_ip, sizeof(client_ip)); + qnm[0]=0; + if(sldns_buffer_limit(buffer)>LDNS_HEADER_SIZE && + LDNS_QDCOUNT(sldns_buffer_begin(buffer))!=0) { + (void)sldns_wire2str_rrquestion_buf( + sldns_buffer_at(buffer, LDNS_HEADER_SIZE), + sldns_buffer_limit(buffer)-LDNS_HEADER_SIZE, + qnm, sizeof(qnm)); + if(strlen(qnm)>0 && qnm[strlen(qnm)-1]=='\n') + qnm[strlen(qnm)-1] = 0; /*remove newline*/ + if(strchr(qnm, '\t')) + *strchr(qnm, '\t') = ' '; + if(strchr(qnm, '\t')) + *strchr(qnm, '\t') = ' '; + verbose(VERB_OPS, "ip_ratelimit exceeded %s %d%s %s", + client_ip, limit, + has_cookie?"(cookie)":"", qnm); + } else { + verbose(VERB_OPS, "ip_ratelimit exceeded %s %d%s (no query name)", + client_ip, limit, + has_cookie?"(cookie)":""); + } + } + return (max <= limit); +} + int infra_ip_ratelimit_inc(struct infra_cache* infra, struct sockaddr_storage* addr, socklen_t addrlen, time_t timenow, - int backoff, struct sldns_buffer* buffer) + int has_cookie, int backoff, struct sldns_buffer* buffer) { int max; struct lruhash_entry* entry; @@ -1070,31 +1116,8 @@ int infra_ip_ratelimit_inc(struct infra_cache* infra, (*cur)++; max = infra_rate_max(entry->data, timenow, backoff); lock_rw_unlock(&entry->lock); - - if(premax <= infra_ip_ratelimit && max > infra_ip_ratelimit) { - char client_ip[128], qnm[LDNS_MAX_DOMAINLEN+1+12+12]; - addr_to_str(addr, addrlen, client_ip, sizeof(client_ip)); - qnm[0]=0; - if(sldns_buffer_limit(buffer)>LDNS_HEADER_SIZE && - LDNS_QDCOUNT(sldns_buffer_begin(buffer))!=0) { - (void)sldns_wire2str_rrquestion_buf( - sldns_buffer_at(buffer, LDNS_HEADER_SIZE), - sldns_buffer_limit(buffer)-LDNS_HEADER_SIZE, - qnm, sizeof(qnm)); - if(strlen(qnm)>0 && qnm[strlen(qnm)-1]=='\n') - qnm[strlen(qnm)-1] = 0; /*remove newline*/ - if(strchr(qnm, '\t')) - *strchr(qnm, '\t') = ' '; - if(strchr(qnm, '\t')) - *strchr(qnm, '\t') = ' '; - verbose(VERB_OPS, "ip_ratelimit exceeded %s %d %s", - client_ip, infra_ip_ratelimit, qnm); - } else { - verbose(VERB_OPS, "ip_ratelimit exceeded %s %d (no query name)", - client_ip, infra_ip_ratelimit); - } - } - return (max <= infra_ip_ratelimit); + return check_ip_ratelimit(addr, addrlen, buffer, premax, max, + has_cookie); } /* create */ diff --git a/services/cache/infra.h b/services/cache/infra.h index faf7fd2f3..525073bf3 100644 --- a/services/cache/infra.h +++ b/services/cache/infra.h @@ -153,6 +153,8 @@ struct rate_key { /** ip ratelimit, 0 is off */ extern int infra_ip_ratelimit; +/** ip ratelimit for DNS Cookie clients, 0 is off */ +extern int infra_ip_ratelimit_cookie; /** * key for ip_ratelimit lookups, a source IP. @@ -419,13 +421,14 @@ int infra_find_ratelimit(struct infra_cache* infra, uint8_t* name, * @param addr: client address * @param addrlen: client address length * @param timenow: what time it is now. + * @param has_cookie: if the request came with a DNS Cookie. * @param backoff: if backoff is enabled. * @param buffer: with query for logging. * @return 1 if it could be incremented. 0 if the increment overshot the * ratelimit and the query should be dropped. */ int infra_ip_ratelimit_inc(struct infra_cache* infra, struct sockaddr_storage* addr, socklen_t addrlen, time_t timenow, - int backoff, struct sldns_buffer* buffer); + int has_cookie, int backoff, struct sldns_buffer* buffer); /** * Get memory used by the infra cache. diff --git a/sldns/rrdef.h b/sldns/rrdef.h index bfe3960a6..f277fd67a 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 */ @@ -483,6 +484,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/smallapp/unbound-control.c b/smallapp/unbound-control.c index 891ce23ac..c4f730061 100644 --- a/smallapp/unbound-control.c +++ b/smallapp/unbound-control.c @@ -204,6 +204,12 @@ static void pr_stats(const char* nm, struct ub_stats_info* s) PR_UL_NM("num.queries", s->svr.num_queries); PR_UL_NM("num.queries_ip_ratelimited", s->svr.num_queries_ip_ratelimited); + PR_UL_NM("num.queries_cookie_valid", + s->svr.num_queries_cookie_valid); + PR_UL_NM("num.queries_cookie_client", + s->svr.num_queries_cookie_client); + PR_UL_NM("num.queries_cookie_invalid", + s->svr.num_queries_cookie_invalid); PR_UL_NM("num.cachehits", s->svr.num_queries - s->svr.num_queries_missed_cache); PR_UL_NM("num.cachemiss", s->svr.num_queries_missed_cache); diff --git a/testcode/fake_event.c b/testcode/fake_event.c index 9d65b3c49..2140b212a 100644 --- a/testcode/fake_event.c +++ b/testcode/fake_event.c @@ -1252,6 +1252,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/testcode/testpkts.c b/testcode/testpkts.c index 3702c3f18..aa852f01e 100644 --- a/testcode/testpkts.c +++ b/testcode/testpkts.c @@ -21,7 +21,6 @@ */ #include "config.h" -struct sockaddr_storage; #include #include #include @@ -140,6 +139,10 @@ static void matchline(char* line, struct entry* e) e->match_noedns = 1; } else if(str_keyword(&parse, "ednsdata")) { e->match_ednsdata_raw = 1; + } else if(str_keyword(&parse, "client_cookie")) { + e->match_client_cookie = 1; + } else if(str_keyword(&parse, "server_cookie")) { + e->match_server_cookie = 1; } else if(str_keyword(&parse, "UDP")) { e->match_transport = transport_udp; } else if(str_keyword(&parse, "TCP")) { @@ -905,37 +908,64 @@ get_do_flag(uint8_t* pkt, size_t len) return (int)(edns_bits&LDNS_EDNS_MASK_DO_BIT); } -/** Snips the EDE option out of the OPT record and returns the EDNS EDE - * INFO-CODE if found, else -1 */ +/** Snips the specified EDNS option out of the OPT record and puts it in the + * provided buffer. The buffer should be able to hold any opt data ie 65535. + * Returns the length of the option written, + * or 0 if not found, else -1 on error. */ static int -extract_ede(uint8_t* pkt, size_t len) +pkt_snip_edns_option(uint8_t* pkt, size_t len, sldns_edns_option code, + uint8_t* buf) { uint8_t *rdata, *opt_position = pkt; uint16_t rdlen, optlen; size_t remaining = len; - int ede_code; - if(!pkt_find_edns_opt(&opt_position, &remaining)) return -1; + if(!pkt_find_edns_opt(&opt_position, &remaining)) return 0; if(remaining < 8) return -1; /* malformed */ rdlen = sldns_read_uint16(opt_position+6); rdata = opt_position + 8; while(rdlen > 0) { if(rdlen < 4) return -1; /* malformed */ optlen = sldns_read_uint16(rdata+2); - if(sldns_read_uint16(rdata) == LDNS_EDNS_EDE) { - if(rdlen < 6) return -1; /* malformed */ - ede_code = sldns_read_uint16(rdata+4); + if(sldns_read_uint16(rdata) == code) { + /* save data to buf for caller inspection */ + memmove(buf, rdata+4, optlen); /* snip option from packet; assumes len is correct */ memmove(rdata, rdata+4+optlen, (pkt+len)-(rdata+4+optlen)); /* update OPT size */ sldns_write_uint16(opt_position+6, sldns_read_uint16(opt_position+6)-(4+optlen)); - return ede_code; + return optlen; } rdlen -= 4 + optlen; rdata += 4 + optlen; } - return -1; + return 0; +} + +/** Snips the EDE option out of the OPT record and returns the EDNS EDE + * INFO-CODE if found, else -1 */ +static int +extract_ede(uint8_t* pkt, size_t len) +{ + uint8_t buf[65535]; + int buflen = pkt_snip_edns_option(pkt, len, LDNS_EDNS_EDE, buf); + if(buflen < 2 /*ede without text at minimum*/) return -1; + return sldns_read_uint16(buf); +} + +/** Snips the DNS Cookie option out of the OPT record and puts it in the + * provided cookie buffer (should be at least 24 octets). + * Returns the length of the cookie if found, else -1. */ +static int +extract_cookie(uint8_t* pkt, size_t len, uint8_t* cookie) +{ + uint8_t buf[65535]; + int buflen = pkt_snip_edns_option(pkt, len, LDNS_EDNS_COOKIE, buf); + if(buflen != 8 /*client cookie*/ && + buflen != 8 + 16 /*server cookie*/) return -1; + memcpy(cookie, buf, buflen); + return buflen; } /** zero TTLs in packet */ @@ -1530,6 +1560,27 @@ find_match(struct entry* entries, uint8_t* query_pkt, size_t len, continue; } } + /* Cookies could also modify the query_pkt; keep them early */ + if(p->match_client_cookie || p->match_server_cookie) { + uint8_t cookie[24]; + int cookie_len = extract_cookie(query_pkt, len, + cookie); + if(cookie_len == -1) { + verbose(3, "bad DNS Cookie. " + "Expected but not found\n"); + continue; + } else if(p->match_client_cookie && + cookie_len != 8) { + verbose(3, "bad DNS Cookie. Expected client " + "cookie of length 8."); + continue; + } else if((p->match_server_cookie) && + cookie_len != 24) { + verbose(3, "bad DNS Cookie. Expected server " + "cookie of length 24."); + continue; + } + } if(p->match_opcode && get_opcode(query_pkt, len) != get_opcode(reply, rlen)) { verbose(3, "bad opcode\n"); diff --git a/testcode/testpkts.h b/testcode/testpkts.h index 2768040c6..c6a3725f3 100644 --- a/testcode/testpkts.h +++ b/testcode/testpkts.h @@ -64,6 +64,14 @@ struct sldns_file_parse_state; ; 'ede=any' makes the query match any EDNS EDE info-code. ; It also snips the EDE record out of the packet to facilitate ; other matches. + ; 'client_cookie' makes the query match any DNS Cookie option with + ; with a length of 8 octets. + ; It also snips the DNS Cookie record out of the packet to + ; facilitate other matches. + ; 'server_cookie' makes the query match any DNS Cookie option with + ; with a length of 24 octets. + ; It also snips the DNS Cookie record out of the packet to + ; facilitate other matches. MATCH [opcode] [qtype] [qname] [serial=] [all] [ttl] MATCH [UDP|TCP] DO MATCH ... @@ -104,11 +112,11 @@ struct sldns_file_parse_state; ; be parsed, ADJUST rules for the answer packet ; are ignored. Only copy_id is done. HEX_ANSWER_END - HEX_EDNS_BEGIN ; follow with hex data. + HEX_EDNSDATA_BEGIN ; follow with hex data. ; Raw EDNS data to match against. It must be an ; exact match (all options are matched) and will be ; evaluated only when 'MATCH ednsdata' given. - HEX_EDNS_END + HEX_EDNSDATA_END ENTRY_END @@ -214,6 +222,10 @@ struct entry { uint8_t match_noedns; /** match edns data field given in hex */ uint8_t match_ednsdata_raw; + /** match an DNS cookie of length 8 */ + uint8_t match_client_cookie; + /** match an DNS cookie of length 24 */ + uint8_t match_server_cookie; /** match query serial with this value. */ uint32_t ixfr_soa_serial; /** match on UDP/TCP */ @@ -235,7 +247,7 @@ struct entry { /** increment the ECS scope copied from the sourcemask by one */ uint8_t increment_ecs_scope; /** in seconds */ - unsigned int sleeptime; + unsigned int sleeptime; /** some number that names this entry, line number in file or so */ int lineno; diff --git a/testcode/unitmain.c b/testcode/unitmain.c index b0bf6bb54..647cbca3b 100644 --- a/testcode/unitmain.c +++ b/testcode/unitmain.c @@ -530,6 +530,207 @@ infra_test(void) config_delete(cfg); } +#include "util/edns.h" +/* Complete version-invalid client cookie; needs a new one. + * Based on edns_cookie_rfc9018_a2 */ +static void +edns_cookie_invalid_version(void) +{ + uint32_t timestamp = 1559734385; + uint8_t client_cookie[] = { + 0x24, 0x64, 0xc4, 0xab, 0xcf, 0x10, 0xc9, 0x57, + 0x99, 0x00, 0x00, 0x00, + 0x5c, 0xf7, 0x9f, 0x11, + 0x1f, 0x81, 0x30, 0xc3, 0xee, 0xe2, 0x94, 0x80 }; + uint8_t server_cookie[] = { + 0x24, 0x64, 0xc4, 0xab, 0xcf, 0x10, 0xc9, 0x57, + 0x01, 0x00, 0x00, 0x00, + 0x5c, 0xf7, 0xa8, 0x71, + 0xd4, 0xa5, 0x64, 0xa1, 0x44, 0x2a, 0xca, 0x77 }; + uint8_t server_secret[] = { + 0xe5, 0xe9, 0x73, 0xe5, 0xa6, 0xb2, 0xa4, 0x3f, + 0x48, 0xe7, 0xdc, 0x84, 0x9e, 0x37, 0xbf, 0xcf }; + uint8_t buf[32]; + /* copy client cookie|version|reserved|timestamp */ + memcpy(buf, client_cookie, 8 + 4 + 4); + /* copy ip 198.51.100.100 */ + memcpy(buf + 16, "\306\063\144\144", 4); + unit_assert(edns_cookie_server_validate(client_cookie, + sizeof(client_cookie), server_secret, sizeof(server_secret), 1, + buf, timestamp) == COOKIE_STATUS_INVALID); + edns_cookie_server_write(buf, server_secret, 1, timestamp); + unit_assert(memcmp(server_cookie, buf, 24) == 0); +} + +/* Complete hash-invalid client cookie; needs a new one. */ +static void +edns_cookie_invalid_hash(void) +{ + uint32_t timestamp = 0; + uint8_t client_cookie[] = { + 0xfc, 0x93, 0xfc, 0x62, 0x80, 0x7d, 0xdb, 0x86, + 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x32, 0xF2, 0x43, 0xB9, 0xBC, 0xFE, 0xC4, 0x06 }; + uint8_t server_cookie[] = { + 0xfc, 0x93, 0xfc, 0x62, 0x80, 0x7d, 0xdb, 0x86, + 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0xBA, 0x0D, 0x82, 0x90, 0x8F, 0xAA, 0xEB, 0xBD }; + uint8_t server_secret[] = { + 0xe5, 0xe9, 0x73, 0xe5, 0xa6, 0xb2, 0xa4, 0x3f, + 0x48, 0xe7, 0xdc, 0x84, 0x9e, 0x37, 0xbf, 0xcf }; + uint8_t buf[32]; + /* copy client cookie|version|reserved|timestamp */ + memcpy(buf, client_cookie, 8 + 4 + 4); + /* copy ip 203.0.113.203 */ + memcpy(buf + 16, "\313\000\161\313", 4); + unit_assert(edns_cookie_server_validate(client_cookie, + sizeof(client_cookie), server_secret, sizeof(server_secret), 1, + buf, timestamp) == COOKIE_STATUS_INVALID); + edns_cookie_server_write(buf, server_secret, 1, timestamp); + unit_assert(memcmp(server_cookie, buf, 24) == 0); +} + +/* Complete hash-valid client cookie; more than 30 minutes old; needs a + * refreshed server cookie. + * A slightly better variation of edns_cookie_rfc9018_a3 for Unbound to check + * that RESERVED bits do not influence cookie validation. */ +static void +edns_cookie_rfc9018_a3_better(void) +{ + uint32_t timestamp = 1800 + 1; + uint8_t client_cookie[] = { + 0xfc, 0x93, 0xfc, 0x62, 0x80, 0x7d, 0xdb, 0x86, + 0x01, 0xab, 0xcd, 0xef, + 0x00, 0x00, 0x00, 0x00, + 0x32, 0xF2, 0x43, 0xB9, 0xBC, 0xFE, 0xC4, 0x06 }; + uint8_t server_cookie[] = { + 0xfc, 0x93, 0xfc, 0x62, 0x80, 0x7d, 0xdb, 0x86, + 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x07, 0x09, + 0x62, 0xD5, 0x93, 0x09, 0x14, 0x5C, 0x23, 0x9D }; + uint8_t server_secret[] = { + 0xe5, 0xe9, 0x73, 0xe5, 0xa6, 0xb2, 0xa4, 0x3f, + 0x48, 0xe7, 0xdc, 0x84, 0x9e, 0x37, 0xbf, 0xcf }; + uint8_t buf[32]; + /* copy client cookie|version|reserved|timestamp */ + memcpy(buf, client_cookie, 8 + 4 + 4); + /* copy ip 203.0.113.203 */ + memcpy(buf + 16, "\313\000\161\313", 4); + unit_assert(edns_cookie_server_validate(client_cookie, + sizeof(client_cookie), server_secret, sizeof(server_secret), 1, + buf, timestamp) == COOKIE_STATUS_VALID_RENEW); + edns_cookie_server_write(buf, server_secret, 1, timestamp); + unit_assert(memcmp(server_cookie, buf, 24) == 0); +} + +/* Complete hash-valid client cookie; more than 60 minutes old (expired); + * needs a refreshed server cookie. */ +static void +edns_cookie_rfc9018_a3(void) +{ + uint32_t timestamp = 1559734700; + uint8_t client_cookie[] = { + 0xfc, 0x93, 0xfc, 0x62, 0x80, 0x7d, 0xdb, 0x86, + 0x01, 0xab, 0xcd, 0xef, + 0x5c, 0xf7, 0x8f, 0x71, + 0xa3, 0x14, 0x22, 0x7b, 0x66, 0x79, 0xeb, 0xf5 }; + uint8_t server_cookie[] = { + 0xfc, 0x93, 0xfc, 0x62, 0x80, 0x7d, 0xdb, 0x86, + 0x01, 0x00, 0x00, 0x00, + 0x5c, 0xf7, 0xa9, 0xac, + 0xf7, 0x3a, 0x78, 0x10, 0xac, 0xa2, 0x38, 0x1e }; + uint8_t server_secret[] = { + 0xe5, 0xe9, 0x73, 0xe5, 0xa6, 0xb2, 0xa4, 0x3f, + 0x48, 0xe7, 0xdc, 0x84, 0x9e, 0x37, 0xbf, 0xcf }; + uint8_t buf[32]; + /* copy client cookie|version|reserved|timestamp */ + memcpy(buf, client_cookie, 8 + 4 + 4); + /* copy ip 203.0.113.203 */ + memcpy(buf + 16, "\313\000\161\313", 4); + unit_assert(edns_cookie_server_validate(client_cookie, + sizeof(client_cookie), server_secret, sizeof(server_secret), 1, + buf, timestamp) == COOKIE_STATUS_EXPIRED); + edns_cookie_server_write(buf, server_secret, 1, timestamp); + unit_assert(memcmp(server_cookie, buf, 24) == 0); +} + +/* Complete hash-valid client cookie; more than 30 minutes old; needs a + * refreshed server cookie. */ +static void +edns_cookie_rfc9018_a2(void) +{ + uint32_t timestamp = 1559734385; + uint8_t client_cookie[] = { + 0x24, 0x64, 0xc4, 0xab, 0xcf, 0x10, 0xc9, 0x57, + 0x01, 0x00, 0x00, 0x00, + 0x5c, 0xf7, 0x9f, 0x11, + 0x1f, 0x81, 0x30, 0xc3, 0xee, 0xe2, 0x94, 0x80 }; + uint8_t server_cookie[] = { + 0x24, 0x64, 0xc4, 0xab, 0xcf, 0x10, 0xc9, 0x57, + 0x01, 0x00, 0x00, 0x00, + 0x5c, 0xf7, 0xa8, 0x71, + 0xd4, 0xa5, 0x64, 0xa1, 0x44, 0x2a, 0xca, 0x77 }; + uint8_t server_secret[] = { + 0xe5, 0xe9, 0x73, 0xe5, 0xa6, 0xb2, 0xa4, 0x3f, + 0x48, 0xe7, 0xdc, 0x84, 0x9e, 0x37, 0xbf, 0xcf }; + uint8_t buf[32]; + /* copy client cookie|version|reserved|timestamp */ + memcpy(buf, client_cookie, 8 + 4 + 4); + /* copy ip 198.51.100.100 */ + memcpy(buf + 16, "\306\063\144\144", 4); + unit_assert(edns_cookie_server_validate(client_cookie, + sizeof(client_cookie), server_secret, sizeof(server_secret), 1, + buf, timestamp) == COOKIE_STATUS_VALID_RENEW); + edns_cookie_server_write(buf, server_secret, 1, timestamp); + unit_assert(memcmp(server_cookie, buf, 24) == 0); +} + +/* Only client cookie; needs a complete server cookie. */ +static void +edns_cookie_rfc9018_a1(void) +{ + uint32_t timestamp = 1559731985; + uint8_t client_cookie[] = { + 0x24, 0x64, 0xc4, 0xab, 0xcf, 0x10, 0xc9, 0x57 }; + uint8_t server_cookie[] = { + 0x24, 0x64, 0xc4, 0xab, 0xcf, 0x10, 0xc9, 0x57, + 0x01, 0x00, 0x00, 0x00, + 0x5c, 0xf7, 0x9f, 0x11, + 0x1f, 0x81, 0x30, 0xc3, 0xee, 0xe2, 0x94, 0x80 }; + uint8_t server_secret[] = { + 0xe5, 0xe9, 0x73, 0xe5, 0xa6, 0xb2, 0xa4, 0x3f, + 0x48, 0xe7, 0xdc, 0x84, 0x9e, 0x37, 0xbf, 0xcf }; + uint8_t buf[32]; + /* copy client cookie|version|reserved|timestamp */ + memcpy(buf, server_cookie, 8 + 4 + 4); + /* copy ip 198.51.100.100 */ + memcpy(buf + 16, "\306\063\144\144", 4); + unit_assert(edns_cookie_server_validate(client_cookie, + sizeof(client_cookie), + /* these will not be used; it will return invalid + * because of the size. */ + NULL, 0, 1, NULL, 0) == COOKIE_STATUS_CLIENT_ONLY); + edns_cookie_server_write(buf, server_secret, 1, timestamp); + unit_assert(memcmp(server_cookie, buf, 24) == 0); +} + +/** test interoperable DNS cookies (RFC9018) */ +static void +edns_cookie_test(void) +{ + unit_show_feature("interoperable dns cookies"); + /* Check RFC9018 appendix test vectors */ + edns_cookie_rfc9018_a1(); + edns_cookie_rfc9018_a2(); + edns_cookie_rfc9018_a3(); + /* More tests */ + edns_cookie_rfc9018_a3_better(); + edns_cookie_invalid_hash(); + edns_cookie_invalid_version(); +} + #include "util/random.h" /** test randomness */ static void @@ -915,8 +1116,8 @@ static void edns_ede_encode_encodedecode(struct query_info* qinfo, (void)query_dname_len(pkt); sldns_buffer_skip(pkt, 2 + 2); /* decode */ - unit_assert( - parse_edns_from_query_pkt(pkt, edns, NULL, NULL, region)==0); + unit_assert(parse_edns_from_query_pkt(pkt, edns, NULL, NULL, NULL, 0, + region) == 0); } static void edns_ede_encode_check(struct edns_data* edns, int* found_ede, @@ -1119,6 +1320,7 @@ main(int argc, char* argv[]) slabhash_test(); infra_test(); ldns_test(); + edns_cookie_test(); zonemd_test(); tcpreuse_test(); msgparse_test(); diff --git a/testdata/01-doc.tdir/01-doc.test b/testdata/01-doc.tdir/01-doc.test index 6a78a9cd3..484b0be42 100644 --- a/testdata/01-doc.tdir/01-doc.test +++ b/testdata/01-doc.tdir/01-doc.test @@ -34,6 +34,7 @@ fgrep -v -e "ldns-src/" hlist > ilist; mv ilist hlist fgrep -v -e "libunbound/python/libunbound_wrap.c" hlist > ilist; mv ilist hlist fgrep -v -e "pythonmod/interface.h" hlist > ilist; mv ilist hlist fgrep -v -e "dnstap" hlist > ilist; mv ilist hlist +fgrep -v -e "util/siphash.c" hlist > ilist; mv ilist hlist # filter out compat fgrep -v -e "compat/" hlist > ilist; mv ilist hlist for h in `cat hlist`; do diff --git a/testdata/edns_downstream_cookies.rpl b/testdata/edns_downstream_cookies.rpl new file mode 100644 index 000000000..820bc5a7c --- /dev/null +++ b/testdata/edns_downstream_cookies.rpl @@ -0,0 +1,235 @@ +; config options +server: + answer-cookie: yes + cookie-secret: "000102030405060708090a0b0c0d0e0f" + access-control: 127.0.0.1 allow_cookie + access-control: 1.2.3.4 allow + local-data: "test. TXT test" + +CONFIG_END + +SCENARIO_BEGIN Test downstream DNS Cookies + +; Note: When a valid hash was required, it was generated by running this test +; with an invalid one and checking the output for the valid one. +; Actual hash generation is tested with unit tests. + +; Query without a client cookie ... +STEP 0 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +test. IN TXT +ENTRY_END +; ... get TC and refused +STEP 1 CHECK_ANSWER +ENTRY_BEGIN +MATCH all +REPLY QR RD RA TC REFUSED +SECTION QUESTION +test. IN TXT +ENTRY_END + +; Query without a client cookie on TCP ... +STEP 10 QUERY +ENTRY_BEGIN +REPLY RD +MATCH TCP +SECTION QUESTION +test. IN TXT +ENTRY_END +; ... get an answer +STEP 11 CHECK_ANSWER +ENTRY_BEGIN +MATCH all +REPLY QR RD RA AA NOERROR +SECTION QUESTION +test. IN TXT +SECTION ANSWER +test. IN TXT "test" +ENTRY_END + +; Query with only a client cookie ... +STEP 20 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +test. IN TXT +SECTION ADDITIONAL +HEX_EDNSDATA_BEGIN + 00 0a ; Opcode 10 + 00 08 ; Length 8 + 31 32 33 34 35 36 37 38 ; Random bits +HEX_EDNSDATA_END +ENTRY_END +; ... get BADCOOKIE and a new cookie +STEP 21 CHECK_ANSWER +ENTRY_BEGIN +MATCH all server_cookie +REPLY QR RD RA DO YXRRSET ; BADCOOKIE is an extended rcode +SECTION QUESTION +test. IN TXT +ENTRY_END + +; Query with an invalid cookie ... +STEP 30 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +test. IN TXT +SECTION ADDITIONAL +HEX_EDNSDATA_BEGIN + 00 0a ; Opcode 10 + 00 18 ; Length 24 + 31 32 33 34 35 36 37 38 ; Random bits + 02 00 00 00 ; wrong version + 00 00 00 00 ; Timestamp + 31 32 33 34 35 36 37 38 ; wrong hash +HEX_EDNSDATA_END +ENTRY_END +; ... get BADCOOKIE and a new cookie +STEP 31 CHECK_ANSWER +ENTRY_BEGIN +MATCH all server_cookie +REPLY QR RD RA DO YXRRSET ; BADCOOKIE is an extended rcode +SECTION QUESTION +test. IN TXT +ENTRY_END + +; Query with an invalid cookie from a non-cookie protected address ... +STEP 40 QUERY ADDRESS 1.2.3.4 +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +test. IN TXT +SECTION ADDITIONAL +HEX_EDNSDATA_BEGIN + 00 0a ; Opcode 10 + 00 18 ; Length 24 + 31 32 33 34 35 36 37 38 ; Random bits + 02 00 00 00 ; wrong version + 00 00 00 00 ; Timestamp + 31 32 33 34 35 36 37 38 ; wrong hash +HEX_EDNSDATA_END +ENTRY_END +; ... get answer and a cookie +STEP 41 CHECK_ANSWER +ENTRY_BEGIN +MATCH all server_cookie +REPLY QR RD RA AA DO NOERROR +SECTION QUESTION +test. IN TXT +SECTION ANSWER +test. IN TXT "test" +ENTRY_END + +; Query with a valid cookie ... +STEP 50 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +test. IN TXT +SECTION ADDITIONAL +HEX_EDNSDATA_BEGIN + 00 0a ; Opcode 10 + 00 18 ; Length 24 + 31 32 33 34 35 36 37 38 ; Random bits + 01 00 00 00 ; Version/Reserved + 00 00 00 00 ; Timestamp + 38 52 7b a8 c6 a4 ea 96 ; Hash +HEX_EDNSDATA_END +ENTRY_END +; ... get answer and the cookie +STEP 51 CHECK_ANSWER +ENTRY_BEGIN +MATCH all server_cookie +REPLY QR RD RA AA DO NOERROR +SECTION QUESTION +test. IN TXT +SECTION ANSWER +test. IN TXT "test" +ENTRY_END + +; Query with a valid >30 minutes old cookie ... +STEP 59 TIME_PASSES ELAPSE 1801 +STEP 60 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +test. IN TXT +SECTION ADDITIONAL +HEX_EDNSDATA_BEGIN + 00 0a ; Opcode 10 + 00 18 ; Length 24 + 31 32 33 34 35 36 37 38 ; Random bits + 01 00 00 00 ; Version/Reserved + 00 00 00 00 ; Timestamp + 38 52 7b a8 c6 a4 ea 96 ; Hash +HEX_EDNSDATA_END +ENTRY_END +; ... Get answer and a refreshed cookie +; (we don't check the re-freshness here; it has its own unit test) +STEP 61 CHECK_ANSWER +ENTRY_BEGIN +MATCH all server_cookie +REPLY QR RD RA AA DO NOERROR +SECTION QUESTION +test. IN TXT +SECTION ANSWER +test. IN TXT "test" +ENTRY_END + +; Query with a hash-valid >60 minutes old cookie ... +STEP 69 TIME_PASSES ELAPSE 3601 +STEP 70 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +test. IN TXT +SECTION ADDITIONAL +HEX_EDNSDATA_BEGIN + 00 0a ; Opcode 10 + 00 18 ; Length 24 + 31 32 33 34 35 36 37 38 ; Random bits + 01 00 00 00 ; Version/Reserved + 00 00 07 09 ; Timestamp (1801) + 77 81 38 e3 8f aa 72 86 ; Hash +HEX_EDNSDATA_END +ENTRY_END +; ... get BADCOOKIE and a new cookie +STEP 71 CHECK_ANSWER +ENTRY_BEGIN +MATCH all server_cookie +REPLY QR RD RA DO YXRRSET ; BADCOOKIE is an extended rcode +SECTION QUESTION +test. IN TXT +ENTRY_END + +; Query with a valid future (<5 minutes) cookie ... +STEP 80 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +test. IN TXT +SECTION ADDITIONAL +HEX_EDNSDATA_BEGIN + 00 0a ; Opcode 10 + 00 18 ; Length 24 + 31 32 33 34 35 36 37 38 ; Random bits + 01 00 00 00 ; Version/Reserved + 00 00 16 45 ; Timestamp (1801 + 3601 + 299) + 4a f5 0f df f0 e8 c7 09 ; Hash +HEX_EDNSDATA_END +ENTRY_END +; ... get an answer +STEP 81 CHECK_ANSWER +ENTRY_BEGIN +MATCH all server_cookie +REPLY QR RD RA AA DO NOERROR +SECTION QUESTION +test. IN TXT +SECTION ANSWER +test. IN TXT "test" +ENTRY_END + +SCENARIO_END diff --git a/testdata/ip_ratelimit.tdir/ip_ratelimit.conf b/testdata/ip_ratelimit.tdir/ip_ratelimit.conf new file mode 100644 index 000000000..ae7d0cda0 --- /dev/null +++ b/testdata/ip_ratelimit.tdir/ip_ratelimit.conf @@ -0,0 +1,28 @@ +server: + verbosity: 5 + # num-threads: 1 + interface: 127.0.0.1 + port: @PORT@ + use-syslog: no + directory: . + pidfile: "unbound.pid" + chroot: "" + username: "" + local-data: "test. IN TXT localdata" + + ip-ratelimit: 1 + ip-ratelimit-cookie: 0 + ip-ratelimit-factor: 0 + ip-ratelimit-backoff: yes + answer-cookie: yes + access-control: 127.0.0.0/8 allow_cookie + +remote-control: + control-enable: yes + control-interface: 127.0.0.1 + # control-interface: ::1 + control-port: @CONTROL_PORT@ + server-key-file: "unbound_server.key" + server-cert-file: "unbound_server.pem" + control-key-file: "unbound_control.key" + control-cert-file: "unbound_control.pem" diff --git a/testdata/ip_ratelimit.tdir/ip_ratelimit.dsc b/testdata/ip_ratelimit.tdir/ip_ratelimit.dsc new file mode 100644 index 000000000..a6f619236 --- /dev/null +++ b/testdata/ip_ratelimit.tdir/ip_ratelimit.dsc @@ -0,0 +1,16 @@ +BaseName: ip_ratelimit +Version: 1.0 +Description: Test IP source ratelimit. +CreationDate: Tue Aug 8 00:00:00 CET 2023 +Maintainer: Yorgos Thessalonikefs +Category: +Component: +CmdDepends: +Depends: +Help: +Pre: ip_ratelimit.pre +Post: ip_ratelimit.post +Test: ip_ratelimit.test +AuxFiles: +Passed: +Failure: diff --git a/testdata/ip_ratelimit.tdir/ip_ratelimit.post b/testdata/ip_ratelimit.tdir/ip_ratelimit.post new file mode 100644 index 000000000..1f86d0085 --- /dev/null +++ b/testdata/ip_ratelimit.tdir/ip_ratelimit.post @@ -0,0 +1,13 @@ +# #-- ip_ratelimit.post --# +# source the master var file when it's there +[ -f ../.tpkg.var.master ] && source ../.tpkg.var.master +# source the test var file when it's there +[ -f .tpkg.var.test ] && source .tpkg.var.test +# +# do your teardown here +. ../common.sh +kill_pid $UNBOUND_PID +if test -f unbound.log; then + echo ">>> unbound log" + cat unbound.log +fi diff --git a/testdata/ip_ratelimit.tdir/ip_ratelimit.pre b/testdata/ip_ratelimit.tdir/ip_ratelimit.pre new file mode 100644 index 000000000..c4589a0ea --- /dev/null +++ b/testdata/ip_ratelimit.tdir/ip_ratelimit.pre @@ -0,0 +1,24 @@ +# #-- ip_ratelimit.pre--# +# source the master var file when it's there +[ -f ../.tpkg.var.master ] && source ../.tpkg.var.master +# use .tpkg.var.test for in test variable passing +[ -f .tpkg.var.test ] && source .tpkg.var.test + +PRE="../.." +. ../common.sh +get_random_port 2 +UNBOUND_PORT=$RND_PORT +CONTROL_PORT=$(($RND_PORT + 1)) +echo "UNBOUND_PORT=$UNBOUND_PORT" >> .tpkg.var.test +echo "CONTROL_PORT=$CONTROL_PORT" >> .tpkg.var.test + +# make config file +sed -e 's/@PORT\@/'$UNBOUND_PORT'/' -e 's/@CONTROL_PORT\@/'$CONTROL_PORT'/' < ip_ratelimit.conf > ub.conf +# start unbound in the background +$PRE/unbound -d -c ub.conf >unbound.log 2>&1 & +UNBOUND_PID=$! +echo "UNBOUND_PID=$UNBOUND_PID" >> .tpkg.var.test + +wait_unbound_up unbound.log + +cat .tpkg.var.test diff --git a/testdata/ip_ratelimit.tdir/ip_ratelimit.test b/testdata/ip_ratelimit.tdir/ip_ratelimit.test new file mode 100644 index 000000000..7af7d108c --- /dev/null +++ b/testdata/ip_ratelimit.tdir/ip_ratelimit.test @@ -0,0 +1,159 @@ +# #-- ip_ratelimit.test --# +# source the master var file when it's there +[ -f ../.tpkg.var.master ] && source ../.tpkg.var.master +# use .tpkg.var.test for in test variable passing +[ -f .tpkg.var.test ] && source .tpkg.var.test + +PRE="../.." +. ../common.sh + +get_make +(cd $PRE; $MAKE streamtcp) + +# These tests rely on second time precision. To combat false negatives the +# tests run multiple times and we allow 1/3 of the runs to fail. +total_runs=6 +success_threshold=4 # 2/3*total_runs + +echo "> First get a valid cookie" +dig @127.0.0.1 -p $UNBOUND_PORT +ednsopt=10:0102030405060708 +tcp +retry=0 +time=1 test. TXT >outfile 2>&1 +if test "$?" -ne 0; then + echo "exit status not OK" + echo "> cat logfiles" + cat outfile + cat unbound.log + echo "Not OK" + exit 1 +fi +if test `grep "COOKIE: " outfile | wc -l` -ne 1; then + echo "Could not get cookie" + echo "> cat logfiles" + cat outfile + cat unbound.log + echo "Not OK" + exit 1 +fi +cookie=`grep "COOKIE: " outfile | cut -d ' ' -f 3` + +successes=0 +echo "> Three parallel queries with backoff and cookie" +# For this test we send three parallel queries. The ratelimit should be reached +# for that second. We send a query to verify that there is no reply. +# Then for the next second we again send three parallel queries and we expect +# none of them to be allowed through because of the backoff logic that keeps +# rolling the RATE_WINDOW based on demand. +# Again we send another query but with a valid cookie and we expect to receive +# an answer. +for i in $(seq 1 $total_runs); do + # Try to hit limit + $PRE/streamtcp -nu -f 127.0.0.1@$UNBOUND_PORT test. TXT IN test. TXT IN test. TXT IN >outfile 2>&1 + if test "$?" -ne 0; then + echo "exit status not OK" + echo "> cat logfiles" + cat outfile + cat unbound.log + echo "Not OK" + exit 1 + fi + # Expect no answer because of limit + dig @127.0.0.1 -p $UNBOUND_PORT +retry=0 +time=1 test. TXT >outfile 2>&1 + if test "$?" -eq 0; then + continue + fi + # Try to keep limit + $PRE/streamtcp -nu -f 127.0.0.1@$UNBOUND_PORT test. TXT IN test. TXT IN test. TXT IN >outfile 2>&1 + if test "$?" -ne 0; then + echo "exit status not OK" + echo "> cat logfiles" + cat outfile + cat unbound.log + echo "Not OK" + exit 1 + fi + # Expect answer because of DNS cookie + dig @127.0.0.1 -p $UNBOUND_PORT +ednsopt=10:$cookie +retry=0 +time=1 test. TXT >outfile 2>&1 + if test "$?" -ne 0; then + continue + fi + ((successes++)) + # We don't have to wait for all the runs to complete if we know + # we passed the threshold. + if test $successes -ge $success_threshold; then + break + fi +done + +if test $successes -ge $success_threshold; then + echo "Three parallel queries with backoff and cookie OK" +else + echo "Three parallel queries with backoff and cookie NOT OK" + echo "> cat logfiles" + cat outfile + cat unbound.log + echo "Three parallel queries with backoff and cookie NOT OK" + exit 1 +fi + +echo "> Activating ip-ratelimit-cookie" +echo "$PRE/unbound-control -c ub.conf set_option ip-ratelimit-cookie: 1" +$PRE/unbound-control -c ub.conf set_option ip-ratelimit-cookie: 1 +if test $? -ne 0; then + echo "wrong exit value after success" + exit 1 +fi + +successes=0 +echo "> Three parallel queries with backoff and cookie with ip-ratelimit-cookie" +# This is the exact same test as above with the exception that we don't expect +# an answer on the last query because ip-ratelimit-cookie is now enabled. +for i in $(seq 1 $total_runs); do + # Try to hit limit + $PRE/streamtcp -nu -f 127.0.0.1@$UNBOUND_PORT test. TXT IN test. TXT IN test. TXT IN >outfile 2>&1 + if test "$?" -ne 0; then + echo "exit status not OK" + echo "> cat logfiles" + cat outfile + cat unbound.log + echo "Not OK" + exit 1 + fi + # Expect no answer because of limit + dig @127.0.0.1 -p $UNBOUND_PORT +retry=0 +time=1 test. TXT >outfile 2>&1 + if test "$?" -eq 0; then + continue + fi + # Try to keep limit + $PRE/streamtcp -nu -f 127.0.0.1@$UNBOUND_PORT test. TXT IN test. TXT IN test. TXT IN >outfile 2>&1 + if test "$?" -ne 0; then + echo "exit status not OK" + echo "> cat logfiles" + cat outfile + cat unbound.log + echo "Not OK" + exit 1 + fi + # Expect no answer because of ip-ratelimit-cookie + dig @127.0.0.1 -p $UNBOUND_PORT +ednsopt=10:$cookie +retry=0 +time=1 test. TXT >outfile 2>&1 + if test "$?" -eq 0; then + continue + fi + ((successes++)) + # We don't have to wait for all the runs to complete if we know + # we passed the threshold. + if test $successes -ge $success_threshold; then + break + fi +done + +if test $successes -ge $success_threshold; then + echo "Three parallel queries with backoff and cookie with ip-ratelimit-cookie OK" +else + echo "Three parallel queries with backoff and cookie with ip-ratelimit-cookie NOT OK" + echo "> cat logfiles" + cat outfile + cat unbound.log + echo "Three parallel queries with backoff and cookie with ip-ratelimit-cookie NOT OK" + exit 1 +fi + +exit 0 diff --git a/testdata/ip_ratelimit.tdir/unbound_control.key b/testdata/ip_ratelimit.tdir/unbound_control.key new file mode 100644 index 000000000..753a4ef61 --- /dev/null +++ b/testdata/ip_ratelimit.tdir/unbound_control.key @@ -0,0 +1,39 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIG4gIBAAKCAYEAstEp+Pyh8XGrtZ77A4FhYjvbeB3dMa7Q2rGWxobzlA9przhA +1aChAvUtCOAuM+rB6NTNB8YWfZJbQHawyMNpmC77cg6vXLYCGUQHZyAqidN049RJ +F5T7j4N8Vniv17LiRdr0S6swy4PRvEnIPPV43EQHZqC5jVvHsKkhIfmBF/Dj5TXR +ypeawWV/m5jeU6/4HRYMfytBZdO1mPXuWLh0lgbQ4SCbgrOUVD3rniMk1yZIbQOm +vlDHYqekjDb/vOW2KxUQLG04aZMJ1mWfdbwG0CKQkSjISEDZ1l76vhM6mTM0fwXb +IvyFZ9yPPCle1mF5aSlxS2cmGuGVSRQaw8XF9fe3a9ACJJTr33HdSpyaZkKRAUzL +cKqLCl323daKv3NwwAT03Tj4iQM416ASMoiyfFa/2GWTKQVjddu8Crar7tGaf5xr +lig4DBmrBvdYA3njy72/RD71hLwmlRoCGU7dRuDr9O6KASUm1Ri91ONZ/qdjMvov +15l2vj4GV+KXR00dAgMBAAECggGAHepIL1N0dEQkCdpy+/8lH54L9WhpnOo2HqAf +LU9eaKK7d4jdr9+TkD8cLaPzltPrZNxVALvu/0sA4SP6J1wpyj/x6P7z73qzly5+ +Xo5PD4fEwmi9YaiW/UduAblnEZrnp/AddptJKoL/D5T4XtpiQddPtael4zQ7kB57 +YIexRSQTvEDovA/o3/nvA0TrzOxfgd4ycQP3iOWGN/TMzyLsvjydrUwbOB567iz9 +whL3Etdgvnwh5Sz2blbFfH+nAR8ctvFFz+osPvuIVR21VMEI6wm7kTpSNnQ6sh/c +lrLb/bTADn4g7z/LpIZJ+MrLvyEcoqValrLYeFBhM9CV8woPxvkO2P3pU47HVGax +tC7GV6a/kt5RoKFd/TNdiA3OC7NGZtaeXv9VkPf4fVwBtSO9d5ZZXTGEynDD/rUQ +U4KFJe6OD23APjse08HiiKqTPhsOneOONU67iqoaTdIkT2R4EdlkVEDpXVtWb+G9 +Q+IqYzVljlzuyHrhWXLJw/FMa2aBAoHBAOnZbi4gGpH+P6886WDWVgIlTccuXoyc +Mg9QQYk9UDeXxL0AizR5bZy49Sduegz9vkHpAiZARQsUnizHjZ8YlRcrmn4t6tx3 +ahTIKAjdprnxJfYINM580j8CGbXvX5LhIlm3O267D0Op+co3+7Ujy+cjsIuFQrP+ +1MqMgXSeBjzC1APivmps7HeFE+4w0k2PfN5wSMDNCzLo99PZuUG5XZ93OVOS5dpN +b+WskdcD8NOoJy/X/5A08veEI/jYO/DyqQKBwQDDwUQCOWf41ecvJLtBHKmEnHDz +ftzHino9DRKG8a9XaN4rmetnoWEaM2vHGX3pf3mwH+dAe8vJdAQueDhBKYeEpm6C +TYNOpou1+Zs5s99BilCTNYo8fkMOAyqwRwmz9zgHS6QxXuPwsghKefLJGt6o6RFF +tfWVTfLlYJ+I3GQe3ySsk3wjVz4oUTKiyiq5+KzD+HhEkS7u+RQ7Z0ZI2xd2cF8Y +aN2hjKDpcOiFf3CDoqka5D1qMNLgIHO52AHww1UCgcA1h7o7AMpURRka6hyaODY0 +A4oMYEbwdQjYjIyT998W+rzkbu1us6UtzQEBZ760npkgyU/epbOoV63lnkCC/MOU +LD0PST+L/CHiY/cWIHb79YG1EifUZKpUFg0Aoq0EGFkepF0MefGCkbRGYA5UZr9U +R80wAu9D+L+JJiS0J0BSRF74DL196zUuHt5zFeXuLzxsRtPAnq9DliS08BACRYZy +7H3I7cWD9Vn5/0jbKWHFcaaWwyETR6uekTcSzZzbCRECgcBeoE3/xUA9SSk34Mmj +7/cB4522Ft0imA3+9RK/qJTZ7Bd5fC4PKjOGNtUiqW/0L2rjeIiQ40bfWvWqgPKw +jSK1PL6uvkl6+4cNsFsYyZpiVDoe7wKju2UuoNlB3RUTqa2r2STFuNj2wRjA57I1 +BIgdnox65jqQsd14g/yaa+75/WP9CE45xzKEyrtvdcqxm0Pod3OrsYK+gikFjiar +kT0GQ8u0QPzh2tjt/2ZnIfOBrl+QYERP0MofDZDjhUdq2wECgcB0Lu841+yP5cdR +qbJhXO4zJNh7oWNcJlOuQp3ZMNFrA1oHpe9pmLukiROOy01k9WxIMQDzU5GSqRv3 +VLkYOIcbhJ3kClKAcM3j95SkKbU2H5/RENb3Ck52xtl4pNU1x/3PnVFZfDVuuHO9 +MZ9YBcIeK98MyP2jr5JtFKnOyPE7xKq0IHIhXadpbc2wjje5FtZ1cUtMyEECCXNa +C1TpXebHGyXGpY9WdWXhjdE/1jPvfS+uO5WyuDpYPr339gsdq1g= +-----END RSA PRIVATE KEY----- diff --git a/testdata/ip_ratelimit.tdir/unbound_control.pem b/testdata/ip_ratelimit.tdir/unbound_control.pem new file mode 100644 index 000000000..a1edf7017 --- /dev/null +++ b/testdata/ip_ratelimit.tdir/unbound_control.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDszCCAhsCFGD5193whHQ2bVdzbaQfdf1gc4SkMA0GCSqGSIb3DQEBCwUAMBIx +EDAOBgNVBAMMB3VuYm91bmQwHhcNMjAwNzA4MTMzMjMwWhcNNDAwMzI1MTMzMjMw +WjAaMRgwFgYDVQQDDA91bmJvdW5kLWNvbnRyb2wwggGiMA0GCSqGSIb3DQEBAQUA +A4IBjwAwggGKAoIBgQCy0Sn4/KHxcau1nvsDgWFiO9t4Hd0xrtDasZbGhvOUD2mv +OEDVoKEC9S0I4C4z6sHo1M0HxhZ9kltAdrDIw2mYLvtyDq9ctgIZRAdnICqJ03Tj +1EkXlPuPg3xWeK/XsuJF2vRLqzDLg9G8Scg89XjcRAdmoLmNW8ewqSEh+YEX8OPl +NdHKl5rBZX+bmN5Tr/gdFgx/K0Fl07WY9e5YuHSWBtDhIJuCs5RUPeueIyTXJkht +A6a+UMdip6SMNv+85bYrFRAsbThpkwnWZZ91vAbQIpCRKMhIQNnWXvq+EzqZMzR/ +Bdsi/IVn3I88KV7WYXlpKXFLZyYa4ZVJFBrDxcX197dr0AIklOvfcd1KnJpmQpEB +TMtwqosKXfbd1oq/c3DABPTdOPiJAzjXoBIyiLJ8Vr/YZZMpBWN127wKtqvu0Zp/ +nGuWKDgMGasG91gDeePLvb9EPvWEvCaVGgIZTt1G4Ov07ooBJSbVGL3U41n+p2My ++i/XmXa+PgZX4pdHTR0CAwEAATANBgkqhkiG9w0BAQsFAAOCAYEAd++Wen6l8Ifj +4h3p/y16PhSsWJWuJ4wdNYy3/GM84S26wGjzlEEwiW76HpH6VJzPOiBAeWnFKE83 +hFyetEIxgJeIPbcs9ZP/Uoh8GZH9tRISBSN9Hgk2Slr9llo4t1H0g/XTgA5HqMQU +9YydlBh43G7Vw3FVwh09OM6poNOGQKNc/tq2/QdKeUMtyBbLWpRmjH5XcCT35fbn +ZiVOUldqSHD4kKrFO4nJYXZyipRbcXybsLiX9GP0GLemc3IgIvOXyJ2RPp06o/SJ +pzlMlkcAfLJaSuEW57xRakhuNK7m051TKKzJzIEX+NFYOVdafFHS8VwGrYsdrFvD +72tMfu+Fu55y3awdWWGc6YlaGogZiuMnJkvQphwgn+5qE/7CGEckoKEsH601rqIZ +muaIc85+nEcHJeijd/ZlBN9zeltjFoMuqTUENgmv8+tUAdVm/UMY9Vjme6b43ydP +uv6DS02+k9z8toxXworLiPr94BGaiGV1NxgwZKLZigYJt/Fi2Qte +-----END CERTIFICATE----- diff --git a/testdata/ip_ratelimit.tdir/unbound_server.key b/testdata/ip_ratelimit.tdir/unbound_server.key new file mode 100644 index 000000000..370a7bbb2 --- /dev/null +++ b/testdata/ip_ratelimit.tdir/unbound_server.key @@ -0,0 +1,39 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/testdata/ip_ratelimit.tdir/unbound_server.pem b/testdata/ip_ratelimit.tdir/unbound_server.pem new file mode 100644 index 000000000..986807310 --- /dev/null +++ b/testdata/ip_ratelimit.tdir/unbound_server.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDqzCCAhMCFBHWXeQ6ZIa9QcQbXLFfC6tj+KA+MA0GCSqGSIb3DQEBCwUAMBIx +EDAOBgNVBAMMB3VuYm91bmQwHhcNMjAwNzA4MTMzMjI5WhcNNDAwMzI1MTMzMjI5 +WjASMRAwDgYDVQQDDAd1bmJvdW5kMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB +igKCAYEAvjSVSN2QMXudpzukdLCqgg/IOhCX8KYkD0FFFfWcQjgKq5wI0x41iG32 +a6wbGanre4IX7VxaSPu9kkHfnGgynCk5nwDRedE/FLFhAU78PoT0+NqqGRS7XVQ2 +4vLmIz9Hqc2Ozx1um1BXBTmIT0UfN2e22I0LWQ6a3seZlEDRj45gnk7Zuh9MDgot +aBdm+v1JAbupSf6Zis4VEH3JNdvVGE3O1DHEIeuuz/3BDhpf6WBDH+8KWaBe1ca4 +TZHr9ThL2gEMEfAQl0wXDwRWRoi3NjNMH+mw0L1rjwThI5GXqNIee7o5FzUReSXZ +uTdFMyGe3Owcx+XoYnwi6cplSNoGsDBu4B9bKKglR9YleJVw4L4Xi8xPq6O9UPj4 ++nypHk/DOoC7DIM3ufN0yxPBsFo5TVowxfhdjZXJbbftd2TZv7AH8+XLA5UoZgRz +XgzECelXSCTBFlMTnT48LfA9pMLydyjAz2UdPHs5Iv+TK5nnI+aJoeaP7kFZSngx +dy1+A/bNAgMBAAEwDQYJKoZIhvcNAQELBQADggGBABunf93MKaCUHiZgnoOTinsW +84/EgInrgtKzAyH+BhnKkJOhhR0kkIAx5d9BpDlaSiRTACFon9moWCgDIIsK/Ar7 +JE0Kln9cV//wiiNoFU0O4mnzyGUIMvlaEX6QHMJJQYvL05+w/3AAcf5XmMJtR5ca +fJ8FqvGC34b2WxX9lTQoyT52sRt+1KnQikiMEnEyAdKktMG+MwKsFDdOwDXyZhZg +XZhRrfX3/NVJolqB6EahjWIGXDeKuSSKZVtCyib6LskyeMzN5lcRfvubKDdlqFVF +qlD7rHBsKhQUWK/IO64mGf7y/de+CgHtED5vDvr/p2uj/9sABATfbrOQR3W/Of25 +sLBj4OEfrJ7lX8hQgFaxkMI3x6VFT3W8dTCp7xnQgb6bgROWB5fNEZ9jk/gjSRmD +yIU+r0UbKe5kBk/CmZVFXL2TyJ92V5NYEQh8V4DGy19qZ6u/XKYyNJL4ocs35GGe +CA8SBuyrmdhx38h1RHErR2Skzadi1S7MwGf1y431fQ== +-----END CERTIFICATE----- diff --git a/testdata/stat_values.tdir/stat_values.pre b/testdata/stat_values.tdir/stat_values.pre index ad1166a06..7b6eefdfa 100644 --- a/testdata/stat_values.tdir/stat_values.pre +++ b/testdata/stat_values.tdir/stat_values.pre @@ -37,6 +37,7 @@ echo "FWD_EXPIRED_PID=$FWD_EXPIRED_PID" >> .tpkg.var.test # make config file sed -e 's/@PORT\@/'$UNBOUND_PORT'/' -e 's/@TOPORT\@/'$FWD_PORT'/' -e 's/@EXPIREDPORT\@/'$FWD_EXPIRED_PORT'/' -e 's/@CONTROL_PORT\@/'$CONTROL_PORT'/' < stat_values.conf > ub.conf sed -e 's/@PORT\@/'$UNBOUND_PORT'/' -e 's/@TOPORT\@/'$FWD_PORT'/' -e 's/@EXPIREDPORT\@/'$FWD_EXPIRED_PORT'/' -e 's/@CONTROL_PORT\@/'$CONTROL_PORT'/' < stat_values_cachedb.conf > ub_cachedb.conf +sed -e 's/@PORT\@/'$UNBOUND_PORT'/' -e 's/@CONTROL_PORT\@/'$CONTROL_PORT'/' < stat_values_downstream_cookies.conf > ub_downstream_cookies.conf # start unbound in the background $PRE/unbound -d -c ub.conf >unbound.log 2>&1 & UNBOUND_PID=$! diff --git a/testdata/stat_values.tdir/stat_values.test b/testdata/stat_values.tdir/stat_values.test index c9ed66d82..8366ba88b 100644 --- a/testdata/stat_values.tdir/stat_values.test +++ b/testdata/stat_values.tdir/stat_values.test @@ -414,6 +414,97 @@ rrset.cache.count=3 infra.cache.count=2" +# Bring the downstream DNS Cookies configured Unbound up +kill_pid $UNBOUND_PID # kill current Unbound +$PRE/unbound -d -c ub_downstream_cookies.conf >unbound.log 2>&1 & +UNBOUND_PID=$! +echo "UNBOUND_PID=$UNBOUND_PID" >> .tpkg.var.test +wait_unbound_up unbound.log + +echo +echo "[ Get a DNS Cookie. ]" +echo "> dig www.local.zone +tcp +ednsopt=10:0102030405060708" +dig @127.0.0.1 -p $UNBOUND_PORT +tcp +ednsopt=10:0102030405060708 +retry=0 +time=1 www.local.zone. | tee outfile +echo "> check answer" +if grep "192.0.2.1" outfile; then + echo "OK" +else + end 1 +fi +# Save valid cookie +valid_cookie=`grep "COOKIE: " outfile | cut -d ' ' -f 3` +invalid_cookie=`echo $valid_cookie | tr '0' '4'` +check_stats "\ +total.num.queries=1 +total.num.queries_cookie_client=1 +total.num.cachehits=1 +num.query.type.A=1 +num.query.class.IN=1 +num.query.opcode.QUERY=1 +num.query.flags.RD=1 +num.query.flags.AD=1 +num.query.edns.present=1 +num.query.tcp=1 +num.answer.rcode.NOERROR=1" + +echo +echo "[ Present the valid DNS Cookie. ]" +echo "> dig www.local.zone +ednsopt=10:valid_cookie" +dig @127.0.0.1 -p $UNBOUND_PORT +ednsopt=10:$valid_cookie +retry=0 +time=1 www.local.zone. | tee outfile +echo "> check answer" +if grep "192.0.2.1" outfile; then + echo "OK" +else + end 1 +fi +check_stats "\ +total.num.queries=1 +total.num.queries_cookie_valid=1 +total.num.cachehits=1 +num.query.type.A=1 +num.query.class.IN=1 +num.query.opcode.QUERY=1 +num.query.flags.RD=1 +num.query.flags.AD=1 +num.query.edns.present=1 +num.answer.rcode.NOERROR=1" + +echo +echo "[ Present an invalid DNS Cookie. ]" +echo "> dig www.local.zone +ednsopt=10:invalid_cookie" +dig @127.0.0.1 -p $UNBOUND_PORT +ednsopt=10:$invalid_cookie +retry=0 +time=1 www.local.zone. | tee outfile +echo "> check answer" +if grep "192.0.2.1" outfile; then + end 1 +else + echo "OK" +fi +# A lot of stats are missing since BADCOOKIE error response is before +# those stat calculations. +# BADCOOKIE is an extended error code; we record YXRRSET below. +check_stats "\ +total.num.queries=1 +total.num.queries_cookie_invalid=1 +total.num.cachehits=1 +num.answer.rcode.YXRRSET=1" + +echo +echo "[ Present no DNS Cookie. ]" +echo "> dig www.local.zone +ignore" +dig @127.0.0.1 -p $UNBOUND_PORT +ignore +retry=0 +time=1 www.local.zone. | tee outfile +echo "> check answer" +if grep "192.0.2.1" outfile; then + end 1 +else + echo "OK" +fi +# A lot of stats are missing since REFUSED error response because of no DNS +# Cookie is before those stat calculations. +check_stats "\ +total.num.queries=1 +total.num.cachehits=1 +num.answer.rcode.REFUSED=1" + if test x$USE_CACHEDB = "x1"; then # Bring the cachedb configured Unbound up diff --git a/testdata/stat_values.tdir/stat_values_downstream_cookies.conf b/testdata/stat_values.tdir/stat_values_downstream_cookies.conf new file mode 100644 index 000000000..21e78829f --- /dev/null +++ b/testdata/stat_values.tdir/stat_values_downstream_cookies.conf @@ -0,0 +1,32 @@ +server: + verbosity: 5 + module-config: "iterator" + num-threads: 1 + interface: 127.0.0.1 + port: @PORT@ + use-syslog: no + directory: "" + pidfile: "unbound.pid" + chroot: "" + username: "" + extended-statistics: yes + identity: "stat_values" + outbound-msg-retry: 0 + root-key-sentinel: no + trust-anchor-signaling: no + + local-zone: local.zone static + local-data: "www.local.zone A 192.0.2.1" + + answer-cookie: yes + access-control: 127.0.0.1 allow_cookie + +remote-control: + control-enable: yes + control-interface: 127.0.0.1 + # control-interface: ::1 + control-port: @CONTROL_PORT@ + server-key-file: "unbound_server.key" + server-cert-file: "unbound_server.pem" + control-key-file: "unbound_control.key" + control-cert-file: "unbound_control.pem" diff --git a/util/config_file.c b/util/config_file.c index 54bd5f952..454096342 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) { @@ -326,6 +330,7 @@ config_create(void) cfg->dnstap_bidirectional = 1; cfg->dnstap_tls = 1; cfg->disable_dnssec_lame_check = 0; + cfg->ip_ratelimit_cookie = 0; cfg->ip_ratelimit = 0; cfg->ratelimit = 0; cfg->ip_ratelimit_slabs = 4; @@ -369,6 +374,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; @@ -771,6 +780,10 @@ int config_set_option(struct config_file* cfg, const char* opt, else S_POW2("dnscrypt-nonce-cache-slabs:", dnscrypt_nonce_cache_slabs) #endif + else if(strcmp(opt, "ip-ratelimit-cookie:") == 0) { + IS_NUMBER_OR_ZERO; cfg->ip_ratelimit_cookie = atoi(val); + infra_ip_ratelimit_cookie=cfg->ip_ratelimit_cookie; + } else if(strcmp(opt, "ip-ratelimit:") == 0) { IS_NUMBER_OR_ZERO; cfg->ip_ratelimit = atoi(val); infra_ip_ratelimit=cfg->ip_ratelimit; @@ -1240,6 +1253,7 @@ config_get_option(struct config_file* cfg, const char* opt, else O_LST(opt, "python-script", python_script) else O_LST(opt, "dynlib-file", dynlib_file) else O_YNO(opt, "disable-dnssec-lame-check", disable_dnssec_lame_check) + else O_DEC(opt, "ip-ratelimit-cookie", ip_ratelimit_cookie) else O_DEC(opt, "ip-ratelimit", ip_ratelimit) else O_DEC(opt, "ratelimit", ratelimit) else O_MEM(opt, "ip-ratelimit-size", ip_ratelimit_size) @@ -1685,6 +1699,20 @@ 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 5b7569110..452f3c6a7 100644 --- a/util/config_file.h +++ b/util/config_file.h @@ -590,6 +590,9 @@ struct config_file { /** ratelimit for ip addresses. 0 is off, otherwise qps (unless overridden) */ int ip_ratelimit; + /** ratelimit for ip addresses with a valid DNS Cookie. 0 is off, + * otherwise qps (unless overridden) */ + int ip_ratelimit_cookie; /** number of slabs for ip_ratelimit cache */ size_t ip_ratelimit_slabs; /** memory size in bytes for ip_ratelimit cache */ @@ -711,6 +714,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 c1f58af71..3fcdfa62e 100644 --- a/util/configlexer.lex +++ b/util/configlexer.lex @@ -507,6 +507,7 @@ dnstap-log-forwarder-response-messages{COLON} { YDVAR(1, VAR_DNSTAP_LOG_FORWARDER_RESPONSE_MESSAGES) } disable-dnssec-lame-check{COLON} { YDVAR(1, VAR_DISABLE_DNSSEC_LAME_CHECK) } ip-ratelimit{COLON} { YDVAR(1, VAR_IP_RATELIMIT) } +ip-ratelimit-cookie{COLON} { YDVAR(1, VAR_IP_RATELIMIT_COOKIE) } ratelimit{COLON} { YDVAR(1, VAR_RATELIMIT) } ip-ratelimit-slabs{COLON} { YDVAR(1, VAR_IP_RATELIMIT_SLABS) } ratelimit-slabs{COLON} { YDVAR(1, VAR_RATELIMIT_SLABS) } @@ -567,6 +568,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 d07b8788f..d8f25a67e 100644 --- a/util/configparser.y +++ b/util/configparser.y @@ -47,6 +47,7 @@ #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); @@ -183,6 +184,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 VAR_IP_RATELIMIT_COOKIE %token VAR_FORWARD_NO_CACHE VAR_STUB_NO_CACHE VAR_LOG_SERVFAIL VAR_DENY_ANY %token VAR_UNKNOWN_SERVER_TIME_LIMIT VAR_LOG_TAG_QUERYREPLY %token VAR_STREAM_WAIT_SIZE VAR_TLS_CIPHERS VAR_TLS_CIPHERSUITES VAR_TLS_USE_SNI @@ -323,6 +325,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_ip_ratelimit_cookie | server_tls_use_sni | server_edns_client_string | server_edns_client_string_opcode | server_nsid | server_zonemd_permissive_mode | server_max_reuse_tcp_queries | @@ -1160,7 +1163,7 @@ server_http_nodelay: VAR_HTTP_NODELAY STRING_ARG yyerror("expected yes or no."); else cfg_parser->cfg->http_nodelay = (strcmp($2, "yes")==0); free($2); - } + }; server_http_notls_downstream: VAR_HTTP_NOTLS_DOWNSTREAM STRING_ARG { OUTYY(("P(server_http_notls_downstream:%s)\n", $2)); @@ -2207,6 +2210,7 @@ server_permit_small_holddown: VAR_PERMIT_SMALL_HOLDDOWN STRING_ARG (strcmp($2, "yes")==0); free($2); } + ; server_key_cache_size: VAR_KEY_CACHE_SIZE STRING_ARG { OUTYY(("P(server_key_cache_size:%s)\n", $2)); @@ -2564,6 +2568,15 @@ server_ip_ratelimit: VAR_IP_RATELIMIT STRING_ARG free($2); } ; +server_ip_ratelimit_cookie: VAR_IP_RATELIMIT_COOKIE STRING_ARG + { + OUTYY(("P(server_ip_ratelimit_cookie:%s)\n", $2)); + if(atoi($2) == 0 && strcmp($2, "0") != 0) + yyerror("number expected"); + else cfg_parser->cfg->ip_ratelimit_cookie = atoi($2); + free($2); + } + ; server_ratelimit: VAR_RATELIMIT STRING_ARG { OUTYY(("P(server_ratelimit:%s)\n", $2)); @@ -3517,6 +3530,7 @@ py_script: VAR_PYTHON_SCRIPT STRING_ARG if(!cfg_strlist_append_ex(&cfg_parser->cfg->python_script, $2)) yyerror("out of memory"); } + ; dynlibstart: VAR_DYNLIB { OUTYY(("\nP(dynlib:)\n")); @@ -3533,6 +3547,7 @@ dl_file: VAR_DYNLIB_FILE STRING_ARG if(!cfg_strlist_append_ex(&cfg_parser->cfg->dynlib_file, $2)) yyerror("out of memory"); } + ; server_disable_dnssec_lame_check: VAR_DISABLE_DNSSEC_LAME_CHECK STRING_ARG { OUTYY(("P(disable_dnssec_lame_check:%s)\n", $2)); @@ -3593,7 +3608,6 @@ dnsc_dnscrypt_enable: VAR_DNSCRYPT_ENABLE STRING_ARG free($2); } ; - dnsc_dnscrypt_port: VAR_DNSCRYPT_PORT STRING_ARG { OUTYY(("P(dnsc_dnscrypt_port:%s)\n", $2)); @@ -3801,6 +3815,31 @@ 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")); @@ -3870,10 +3909,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/msgencode.c b/util/data/msgencode.c index 81d3fe7ef..a170eb7b8 100644 --- a/util/data/msgencode.c +++ b/util/data/msgencode.c @@ -1062,15 +1062,17 @@ qinfo_query_encode(sldns_buffer* pkt, struct query_info* qinfo) sldns_buffer_flip(pkt); } -void -error_encode(sldns_buffer* buf, int r, struct query_info* qinfo, - uint16_t qid, uint16_t qflags, struct edns_data* edns) +void +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; @@ -1097,7 +1099,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) { @@ -1111,3 +1113,11 @@ 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 d92bd3b03..6aff06099 100644 --- a/util/data/msgencode.h +++ b/util/data/msgencode.h @@ -137,11 +137,11 @@ uint16_t calc_ede_option_size(struct edns_data* edns, uint16_t* txt_size); */ 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. @@ -151,4 +151,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/util/data/msgparse.c b/util/data/msgparse.c index 5bb69d6ed..b5414c6d0 100644 --- a/util/data/msgparse.c +++ b/util/data/msgparse.c @@ -45,6 +45,8 @@ #include "util/netevent.h" #include "util/storage/lookup3.h" #include "util/regional.h" +#include "util/rfc_1982.h" +#include "util/edns.h" #include "sldns/rrdef.h" #include "sldns/sbuffer.h" #include "sldns/parseutil.h" @@ -940,22 +942,11 @@ parse_packet(sldns_buffer* pkt, struct msg_parse* msg, struct regional* region) return 0; } -static int -edns_opt_list_append_keepalive(struct edns_option** list, int msec, - struct regional* region) -{ - uint8_t data[2]; /* For keepalive value */ - data[0] = (uint8_t)((msec >> 8) & 0xff); - data[1] = (uint8_t)(msec & 0xff); - return edns_opt_list_append(list, LDNS_EDNS_KEEPALIVE, sizeof(data), - data, region); -} - /** 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 +970,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]; + enum edns_cookie_val_status cookie_val_status; + int cookie_is_v4 = 1; + rdata_ptr += 4; rdata_len -= 4; if(opt_len > rdata_len) @@ -1041,6 +1036,76 @@ 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 || !repinfo) + 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. + */ + if(opt_len >= 16) { + memmove(server_cookie, rdata_ptr, 16); + } else { + memset(server_cookie, 0, 16); + memmove(server_cookie, rdata_ptr, opt_len); + } + + /* Copy client ip for validation and creation + * purposes. It will be overwritten if (re)creation + * is needed. + */ + if(repinfo->remote_addr.ss_family == AF_INET) { + memcpy(server_cookie + 16, + &((struct sockaddr_in*)&repinfo->remote_addr)->sin_addr, 4); + } else { + cookie_is_v4 = 0; + memcpy(server_cookie + 16, + &((struct sockaddr_in6*)&repinfo->remote_addr)->sin6_addr, 16); + } + + cookie_val_status = edns_cookie_server_validate( + rdata_ptr, opt_len, cfg->cookie_secret, + cfg->cookie_secret_len, cookie_is_v4, + server_cookie, now); + switch(cookie_val_status) { + case COOKIE_STATUS_VALID: + case COOKIE_STATUS_VALID_RENEW: + edns->cookie_valid = 1; + /* Reuse cookie */ + if(!edns_opt_list_append( + &edns->opt_list_out, LDNS_EDNS_COOKIE, + opt_len, rdata_ptr, region)) { + log_err("out of memory"); + return LDNS_RCODE_SERVFAIL; + } + /* Cookie to be reused added to outgoing + * options. Done! + */ + break; + case COOKIE_STATUS_CLIENT_ONLY: + edns->cookie_client = 1; + /* fallthrough */ + case COOKIE_STATUS_FUTURE: + case COOKIE_STATUS_EXPIRED: + case COOKIE_STATUS_INVALID: + default: + edns_cookie_server_write(server_cookie, + cfg->cookie_secret, cookie_is_v4, now); + 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; + } + break; default: break; } @@ -1115,6 +1180,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 +1237,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 +1274,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 +1284,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..b7dc235d6 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,15 @@ 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; + /** if the cookie holds only the client part */ + unsigned int cookie_client : 1; +}; /** * EDNS option @@ -310,12 +317,15 @@ int skip_pkt_rrs(struct sldns_buffer* pkt, int num); * initialised. * @param cfg: the configuration (with nsid value etc.) * @param c: commpoint to determine transport (if needed) + * @param repinfo: commreply to determine the client address + * @param now: current time * @param region: region to alloc results in (edns option contents) * @return: 0 on success, or an RCODE on error. * 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/data/msgreply.c b/util/data/msgreply.c index 792387444..920a0a939 100644 --- a/util/data/msgreply.c +++ b/util/data/msgreply.c @@ -1049,6 +1049,16 @@ int edns_opt_list_append_ede(struct edns_option** list, struct regional* region, return 1; } +int edns_opt_list_append_keepalive(struct edns_option** list, int msec, + struct regional* region) +{ + uint8_t data[2]; /* For keepalive value */ + data[0] = (uint8_t)((msec >> 8) & 0xff); + data[1] = (uint8_t)(msec & 0xff); + return edns_opt_list_append(list, LDNS_EDNS_KEEPALIVE, sizeof(data), + data, region); +} + int edns_opt_list_append(struct edns_option** list, uint16_t code, size_t len, uint8_t* data, struct regional* region) { diff --git a/util/data/msgreply.h b/util/data/msgreply.h index 1339fd9cc..a9af3d7e6 100644 --- a/util/data/msgreply.h +++ b/util/data/msgreply.h @@ -577,6 +577,16 @@ int edns_opt_list_append(struct edns_option** list, uint16_t code, size_t len, int edns_opt_list_append_ede(struct edns_option** list, struct regional* region, sldns_ede_code code, const char *txt); +/** + * Append edns keep alive option to edns options list + * @param list: the edns option list to append the edns option to. + * @param msec: the duration in msecs for the keep alive. + * @param region: region to allocate the new edns option. + * @return false on failure. + */ +int edns_opt_list_append_keepalive(struct edns_option** list, int msec, + struct regional* region); + /** * Remove any option found on the edns option list that matches the code. * @param list: the list of edns options. diff --git a/util/edns.c b/util/edns.c index f55dcb97e..2b4047f0b 100644 --- a/util/edns.c +++ b/util/edns.c @@ -45,8 +45,11 @@ #include "util/netevent.h" #include "util/net_help.h" #include "util/regional.h" +#include "util/rfc_1982.h" +#include "util/siphash.h" #include "util/data/msgparse.h" #include "util/data/msgreply.h" +#include "sldns/sbuffer.h" struct edns_strings* edns_strings_create(void) { @@ -128,3 +131,59 @@ edns_string_addr_lookup(rbtree_type* tree, struct sockaddr_storage* addr, return (struct edns_string_addr*)addr_tree_lookup(tree, addr, addrlen); } +uint8_t* +edns_cookie_server_hash(const uint8_t* in, const uint8_t* secret, int v4, + uint8_t* hash) +{ + v4?siphash(in, 20, secret, hash, 8):siphash(in, 32, secret, hash, 8); + return hash; +} + +void +edns_cookie_server_write(uint8_t* buf, const uint8_t* secret, int v4, + uint32_t timestamp) +{ + uint8_t hash[8]; + buf[ 8] = 1; /* Version */ + buf[ 9] = 0; /* Reserved */ + buf[10] = 0; /* Reserved */ + buf[11] = 0; /* Reserved */ + sldns_write_uint32(buf + 12, timestamp); + (void)edns_cookie_server_hash(buf, secret, v4, hash); + memcpy(buf + 16, hash, 8); +} + +enum edns_cookie_val_status +edns_cookie_server_validate(const uint8_t* cookie, size_t cookie_len, + const uint8_t* secret, size_t secret_len, int v4, + const uint8_t* hash_input, uint32_t now) +{ + uint8_t hash[8]; + uint32_t timestamp; + uint32_t subt_1982 = 0; /* Initialize for the compiler; unused value */ + int comp_1982; + if(cookie_len != 24) + /* RFC9018 cookies are 24 bytes long */ + return COOKIE_STATUS_CLIENT_ONLY; + if(secret_len != 16 || /* RFC9018 cookies have 16 byte secrets */ + cookie[8] != 1) /* RFC9018 cookies are cookie version 1 */ + return COOKIE_STATUS_INVALID; + timestamp = sldns_read_uint32(cookie + 12); + if((comp_1982 = compare_1982(now, timestamp)) > 0 + && (subt_1982 = subtract_1982(timestamp, now)) > 3600) + /* Cookie is older than 1 hour (see RFC9018 Section 4.3.) */ + return COOKIE_STATUS_EXPIRED; + if(comp_1982 <= 0 && subtract_1982(now, timestamp) > 300) + /* Cookie time is more than 5 minutes in the future. + * (see RFC9018 Section 4.3.) */ + return COOKIE_STATUS_FUTURE; + if(memcmp(edns_cookie_server_hash(hash_input, secret, v4, hash), + cookie + 16, 8) != 0) + /* Hashes do not match */ + return COOKIE_STATUS_INVALID; + if(comp_1982 > 0 && subt_1982 > 1800) + /* Valid cookie but older than 30 minutes, so create a new one + * anyway */ + return COOKIE_STATUS_VALID_RENEW; + return COOKIE_STATUS_VALID; +} diff --git a/util/edns.h b/util/edns.h index d9ded0b84..5da0ecb29 100644 --- a/util/edns.h +++ b/util/edns.h @@ -75,6 +75,15 @@ struct edns_string_addr { size_t string_len; }; +enum edns_cookie_val_status { + COOKIE_STATUS_CLIENT_ONLY = -3, + COOKIE_STATUS_FUTURE = -2, + COOKIE_STATUS_EXPIRED = -1, + COOKIE_STATUS_INVALID = 0, + COOKIE_STATUS_VALID = 1, + COOKIE_STATUS_VALID_RENEW = 2, +}; + /** * Create structure to hold EDNS strings * @return: newly created edns_strings, NULL on alloc failure. @@ -106,4 +115,54 @@ struct edns_string_addr* edns_string_addr_lookup(rbtree_type* tree, struct sockaddr_storage* addr, socklen_t addrlen); +/** + * Compute the interoperable DNS cookie (RFC9018) hash. + * @param in: buffer input for the hash generation. It needs to be: + * Client Cookie | Version | Reserved | Timestamp | Client-IP + * @param secret: the server secret; implicit length of 16 octets. + * @param v4: if the client IP is v4 or v6. + * @param hash: buffer to write the hash to. + * return a pointer to the hash. + */ +uint8_t* edns_cookie_server_hash(const uint8_t* in, const uint8_t* secret, + int v4, uint8_t* hash); + +/** + * Write an interoperable DNS server cookie (RFC9018). + * @param buf: buffer to write to. It should have a size of at least 32 octets + * as it doubles as the output buffer and the hash input buffer. + * The first 8 octets are expected to be the Client Cookie and will be + * left untouched. + * The next 8 octets will be written with Version | Reserved | Timestamp. + * The next 4 or 16 octets are expected to be the IPv4 or the IPv6 address + * based on the v4 flag. + * Thus the first 20 or 32 octets, based on the v4 flag, will be used as + * the hash input. + * The server hash (8 octets) will be written after the first 16 octets; + * overwriting the address information. + * The caller expects a complete, 24 octet long cookie in the buffer. + * @param secret: the server secret; implicit length of 16 octets. + * @param v4: if the client IP is v4 or v6. + * @param timestamp: the timestamp to use. + */ +void edns_cookie_server_write(uint8_t* buf, const uint8_t* secret, int v4, + uint32_t timestamp); + +/** + * Validate an interoperable DNS cookie (RFC9018). + * @param cookie: pointer to the cookie data. + * @param cookie_len: the length of the cookie data. + * @param secret: pointer to the server secret. + * @param secret_len: the length of the secret. + * @param v4: if the client IP is v4 or v6. + * @param hash_input: pointer to the hash input for validation. It needs to be: + * Client Cookie | Version | Reserved | Timestamp | Client-IP + * @param now: the current time. + * return edns_cookie_val_status with the cookie validation status i.e., + * <=0 for invalid, else valid. + */ +enum edns_cookie_val_status edns_cookie_server_validate(const uint8_t* cookie, + size_t cookie_len, const uint8_t* secret, size_t secret_len, int v4, + const uint8_t* hash_input, uint32_t now); + #endif diff --git a/util/rfc_1982.c b/util/rfc_1982.c new file mode 100644 index 000000000..c28deded6 --- /dev/null +++ b/util/rfc_1982.c @@ -0,0 +1,74 @@ +/* + * util/rfc_1982.c - RFC 1982 Serial Number Arithmetic + * + * Copyright (c) 2023, NLnet Labs. All rights reserved. + * + * This software is open source. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of the NLNET LABS nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * \file + * + * This file contains functions for RFC 1982 serial number arithmetic. + */ +#include "config.h" + +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; + } +} + +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; +} diff --git a/util/rfc_1982.h b/util/rfc_1982.h new file mode 100644 index 000000000..bae383d0e --- /dev/null +++ b/util/rfc_1982.h @@ -0,0 +1,63 @@ +/* + * util/rfc_1982.h - RFC 1982 Serial Number Arithmetic + * + * Copyright (c) 2023, NLnet Labs. All rights reserved. + * + * This software is open source. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of the NLNET LABS nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * \file + * + * This file contains functions for RFC 1982 serial number arithmetic. + */ +#ifndef RFC_1982_H +#define RFC_1982_H + +/** + * RFC 1982 comparison, uses unsigned integers, and tries to avoid + * compiler optimization (eg. by avoiding a-b<0 comparisons). + * @param a: value to compare. + * @param b: value to compare. + * @return 0 if equal, 1 if a > b, else -1. + */ +int compare_1982(uint32_t a, uint32_t b); + +/** + * RFC 1982 subtraction, uses unsigned integers, and tries to avoid + * compiler optimization (eg. by avoiding a-b<0 comparisons). + * @param a: value to subtract from. + * @param b: value to subtract. + * @return the difference between them if we know that b is larger than a, + * that is the distance between them in serial number arithmetic. + */ +uint32_t subtract_1982(uint32_t a, uint32_t b); + +#endif /* RFC_1982_H */ diff --git a/util/siphash.c b/util/siphash.c new file mode 100644 index 000000000..0e1b597d0 --- /dev/null +++ b/util/siphash.c @@ -0,0 +1,187 @@ +/* + 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 + . + */ +/** + * Edited slightly for integration in Unbound. Edits are noted with 'EDIT'. + */ +/** EDIT + * \#include + * \#include + * \#include + * \#include + * Replaced the above includes with Unbound's config.h + */ +#include "config.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) { + + 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; + /** EDIT + * The following assert moved here from the top for C90 compliance. + */ + assert((outlen == 8) || (outlen == 16)); + 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; + /** EDIT annotate case statement fallthrough for gcc */ + /* fallthrough */ + case 6: + b |= ((uint64_t)in[5]) << 40; + /** EDIT annotate case statement fallthrough for gcc */ + /* fallthrough */ + case 5: + b |= ((uint64_t)in[4]) << 32; + /** EDIT annotate case statement fallthrough for gcc */ + /* fallthrough */ + case 4: + b |= ((uint64_t)in[3]) << 24; + /** EDIT annotate case statement fallthrough for gcc */ + /* fallthrough */ + case 3: + b |= ((uint64_t)in[2]) << 16; + /** EDIT annotate case statement fallthrough for gcc */ + /* fallthrough */ + case 2: + b |= ((uint64_t)in[1]) << 8; + /** EDIT annotate case statement fallthrough for gcc */ + /* fallthrough */ + 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/util/siphash.h b/util/siphash.h new file mode 100644 index 000000000..63da2175c --- /dev/null +++ b/util/siphash.h @@ -0,0 +1,43 @@ +/* + * util/siphash.h - header for SipHash reference C implementation. + * + * Copyright (c) 2023, NLnet Labs. All rights reserved. + * + * This software is open source. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of the NLNET LABS nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/** + * \file + * Contains the SipHash reference C implementation. + */ +#ifndef UTIL_SIPHASH_H +#define UTIL_SIPHASH_H +int siphash(const uint8_t *in, const size_t inlen, const uint8_t *k, + uint8_t *out, const size_t outlen); +#endif /* UTIL_SIPHASH_H */ 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/validator/val_sigcrypt.c b/validator/val_sigcrypt.c index bd4891e3b..37730f179 100644 --- a/validator/val_sigcrypt.c +++ b/validator/val_sigcrypt.c @@ -48,6 +48,7 @@ #include "util/data/msgparse.h" #include "util/data/dname.h" #include "util/rbtree.h" +#include "util/rfc_1982.h" #include "util/module.h" #include "util/net_help.h" #include "util/regional.h" @@ -1378,44 +1379,6 @@ sigdate_error(const char* str, int32_t expi, int32_t incep, int32_t now) (unsigned)incep, (unsigned)now); } -/** 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; -} - /** check rrsig dates */ static int check_dates(struct val_env* ve, uint32_t unow, uint8_t* expi_p,