]> git.ipfire.org Git - people/ms/dnsmasq.git/commitdiff
Add --dns-loop-detect feature.
authorSimon Kelley <simon@thekelleys.org.uk>
Tue, 29 Jul 2014 15:34:14 +0000 (16:34 +0100)
committerSimon Kelley <simon@thekelleys.org.uk>
Tue, 29 Jul 2014 15:34:14 +0000 (16:34 +0100)
12 files changed:
CHANGELOG
Makefile
bld/Android.mk
man/dnsmasq.8
src/config.h
src/dnsmasq.c
src/dnsmasq.h
src/forward.c
src/loop.c [new file with mode: 0644]
src/network.c
src/option.c
src/util.c

index 5b7dfb852d1925813933ed291ba690773ca2a698..62f2e4a845134458d70e7f074e7911b889aa2d31 100644 (file)
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -20,6 +20,13 @@ version 2.72
            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 
index 17eeb27b1b32b10609c4207337d3f6bb32937408..58a7975f60b5b50cd850efa2df9daa71bbdcc21d 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -69,7 +69,7 @@ objs = cache.o rfc1035.o util.o option.o forward.o network.o \
        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
index 5255ec90a4312ba302a63e3e424baf6ad954d0cc..d855094eb2649f9a0bc3855705deac1f341fa360 100644 (file)
@@ -9,7 +9,8 @@ LOCAL_SRC_FILES :=  bpf.c cache.c dbus.c dhcp.c dnsmasq.c \
                    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
 
index 0530a19772639a9095190e187754d450332ecbf5..7b4cc98965988fdcfe92ff37872e00ce45592f55 100644 (file)
@@ -334,6 +334,16 @@ it will send queries to just one server. Setting this flag forces
 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
index 87f8f8a5db30f3edfb393d8db01a105dbc937753..145820ad25107cbf002d27c3854d48434c65b9a7 100644 (file)
@@ -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_AUTH
 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 @@ RESOLVFILE
 #define HAVE_SCRIPT
 #define HAVE_AUTH
 #define HAVE_IPSET 
+#define HAVE_LOOP
 
 /* Build options which require external libraries.
    
@@ -342,6 +349,10 @@ HAVE_SOCKADDR_SA_LEN
 #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 @@ static char *compile_opts =
 #ifndef HAVE_DNSSEC
 "no-"
 #endif
-"DNSSEC";
+"DNSSEC "
+#ifndef HAVE_LOOP
+"no-"
+#endif
+"loop-detect";
 
 
 #endif
index 8b375dee42e7328a04c030fff3af440a2d257737..f4a89fc3818306a82675569f4a842b55a66840e3 100644 (file)
@@ -80,7 +80,9 @@ int main (int argc, char **argv)
   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 @@ int main (int argc, char **argv)
     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();
 
index e70d10af55631a116ae39db3e01b82bd9b694a06..a1ac1d1ddd21201f4d587adfe103b250c26c575c 100644 (file)
@@ -237,7 +237,8 @@ struct event_desc {
 #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 @@ union mysockaddr {
 #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 @@ struct server {
   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 @@ unsigned char* hash_questions(struct dns_header *header, size_t plen, char *name
 /* 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 @@ int send_from(int fd, int nowild, char *packet, size_t len,
               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 @@ void slaac_add_addrs(struct dhcp_lease *lease, time_t now, int force);
 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
+
index 1a657bbbc79bc0244c8fe97d848ef1218771a9db..3afd1b197d39ba3674967f0bb0dfb804f45e7ee6 100644 (file)
@@ -22,7 +22,6 @@ static struct frec *lookup_frec_by_sender(unsigned short id,
                                          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 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr,
          
          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 @@ void receive_query(struct listener *listen, time_t now)
              break;
            }
 #endif
+      
+#ifdef HAVE_LOOP
+      /* Check for forwarding loop */
+      if (detect_loop(daemon->namebuff, type))
+       return;
+#endif
     }
   
 #ifdef HAVE_AUTH
@@ -1782,7 +1787,8 @@ unsigned char *tcp_request(int confd, time_t now,
                      
                      /* 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 @@ static struct frec *allocate_frec(time_t now)
   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 @@ static struct randfd *allocate_rfd(int family)
 
   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 (file)
index 0000000..bb377ad
--- /dev/null
@@ -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
index b188f50ee5f044d78d367c6adb03a9c0eafcda59..9c46b8a0d137c16d2fcf24d59832a6ff2ea7eb28 100644 (file)
@@ -1298,7 +1298,13 @@ void mark_servers(int flag)
   /* 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 @@ void cleanup_servers(void)
       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 @@ void add_update_server(int flags,
       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 @@ void check_servers(void)
              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
index a0a6e46a48fee434fa2b3b4dd2df68cfba042d2f..07e7d36b2f3e884f13610098355d861eefb879d4 100644 (file)
@@ -146,6 +146,7 @@ struct myoption {
 #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 @@ static const struct myoption opts[] =
     { "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 @@ static struct {
   { 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 @@ static int one_opt(int option, char *arg, char *errstr, char *gen_err, int comma
          {
            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 @@ void read_opts(int argc, char **argv, char *compile_opts)
   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);
index 660347f09b754ceed302313b5cbc8d507dafbd48..df751c7f8ea4aad8b1d4e4ef025e39aa7c602097 100644 (file)
@@ -81,6 +81,18 @@ unsigned short rand16(void)
   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;