From 5226b712a33948e34862de698ba4580ca8bcfb99 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Mon, 3 Feb 2025 21:02:12 +0000 Subject: [PATCH] Add --no-0x20-encode config option. The "bit 0x20 encoding" implemented in 995a16ca0cd9767460c72a856909962a34fdbfbd can interact badly with (hopefully) rare broken upstream servers. Provide an option to turn it off and a log message to give a clue as to why DNS service is non-functional. --- CHANGELOG | 7 ++++++- man/dnsmasq.8 | 8 ++++++++ src/dnsmasq.h | 3 ++- src/forward.c | 45 +++++++++++++++++++++++++++++---------------- src/option.c | 3 +++ src/rfc1035.c | 15 ++++++++++++++- 6 files changed, 62 insertions(+), 19 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 594c77b..c3a5f25 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -109,7 +109,12 @@ version 2.91 on the number of a-z and A-Z characters in the query, and this implementation puts a hard limit of 32 bits to make rescource allocation easy. This about doubles entropy over the standard - random ID and random port combination. + random ID and random port combination. This technique can interact + badly with rare broken DNS servers which don't preserve the case + of the query in their reply. The first time a reply is returned + which matches the query in all respects except case, a warning + will be logged. If this coincides with DNS not functioning, it + is necessary to disable bit 0x20 encoding with --no-0x20-encode. version 2.90 diff --git a/man/dnsmasq.8 b/man/dnsmasq.8 index 5e3ce3b..c3bad0a 100644 --- a/man/dnsmasq.8 +++ b/man/dnsmasq.8 @@ -852,6 +852,14 @@ name on successive queries, for load-balancing. This turns off that behaviour, so that the records are always returned in the order that they are received from upstream. .TP +.B --no-0x20-encode +By default, dnsmasq scambles the case of letters in DNS queries it sends upstream as a security feature. +This technique can interact badly with rare broken DNS servers which don't preserve the case +of the query in their reply. The first time a reply is returned +which matches the query in all respects except case, a warning +will be logged. If this coincides with DNS not functioning, it +is necessary to disable this scrambling with --no-0x20-encode. +.TP .B --use-stale-cache[=] When set, if a DNS name exists in the cache, but its time-to-live has expired, dnsmasq will return the data anyway. (It attempts to refresh the data with an upstream query after returning the stale data.) This can improve speed and reliability. It comes at the expense diff --git a/src/dnsmasq.h b/src/dnsmasq.h index 449668f..d563bf7 100644 --- a/src/dnsmasq.h +++ b/src/dnsmasq.h @@ -279,7 +279,8 @@ struct event_desc { #define OPT_CACHE_RR 71 #define OPT_LOCALHOST_SERVICE 72 #define OPT_LOG_PROTO 73 -#define OPT_LAST 74 +#define OPT_NO_0x20 74 +#define OPT_LAST 75 #define OPTION_BITS (sizeof(unsigned int)*8) #define OPTION_SIZE ( (OPT_LAST/OPTION_BITS)+((OPT_LAST%OPTION_BITS)!=0) ) diff --git a/src/forward.c b/src/forward.c index 3c126c2..ad76616 100644 --- a/src/forward.c +++ b/src/forward.c @@ -326,7 +326,7 @@ static void forward_query(int udpfd, union mysockaddr *udpaddr, forward->new_id = get_id(); header->id = ntohs(forward->new_id); - forward->encode_bitmap = rand32(); + forward->encode_bitmap = option_bool(OPT_NO_0x20) ? 0 : rand32(); p = (unsigned char *)(header+1); if (!extract_name(header, plen, &p, NULL, EXTR_NAME_FLIP, forward->encode_bitmap)) goto reply; @@ -2016,7 +2016,7 @@ static ssize_t tcp_talk(int first, int last, int start, unsigned char *packet, sending replies containing questions and bogus answers. Try another server, or give up */ p = (unsigned char *)(header+1); - if (extract_name(header, rsize, &p, daemon->namebuff, EXTR_NAME_NOCASE, 4) != 1) + if (extract_name(header, rsize, &p, daemon->namebuff, EXTR_NAME_COMPARE, 4) != 1) continue; GETSHORT(rtype, p); GETSHORT(rclass, p); @@ -3057,22 +3057,35 @@ static struct frec *lookup_frec(char *target, int class, int rrtype, int id, int (header = blockdata_retrieve(f->stash, f->stash_len, NULL))) { unsigned char *p = (unsigned char *)(header+1); - int hclass, hrrtype; + int hclass, hrrtype, rc; /* Case sensitive compare for DNS-0x20 encoding. */ - if (extract_name(header, f->stash_len, &p, target, EXTR_NAME_NOCASE, 4) != 1) - continue; - - GETSHORT(hrrtype, p); - GETSHORT(hclass, p); - - /* type checked by flags for DNSSEC queries. */ - if (rrtype != -1 && rrtype != hrrtype) - continue; - - if (class != hclass) - continue; - + if ((rc = extract_name(header, f->stash_len, &p, target, option_bool(OPT_NO_0x20) ? EXTR_NAME_COMPARE : EXTR_NAME_NOCASE, 4))) + { + GETSHORT(hrrtype, p); + GETSHORT(hclass, p); + + /* type checked by flags for DNSSEC queries. */ + if (rrtype != -1 && rrtype != hrrtype) + continue; + + if (class != hclass) + continue; + } + + if (rc != 1) + { + static int warned = 0; + + if (rc == 3 && !warned) + { + my_syslog(LOG_WARNING, _("Case mismatch in DNS reply - check bit 0x20 encoding.")); + warned = 1; + } + + continue; + } + return f; } diff --git a/src/option.c b/src/option.c index f3dee87..a87aeee 100644 --- a/src/option.c +++ b/src/option.c @@ -193,6 +193,7 @@ struct myoption { #define LOPT_MAX_PROCS 384 #define LOPT_DNSSEC_LIMITS 385 #define LOPT_PXE_OPT 386 +#define LOPT_NO_ENCODE 387 #ifdef HAVE_GETOPT_LONG static const struct option opts[] = @@ -247,6 +248,7 @@ static const struct myoption opts[] = { "local-ttl", 1, 0, 'T' }, { "no-negcache", 0, 0, 'N' }, { "no-round-robin", 0, 0, LOPT_NORR }, + { "no-0x20-encode", 0, 0, LOPT_NO_ENCODE }, { "cache-rr", 1, 0, LOPT_CACHE_RR }, { "addn-hosts", 1, 0, 'H' }, { "hostsdir", 1, 0, LOPT_HOST_INOTIFY }, @@ -591,6 +593,7 @@ static struct { { LOPT_UMBRELLA, ARG_ONE, "[=]", gettext_noop("Send Cisco Umbrella identifiers including remote IP."), NULL }, { LOPT_QUIET_TFTP, OPT_QUIET_TFTP, NULL, gettext_noop("Do not log routine TFTP."), NULL }, { LOPT_NORR, OPT_NORR, NULL, gettext_noop("Suppress round-robin ordering of DNS records."), NULL }, + { LOPT_NO_ENCODE, OPT_NO_0x20, NULL, gettext_noop("Suppress DNS bit 0x20 encoding."), NULL }, { LOPT_NO_IDENT, OPT_NO_IDENT, NULL, gettext_noop("Do not add CHAOS TXT records."), NULL }, { LOPT_CACHE_RR, ARG_DUP, "", gettext_noop("Cache this DNS resource record type."), NULL }, { LOPT_MAX_PROCS, ARG_ONE, "", gettext_noop("Maximum number of concurrent tcp connections."), NULL }, diff --git a/src/rfc1035.c b/src/rfc1035.c index e92c00b..254d7cf 100644 --- a/src/rfc1035.c +++ b/src/rfc1035.c @@ -24,6 +24,7 @@ return = 0 -> error return = 1 -> extract OK, compare OK, flip OK return = 2 -> extract OK, compare failed. + return = 3 -> extract OK, compare failed but only on case. */ int extract_name(struct dns_header *header, size_t plen, unsigned char **pp, char *name, int func, unsigned int parm) @@ -140,9 +141,21 @@ int extract_name(struct dns_header *header, size_t plen, unsigned char **pp, if (case_insens && c2 >= 'A' && c2 <= 'Z') c2 += 'a' - 'A'; + + if (!case_insens && retvalue != 2 && c1 != c2) + { + if (c1 >= 'A' && c1 <= 'Z') + c1 += 'a' - 'A'; + + if (c2 >= 'A' && c2 <= 'Z') + c2 += 'a' - 'A'; + + if (c1 == c2) + retvalue = 3; + } if (c1 != c2) - retvalue = 2; + retvalue = 2; } }