summaryrefslogtreecommitdiff
path: root/resolv/tst-resolv-edns.c
diff options
context:
space:
mode:
authorFlorian Weimer <fweimer@redhat.com>2017-04-13 13:09:38 +0200
committerFlorian Weimer <fweimer@redhat.com>2017-04-13 13:09:38 +0200
commite14a27723cc3a154d67f3f26e719d08c0ba9ad25 (patch)
treec4706acf27f91784a8b592772d03e0c8da0b4731 /resolv/tst-resolv-edns.c
parentc803cb9b24c6cea15698768e4301e963b98e742c (diff)
resolv: Reduce EDNS payload size to 1200 bytes [BZ #21361]
This hardens the stub resolver against fragmentation-based attacks.
Diffstat (limited to 'resolv/tst-resolv-edns.c')
-rw-r--r--resolv/tst-resolv-edns.c501
1 files changed, 501 insertions, 0 deletions
diff --git a/resolv/tst-resolv-edns.c b/resolv/tst-resolv-edns.c
new file mode 100644
index 0000000000..f17dbc3450
--- /dev/null
+++ b/resolv/tst-resolv-edns.c
@@ -0,0 +1,501 @@
+/* Test EDNS handling in the stub resolver.
+ Copyright (C) 2016-2017 Free Software Foundation, Inc.
+ This file is part of the GNU C Library.
+
+ The GNU C Library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ The GNU C Library 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
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with the GNU C Library; if not, see
+ <http://www.gnu.org/licenses/>. */
+
+#include <errno.h>
+#include <netdb.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <support/check.h>
+#include <support/resolv_test.h>
+#include <support/support.h>
+#include <support/test-driver.h>
+#include <support/xthread.h>
+
+/* Data produced by a test query. */
+struct response_data
+{
+ char *qname;
+ uint16_t qtype;
+ struct resolv_edns_info edns;
+};
+
+/* Global array used by put_response and get_response to record
+ response data. The test DNS server returns the index of the array
+ element which contains the actual response data. This enables the
+ test case to return arbitrary amounts of data with the limited
+ number of bits which fit into an IP addres.
+
+ The volatile specifier is needed because the test case accesses
+ these variables from a callback function called from a function
+ which is marked as __THROW (i.e., a leaf function which actually is
+ not). */
+static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
+static struct response_data ** volatile response_data_array;
+volatile static size_t response_data_count;
+
+/* Extract information from the query, store it in a struct
+ response_data object, and return its index in the
+ response_data_array. */
+static unsigned int
+put_response (const struct resolv_response_context *ctx,
+ const char *qname, uint16_t qtype)
+{
+ xpthread_mutex_lock (&mutex);
+ ++response_data_count;
+ /* We only can represent 2**24 indexes in 10.0.0.0/8. */
+ TEST_VERIFY (response_data_count < (1 << 24));
+ response_data_array = xrealloc
+ (response_data_array, sizeof (*response_data_array) * response_data_count);
+ unsigned int index = response_data_count - 1;
+ struct response_data *data = xmalloc (sizeof (*data));
+ *data = (struct response_data)
+ {
+ .qname = xstrdup (qname),
+ .qtype = qtype,
+ .edns = ctx->edns,
+ };
+ response_data_array[index] = data;
+ xpthread_mutex_unlock (&mutex);
+ return index;
+}
+
+/* Verify the index into the response_data array and return the data
+ at it. */
+static struct response_data *
+get_response (unsigned int index)
+{
+ xpthread_mutex_lock (&mutex);
+ TEST_VERIFY_EXIT (index < response_data_count);
+ struct response_data *result = response_data_array[index];
+ xpthread_mutex_unlock (&mutex);
+ return result;
+}
+
+/* Deallocate all response data. */
+static void
+free_response_data (void)
+{
+ xpthread_mutex_lock (&mutex);
+ size_t count = response_data_count;
+ struct response_data **array = response_data_array;
+ for (unsigned int i = 0; i < count; ++i)
+ {
+ struct response_data *data = array[i];
+ free (data->qname);
+ free (data);
+ }
+ free (array);
+ response_data_array = NULL;
+ response_data_count = 0;
+ xpthread_mutex_unlock (&mutex);
+}
+
+#define EDNS_PROBE_EXAMPLE "edns-probe.example"
+
+static void
+response (const struct resolv_response_context *ctx,
+ struct resolv_response_builder *b,
+ const char *qname, uint16_t qclass, uint16_t qtype)
+{
+ TEST_VERIFY_EXIT (qname != NULL);
+
+ /* The "tcp." prefix can be used to request TCP fallback. */
+ const char *qname_compare = qname;
+ bool force_tcp;
+ if (strncmp ("tcp.", qname_compare, strlen ("tcp.")) == 0)
+ {
+ force_tcp = true;
+ qname_compare += strlen ("tcp.");
+ }
+ else
+ force_tcp = false;
+
+ enum {edns_probe} requested_qname;
+ if (strcmp (qname_compare, EDNS_PROBE_EXAMPLE) == 0)
+ requested_qname = edns_probe;
+ else
+ {
+ support_record_failure ();
+ printf ("error: unexpected QNAME: %s\n", qname);
+ return;
+ }
+ TEST_VERIFY_EXIT (qclass == C_IN);
+ struct resolv_response_flags flags = {.tc = force_tcp && !ctx->tcp};
+ resolv_response_init (b, flags);
+ resolv_response_add_question (b, qname, qclass, qtype);
+ if (flags.tc)
+ return;
+
+ if (test_verbose)
+ printf ("info: edns=%d payload_size=%d\n",
+ ctx->edns.active, ctx->edns.payload_size);
+
+ /* Encode the response_data object in multiple address records.
+ Each record carries two bytes of payload data, and an index. */
+ resolv_response_section (b, ns_s_an);
+ switch (requested_qname)
+ {
+ case edns_probe:
+ {
+ unsigned int index = put_response (ctx, qname, qtype);
+ switch (qtype)
+ {
+ case T_A:
+ {
+ uint32_t addr = htonl (0x0a000000 | index);
+ resolv_response_open_record (b, qname, qclass, qtype, 0);
+ resolv_response_add_data (b, &addr, sizeof (addr));
+ resolv_response_close_record (b);
+ }
+ break;
+ case T_AAAA:
+ {
+ char addr[16]
+ = {0x20, 0x01, 0xd, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ index >> 16, index >> 8, index};
+ resolv_response_open_record (b, qname, qclass, qtype, 0);
+ resolv_response_add_data (b, &addr, sizeof (addr));
+ resolv_response_close_record (b);
+ }
+ }
+ }
+ break;
+ }
+}
+
+/* Update *DATA with data from ADDRESS of SIZE. Set the corresponding
+ flag in SHADOW for each byte written. */
+static struct response_data *
+decode_address (const void *address, size_t size)
+{
+ switch (size)
+ {
+ case 4:
+ TEST_VERIFY (memcmp (address, "\x0a", 1) == 0);
+ break;
+ case 16:
+ TEST_VERIFY (memcmp (address, "\x20\x01\x0d\xb8", 4) == 0);
+ break;
+ default:
+ FAIL_EXIT1 ("unexpected address size %zu", size);
+ }
+ const unsigned char *addr = address;
+ unsigned int index = addr[size - 3] * 256 * 256
+ + addr[size - 2] * 256
+ + addr[size - 1];
+ return get_response (index);
+}
+
+static struct response_data *
+decode_hostent (struct hostent *e)
+{
+ TEST_VERIFY_EXIT (e != NULL);
+ TEST_VERIFY_EXIT (e->h_addr_list[0] != NULL);
+ TEST_VERIFY (e->h_addr_list[1] == NULL);
+ return decode_address (e->h_addr_list[0], e->h_length);
+}
+
+static struct response_data *
+decode_addrinfo (struct addrinfo *ai, int family)
+{
+ struct response_data *data = NULL;
+ while (ai != NULL)
+ {
+ if (ai->ai_family == family)
+ {
+ struct response_data *new_data;
+ switch (family)
+ {
+ case AF_INET:
+ {
+ struct sockaddr_in *pin = (struct sockaddr_in *) ai->ai_addr;
+ new_data = decode_address (&pin->sin_addr.s_addr, 4);
+ }
+ break;
+ case AF_INET6:
+ {
+ struct sockaddr_in6 *pin = (struct sockaddr_in6 *) ai->ai_addr;
+ new_data = decode_address (&pin->sin6_addr.s6_addr, 16);
+ }
+ break;
+ default:
+ FAIL_EXIT1 ("invalid address family %d", ai->ai_family);
+ }
+ if (data == NULL)
+ data = new_data;
+ else
+ /* Check pointer equality because this should be the same
+ response (same index). */
+ TEST_VERIFY (data == new_data);
+ }
+ ai = ai->ai_next;
+ }
+ TEST_VERIFY_EXIT (data != NULL);
+ return data;
+}
+
+/* Updated by the main test loop in accordance with what is set in
+ _res.options. */
+static bool use_edns;
+static bool use_dnssec;
+
+/* Verify the decoded response data against the flags above. */
+static void
+verify_response_data_payload (struct response_data *data,
+ size_t expected_payload)
+{
+ bool edns = use_edns || use_dnssec;
+ TEST_VERIFY (data->edns.active == edns);
+ if (!edns)
+ expected_payload = 0;
+ if (data->edns.payload_size != expected_payload)
+ {
+ support_record_failure ();
+ printf ("error: unexpected payload size %d (edns=%d)\n",
+ (int) data->edns.payload_size, edns);
+ }
+ uint16_t expected_flags = 0;
+ if (use_dnssec)
+ expected_flags |= 0x8000; /* DO flag. */
+ if (data->edns.flags != expected_flags)
+ {
+ support_record_failure ();
+ printf ("error: unexpected EDNS flags 0x%04x (edns=%d)\n",
+ (int) data->edns.flags, edns);
+ }
+}
+
+/* Same as verify_response_data_payload, but use the default
+ payload. */
+static void
+verify_response_data (struct response_data *data)
+{
+ verify_response_data_payload (data, 1200);
+}
+
+static void
+check_hostent (struct hostent *e)
+{
+ TEST_VERIFY_EXIT (e != NULL);
+ verify_response_data (decode_hostent (e));
+}
+
+static void
+do_ai (int family)
+{
+ struct addrinfo hints = { .ai_family = family };
+ struct addrinfo *ai;
+ int ret = getaddrinfo (EDNS_PROBE_EXAMPLE, "80", &hints, &ai);
+ TEST_VERIFY_EXIT (ret == 0);
+ switch (family)
+ {
+ case AF_INET:
+ case AF_INET6:
+ verify_response_data (decode_addrinfo (ai, family));
+ break;
+ case AF_UNSPEC:
+ verify_response_data (decode_addrinfo (ai, AF_INET));
+ verify_response_data (decode_addrinfo (ai, AF_INET6));
+ break;
+ default:
+ FAIL_EXIT1 ("invalid address family %d", family);
+ }
+ freeaddrinfo (ai);
+}
+
+enum res_op
+{
+ res_op_search,
+ res_op_query,
+ res_op_querydomain,
+ res_op_nsearch,
+ res_op_nquery,
+ res_op_nquerydomain,
+
+ res_op_last = res_op_nquerydomain,
+};
+
+static const char *
+res_op_string (enum res_op op)
+{
+ switch (op)
+ {
+ case res_op_search:
+ return "res_search";
+ case res_op_query:
+ return "res_query";
+ case res_op_querydomain:
+ return "res_querydomain";
+ case res_op_nsearch:
+ return "res_nsearch";
+ case res_op_nquery:
+ return "res_nquery";
+ case res_op_nquerydomain:
+ return "res_nquerydomain";
+ }
+ FAIL_EXIT1 ("invalid res_op value %d", (int) op);
+}
+
+/* Call libresolv function OP to look up PROBE_NAME, with an answer
+ buffer of SIZE bytes. Check that the advertised UDP buffer size is
+ in fact EXPECTED_BUFFER_SIZE. */
+static void
+do_res_search (const char *probe_name, enum res_op op, size_t size,
+ size_t expected_buffer_size)
+{
+ if (test_verbose)
+ printf ("info: testing %s with buffer size %zu\n",
+ res_op_string (op), size);
+ unsigned char *buffer = xmalloc (size);
+ int ret = -1;
+ switch (op)
+ {
+ case res_op_search:
+ ret = res_search (probe_name, C_IN, T_A, buffer, size);
+ break;
+ case res_op_query:
+ ret = res_query (probe_name, C_IN, T_A, buffer, size);
+ break;
+ case res_op_nsearch:
+ ret = res_nsearch (&_res, probe_name, C_IN, T_A, buffer, size);
+ break;
+ case res_op_nquery:
+ ret = res_nquery (&_res, probe_name, C_IN, T_A, buffer, size);
+ break;
+ case res_op_querydomain:
+ case res_op_nquerydomain:
+ {
+ char *example_stripped = xstrdup (probe_name);
+ char *dot_example = strstr (example_stripped, ".example");
+ if (dot_example != NULL && strcmp (dot_example, ".example") == 0)
+ {
+ /* Truncate the domain name. */
+ *dot_example = '\0';
+ if (op == res_op_querydomain)
+ ret = res_querydomain
+ (example_stripped, "example", C_IN, T_A, buffer, size);
+ else
+ ret = res_nquerydomain
+ (&_res, example_stripped, "example", C_IN, T_A, buffer, size);
+ }
+ else
+ FAIL_EXIT1 ("invalid probe name: %s", probe_name);
+ free (example_stripped);
+ }
+ break;
+ }
+ TEST_VERIFY_EXIT (ret > 12);
+ unsigned char *end = buffer + ret;
+
+ HEADER *hd = (HEADER *) buffer;
+ TEST_VERIFY (ntohs (hd->qdcount) == 1);
+ TEST_VERIFY (ntohs (hd->ancount) == 1);
+ /* Skip over the header. */
+ unsigned char *p = buffer + sizeof (*hd);
+ /* Skip over the question. */
+ ret = dn_skipname (p, end);
+ TEST_VERIFY_EXIT (ret > 0);
+ p += ret;
+ TEST_VERIFY_EXIT (end - p >= 4);
+ p += 4;
+ /* Skip over the RNAME and the RR header, but stop at the RDATA
+ length. */
+ ret = dn_skipname (p, end);
+ TEST_VERIFY_EXIT (ret > 0);
+ p += ret;
+ TEST_VERIFY_EXIT (end - p >= 2 + 2 + 4 + 2 + 4);
+ p += 2 + 2 + 4;
+ /* The IP address should be 4 bytes long. */
+ TEST_VERIFY_EXIT (p[0] == 0);
+ TEST_VERIFY_EXIT (p[1] == 4);
+ /* Extract the address information. */
+ p += 2;
+ struct response_data *data = decode_address (p, 4);
+
+ verify_response_data_payload (data, expected_buffer_size);
+
+ free (buffer);
+}
+
+static void
+run_test (const char *probe_name)
+{
+ if (test_verbose)
+ printf ("\ninfo: * use_edns=%d use_dnssec=%d\n",
+ use_edns, use_dnssec);
+ check_hostent (gethostbyname (probe_name));
+ check_hostent (gethostbyname2 (probe_name, AF_INET));
+ check_hostent (gethostbyname2 (probe_name, AF_INET6));
+ do_ai (AF_UNSPEC);
+ do_ai (AF_INET);
+ do_ai (AF_INET6);
+
+ for (int op = 0; op <= res_op_last; ++op)
+ {
+ do_res_search (probe_name, op, 301, 512);
+ do_res_search (probe_name, op, 511, 512);
+ do_res_search (probe_name, op, 512, 512);
+ do_res_search (probe_name, op, 513, 513);
+ do_res_search (probe_name, op, 657, 657);
+ do_res_search (probe_name, op, 1199, 1199);
+ do_res_search (probe_name, op, 1200, 1200);
+ do_res_search (probe_name, op, 1201, 1200);
+ do_res_search (probe_name, op, 65535, 1200);
+ }
+}
+
+static int
+do_test (void)
+{
+ for (int do_edns = 0; do_edns < 2; ++do_edns)
+ for (int do_dnssec = 0; do_dnssec < 2; ++do_dnssec)
+ for (int do_tcp = 0; do_tcp < 2; ++do_tcp)
+ {
+ struct resolv_test *aux = resolv_test_start
+ ((struct resolv_redirect_config)
+ {
+ .response_callback = response,
+ });
+
+ use_edns = do_edns;
+ if (do_edns)
+ _res.options |= RES_USE_EDNS0;
+ use_dnssec = do_dnssec;
+ if (do_dnssec)
+ _res.options |= RES_USE_DNSSEC;
+
+ char *probe_name = xstrdup (EDNS_PROBE_EXAMPLE);
+ if (do_tcp)
+ {
+ char *n = xasprintf ("tcp.%s", probe_name);
+ free (probe_name);
+ probe_name = n;
+ }
+
+ run_test (probe_name);
+
+ free (probe_name);
+ resolv_test_end (aux);
+ }
+
+ free_response_data ();
+ return 0;
+}
+
+#include <support/test-driver.c>