From ab177cb153b2b29c32df03fb63d5c05619f668be Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Fri, 13 Dec 2024 23:00:21 +0000 Subject: [PATCH] Improve handling of non-QUERY DNS requests. We can't answer and shouldn't forward non-QUERY DNS requests. This patch fixes handling such requests from TCP connections; before the connection would be closed without reply. It also changes the RCODE in the answer from REFUSED to NOTIMP and provides clearer logging. --- src/cache.c | 4 +- src/domain-match.c | 14 +- src/forward.c | 415 +++++++++++++++++++++++---------------------- src/rfc1035.c | 8 +- 4 files changed, 234 insertions(+), 207 deletions(-) diff --git a/src/cache.c b/src/cache.c index 7acefc2..e41af4b 100644 --- a/src/cache.c +++ b/src/cache.c @@ -2256,12 +2256,12 @@ void log_query(unsigned int flags, char *name, union all_addr *addr, char *arg, } else if (flags & F_AUTH) source = "auth"; - else if (flags & F_DNSSEC) + else if (flags & F_DNSSEC) { source = arg; verb = "to"; } - else if (flags & F_SERVER) + else if (flags & F_SERVER) { source = "forwarded"; verb = "to"; diff --git a/src/domain-match.c b/src/domain-match.c index a647fe8..642db2a 100644 --- a/src/domain-match.c +++ b/src/domain-match.c @@ -405,11 +405,19 @@ size_t make_local_answer(int flags, int gotname, size_t size, struct dns_header int start; union all_addr addr; + setup_reply(header, flags, ede); + if (flags & (F_NXDOMAIN | F_NOERR)) log_query(flags | gotname | F_NEG | F_CONFIG | F_FORWARD, name, NULL, NULL, 0); - - setup_reply(header, flags, ede); - + + if (flags & F_RCODE) + { + union all_addr a; + a.log.rcode = RCODE(header); + a.log.ede = ede; + log_query(F_UPSTREAM | F_RCODE, "opcode", &a, NULL, 0); + } + if (!(p = skip_questions(header, size))) return 0; diff --git a/src/forward.c b/src/forward.c index 20c10b0..5b31dc9 100644 --- a/src/forward.c +++ b/src/forward.c @@ -278,10 +278,15 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr, /* new query */ if (!forward) { - /* If the query is malformed, we can't forward it because - we can't recognise the answer. */ - if (!gotname) + if (OPCODE(header) != QUERY) { + flags = F_RCODE; + goto reply; + } + else if (!gotname) + { + /* If the query is malformed, we can't forward it because + we can't recognise the answer. */ flags = 0; ede = EDE_INVALID_DATA; goto reply; @@ -1685,8 +1690,10 @@ void receive_query(struct listener *listen, time_t now) if (option_bool(OPT_CMARK_ALST_EN)) have_mark = get_incoming_mark(&source_addr, &dst_addr, /* istcp: */ 0, &mark); #endif - - if (extract_request(header, (size_t)n, daemon->namebuff, &type)) + + if (OPCODE(header) != QUERY) + log_query_mysockaddr(F_QUERY | F_FORWARD, "opcode", &source_addr, "non-query", 0); + else if (extract_request(header, (size_t)n, daemon->namebuff, &type)) { #ifdef HAVE_AUTH struct auth_zone *zone; @@ -2292,6 +2299,8 @@ unsigned char *tcp_request(int confd, time_t now, { int ede = EDE_UNSET; + stale = 0; + if (!do_stale) { if (query_count >= TCP_MAX_QUERIES) @@ -2322,8 +2331,15 @@ unsigned char *tcp_request(int confd, time_t now, if ((checking_disabled = header->hb4 & HB4_CD)) no_cache_dnssec = 1; - if ((gotname = extract_request(header, (unsigned int)size, daemon->namebuff, &qtype))) + if (OPCODE(header) != QUERY) { + log_query_mysockaddr(F_QUERY | F_FORWARD, "opcode", &peer_addr, "non-query", 0); + gotname = 0; + m = 0; + flags = F_RCODE; + } + else if ((gotname = extract_request(header, (unsigned int)size, daemon->namebuff, &qtype))) + { #ifdef HAVE_AUTH struct auth_zone *zone; #endif @@ -2349,223 +2365,224 @@ unsigned char *tcp_request(int confd, time_t now, } #endif } - } - - norebind = domain_no_rebind(daemon->namebuff); - - if (local_addr->sa.sa_family == AF_INET) - dst_addr_4 = local_addr->in.sin_addr; - else - dst_addr_4.s_addr = 0; - - do_bit = 0; - - if (find_pseudoheader(header, (size_t)size, NULL, &pheader, NULL, NULL)) - { - unsigned short flags; - - have_pseudoheader = 1; - pheader += 4; /* udp_size, ext_rcode */ - GETSHORT(flags, pheader); - - if (flags & 0x8000) - do_bit = 1; /* do bit */ - } - -#ifdef HAVE_CONNTRACK -#ifdef HAVE_AUTH - if (!auth_dns || local_auth) -#endif - if (option_bool(OPT_CMARK_ALST_EN) && have_mark && ((u32)mark & daemon->allowlist_mask)) - allowed = is_query_allowed_for_mark((u32)mark, is_single_query ? daemon->namebuff : NULL); -#endif - - if (0); -#ifdef HAVE_CONNTRACK - else if (!allowed) - { - u16 swap = htons(EDE_BLOCKED); - - m = answer_disallowed(header, size, (u32)mark, is_single_query ? daemon->namebuff : NULL); - - if (have_pseudoheader && m != 0) - m = add_pseudoheader(header, m, ((unsigned char *) header) + 65536, - EDNS0_OPTION_EDE, (unsigned char *)&swap, 2, do_bit, 0); - } -#endif -#ifdef HAVE_AUTH - else if (auth_dns) - { - m = answer_auth(header, ((char *) header) + 65536, (size_t)size, now, &peer_addr, local_auth); - if (m >= 1 && have_pseudoheader) - m = add_pseudoheader(header, m, ((unsigned char *) header) + 65536, - 0, NULL, 0, do_bit, 0); - } -#endif - else - { - int ad_reqd = do_bit; - /* RFC 6840 5.7 */ - if (header->hb4 & HB4_AD) - ad_reqd = 1; - - if (do_stale) - m = 0; - else - { - if (saved_question) - blockdata_free(saved_question); - saved_question = blockdata_alloc((char *)header, (size_t)size); - saved_size = size; - - /* m > 0 if answered from cache */ - m = answer_request(header, ((char *) header) + 65536, (size_t)size, - dst_addr_4, netmask, now, ad_reqd, do_bit, &stale, &filtered); - - if (m >= 1 && have_pseudoheader) - m = add_pseudoheader(header, m, ((unsigned char *) header) + 65536, - 0, NULL, 0, do_bit, 0); - } - /* Do this by steam now we're not in the select() loop */ - check_log_writer(1); + norebind = domain_no_rebind(daemon->namebuff); - if (m == 0 && saved_question) - { - struct server *master; - int start; - - blockdata_retrieve(saved_question, (size_t)saved_size, header); - size = saved_size; + if (local_addr->sa.sa_family == AF_INET) + dst_addr_4 = local_addr->in.sin_addr; + else + dst_addr_4.s_addr = 0; + + do_bit = 0; + + if (find_pseudoheader(header, (size_t)size, NULL, &pheader, NULL, NULL)) + { + unsigned short flags; - if (lookup_domain(daemon->namebuff, gotname, &first, &last)) - flags = is_local_answer(now, first, daemon->namebuff); + have_pseudoheader = 1; + pheader += 4; /* udp_size, ext_rcode */ + GETSHORT(flags, pheader); + + if (flags & 0x8000) + do_bit = 1; /* do bit */ + } + +#ifdef HAVE_CONNTRACK +#ifdef HAVE_AUTH + if (!auth_dns || local_auth) +#endif + if (option_bool(OPT_CMARK_ALST_EN) && have_mark && ((u32)mark & daemon->allowlist_mask)) + allowed = is_query_allowed_for_mark((u32)mark, is_single_query ? daemon->namebuff : NULL); +#endif + + if (0); +#ifdef HAVE_CONNTRACK + else if (!allowed) + { + u16 swap = htons(EDE_BLOCKED); + + m = answer_disallowed(header, size, (u32)mark, is_single_query ? daemon->namebuff : NULL); + + if (have_pseudoheader && m != 0) + m = add_pseudoheader(header, m, ((unsigned char *) header) + 65536, + EDNS0_OPTION_EDE, (unsigned char *)&swap, 2, do_bit, 0); + } +#endif +#ifdef HAVE_AUTH + else if (auth_dns) + { + m = answer_auth(header, ((char *) header) + 65536, (size_t)size, now, &peer_addr, local_auth); + if (m >= 1 && have_pseudoheader) + m = add_pseudoheader(header, m, ((unsigned char *) header) + 65536, + 0, NULL, 0, do_bit, 0); + } +#endif + else + { + int ad_reqd = do_bit; + /* RFC 6840 5.7 */ + if (header->hb4 & HB4_AD) + ad_reqd = 1; + + if (do_stale) + m = 0; else { - /* No configured servers */ - ede = EDE_NOT_READY; - flags = 0; + if (saved_question) + blockdata_free(saved_question); + + saved_question = blockdata_alloc((char *)header, (size_t)size); + saved_size = size; + + /* m > 0 if answered from cache */ + m = answer_request(header, ((char *) header) + 65536, (size_t)size, + dst_addr_4, netmask, now, ad_reqd, do_bit, &stale, &filtered); + + if (m >= 1 && have_pseudoheader) + m = add_pseudoheader(header, m, ((unsigned char *) header) + 65536, + 0, NULL, 0, do_bit, 0); } + + /* Do this by steam now we're not in the select() loop */ + check_log_writer(1); - /* don't forward A or AAAA queries for simple names, except the empty name */ - if (!flags && - option_bool(OPT_NODOTS_LOCAL) && - (gotname & (F_IPV4 | F_IPV6)) && - !strchr(daemon->namebuff, '.') && - strlen(daemon->namebuff) != 0) - flags = check_for_local_domain(daemon->namebuff, now) ? F_NOERR : F_NXDOMAIN; - - if (!flags && ede != EDE_NOT_READY) + if (m == 0 && saved_question) { - master = daemon->serverarray[first]; + struct server *master; + int start; - if (option_bool(OPT_ORDER) || master->last_server == -1) - start = first; + blockdata_retrieve(saved_question, (size_t)saved_size, header); + size = saved_size; + + if (lookup_domain(daemon->namebuff, gotname, &first, &last)) + flags = is_local_answer(now, first, daemon->namebuff); else - start = master->last_server; - - size = add_edns0_config(header, size, ((unsigned char *) header) + 65536, &peer_addr, now, &cacheable); - -#ifdef HAVE_DNSSEC - if (option_bool(OPT_DNSSEC_VALID) && (master->flags & SERV_DO_DNSSEC)) { - size = add_do_bit(header, size, ((unsigned char *) header) + 65536); - - /* For debugging, set Checking Disabled, otherwise, have the upstream check too, - this allows it to select auth servers when one is returning bad data. */ - if (option_bool(OPT_DNSSEC_DEBUG)) - header->hb4 |= HB4_CD; - } -#endif - - /* Check if we added a pheader on forwarding - may need to - strip it from the reply. */ - if (!have_pseudoheader && find_pseudoheader(header, size, NULL, NULL, NULL, NULL)) - added_pheader = 1; - - /* Loop round available servers until we succeed in connecting to one. */ - if ((m = tcp_talk(first, last, start, packet, size, have_mark, mark, &serv)) == 0) - { - ede = EDE_NETERR; - break; + /* No configured servers */ + ede = EDE_NOT_READY; + flags = 0; } - /* get query name again for logging - may have been overwritten */ - if (!(gotname = extract_request(header, (unsigned int)size, daemon->namebuff, &qtype))) - strcpy(daemon->namebuff, "query"); - log_query_mysockaddr(F_SERVER | F_FORWARD, daemon->namebuff, &serv->addr, NULL, 0); + /* don't forward A or AAAA queries for simple names, except the empty name */ + if (!flags && + option_bool(OPT_NODOTS_LOCAL) && + (gotname & (F_IPV4 | F_IPV6)) && + !strchr(daemon->namebuff, '.') && + strlen(daemon->namebuff) != 0) + flags = check_for_local_domain(daemon->namebuff, now) ? F_NOERR : F_NXDOMAIN; -#ifdef HAVE_DNSSEC - if (option_bool(OPT_DNSSEC_VALID)) + if (!flags && ede != EDE_NOT_READY) { - /* Clear this in case we don't call tcp_key_recurse() below */ - memset(daemon->rr_status, 0, sizeof(*daemon->rr_status) * daemon->rr_status_sz); + master = daemon->serverarray[first]; - if (!checking_disabled && (master->flags & SERV_DO_DNSSEC)) + if (option_bool(OPT_ORDER) || master->last_server == -1) + start = first; + else + start = master->last_server; + + size = add_edns0_config(header, size, ((unsigned char *) header) + 65536, &peer_addr, now, &cacheable); + +#ifdef HAVE_DNSSEC + if (option_bool(OPT_DNSSEC_VALID) && (master->flags & SERV_DO_DNSSEC)) { - int keycount = daemon->limit[LIMIT_WORK]; /* Limit to number of DNSSEC questions, to catch loops and avoid filling cache. */ - int validatecount = daemon->limit[LIMIT_CRYPTO]; - int status = tcp_key_recurse(now, STAT_OK, header, m, 0, daemon->namebuff, daemon->keyname, - serv, have_mark, mark, &keycount, &validatecount); - char *result, *domain = "result"; + size = add_do_bit(header, size, ((unsigned char *) header) + 65536); - union all_addr a; - a.log.ede = ede = errflags_to_ede(status); - - if (STAT_ISEQUAL(status, STAT_ABANDONED)) - { - result = "ABANDONED"; - status = STAT_BOGUS; - } - else - result = (STAT_ISEQUAL(status, STAT_SECURE) ? "SECURE" : (STAT_ISEQUAL(status, STAT_INSECURE) ? "INSECURE" : "BOGUS")); - - if (STAT_ISEQUAL(status, STAT_SECURE)) - cache_secure = 1; - else if (STAT_ISEQUAL(status, STAT_BOGUS)) - { - no_cache_dnssec = 1; - bogusanswer = 1; - - if (extract_request(header, m, daemon->namebuff, NULL)) - domain = daemon->namebuff; - } - - log_query(F_SECSTAT, domain, &a, result, 0); - - if ((daemon->limit[LIMIT_CRYPTO] - validatecount) > (int)daemon->metrics[METRIC_CRYPTO_HWM]) - daemon->metrics[METRIC_CRYPTO_HWM] = daemon->limit[LIMIT_CRYPTO] - validatecount; - - if ((daemon->limit[LIMIT_WORK] - keycount) > (int)daemon->metrics[METRIC_WORK_HWM]) - daemon->metrics[METRIC_WORK_HWM] = daemon->limit[LIMIT_WORK] - keycount; - - /* include DNSSEC queries in the limit for a connection. */ - query_count += daemon->limit[LIMIT_WORK] - keycount; + /* For debugging, set Checking Disabled, otherwise, have the upstream check too, + this allows it to select auth servers when one is returning bad data. */ + if (option_bool(OPT_DNSSEC_DEBUG)) + header->hb4 |= HB4_CD; } - } #endif - - /* restore CD bit to the value in the query */ - if (checking_disabled) - header->hb4 |= HB4_CD; - else - header->hb4 &= ~HB4_CD; - - /* Never cache answers which are contingent on the source or MAC address EDSN0 option, - since the cache is ignorant of such things. */ - if (!cacheable) - no_cache_dnssec = 1; - - m = process_reply(header, now, serv, (unsigned int)m, - option_bool(OPT_NO_REBIND) && !norebind, no_cache_dnssec, cache_secure, bogusanswer, - ad_reqd, do_bit, added_pheader, &peer_addr, ((unsigned char *)header) + 65536, ede); + + /* Check if we added a pheader on forwarding - may need to + strip it from the reply. */ + if (!have_pseudoheader && find_pseudoheader(header, size, NULL, NULL, NULL, NULL)) + added_pheader = 1; + + /* Loop round available servers until we succeed in connecting to one. */ + if ((m = tcp_talk(first, last, start, packet, size, have_mark, mark, &serv)) == 0) + { + ede = EDE_NETERR; + break; + } + + /* get query name again for logging - may have been overwritten */ + if (!(gotname = extract_request(header, (unsigned int)size, daemon->namebuff, &qtype))) + strcpy(daemon->namebuff, "query"); + log_query_mysockaddr(F_SERVER | F_FORWARD, daemon->namebuff, &serv->addr, NULL, 0); + +#ifdef HAVE_DNSSEC + if (option_bool(OPT_DNSSEC_VALID)) + { + /* Clear this in case we don't call tcp_key_recurse() below */ + memset(daemon->rr_status, 0, sizeof(*daemon->rr_status) * daemon->rr_status_sz); + + if (!checking_disabled && (master->flags & SERV_DO_DNSSEC)) + { + int keycount = daemon->limit[LIMIT_WORK]; /* Limit to number of DNSSEC questions, to catch loops and avoid filling cache. */ + int validatecount = daemon->limit[LIMIT_CRYPTO]; + int status = tcp_key_recurse(now, STAT_OK, header, m, 0, daemon->namebuff, daemon->keyname, + serv, have_mark, mark, &keycount, &validatecount); + char *result, *domain = "result"; + + union all_addr a; + a.log.ede = ede = errflags_to_ede(status); + + if (STAT_ISEQUAL(status, STAT_ABANDONED)) + { + result = "ABANDONED"; + status = STAT_BOGUS; + } + else + result = (STAT_ISEQUAL(status, STAT_SECURE) ? "SECURE" : (STAT_ISEQUAL(status, STAT_INSECURE) ? "INSECURE" : "BOGUS")); + + if (STAT_ISEQUAL(status, STAT_SECURE)) + cache_secure = 1; + else if (STAT_ISEQUAL(status, STAT_BOGUS)) + { + no_cache_dnssec = 1; + bogusanswer = 1; + + if (extract_request(header, m, daemon->namebuff, NULL)) + domain = daemon->namebuff; + } + + log_query(F_SECSTAT, domain, &a, result, 0); + + if ((daemon->limit[LIMIT_CRYPTO] - validatecount) > (int)daemon->metrics[METRIC_CRYPTO_HWM]) + daemon->metrics[METRIC_CRYPTO_HWM] = daemon->limit[LIMIT_CRYPTO] - validatecount; + + if ((daemon->limit[LIMIT_WORK] - keycount) > (int)daemon->metrics[METRIC_WORK_HWM]) + daemon->metrics[METRIC_WORK_HWM] = daemon->limit[LIMIT_WORK] - keycount; + + /* include DNSSEC queries in the limit for a connection. */ + query_count += daemon->limit[LIMIT_WORK] - keycount; + } + } +#endif + + /* restore CD bit to the value in the query */ + if (checking_disabled) + header->hb4 |= HB4_CD; + else + header->hb4 &= ~HB4_CD; + + /* Never cache answers which are contingent on the source or MAC address EDSN0 option, + since the cache is ignorant of such things. */ + if (!cacheable) + no_cache_dnssec = 1; + + m = process_reply(header, now, serv, (unsigned int)m, + option_bool(OPT_NO_REBIND) && !norebind, no_cache_dnssec, cache_secure, bogusanswer, + ad_reqd, do_bit, added_pheader, &peer_addr, ((unsigned char *)header) + 65536, ede); + } } } - } - + } + if (do_stale) break; - + /* In case of local answer or no connections made. */ if (m == 0) { diff --git a/src/rfc1035.c b/src/rfc1035.c index 8dbe652..bbef6f3 100644 --- a/src/rfc1035.c +++ b/src/rfc1035.c @@ -1240,6 +1240,8 @@ void setup_reply(struct dns_header *header, unsigned int flags, int ede) SET_RCODE(header, NOERROR); /* empty domain */ else if (flags == F_NXDOMAIN) SET_RCODE(header, NXDOMAIN); + else if (flags == F_RCODE) + SET_RCODE(header, NOTIMP); else if (flags & ( F_IPV4 | F_IPV6)) { SET_RCODE(header, NOERROR); @@ -2182,7 +2184,7 @@ size_t answer_request(struct dns_header *header, char *limit, size_t qlen, if (flags & F_NXDOMAIN) nxdomain = 1; else if (qtype != T_ANY && rr_on_list(daemon->filter_rr, qtype)) - flags |= F_NEG | F_CONFIG; + flags |= F_NEG | F_CONFIG; auth = 0; ans = 1; @@ -2210,8 +2212,8 @@ size_t answer_request(struct dns_header *header, char *limit, size_t qlen, anscount++; /* log after cache insertion as log_txt mangles rrdata */ - if (qtype == T_TXT && !(crecp->flags & F_NEG)) - log_txt(name, (unsigned char *)rrdata, rrlen, crecp->flags & F_DNSSECOK); + if (qtype == T_TXT && !(flags & F_NEG)) + log_txt(name, (unsigned char *)rrdata, rrlen, flags & (F_DNSSECOK | F_STALE)); else log_query(flags, name, &crecp->addr, NULL, 0); }