Add --dns-loop-detect feature.
diff --git a/CHANGELOG b/CHANGELOG
index 5b7dfb8..62f2e4a 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -20,6 +20,13 @@
longer prefix length.) Thanks to Lung-Pin Chang for the
patch.
+ Add a mode which detects and removes DNS forwarding loops, ie
+ a query sent to an upstream server returns as a new query to
+ dnsmasq, and would therefore be forwarded again, resulting in
+ a query which loops many times before being dropped. Upstream
+ servers which loop back are disabled and this event is logged.
+ Thanks to Smoothwall for their sponsorship of this feature.
+
version 2.71
Subtle change to error handling to help DNSSEC validation
diff --git a/Makefile b/Makefile
index 17eeb27..58a7975 100644
--- a/Makefile
+++ b/Makefile
@@ -69,7 +69,7 @@
dnsmasq.o dhcp.o lease.o rfc2131.o netlink.o dbus.o bpf.o \
helper.o tftp.o log.o conntrack.o dhcp6.o rfc3315.o \
dhcp-common.o outpacket.o radv.o slaac.o auth.o ipset.o \
- domain.o dnssec.o blockdata.o tables.o
+ domain.o dnssec.o blockdata.o tables.o loop.o
hdrs = dnsmasq.h config.h dhcp-protocol.h dhcp6-protocol.h \
dns-protocol.h radv-protocol.h ip6addr.h
diff --git a/bld/Android.mk b/bld/Android.mk
index 5255ec9..d855094 100644
--- a/bld/Android.mk
+++ b/bld/Android.mk
@@ -9,7 +9,8 @@
rfc2131.c tftp.c util.c conntrack.c \
dhcp6.c rfc3315.c dhcp-common.c outpacket.c \
radv.c slaac.c auth.c ipset.c domain.c \
- dnssec.c dnssec-openssl.c blockdata.c tables.c
+ dnssec.c dnssec-openssl.c blockdata.c tables.c \
+ loop.c
LOCAL_MODULE := dnsmasq
diff --git a/man/dnsmasq.8 b/man/dnsmasq.8
index 0530a19..7b4cc98 100644
--- a/man/dnsmasq.8
+++ b/man/dnsmasq.8
@@ -334,6 +334,16 @@
dnsmasq to send all queries to all available servers. The reply from
the server which answers first will be returned to the original requester.
.TP
+.B --dns-loop-detect
+Enable code to detect DNS forwarding loops; ie the situation where a query sent to one
+of the upstream server eventually returns as a new query to the dnsmasq instance. The
+process works by generating TXT queries of the form <hex>.test and sending them to
+each upstream server. The hex is a UID which encodes the instance of dnsmasq sending the query
+and the upstream server to which it was sent. If the query returns to the server which sent it, then
+the upstream server through which it was sent is disabled and this event is logged. Each time the
+set of upstream servers changes, the test is re-run on all of them, including ones which
+were previously disabled.
+.TP
.B --stop-dns-rebind
Reject (and log) addresses from upstream nameservers which are in the
private IP ranges. This blocks an attack where a browser behind a
diff --git a/src/config.h b/src/config.h
index 87f8f8a..145820a 100644
--- a/src/config.h
+++ b/src/config.h
@@ -47,6 +47,8 @@
#define SOA_REFRESH 1200 /* SOA refresh default */
#define SOA_RETRY 180 /* SOA retry default */
#define SOA_EXPIRY 1209600 /* SOA expiry default */
+#define LOOP_TEST_DOMAIN "test" /* domain for loop testing, "test" is reserved by RFC 2606 and won't therefore clash */
+#define LOOP_TEST_TYPE T_TXT
/* compile-time options: uncomment below to enable or do eg.
make COPTS=-DHAVE_BROKEN_RTC
@@ -108,6 +110,10 @@
HAVE_DNSSEC
include DNSSEC validator.
+HAVE_LOOP
+ include functionality to probe for and remove DNS forwarding loops.
+
+
NO_IPV6
NO_TFTP
NO_DHCP
@@ -148,6 +154,7 @@
#define HAVE_SCRIPT
#define HAVE_AUTH
#define HAVE_IPSET
+#define HAVE_LOOP
/* Build options which require external libraries.
@@ -342,6 +349,10 @@
#undef HAVE_IPSET
#endif
+#ifdef NO_LOOP
+#undef HAVE_LOOP
+#endif
+
/* Define a string indicating which options are in use.
DNSMASQP_COMPILE_OPTS is only defined in dnsmasq.c */
@@ -411,7 +422,11 @@
#ifndef HAVE_DNSSEC
"no-"
#endif
-"DNSSEC";
+"DNSSEC "
+#ifndef HAVE_LOOP
+"no-"
+#endif
+"loop-detect";
#endif
diff --git a/src/dnsmasq.c b/src/dnsmasq.c
index 8b375de..f4a89fc 100644
--- a/src/dnsmasq.c
+++ b/src/dnsmasq.c
@@ -80,7 +80,9 @@
sigaction(SIGPIPE, &sigact, NULL);
umask(022); /* known umask, create leases and pid files as 0644 */
-
+
+ rand_init(); /* Must precede read_opts() */
+
read_opts(argc, argv, compile_opts);
if (daemon->edns_pktsz < PACKETSZ)
@@ -186,7 +188,10 @@
die(_("authoritative DNS not available: set HAVE_AUTH in src/config.h"), NULL, EC_BADCONF);
#endif
- rand_init();
+#ifndef HAVE_LOOP
+ if (option_bool(OPT_LOOP_DETECT))
+ die(_("Loop detection not available: set HAVE_LOOP in src/config.h"), NULL, EC_BADCONF);
+#endif
now = dnsmasq_time();
diff --git a/src/dnsmasq.h b/src/dnsmasq.h
index e70d10a..a1ac1d1 100644
--- a/src/dnsmasq.h
+++ b/src/dnsmasq.h
@@ -237,7 +237,8 @@
#define OPT_DNSSEC_DEBUG 47
#define OPT_DNSSEC_NO_SIGN 48
#define OPT_LOCAL_SERVICE 49
-#define OPT_LAST 50
+#define OPT_LOOP_DETECT 50
+#define OPT_LAST 51
/* extra flags for my_syslog, we use a couple of facilities since they are known
not to occupy the same bits as priorities, no matter how syslog.h is set up. */
@@ -478,6 +479,7 @@
#define SERV_USE_RESOLV 1024 /* forward this domain in the normal way */
#define SERV_NO_REBIND 2048 /* inhibit dns-rebind protection */
#define SERV_FROM_FILE 4096 /* read from --servers-file */
+#define SERV_LOOP 8192 /* server causes forwarding loop */
struct serverfd {
int fd;
@@ -498,6 +500,9 @@
char *domain; /* set if this server only handles a domain. */
int flags, tcpfd;
unsigned int queries, failed_queries;
+#ifdef HAVE_LOOP
+ u32 uid;
+#endif
struct server *next;
};
@@ -1123,6 +1128,7 @@
/* util.c */
void rand_init(void);
unsigned short rand16(void);
+u32 rand32(void);
u64 rand64(void);
int legal_hostname(char *c);
char *canonicalise(char *s, int *nomem);
@@ -1188,6 +1194,8 @@
union mysockaddr *to, struct all_addr *source,
unsigned int iface);
void resend_query();
+struct randfd *allocate_rfd(int family);
+void free_rfd(struct randfd *rfd);
/* network.c */
int indextoname(int fd, int index, char *name);
@@ -1453,3 +1461,10 @@
time_t periodic_slaac(time_t now, struct dhcp_lease *leases);
void slaac_ping_reply(struct in6_addr *sender, unsigned char *packet, char *interface, struct dhcp_lease *leases);
#endif
+
+/* loop.c */
+#ifdef HAVE_LOOP
+void loop_send_probes();
+int detect_loop(char *query, int type);
+#endif
+
diff --git a/src/forward.c b/src/forward.c
index 1a657bb..3afd1b1 100644
--- a/src/forward.c
+++ b/src/forward.c
@@ -22,7 +22,6 @@
void *hash);
static unsigned short get_id(void);
static void free_frec(struct frec *f);
-static struct randfd *allocate_rfd(int family);
#ifdef HAVE_DNSSEC
static int tcp_key_recurse(time_t now, int status, struct dns_header *header, size_t n,
@@ -427,7 +426,7 @@
if (type == (start->flags & SERV_TYPE) &&
(type != SERV_HAS_DOMAIN || hostname_isequal(domain, start->domain)) &&
- !(start->flags & SERV_LITERAL_ADDRESS))
+ !(start->flags & (SERV_LITERAL_ADDRESS | SERV_LOOP)))
{
int fd;
@@ -1271,6 +1270,12 @@
break;
}
#endif
+
+#ifdef HAVE_LOOP
+ /* Check for forwarding loop */
+ if (detect_loop(daemon->namebuff, type))
+ return;
+#endif
}
#ifdef HAVE_AUTH
@@ -1782,7 +1787,8 @@
/* server for wrong domain */
if (type != (last_server->flags & SERV_TYPE) ||
- (type == SERV_HAS_DOMAIN && !hostname_isequal(domain, last_server->domain)))
+ (type == SERV_HAS_DOMAIN && !hostname_isequal(domain, last_server->domain)) ||
+ (last_server->flags & (SERV_LITERAL_ADDRESS | SERV_LOOP)))
continue;
if (last_server->tcpfd == -1)
@@ -1958,7 +1964,7 @@
return f;
}
-static struct randfd *allocate_rfd(int family)
+struct randfd *allocate_rfd(int family)
{
static int finger = 0;
int i;
@@ -1993,19 +1999,22 @@
return NULL; /* doom */
}
+
+void free_rfd(struct randfd *rfd)
+{
+ if (rfd && --(rfd->refcount) == 0)
+ close(rfd->fd);
+}
+
static void free_frec(struct frec *f)
{
- if (f->rfd4 && --(f->rfd4->refcount) == 0)
- close(f->rfd4->fd);
-
+ free_rfd(f->rfd4);
f->rfd4 = NULL;
f->sentto = NULL;
f->flags = 0;
#ifdef HAVE_IPV6
- if (f->rfd6 && --(f->rfd6->refcount) == 0)
- close(f->rfd6->fd);
-
+ free_rfd(f->rfd6);
f->rfd6 = NULL;
#endif
diff --git a/src/loop.c b/src/loop.c
new file mode 100644
index 0000000..bb377ad
--- /dev/null
+++ b/src/loop.c
@@ -0,0 +1,116 @@
+/* dnsmasq is Copyright (c) 2000-2014 Simon Kelley
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; version 2 dated June, 1991, or
+ (at your option) version 3 dated 29 June, 2007.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "dnsmasq.h"
+
+#ifdef HAVE_LOOP
+static ssize_t loop_make_probe(u32 uid);
+
+void loop_send_probes()
+{
+ struct server *serv;
+
+ if (!option_bool(OPT_LOOP_DETECT))
+ return;
+
+ /* Loop through all upstream servers not for particular domains, and send a query to that server which is
+ identifiable, via the uid. If we see that query back again, then the server is looping, and we should not use it. */
+ for (serv = daemon->servers; serv; serv = serv->next)
+ if (!(serv->flags &
+ (SERV_LITERAL_ADDRESS | SERV_NO_ADDR | SERV_USE_RESOLV | SERV_NO_REBIND | SERV_HAS_DOMAIN | SERV_FOR_NODOTS | SERV_LOOP)))
+ {
+ ssize_t len = loop_make_probe(serv->uid);
+ int fd;
+ struct randfd *rfd = NULL;
+
+ if (serv->sfd)
+ fd = serv->sfd->fd;
+ else
+ {
+ if (!(rfd = allocate_rfd(serv->addr.sa.sa_family)))
+ continue;
+ fd = rfd->fd;
+ }
+
+ while (sendto(fd, daemon->packet, len, 0, &serv->addr.sa, sa_len(&serv->addr)) == -1 && retry_send());
+
+ free_rfd(rfd);
+ }
+}
+
+static ssize_t loop_make_probe(u32 uid)
+{
+ struct dns_header *header = (struct dns_header *)daemon->packet;
+ unsigned char *p = (unsigned char *)(header+1);
+
+ /* packet buffer overwritten */
+ daemon->srv_save = NULL;
+
+ header->id = rand16();
+ header->ancount = header->nscount = header->arcount = htons(0);
+ header->qdcount = htons(1);
+ header->hb3 = HB3_RD;
+ header->hb4 = 0;
+ SET_OPCODE(header, QUERY);
+
+ *p++ = 8;
+ sprintf((char *)p, "%.8x", uid);
+ p += 8;
+ *p++ = strlen(LOOP_TEST_DOMAIN);
+ strcpy((char *)p, LOOP_TEST_DOMAIN); /* Add terminating zero */
+ p += strlen(LOOP_TEST_DOMAIN) + 1;
+
+ PUTSHORT(LOOP_TEST_TYPE, p);
+ PUTSHORT(C_IN, p);
+
+ return p - (unsigned char *)header;
+}
+
+
+int detect_loop(char *query, int type)
+{
+ int i;
+ u32 uid;
+ struct server *serv;
+
+ if (!option_bool(OPT_LOOP_DETECT))
+ return 0;
+
+ if (type != LOOP_TEST_TYPE ||
+ strlen(LOOP_TEST_DOMAIN) + 9 != strlen(query) ||
+ strstr(query, LOOP_TEST_DOMAIN) != query + 9)
+ return 0;
+
+ for (i = 0; i < 8; i++)
+ if (!isxdigit(query[i]))
+ return 0;
+
+ uid = strtol(query, NULL, 16);
+
+ for (serv = daemon->servers; serv; serv = serv->next)
+ if (!(serv->flags &
+ (SERV_LITERAL_ADDRESS | SERV_NO_ADDR | SERV_USE_RESOLV | SERV_NO_REBIND | SERV_HAS_DOMAIN | SERV_FOR_NODOTS | SERV_LOOP)) &&
+ uid == serv->uid)
+ {
+ serv->flags |= SERV_LOOP;
+ check_servers(); /* log new state */
+ return 1;
+ }
+
+ return 0;
+}
+
+#endif
diff --git a/src/network.c b/src/network.c
index b188f50..9c46b8a 100644
--- a/src/network.c
+++ b/src/network.c
@@ -1298,7 +1298,13 @@
/* mark everything with argument flag */
for (serv = daemon->servers; serv; serv = serv->next)
if (serv->flags & flag)
- serv->flags |= SERV_MARK;
+ {
+ serv->flags |= SERV_MARK;
+#ifdef HAVE_LOOP
+ /* Give looped servers another chance */
+ serv->flags &= ~SERV_LOOP;
+#endif
+ }
}
void cleanup_servers(void)
@@ -1320,6 +1326,11 @@
else
up = &serv->next;
}
+
+#ifdef HAVE_LOOP
+ /* Now we have a new set of servers, test for loops. */
+ loop_send_probes();
+#endif
}
void add_update_server(int flags,
@@ -1385,7 +1396,10 @@
serv->domain = domain_str;
serv->next = next;
serv->queries = serv->failed_queries = 0;
-
+#ifdef HAVE_LOOP
+ serv->uid = rand32();
+#endif
+
if (domain)
serv->flags |= SERV_HAS_DOMAIN;
@@ -1464,6 +1478,10 @@
else if (!(serv->flags & SERV_LITERAL_ADDRESS))
my_syslog(LOG_INFO, _("using nameserver %s#%d for %s %s"), daemon->namebuff, port, s1, s2);
}
+#ifdef HAVE_LOOP
+ else if (serv->flags & SERV_LOOP)
+ my_syslog(LOG_INFO, _("NOT using nameserver %s#%d - query loop detected"), daemon->namebuff, port);
+#endif
else if (serv->interface[0] != 0)
my_syslog(LOG_INFO, _("using nameserver %s#%d(via %s)"), daemon->namebuff, port, serv->interface);
else
diff --git a/src/option.c b/src/option.c
index a0a6e46..07e7d36 100644
--- a/src/option.c
+++ b/src/option.c
@@ -146,6 +146,7 @@
#define LOPT_DNSSEC_CHECK 334
#define LOPT_LOCAL_SERVICE 335
#define LOPT_DNSSEC_TIME 336
+#define LOPT_LOOP_DETECT 337
#ifdef HAVE_GETOPT_LONG
static const struct option opts[] =
@@ -297,6 +298,7 @@
{ "quiet-dhcp", 0, 0, LOPT_QUIET_DHCP },
{ "quiet-dhcp6", 0, 0, LOPT_QUIET_DHCP6 },
{ "quiet-ra", 0, 0, LOPT_QUIET_RA },
+ { "dns-loop-detect", 0, 0, LOPT_LOOP_DETECT },
{ NULL, 0, 0, 0 }
};
@@ -454,6 +456,7 @@
{ LOPT_QUIET_DHCP6, OPT_QUIET_DHCP6, NULL, gettext_noop("Do not log routine DHCPv6."), NULL },
{ LOPT_QUIET_RA, OPT_QUIET_RA, NULL, gettext_noop("Do not log RA."), NULL },
{ LOPT_LOCAL_SERVICE, OPT_LOCAL_SERVICE, NULL, gettext_noop("Accept queries only from directly-connected networks"), NULL },
+ { LOPT_LOOP_DETECT, OPT_LOOP_DETECT, NULL, gettext_noop("Detect and remove DNS forwarding loops"), NULL },
{ 0, 0, NULL, NULL, NULL }
};
@@ -2171,6 +2174,9 @@
{
newlist = opt_malloc(sizeof(struct server));
memset(newlist, 0, sizeof(struct server));
+#ifdef HAVE_LOOP
+ newlist->uid = rand32();
+#endif
}
if (servers_only && option == 'S')
@@ -4269,6 +4275,7 @@
daemon->soa_refresh = SOA_REFRESH;
daemon->soa_retry = SOA_RETRY;
daemon->soa_expiry = SOA_EXPIRY;
+
add_txt("version.bind", "dnsmasq-" VERSION, 0 );
add_txt("authors.bind", "Simon Kelley", 0);
add_txt("copyright.bind", COPYRIGHT, 0);
diff --git a/src/util.c b/src/util.c
index 660347f..df751c7 100644
--- a/src/util.c
+++ b/src/util.c
@@ -81,6 +81,18 @@
return (unsigned short) out[--outleft];
}
+u32 rand32(void)
+{
+ if (!outleft)
+ {
+ if (!++in[0]) if (!++in[1]) if (!++in[2]) ++in[3];
+ surf();
+ outleft = 8;
+ }
+
+ return out[--outleft];
+}
+
u64 rand64(void)
{
static int outleft = 0;