From b6f926fbefcd2471699599e44f32b8d25b87b471 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Tue, 21 Aug 2018 17:46:52 +0100 Subject: [PATCH] Don't return NXDOMAIN to empty non-terminals. When a record is defined locally, eg an A record for one.two.example then we already know that if we forward, eg an AAAA query for one.two.example, and get back NXDOMAIN, then we need to alter that to NODATA. This is handled by check_for_local_domain(). But, if we forward two.example, because one.two.example exists, then the answer to two.example should also be a NODATA. For most local records this is easy, just to substring matching. for A, AAAA and CNAME records that are in the cache, it's more difficult. The cache has no efficient way to find such records. The fix is to insert empty (none of F_IPV4, F_IPV6 F_CNAME set) records for each non-terminal. The same considerations apply in auth mode, and the same basic mechanism is used there too. --- src/auth.c | 28 ++++++------ src/cache.c | 116 +++++++++++++++++++++++++++++++++++++++++++++++++- src/dnsmasq.h | 2 + src/rfc1035.c | 23 ++++------ src/util.c | 38 +++++++++++++++++ 5 files changed, 179 insertions(+), 28 deletions(-) diff --git a/src/auth.c b/src/auth.c index c8b89b5..93f9077 100644 --- a/src/auth.c +++ b/src/auth.c @@ -103,7 +103,7 @@ size_t answer_auth(struct dns_header *header, char *limit, size_t qlen, time_t n { char *name = daemon->namebuff; unsigned char *p, *ansp; - int qtype, qclass; + int qtype, qclass, rc; int nameoffset, axfroffset = 0; int q, anscount = 0, authcount = 0; struct crec *crecp; @@ -283,11 +283,11 @@ size_t answer_auth(struct dns_header *header, char *limit, size_t qlen, time_t n } for (rec = daemon->mxnames; rec; rec = rec->next) - if (!rec->issrv && hostname_isequal(name, rec->name)) + if (!rec->issrv && (rc = hostname_issubdomain(name, rec->name))) { nxdomain = 0; - if (qtype == T_MX) + if (rc == 2 && qtype == T_MX) { found = 1; log_query(F_CONFIG | F_RRNAME, name, NULL, ""); @@ -298,11 +298,11 @@ size_t answer_auth(struct dns_header *header, char *limit, size_t qlen, time_t n } for (move = NULL, up = &daemon->mxnames, rec = daemon->mxnames; rec; rec = rec->next) - if (rec->issrv && hostname_isequal(name, rec->name)) + if (rec->issrv && (rc = hostname_issubdomain(name, rec->name))) { nxdomain = 0; - if (qtype == T_SRV) + if (rc == 2 && qtype == T_SRV) { found = 1; log_query(F_CONFIG | F_RRNAME, name, NULL, ""); @@ -333,10 +333,10 @@ size_t answer_auth(struct dns_header *header, char *limit, size_t qlen, time_t n } for (txt = daemon->rr; txt; txt = txt->next) - if (hostname_isequal(name, txt->name)) + if ((rc = hostname_issubdomain(name, txt->name))) { nxdomain = 0; - if (txt->class == qtype) + if (rc == 2 && txt->class == qtype) { found = 1; log_query(F_CONFIG | F_RRNAME, name, NULL, ""); @@ -347,10 +347,10 @@ size_t answer_auth(struct dns_header *header, char *limit, size_t qlen, time_t n } for (txt = daemon->txt; txt; txt = txt->next) - if (txt->class == C_IN && hostname_isequal(name, txt->name)) + if (txt->class == C_IN && (rc = hostname_issubdomain(name, txt->name))) { nxdomain = 0; - if (qtype == T_TXT) + if (rc == 2 && qtype == T_TXT) { found = 1; log_query(F_CONFIG | F_RRNAME, name, NULL, ""); @@ -361,10 +361,10 @@ size_t answer_auth(struct dns_header *header, char *limit, size_t qlen, time_t n } for (na = daemon->naptr; na; na = na->next) - if (hostname_isequal(name, na->name)) + if ((rc = hostname_issubdomain(name, na->name))) { nxdomain = 0; - if (qtype == T_NAPTR) + if (rc == 2 && qtype == T_NAPTR) { found = 1; log_query(F_CONFIG | F_RRNAME, name, NULL, ""); @@ -384,13 +384,13 @@ size_t answer_auth(struct dns_header *header, char *limit, size_t qlen, time_t n #endif for (intr = daemon->int_names; intr; intr = intr->next) - if (hostname_isequal(name, intr->name)) + if ((rc = hostname_issubdomain(name, intr->name))) { struct addrlist *addrlist; nxdomain = 0; - if (flag) + if (rc == 2 && flag) for (addrlist = intr->addr; addrlist; addrlist = addrlist->next) if (((addrlist->flags & ADDRLIST_IPV6) ? T_AAAA : T_A) == qtype && (local_query || filter_zone(zone, flag, &addrlist->addr))) @@ -567,6 +567,8 @@ size_t answer_auth(struct dns_header *header, char *limit, size_t qlen, time_t n goto cname_restart; } + else if (cache_find_non_terminal(name, now)) + nxdomain = 0; log_query(flag | F_NEG | (nxdomain ? F_NXDOMAIN : 0) | F_FORWARD | F_AUTH, name, NULL, NULL); } diff --git a/src/cache.c b/src/cache.c index 9e0923d..e64335a 100644 --- a/src/cache.c +++ b/src/cache.c @@ -25,6 +25,8 @@ static int insert_error; static union bigname *big_free = NULL; static int bignames_left, hash_size; +static void make_non_terminals(struct crec *source); + /* type->string mapping: this is also used by the name-hash function as a mixing table. */ static const struct { unsigned int type; @@ -648,6 +650,20 @@ void cache_end_insert(void) new_chain = NULL; } +int cache_find_non_terminal(char *name, time_t now) +{ + struct crec *crecp; + + for (crecp = *hash_bucket(name); crecp; crecp = crecp->hash_next) + if (!is_outdated_cname_pointer(crecp) && + !is_expired(now, crecp) && + (crecp->flags & F_FORWARD) && + hostname_isequal(name, cache_get_name(crecp))) + return 1; + + return 0; +} + struct crec *cache_find_by_name(struct crec *crecp, char *name, time_t now, unsigned int prot) { struct crec *ans; @@ -818,6 +834,8 @@ static void add_hosts_cname(struct crec *target) crec->addr.cname.uid = target->uid; crec->uid = UID_NONE; cache_hash(crec); + make_non_terminals(crec); + add_hosts_cname(crec); /* handle chains */ } } @@ -889,6 +907,7 @@ static void add_hosts_entry(struct crec *cache, struct all_addr *addr, int addrl cache->uid = index; memcpy(&cache->addr.addr, addr, addrlen); cache_hash(cache); + make_non_terminals(cache); /* don't need to do alias stuff for second and subsequent addresses. */ if (!nameexists) @@ -1102,6 +1121,7 @@ void cache_reload(void) cache->addr.cname.uid = SRC_INTERFACE; cache->uid = UID_NONE; cache_hash(cache); + make_non_terminals(cache); add_hosts_cname(cache); /* handle chains */ } @@ -1119,6 +1139,7 @@ void cache_reload(void) cache->addr.ds.digest = ds->digest_type; cache->uid = ds->class; cache_hash(cache); + make_non_terminals(cache); } #endif @@ -1234,6 +1255,7 @@ static void add_dhcp_cname(struct crec *target, time_t ttd) aliasc->addr.cname.uid = target->uid; aliasc->uid = UID_NONE; cache_hash(aliasc); + make_non_terminals(aliasc); add_dhcp_cname(aliasc, ttd); } } @@ -1322,12 +1344,104 @@ void cache_add_dhcp_entry(char *host_name, int prot, crec->name.namep = host_name; crec->uid = UID_NONE; cache_hash(crec); + make_non_terminals(crec); add_dhcp_cname(crec, ttd); } } #endif +/* Called when we put a local or DHCP name into the cache. + Creates empty cache entries for subnames (ie, + for three.two.one, for two.one and one), without + F_IPV4 or F_IPV6 or F_CNAME set. These convert + NXDOMAIN answers to NoData ones. */ +static void make_non_terminals(struct crec *source) +{ + char *name = cache_get_name(source); + struct crec* crecp, *tmp, **up; + int type = F_HOSTS | F_CONFIG; +#ifdef HAVE_DHCP + if (source->flags & F_DHCP) + type = F_DHCP; +#endif + + /* First delete any empty entries for our new real name. Note that + we only delete empty entries deriving from DHCP for a new DHCP-derived + entry and vice-versa for HOSTS and CONFIG. This ensures that + non-terminals from DHCP go when we reload DHCP and + for HOSTS/CONFIG when we re-read. */ + for (up = hash_bucket(name), crecp = *up; crecp; crecp = tmp) + { + tmp = crecp->hash_next; + + if (!is_outdated_cname_pointer(crecp) && + (crecp->flags & F_FORWARD) && + (crecp->flags & type) && + !(crecp->flags & (F_IPV4 | F_IPV6 | F_CNAME)) && + hostname_isequal(name, cache_get_name(crecp))) + { + *up = crecp->hash_next; +#ifdef HAVE_DHCP + if (type & F_DHCP) + { + crecp->next = dhcp_spare; + dhcp_spare = crecp; + } + else +#endif + free(crecp); + break; + } + else + up = &crecp->hash_next; + } + + while ((name = strchr(name, '.'))) + { + name++; + + /* Look for one existing, don't need another */ + for (crecp = *hash_bucket(name); crecp; crecp = crecp->hash_next) + if (!is_outdated_cname_pointer(crecp) && + (crecp->flags & F_FORWARD) && + (crecp->flags & type) && + hostname_isequal(name, cache_get_name(crecp))) + break; + + if (crecp) + { + /* If the new name expires later, transfer that time to + empty non-terminal entry. */ + if (!(crecp->flags & F_IMMORTAL)) + { + if (source->flags & F_IMMORTAL) + crecp->flags |= F_IMMORTAL; + else if (difftime(crecp->ttd, source->ttd) < 0) + crecp->ttd = source->ttd; + } + continue; + } + +#ifdef HAVE_DHCP + if ((source->flags & F_DHCP) && dhcp_spare) + { + crecp = dhcp_spare; + dhcp_spare = dhcp_spare->next; + } + else +#endif + crecp = whine_malloc(sizeof(struct crec)); + + *crecp = *source; + crecp->flags &= ~(F_IPV4 | F_IPV6 | F_CNAME | F_REVERSE); + crecp->flags |= F_NAMEP; + crecp->name.namep = name; + + cache_hash(crecp); + } +} + #ifndef NO_ID int cache_make_stat(struct txt_record *t) { @@ -1443,7 +1557,6 @@ static char *sanitise(char *name) void dump_cache(time_t now) { struct server *serv, *serv1; - char *t = ""; my_syslog(LOG_INFO, _("time %lu"), (unsigned long)now); my_syslog(LOG_INFO, _("cache size %d, %d/%d cache insertions re-used unexpired cache entries."), @@ -1489,6 +1602,7 @@ void dump_cache(time_t now) for (i=0; ihash_next) { + char *t = " "; char *a = daemon->addrbuff, *p = daemon->namebuff, *n = cache_get_name(cache); *a = 0; if (strlen(n) == 0 && !(cache->flags & F_REVERSE)) diff --git a/src/dnsmasq.h b/src/dnsmasq.h index 201fc80..712fe56 100644 --- a/src/dnsmasq.h +++ b/src/dnsmasq.h @@ -1135,6 +1135,7 @@ void next_uid(struct crec *crecp); void log_query(unsigned int flags, char *name, struct all_addr *addr, char *arg); char *record_source(unsigned int index); char *querystr(char *desc, unsigned short type); +int cache_find_non_terminal(char *name, time_t now); struct crec *cache_find_by_addr(struct crec *crecp, struct all_addr *addr, time_t now, unsigned int prot); @@ -1250,6 +1251,7 @@ void *whine_malloc(size_t size); int sa_len(union mysockaddr *addr); int sockaddr_isequal(union mysockaddr *s1, union mysockaddr *s2); int hostname_isequal(const char *a, const char *b); +int hostname_issubdomain(char *a, char *b); time_t dnsmasq_time(void); int netmask_length(struct in_addr mask); int is_same_net(struct in_addr a, struct in_addr b, struct in_addr mask); diff --git a/src/rfc1035.c b/src/rfc1035.c index e179865..67f8fa9 100644 --- a/src/rfc1035.c +++ b/src/rfc1035.c @@ -986,40 +986,35 @@ size_t setup_reply(struct dns_header *header, size_t qlen, /* check if name matches local names ie from /etc/hosts or DHCP or local mx names. */ int check_for_local_domain(char *name, time_t now) { - struct crec *crecp; struct mx_srv_record *mx; struct txt_record *txt; struct interface_name *intr; struct ptr_record *ptr; struct naptr *naptr; - /* Note: the call to cache_find_by_name is intended to find any record which matches - ie A, AAAA, CNAME. */ - - if ((crecp = cache_find_by_name(NULL, name, now, F_IPV4 | F_IPV6 | F_CNAME | F_NO_RR)) && - (crecp->flags & (F_HOSTS | F_DHCP | F_CONFIG))) - return 1; - for (naptr = daemon->naptr; naptr; naptr = naptr->next) - if (hostname_isequal(name, naptr->name)) + if (hostname_issubdomain(name, naptr->name)) return 1; for (mx = daemon->mxnames; mx; mx = mx->next) - if (hostname_isequal(name, mx->name)) + if (hostname_issubdomain(name, mx->name)) return 1; for (txt = daemon->txt; txt; txt = txt->next) - if (hostname_isequal(name, txt->name)) + if (hostname_issubdomain(name, txt->name)) return 1; for (intr = daemon->int_names; intr; intr = intr->next) - if (hostname_isequal(name, intr->name)) + if (hostname_issubdomain(name, intr->name)) return 1; for (ptr = daemon->ptr; ptr; ptr = ptr->next) - if (hostname_isequal(name, ptr->name)) + if (hostname_issubdomain(name, ptr->name)) return 1; - + + if (cache_find_non_terminal(name, now)) + return 1; + return 0; } diff --git a/src/util.c b/src/util.c index 532bc16..b27d50e 100644 --- a/src/util.c +++ b/src/util.c @@ -355,6 +355,44 @@ int hostname_isequal(const char *a, const char *b) return 1; } +/* is b equal to or a subdomain of a return 2 for equal, 1 for subdomain */ +int hostname_issubdomain(char *a, char *b) +{ + char *ap, *bp; + unsigned int c1, c2; + + /* move to the end */ + for (ap = a; *ap; ap++); + for (bp = b; *bp; bp++); + + /* a shorter than b or a empty. */ + if ((bp - b) < (ap - a) || ap == a) + return 0; + + do + { + c1 = (unsigned char) *(--ap); + c2 = (unsigned char) *(--bp); + + if (c1 >= 'A' && c1 <= 'Z') + c1 += 'a' - 'A'; + if (c2 >= 'A' && c2 <= 'Z') + c2 += 'a' - 'A'; + + if (c1 != c2) + return 0; + } while (ap != a); + + if (bp == b) + return 2; + + if (*(--bp) == '.') + return 1; + + return 0; +} + + time_t dnsmasq_time(void) { #ifdef HAVE_BROKEN_RTC