| /* Test EDNS handling in the stub resolver. |
| Copyright (C) 2016-2018 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 <resolv.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); |
| |
| const char *qname_compare = qname; |
| |
| /* The "formerr." prefix can be used to request a FORMERR response on the |
| first server. */ |
| bool send_formerr; |
| if (strncmp ("formerr.", qname, strlen ("formerr.")) == 0) |
| { |
| send_formerr = true; |
| qname_compare = qname + strlen ("formerr."); |
| } |
| else |
| { |
| send_formerr = false; |
| qname_compare = qname; |
| } |
| |
| /* The "tcp." prefix can be used to request TCP fallback. */ |
| 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 (reduced: %s)\n", |
| qname, qname_compare); |
| return; |
| } |
| TEST_VERIFY_EXIT (qclass == C_IN); |
| struct resolv_response_flags flags = { }; |
| flags.tc = force_tcp && !ctx->tcp; |
| if (!flags.tc && send_formerr && ctx->server_index == 0) |
| /* Send a FORMERR for the first full response from the first |
| server. */ |
| flags.rcode = 1; /* FORMERR */ |
| resolv_response_init (b, flags); |
| resolv_response_add_question (b, qname, qclass, qtype); |
| if (flags.tc || flags.rcode != 0) |
| 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) |
| for (int do_formerr = 0; do_formerr < 2; ++do_formerr) |
| { |
| 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; |
| } |
| if (do_formerr) |
| { |
| /* Send a garbage query in an attempt to trigger EDNS |
| fallback. */ |
| char *n = xasprintf ("formerr.%s", probe_name); |
| gethostbyname (n); |
| free (n); |
| } |
| |
| run_test (probe_name); |
| |
| free (probe_name); |
| resolv_test_end (aux); |
| } |
| |
| free_response_data (); |
| return 0; |
| } |
| |
| #include <support/test-driver.c> |