resolv: Implement faking A and AAAA DNS replies
authorJakub Hrozek <jakub.hrozek@gmail.com>
Sun, 14 Sep 2014 09:12:56 +0000 (11:12 +0200)
committerMichael Adam <obnox@samba.org>
Tue, 21 Oct 2014 11:39:39 +0000 (13:39 +0200)
Adds the possibility of faking a DNS reply based on a hosts-like file.

Signed-off-by: Jakub Hrozek <jakub.hrozek@gmail.com>
Reviewed-by: Andreas Schneider <asn@samba.org>
Reviewed-by: Michael Adam <obnox@samba.org>
doc/resolv_wrapper.1.txt
src/resolv_wrapper.c
tests/CMakeLists.txt
tests/fake_hosts.in [new file with mode: 0644]
tests/test_dns_fake.c [new file with mode: 0644]

index 132ec36761c04524306f44ac8a5e52ba2e9489e9..c04c7c8e36d9b60ee4e1d667bb9a370464e1e5f1 100644 (file)
@@ -40,6 +40,15 @@ debug symbols.
 - 2 = DEBUG
 - 3 = TRACE
 
+*RESOLV_WRAPPER_HOSTS*::
+
+This environment variable is used for DNS faking. It must point to a
+hosts-like text file that specifies fake records for custom queries. The
+format of the file is:
+TYPE   RECORD_NAME RECORD_VALUE
+For example:
+A      www.cwrap.org   127.0.0.10
+
 EXAMPLE
 -------
 
index b687551337f822e32fe9befa82b02b19e23b6f80..8fd672970924b364ada7d6015224e734f88f2b51 100644 (file)
@@ -1,5 +1,6 @@
 /*
  * Copyright (c) 2014      Andreas Schneider <asn@samba.org>
+ * Copyright (c) 2014      Jakub Hrozek <jakub.hrozek@gmail.com>
  *
  * All rights reserved.
  *
 #define DESTRUCTOR_ATTRIBUTE
 #endif /* HAVE_DESTRUCTOR_ATTRIBUTE */
 
+#ifndef RWRAP_DEFAULT_FAKE_TTL
+#define RWRAP_DEFAULT_FAKE_TTL 600
+#endif  /* RWRAP_DEFAULT_FAKE_TTL */
+
 enum rwrap_dbglvl_e {
        RWRAP_LOG_ERROR = 0,
        RWRAP_LOG_WARN,
@@ -120,6 +125,327 @@ static void rwrap_log(enum rwrap_dbglvl_e dbglvl,
 }
 #endif /* NDEBUG RWRAP_LOG */
 
+
+/* Prepares a fake header with a single response. Advances header_blob */
+static ssize_t rwrap_fake_header(uint8_t **header_blob, size_t remaining,
+                                size_t rdata_size)
+{
+       uint8_t *hb;
+       HEADER *h;
+       int answers;
+
+       /* If rdata_size is zero, the answer is empty */
+       answers = rdata_size > 0 ? 1 : 0;
+
+       if (remaining < NS_HFIXEDSZ) {
+               RWRAP_LOG(RWRAP_LOG_ERROR, "Buffer too small!\n");
+               return -1;
+       }
+
+       hb = *header_blob;
+       memset(hb, 0, NS_HFIXEDSZ);
+
+       h = (HEADER *) hb;
+       h->id = res_randomid();         /* random query ID */
+       h->qr = htons(1);               /* response flag */
+       h->rd = htons(1);               /* recursion desired */
+       h->ra = htons(1);               /* resursion available */
+
+       h->qdcount = htons(1);          /* no. of questions */
+       h->ancount = htons(answers);    /* no. of answers */
+
+       hb += NS_HFIXEDSZ;              /* move past the header */
+       *header_blob = hb;
+
+       return NS_HFIXEDSZ;
+}
+
+static ssize_t rwrap_fake_question(const char *question,
+                                  uint16_t type,
+                                  uint8_t **question_ptr,
+                                  size_t remaining)
+{
+       uint8_t *qb = *question_ptr;
+       int n;
+
+       n = ns_name_compress(question, qb, remaining, NULL, NULL);
+       if (n < 0) {
+               RWRAP_LOG(RWRAP_LOG_ERROR,
+                         "Failed to compress [%s]\n", question);
+               return -1;
+       }
+
+       qb += n;
+       remaining -= n;
+
+       if (remaining < 2 * sizeof(uint16_t)) {
+               RWRAP_LOG(RWRAP_LOG_ERROR, "Buffer too small!\n");
+               return -1;
+       }
+
+       NS_PUT16(type, qb);
+       NS_PUT16(ns_c_in, qb);
+
+       *question_ptr = qb;
+       return n + 2 * sizeof(uint16_t);
+}
+
+static ssize_t rwrap_fake_rdata_common(uint16_t type,
+                                      size_t rdata_size,
+                                      const char *key,
+                                      size_t remaining,
+                                      uint8_t **rdata_ptr)
+{
+       uint8_t *rd = *rdata_ptr;
+       ssize_t written = 0;
+
+       written = ns_name_compress(key, rd, remaining, NULL, NULL);
+       if (written < 0) {
+               RWRAP_LOG(RWRAP_LOG_ERROR,
+                         "Failed to compress [%s]\n", key);
+               return -1;
+       }
+       rd += written;
+       remaining -= written;
+
+       if (remaining < 3 * sizeof(uint16_t) + sizeof(uint32_t)) {
+               RWRAP_LOG(RWRAP_LOG_ERROR, "Buffer too small\n");
+               return -1;
+       }
+
+       NS_PUT16(type, rd);
+       NS_PUT16(ns_c_in, rd);
+       NS_PUT32(RWRAP_DEFAULT_FAKE_TTL, rd);
+       NS_PUT16(rdata_size, rd);
+
+       if (remaining < rdata_size) {
+               RWRAP_LOG(RWRAP_LOG_ERROR, "Buffer too small\n");
+               return -1;
+       }
+
+       *rdata_ptr = rd;
+       return written + 3 * sizeof(uint16_t) + sizeof(uint32_t);
+}
+
+static ssize_t rwrap_fake_common(uint16_t type,
+                                const char *question,
+                                size_t rdata_size,
+                                uint8_t **answer_ptr,
+                                size_t anslen)
+{
+       uint8_t *a = *answer_ptr;
+       ssize_t written;
+       size_t remaining;
+
+       remaining = anslen;
+
+       written = rwrap_fake_header(&a, remaining, rdata_size);
+       if (written < 0) {
+               return -1;
+       }
+       remaining -= written;
+
+       written = rwrap_fake_question(question, type, &a, remaining);
+       if (written < 0) {
+               return -1;
+       }
+       remaining -= written;
+
+       /* rdata_size = 0 denotes an empty answer */
+       if (rdata_size > 0) {
+               written = rwrap_fake_rdata_common(type, rdata_size, question,
+                                               remaining, &a);
+               if (written < 0) {
+                       return -1;
+               }
+       }
+
+       *answer_ptr = a;
+       return written;
+}
+
+static int rwrap_fake_a(const char *key,
+                       const char *value,
+                       uint8_t *answer_ptr,
+                       size_t anslen)
+{
+       uint8_t *a = answer_ptr;
+       struct in_addr a_rec;
+       int rc;
+       int ok;
+
+       if (value == NULL) {
+               RWRAP_LOG(RWRAP_LOG_ERROR, "Malformed record, no value!\n");
+               return -1;
+       }
+
+       rc = rwrap_fake_common(ns_t_a, key, sizeof(a_rec), &a, anslen);
+       if (rc < 0) {
+               return -1;
+       }
+
+       ok = inet_pton(AF_INET, value, &a_rec);
+       if (!ok) {
+               RWRAP_LOG(RWRAP_LOG_ERROR,
+                         "Failed to convert [%s] to binary\n", value);
+               return -1;
+       }
+       memcpy(a, &a_rec, sizeof(struct in_addr));
+
+       return 0;
+}
+
+static int rwrap_fake_aaaa(const char *key,
+                          const char *value,
+                          uint8_t *answer,
+                          size_t anslen)
+{
+       uint8_t *a = answer;
+       struct in6_addr aaaa_rec;
+       int rc;
+       int ok;
+
+       if (value == NULL) {
+               RWRAP_LOG(RWRAP_LOG_ERROR, "Malformed record, no value!\n");
+               return -1;
+       }
+
+       rc = rwrap_fake_common(ns_t_aaaa, key, sizeof(aaaa_rec), &a, anslen);
+       if (rc < 0) {
+               return -1;
+       }
+
+       ok = inet_pton(AF_INET6, value, &aaaa_rec);
+       if (!ok) {
+               RWRAP_LOG(RWRAP_LOG_ERROR,
+                         "Failed to convert [%s] to binary\n", value);
+               return -1;
+       }
+       memcpy(a, &aaaa_rec, sizeof(struct in6_addr));
+
+       return 0;
+}
+
+static int rwrap_fake_empty_query(const char *key,
+                                 uint16_t type,
+                                 uint8_t *answer,
+                                 size_t anslen)
+{
+       int rc;
+
+       rc = rwrap_fake_common(type, key, 0, &answer, anslen);
+       if (rc < 0) {
+               return -1;
+       }
+
+       return 0;
+}
+
+#define RESOLV_MATCH(line, name) \
+       (strncmp(line, name, sizeof(name) - 1) == 0 && \
+       (line[sizeof(name) - 1] == ' ' || \
+        line[sizeof(name) - 1] == '\t'))
+
+#define NEXT_KEY(buf, key) do {                        \
+       (key) = strpbrk(buf, " \t");            \
+       if ((key) != NULL) {                    \
+               (key)[0] = '\0';                \
+               (key)++;                        \
+       }                                       \
+       while ((key) != NULL                    \
+              && (isblank((int)(key)[0]))) {   \
+               (key)++;                        \
+       }                                       \
+} while(0);
+
+#define TYPE_MATCH(type, ns_type, rec_type, str_type, key, query) \
+       ((type) == (ns_type) && \
+        (strncmp((rec_type), (str_type), sizeof(str_type)) == 0) && \
+        (strcmp(key, query)) == 0)
+
+
+/* Reads in a file in the following format:
+ * TYPE RDATA
+ *
+ * Malformed entried are silently skipped.
+ * Allocates answer buffer of size anslen that has to be freed after use.
+ */
+static int rwrap_res_fake_hosts(const char *hostfile,
+                               const char *query,
+                               int type,
+                               unsigned char *answer,
+                               size_t anslen)
+{
+       FILE *fp = NULL;
+       char buf[BUFSIZ];
+       int rc = ENOENT;
+       char *key = NULL;
+       char *value = NULL;
+
+       RWRAP_LOG(RWRAP_LOG_TRACE,
+                 "Searching in fake hosts file %s\n", hostfile);
+
+       fp = fopen(hostfile, "r");
+       if (fp == NULL) {
+               RWRAP_LOG(RWRAP_LOG_ERROR,
+                         "Opening %s failed: %s",
+                         hostfile, strerror(errno));
+               return -1;
+       }
+
+       while (fgets(buf, sizeof(buf), fp) != NULL) {
+               char *rec_type;
+               char *q;
+
+               rec_type = buf;
+               key = value = NULL;
+
+               NEXT_KEY(rec_type, key);
+               NEXT_KEY(key, value);
+
+               q = value;
+               while(q[0] != '\n' && q[0] != '\0') {
+                       q++;
+               }
+               q[0] = '\0';
+
+               if (key == NULL || value == NULL) {
+                       RWRAP_LOG(RWRAP_LOG_WARN,
+                               "Malformed line: not enough parts, use \"rec_type key data\n"
+                               "For example \"A cwrap.org 10.10.10.10\"");
+                       continue;
+               }
+
+               if (TYPE_MATCH(type, ns_t_a, rec_type, "A", key, query)) {
+                       rc = rwrap_fake_a(key, value, answer, anslen);
+                       break;
+               } else if (TYPE_MATCH(type, ns_t_aaaa,
+                                     rec_type, "AAAA", key, query)) {
+                       rc = rwrap_fake_aaaa(key, value, answer, anslen);
+                       break;
+               }
+       }
+
+       switch (rc) {
+       case 0:
+               RWRAP_LOG(RWRAP_LOG_TRACE,
+                               "Successfully faked answer for [%s]\n", query);
+               break;
+       case -1:
+               RWRAP_LOG(RWRAP_LOG_ERROR,
+                               "Error faking answer for [%s]\n", query);
+               break;
+       case ENOENT:
+               RWRAP_LOG(RWRAP_LOG_TRACE,
+                               "Record for [%s] not found\n", query);
+               rc = rwrap_fake_empty_query(key, type, answer, anslen);
+               break;
+       }
+
+       fclose(fp);
+       return rc;
+}
+
 /*********************************************************
  * RWRAP LOADING LIBC FUNCTIONS
  *********************************************************/
@@ -408,11 +734,6 @@ static int libc_res_nsearch(struct __res_state *state,
  *   RES_HELPER
  ***************************************************************************/
 
-#define RESOLV_MATCH(line, name) \
-       (strncmp(line, name, sizeof(name) - 1) == 0 && \
-       (line[sizeof(name) - 1] == ' ' || \
-        line[sizeof(name) - 1] == '\t'))
-
 static int rwrap_parse_resolv_conf(struct __res_state *state,
                                   const char *resolv_conf)
 {
@@ -636,6 +957,7 @@ static int rwrap_res_nquery(struct __res_state *state,
                            int anslen)
 {
        int rc;
+       const char *fake_hosts;
 #ifndef NDEBUG
        int i;
 #endif
@@ -654,7 +976,13 @@ static int rwrap_res_nquery(struct __res_state *state,
        }
 #endif
 
-       rc = libc_res_nquery(state, dname, class, type, answer, anslen);
+       fake_hosts = getenv("RESOLV_WRAPPER_HOSTS");
+       if (fake_hosts != NULL) {
+               rc = rwrap_res_fake_hosts(fake_hosts, dname, type, answer, anslen);
+       } else {
+               rc = libc_res_nquery(state, dname, class, type, answer, anslen);
+       }
+
 
        RWRAP_LOG(RWRAP_LOG_TRACE,
                  "The returned response length is: %d",
@@ -738,6 +1066,7 @@ static int rwrap_res_nsearch(struct __res_state *state,
                             int anslen)
 {
        int rc;
+       const char *fake_hosts;
 #ifndef NDEBUG
        int i;
 #endif
@@ -756,7 +1085,12 @@ static int rwrap_res_nsearch(struct __res_state *state,
        }
 #endif
 
-       rc = libc_res_nsearch(state, dname, class, type, answer, anslen);
+       fake_hosts = getenv("RESOLV_WRAPPER_HOSTS");
+       if (fake_hosts != NULL) {
+               rc = rwrap_res_fake_hosts(fake_hosts, dname, type, answer, anslen);
+       } else {
+               rc = libc_res_nsearch(state, dname, class, type, answer, anslen);
+       }
 
        RWRAP_LOG(RWRAP_LOG_TRACE,
                  "The returned response length is: %d",
index 06f4903b5d2c37c4a38d792229d181038f89eb37..acd675f214518337075f65438963b8597533993a 100644 (file)
@@ -13,6 +13,8 @@ set(TORTURE_LIBRARY torture)
 add_executable(dns_srv dns_srv.c)
 target_link_libraries(dns_srv ${RWRAP_REQUIRED_LIBRARIES})
 
+configure_file(fake_hosts.in ${CMAKE_CURRENT_BINARY_DIR}/fake_hosts @ONLY)
+
 add_library(${TORTURE_LIBRARY} STATIC torture.c)
 target_link_libraries(${TORTURE_LIBRARY}
     ${CMOCKA_LIBRARY}
@@ -53,3 +55,18 @@ foreach(_RWRAP_TEST ${RWRAP_TESTS})
                 ENVIRONMENT LD_PRELOAD=${PRELOAD_LIBS})
     endif()
 endforeach()
+
+add_cmocka_test(test_dns_fake test_dns_fake.c ${TORTURE_LIBRARY} ${TESTSUITE_LIBRARIES})
+if (OSX)
+    set_property(
+        TEST
+            test_dns_fake
+        PROPERTY
+        ENVIRONMENT DYLD_FORCE_FLAT_NAMESPACE=1;DYLD_INSERT_LIBRARIES=${PRELOAD_LIBS};RESOLV_WRAPPER_HOSTS=${CMAKE_CURRENT_BINARY_DIR}/fake_hosts)
+else ()
+    set_property(
+        TEST
+            test_dns_fake
+        PROPERTY
+            ENVIRONMENT LD_PRELOAD=${PRELOAD_LIBS};RESOLV_WRAPPER_HOSTS=${CMAKE_CURRENT_BINARY_DIR}/fake_hosts)
+endif ()
diff --git a/tests/fake_hosts.in b/tests/fake_hosts.in
new file mode 100644 (file)
index 0000000..4697b7b
--- /dev/null
@@ -0,0 +1,2 @@
+A cwrap.org 127.0.0.21
+AAAA cwrap6.org 2a00:1450:4013:c01::63
diff --git a/tests/test_dns_fake.c b/tests/test_dns_fake.c
new file mode 100644 (file)
index 0000000..49d685c
--- /dev/null
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) Jakub Hrozek 2014 <jakub.hrozek@gmail.com>
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the author nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <setjmp.h>
+#include <cmocka.h>
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <netinet/in.h>
+#include <arpa/nameser.h>
+#include <arpa/inet.h>
+#include <resolv.h>
+
+#define ANSIZE 256
+
+static void test_res_fake_a_query(void **state)
+{
+       int rv;
+       struct __res_state dnsstate;
+       unsigned char answer[ANSIZE];
+       char addr[INET_ADDRSTRLEN];
+       ns_msg handle;
+       ns_rr rr;   /* expanded resource record */
+
+       (void) state; /* unused */
+
+       memset(&dnsstate, 0, sizeof(struct __res_state));
+       rv = res_ninit(&dnsstate);
+       assert_int_equal(rv, 0);
+
+       rv = res_nquery(&dnsstate, "cwrap.org", ns_c_in, ns_t_a,
+                       answer, ANSIZE);
+       assert_int_not_equal(rv, -1);
+
+       ns_initparse(answer, 256, &handle);
+       /* The query must finish w/o an error, have one answer and the answer
+        * must be a parseable RR of type A and have the address that our
+        * fake hosts file contains
+        */
+       assert_int_equal(ns_msg_getflag(handle, ns_f_rcode), ns_r_noerror);
+       assert_int_equal(ns_msg_count(handle, ns_s_an), 1);
+       assert_int_equal(ns_parserr(&handle, ns_s_an, 0, &rr), 0);
+       assert_int_equal(ns_rr_type(rr), ns_t_a);
+       assert_non_null(inet_ntop(AF_INET, ns_rr_rdata(rr), addr, 256));
+       assert_string_equal(addr, "127.0.0.21");
+}
+
+static void test_res_fake_a_query_notfound(void **state)
+{
+       int rv;
+       struct __res_state dnsstate;
+       unsigned char answer[ANSIZE];
+       ns_msg handle;
+
+       (void) state; /* unused */
+
+       memset(&dnsstate, 0, sizeof(struct __res_state));
+       rv = res_ninit(&dnsstate);
+       assert_int_equal(rv, 0);
+
+       rv = res_nquery(&dnsstate, "nosuchentry.org", ns_c_in, ns_t_a,
+                       answer, ANSIZE);
+       assert_int_not_equal(rv, -1);
+
+       ns_initparse(answer, 256, &handle);
+       /* The query must finish w/o an error and have no answer */
+       assert_int_equal(ns_msg_getflag(handle, ns_f_rcode), ns_r_noerror);
+       assert_int_equal(ns_msg_count(handle, ns_s_an), 0);
+}
+
+static void test_res_fake_aaaa_query(void **state)
+{
+       int rv;
+       struct __res_state dnsstate;
+       unsigned char answer[ANSIZE];
+       char addr[INET6_ADDRSTRLEN];
+       ns_msg handle;
+       ns_rr rr;   /* expanded resource record */
+
+       (void) state; /* unused */
+
+       memset(&dnsstate, 0, sizeof(struct __res_state));
+       rv = res_ninit(&dnsstate);
+       assert_int_equal(rv, 0);
+
+       rv = res_nquery(&dnsstate, "cwrap6.org", ns_c_in, ns_t_aaaa,
+                       answer, ANSIZE);
+       assert_int_not_equal(rv, -1);
+
+       ns_initparse(answer, 256, &handle);
+       /* The query must finish w/o an error, have one answer and the answer
+        * must be a parseable RR of type AAAA and have the address that our
+        * fake hosts file contains
+        */
+       assert_int_equal(ns_msg_getflag(handle, ns_f_rcode), ns_r_noerror);
+       assert_int_equal(ns_msg_count(handle, ns_s_an), 1);
+       assert_int_equal(ns_parserr(&handle, ns_s_an, 0, &rr), 0);
+       assert_int_equal(ns_rr_type(rr), ns_t_aaaa);
+       assert_non_null(inet_ntop(AF_INET6, ns_rr_rdata(rr), addr, 256));
+       assert_string_equal(addr, "2a00:1450:4013:c01::63");
+}
+
+static void test_res_fake_aaaa_query_notfound(void **state)
+{
+       int rv;
+       struct __res_state dnsstate;
+       unsigned char answer[ANSIZE];
+       ns_msg handle;
+
+       (void) state; /* unused */
+
+       memset(&dnsstate, 0, sizeof(struct __res_state));
+       rv = res_ninit(&dnsstate);
+       assert_int_equal(rv, 0);
+
+       rv = res_nquery(&dnsstate, "nosuchentry.org", ns_c_in, ns_t_aaaa,
+                       answer, ANSIZE);
+       assert_int_not_equal(rv, -1);
+
+       ns_initparse(answer, 256, &handle);
+       /* The query must finish w/o an error and have no answer */
+       assert_int_equal(ns_msg_getflag(handle, ns_f_rcode), ns_r_noerror);
+       assert_int_equal(ns_msg_count(handle, ns_s_an), 0);
+}
+
+int main(void)
+{
+       int rc;
+
+       const UnitTest tests[] = {
+               unit_test(test_res_fake_a_query),
+               unit_test(test_res_fake_a_query_notfound),
+               unit_test(test_res_fake_aaaa_query),
+               unit_test(test_res_fake_aaaa_query_notfound),
+       };
+
+       rc = run_tests(tests);
+
+       return rc;
+}