diff options
author | Simon Kelley <simon@thekelleys.org.uk> | 2014-02-25 23:02:28 +0000 |
---|---|---|
committer | Simon Kelley <simon@thekelleys.org.uk> | 2014-02-25 23:02:28 +0000 |
commit | 613ad15d02154f39e5335697035136b4dbe92b4d (patch) | |
tree | 9abd71c29a5a24f9cb9e57ecc483592c5dca5541 | |
parent | 24187530fb3cfc5e2eaa6352e7ea9b27f644e213 (diff) | |
download | dnsmasq-613ad15d02154f39e5335697035136b4dbe92b4d.tar.gz |
Strip DNSSEC RRs when query doesn't have DO bit set.
-rw-r--r-- | src/dnsmasq.h | 5 | ||||
-rw-r--r-- | src/dnssec.c | 352 | ||||
-rw-r--r-- | src/forward.c | 52 | ||||
-rw-r--r-- | src/rfc1035.c | 12 |
4 files changed, 327 insertions, 94 deletions
diff --git a/src/dnsmasq.h b/src/dnsmasq.h index d52adfa..f984f3b 100644 --- a/src/dnsmasq.h +++ b/src/dnsmasq.h @@ -542,6 +542,8 @@ struct hostsfile { #define FREC_DNSKEY_QUERY 8 #define FREC_DS_QUERY 16 #define FREC_AD_QUESTION 32 +#define FREC_DO_QUESTION 64 +#define FREC_ADDED_PHEADER 128 #ifdef HAVE_DNSSEC #define HASH_SIZE 20 /* SHA-1 digest size */ @@ -1048,7 +1050,7 @@ int extract_addresses(struct dns_header *header, size_t qlen, char *namebuff, int no_cache, int secure, int *doctored); size_t answer_request(struct dns_header *header, char *limit, size_t qlen, struct in_addr local_addr, struct in_addr local_netmask, - time_t now, int *ad_reqd); + time_t now, int *ad_reqd, int *do_bit); int check_for_bogus_wildcard(struct dns_header *header, size_t qlen, char *name, struct bogus_addr *addr, time_t now); unsigned char *find_pseudoheader(struct dns_header *header, size_t plen, @@ -1085,6 +1087,7 @@ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t n, char int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int class); int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int *class); int dnskey_keytag(int alg, int flags, unsigned char *rdata, int rdlen); +size_t filter_rrsigs(struct dns_header *header, size_t plen); unsigned char* hash_questions(struct dns_header *header, size_t plen, char *name); /* util.c */ diff --git a/src/dnssec.c b/src/dnssec.c index be1b101..a902ded 100644 --- a/src/dnssec.c +++ b/src/dnssec.c @@ -472,6 +472,32 @@ static int get_rdata(struct dns_header *header, size_t plen, unsigned char *end, } } +static int expand_workspace(unsigned char ***wkspc, int *sz, int new) +{ + unsigned char **p; + int new_sz = *sz; + + if (new_sz > new) + return 1; + + if (new >= 100) + return 0; + + new_sz += 5; + + if (!(p = whine_malloc((new_sz) * sizeof(unsigned char **)))) + return 0; + + if (*wkspc) + { + memcpy(p, *wkspc, *sz * sizeof(unsigned char **)); + free(*wkspc); + } + + *wkspc = p; + *sz = new_sz; +} + /* Bubble sort the RRset into the canonical order. Note that the byte-streams from two RRs may get unsynced: consider RRs which have two domain-names at the start and then other data. @@ -615,64 +641,31 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in { if (stype == type) { - if (rrsetidx == rrset_sz) - { - unsigned char **new; - - /* Protect against insane/maliciuos queries which bloat the workspace - and eat CPU in the sort */ - if (rrsetidx >= 100) - return STAT_INSECURE; - - /* expand */ - if (!(new = whine_malloc((rrset_sz + 5) * sizeof(unsigned char **)))) - return STAT_INSECURE; - - if (rrset) - { - memcpy(new, rrset, rrset_sz * sizeof(unsigned char **)); - free(rrset); - } - - rrset = new; - rrset_sz += 5; - } + if (!expand_workspace(&rrset, &rrset_sz, rrsetidx)) + return STAT_INSECURE; + rrset[rrsetidx++] = pstart; } if (stype == T_RRSIG) { - if (rdlen < 18) - return STAT_INSECURE; /* bad packet */ - - GETSHORT(type_covered, p); - - if (type_covered == type) - { - if (sigidx == sig_sz) - { - unsigned char **new; - - /* expand */ - if (!(new = whine_malloc((sig_sz + 5) * sizeof(unsigned char **)))) - return STAT_INSECURE; - - if (sigs) - { - memcpy(new, sigs, sig_sz * sizeof(unsigned char **)); - free(sigs); - } - - sigs = new; - sig_sz += 5; - } - - sigs[sigidx++] = pdata; - } - p = pdata + 2; /* restore for ADD_RDLEN */ + if (rdlen < 18) + return STAT_INSECURE; /* bad packet */ + + GETSHORT(type_covered, p); + + if (type_covered == type) + { + if (!expand_workspace(&sigs, &sig_sz, sigidx)) + return STAT_INSECURE; + + sigs[sigidx++] = pdata; + } + + p = pdata + 2; /* restore for ADD_RDLEN */ } } - + if (!ADD_RDLEN(header, p, plen, rdlen)) return STAT_INSECURE; } @@ -1195,32 +1188,12 @@ static int find_nsec_records(struct dns_header *header, size_t plen, unsigned ch type_found = type; - if (nsecs_found == nsecset_sz) - { - unsigned char **new; - - /* Protect against insane/malicious queries which bloat the workspace - and eat CPU in the sort */ - if (nsecs_found >= 100) - return 0; - - /* expand */ - if (!(new = whine_malloc((nsecset_sz + 5) * sizeof(unsigned char **)))) - return 0; - - if (nsecset) - { - memcpy(new, nsecset, nsecset_sz * sizeof(unsigned char **)); - free(nsecset); - } - - nsecset = new; - nsecset_sz += 5; - } - + if (!expand_workspace(&nsecset, &nsecset_sz, nsecs_found)) + return 0; + nsecset[nsecs_found++] = pstart; } - + if (!ADD_RDLEN(header, p, plen, rdlen)) return 0; } @@ -1908,6 +1881,235 @@ size_t dnssec_generate_query(struct dns_header *header, char *end, char *name, i return add_do_bit(header, p - (unsigned char *)header, end); } +/* Go through a domain name, find "pointers" and fix them up based on how many bytes + we've chopped out of the packet, or check they don't point into an elided part. */ +static int check_name(unsigned char **namep, struct dns_header *header, size_t plen, int fixup, unsigned char **rrs, int rr_count) +{ + unsigned char *ansp = *namep; + + while(1) + { + unsigned int label_type; + + if (!CHECK_LEN(header, ansp, plen, 1)) + return 0; + + label_type = (*ansp) & 0xc0; + + if (label_type == 0xc0) + { + /* pointer for compression. */ + unsigned int offset, i; + unsigned char *p; + + if (!CHECK_LEN(header, ansp, plen, 2)) + return 0; + + offset = ((*ansp++) & 0x3f) << 8; + offset |= *ansp++; + + p = offset + (unsigned char *)header; + + for (i = 0; i < rr_count; i++) + if (p < rrs[i]) + break; + else + if (i & 1) + offset -= rrs[i] - rrs[i-1]; + + /* does the pointer end up in an elided RR? */ + if (i & 1) + return -1; + + /* No, scale the pointer */ + if (fixup) + { + ansp -= 2; + *ansp++ = (offset >> 8) | 0xc0; + *ansp++ = offset & 0xff; + } + break; + } + else if (label_type == 0x80) + return 0; /* reserved */ + else if (label_type == 0x40) + { + /* Extended label type */ + unsigned int count; + + if (!CHECK_LEN(header, ansp, plen, 2)) + return 0; + + if (((*ansp++) & 0x3f) != 1) + return 0; /* we only understand bitstrings */ + + count = *(ansp++); /* Bits in bitstring */ + + if (count == 0) /* count == 0 means 256 bits */ + ansp += 32; + else + ansp += ((count-1)>>3)+1; + } + else + { /* label type == 0 Bottom six bits is length */ + unsigned int len = (*ansp++) & 0x3f; + + if (!ADD_RDLEN(header, ansp, plen, len)) + return 0; + + if (len == 0) + break; /* zero length label marks the end. */ + } + } + + *namep = ansp; + + return 1; +} + +/* Go through RRs and check or fixup the domain names contained within */ +static int check_rrs(unsigned char *p, struct dns_header *header, size_t plen, int fixup, unsigned char **rrs, int rr_count) +{ + int i, type, class, rdlen; + + for (i = 0; i < ntohs(header->ancount) + ntohs(header->nscount); i++) + { + if (type != T_NSEC && type != T_NSEC3 && type != T_RRSIG) + { + if (!check_name(&p, header, plen, fixup, rrs, rr_count)) + return 0; + } + else + { + if (!(p = skip_name(p, header, plen, 10))) + return 0; + } + + GETSHORT(type, p); + GETSHORT(class, p); + p += 4; /* TTL */ + GETSHORT(rdlen, p); + + if (type != T_NSEC && type != T_NSEC3 && type != T_RRSIG) + { + if (class == C_IN) + { + u16 *d; + unsigned char *pp = p; + + for (d = get_desc(type); *d != (u16)-1; d++) + { + if (*d != 0) + pp += *d; + else if (!check_name(&pp, header, plen, fixup, rrs, rr_count)) + return 0; + } + } + } + + if (!ADD_RDLEN(header, p, plen, rdlen)) + return 0; + } + + return 1; +} + + +size_t filter_rrsigs(struct dns_header *header, size_t plen) +{ + static unsigned char **rrs; + static int rr_sz = 0; + + unsigned char *p = (unsigned char *)(header+1); + int i, rdlen, qtype, qclass, rr_found, chop_an, chop_ns; + + if (ntohs(header->qdcount) != 1 || + !(p = skip_name(p, header, plen, 4))) + return plen; + + GETSHORT(qtype, p); + GETSHORT(qclass, p); + + /* First pass, find pointers to start and end of all the records we wish to elide: + records added for DNSSEC, unless explicity queried for */ + for (rr_found = 0, chop_ns = 0, chop_an = 0, i = 0; i < ntohs(header->ancount) + ntohs(header->nscount); i++) + { + unsigned char *pstart = p; + int type, class; + + if (!(p = skip_name(p, header, plen, 10))) + return plen; + + GETSHORT(type, p); + GETSHORT(class, p); + p += 4; /* TTL */ + GETSHORT(rdlen, p); + + if ((type == T_NSEC || type == T_NSEC3 || type == T_RRSIG) && + (type != qtype || class != qclass)) + { + if (!expand_workspace(&rrs, &rr_sz, rr_found + 1)) + return plen; + + rrs[rr_found++] = pstart; + + if (!ADD_RDLEN(header, p, plen, rdlen)) + return plen; + + rrs[rr_found++] = p; + + if (i < ntohs(header->ancount)) + chop_an++; + else + chop_ns++; + } + else if (!ADD_RDLEN(header, p, plen, rdlen)) + return plen; + } + + /* Nothing to do. */ + if (rr_found == 0) + return plen; + + /* Second pass, look for pointers in names in the records we're keeping and make sure they don't + point to records we're going to elide. This is theoretically possible, but unlikely. If + it happens, we give up and leave the answer unchanged. */ + p = (unsigned char *)(header+1); + + /* question first */ + if (!check_name(&p, header, plen, 0, rrs, rr_found)) + return plen; + p += 4; /* qclass, qtype */ + + /* Now answers and NS */ + if (!check_rrs(p, header, plen, 0, rrs, rr_found)) + return plen; + + /* Third pass, elide records */ + for (p = rrs[0], i = 1; i < rr_found; i += 2) + { + unsigned char *start = rrs[i]; + unsigned char *end = (i != rr_found - 1) ? rrs[i+1] : ((unsigned char *)(header+1)) + plen; + + memmove(p, start, end-start); + p += end-start; + } + + plen = p - (unsigned char *)header; + header->ancount = htons(ntohs(header->ancount) - chop_an); + header->nscount = htons(ntohs(header->nscount) - chop_ns); + + /* Fourth pass, fix up pointers in the remaining records */ + p = (unsigned char *)(header+1); + + check_name(&p, header, plen, 1, rrs, rr_found); + p += 4; /* qclass, qtype */ + + check_rrs(p, header, plen, 1, rrs, rr_found); + + return plen; +} + unsigned char* hash_questions(struct dns_header *header, size_t plen, char *name) { int q; diff --git a/src/forward.c b/src/forward.c index 5b9fa1e..c05efe4 100644 --- a/src/forward.c +++ b/src/forward.c @@ -235,7 +235,7 @@ static unsigned int search_servers(time_t now, struct all_addr **addrpp, static int forward_query(int udpfd, union mysockaddr *udpaddr, struct all_addr *dst_addr, unsigned int dst_iface, struct dns_header *header, size_t plen, time_t now, - struct frec *forward, int ad_reqd) + struct frec *forward, int ad_reqd, int do_bit) { char *domain = NULL; int type = 0, norebind = 0; @@ -336,8 +336,10 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr, forward->flags |= FREC_AD_QUESTION; #ifdef HAVE_DNSSEC forward->work_counter = DNSSEC_WORK; + if (do_bit) + forward->flags |= FREC_DO_QUESTION; #endif - + header->id = htons(forward->new_id); /* In strict_order mode, always try servers in the order @@ -393,11 +395,17 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr, #ifdef HAVE_DNSSEC if (option_bool(OPT_DNSSEC_VALID)) { - plen = add_do_bit(header, plen, ((char *) header) + daemon->packet_buff_sz); + size_t new_plen = add_do_bit(header, plen, ((char *) header) + daemon->packet_buff_sz); + /* 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; + + if (new_plen != plen) + forward->flags |= FREC_ADDED_PHEADER; + + plen = new_plen; } #endif @@ -506,7 +514,7 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr, } static size_t process_reply(struct dns_header *header, time_t now, struct server *server, size_t n, int check_rebind, - int no_cache, int cache_secure, int ad_reqd, int check_subnet, union mysockaddr *query_source) + int no_cache, int cache_secure, int ad_reqd, int do_bit, int added_pheader, int check_subnet, union mysockaddr *query_source) { unsigned char *pheader, *sizep; char **sets = 0; @@ -553,6 +561,12 @@ static size_t process_reply(struct dns_header *header, time_t now, struct server my_syslog(LOG_WARNING, _("discarding DNS reply: subnet option mismatch")); return 0; } + + if (added_pheader) + { + pheader = 0; + header->arcount = htons(0); + } } /* RFC 4035 sect 4.6 para 3 */ @@ -624,6 +638,10 @@ static size_t process_reply(struct dns_header *header, time_t now, struct server if (!(header->hb4 & HB4_CD) && ad_reqd && cache_secure) header->hb4 |= HB4_AD; + + /* If the requestor didn't set the DO bit, don't return DNSSEC info. */ + if (!do_bit) + n = filter_rrsigs(header, n); #endif /* do this after extract_addresses. Ensure NODATA reply and remove @@ -708,7 +726,7 @@ void reply_query(int fd, int family, time_t now) if ((nn = resize_packet(header, (size_t)n, pheader, plen))) { header->hb3 &= ~(HB3_QR | HB3_TC); - forward_query(-1, NULL, NULL, 0, header, nn, now, forward, 0); + forward_query(-1, NULL, NULL, 0, header, nn, now, forward, 0, 0); return; } } @@ -927,7 +945,8 @@ void reply_query(int fd, int family, time_t now) header->hb4 &= ~HB4_CD; if ((nn = process_reply(header, now, server, (size_t)n, check_rebind, no_cache_dnssec, cache_secure, - forward->flags & FREC_AD_QUESTION, forward->flags & FREC_HAS_SUBNET, &forward->source))) + forward->flags & FREC_AD_QUESTION, forward->flags & FREC_DO_QUESTION, + forward->flags & FREC_ADDED_PHEADER, forward->flags & FREC_HAS_SUBNET, &forward->source))) { header->id = htons(forward->orig_id); header->hb4 |= HB4_RA; /* recursion if available */ @@ -1169,9 +1188,9 @@ void receive_query(struct listener *listen, time_t now) else #endif { - int ad_reqd; + int ad_reqd, do_bit; m = answer_request(header, ((char *) header) + daemon->packet_buff_sz, (size_t)n, - dst_addr_4, netmask, now, &ad_reqd); + dst_addr_4, netmask, now, &ad_reqd, &do_bit); if (m >= 1) { @@ -1180,7 +1199,7 @@ void receive_query(struct listener *listen, time_t now) daemon->local_answer++; } else if (forward_query(listen->fd, &source_addr, &dst_addr, if_index, - header, (size_t)n, now, NULL, ad_reqd)) + header, (size_t)n, now, NULL, ad_reqd, do_bit)) daemon->queries_forwarded++; else daemon->local_answer++; @@ -1272,7 +1291,8 @@ unsigned char *tcp_request(int confd, time_t now, #ifdef HAVE_AUTH int local_auth = 0; #endif - int checking_disabled, ad_question, check_subnet, no_cache_dnssec = 0, cache_secure = 0; + int checking_disabled, ad_question, do_bit, added_pheader = 0; + int check_subnet, no_cache_dnssec = 0, cache_secure = 0; size_t m; unsigned short qtype; unsigned int gotname; @@ -1350,7 +1370,7 @@ unsigned char *tcp_request(int confd, time_t now, { /* m > 0 if answered from cache */ m = answer_request(header, ((char *) header) + 65536, (size_t)size, - dst_addr_4, netmask, now, &ad_question); + dst_addr_4, netmask, now, &ad_question, &do_bit); /* Do this by steam now we're not in the select() loop */ check_log_writer(NULL); @@ -1430,11 +1450,17 @@ unsigned char *tcp_request(int confd, time_t now, #ifdef HAVE_DNSSEC if (option_bool(OPT_DNSSEC_VALID)) { - size = add_do_bit(header, size, ((char *) header) + 65536); + size_t new_size = add_do_bit(header, size, ((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; + + if (size != new_size) + added_pheader = 1; + + size = new_size; } #endif @@ -1533,7 +1559,7 @@ unsigned char *tcp_request(int confd, time_t now, m = process_reply(header, now, last_server, (unsigned int)m, option_bool(OPT_NO_REBIND) && !norebind, no_cache_dnssec, - cache_secure, ad_question, check_subnet, &peer_addr); + cache_secure, ad_question, do_bit, added_pheader, check_subnet, &peer_addr); break; } diff --git a/src/rfc1035.c b/src/rfc1035.c index b9152e1..5693ef9 100644 --- a/src/rfc1035.c +++ b/src/rfc1035.c @@ -601,11 +601,11 @@ static int filter_mac(int family, char *addrp, char *mac, size_t maclen, void *p if (family == parm->l3->sa.sa_family) { - if (family == AF_INET && memcmp (&parm->l3->in.sin_addr, addrp, INADDRSZ) == 0) + if (family == AF_INET && memcmp(&parm->l3->in.sin_addr, addrp, INADDRSZ) == 0) match = 1; #ifdef HAVE_IPV6 else - if (family == AF_INET6 && memcmp (&parm->l3->in6.sin6_addr, addrp, IN6ADDRSZ) == 0) + if (family == AF_INET6 && memcmp(&parm->l3->in6.sin6_addr, addrp, IN6ADDRSZ) == 0) match = 1; #endif } @@ -1453,7 +1453,7 @@ static unsigned long crec_ttl(struct crec *crecp, time_t now) /* return zero if we can't answer from cache, or packet size if we can */ size_t answer_request(struct dns_header *header, char *limit, size_t qlen, struct in_addr local_addr, struct in_addr local_netmask, - time_t now, int *ad_reqd) + time_t now, int *ad_reqd, int *do_bit) { char *name = daemon->namebuff; unsigned char *p, *ansp, *pheader; @@ -1475,7 +1475,8 @@ size_t answer_request(struct dns_header *header, char *limit, size_t qlen, /* RFC 6840 5.7 */ *ad_reqd = header->hb4 & HB4_AD; - + *do_bit = 0; + /* If there is an RFC2671 pseudoheader then it will be overwritten by partial replies, so we have to do a dry run to see if we can answer the query. We check to see if the do bit is set, if so we always @@ -1493,7 +1494,8 @@ size_t answer_request(struct dns_header *header, char *limit, size_t qlen, pheader += 2; /* ext_rcode */ GETSHORT(flags, pheader); - sec_reqd = flags & 0x8000; /* do bit */ + if ((sec_reqd = flags & 0x8000)) + *do_bit = 1;/* do bit */ *ad_reqd = 1; /* If our client is advertising a larger UDP packet size |