diff --git a/.github/workflows/msys2.yml b/.github/workflows/msys2.yml index a161b7b4..8f0696ce 100644 --- a/.github/workflows/msys2.yml +++ b/.github/workflows/msys2.yml @@ -10,7 +10,7 @@ concurrency: cancel-in-progress: true env: - TEST_FILTER: "--gtest_filter=-*LiveSearchTXT*:*LiveSearchANY*:*LiveGetLocalhostByAddr*" + TEST_FILTER: "-4 --gtest_filter=-*LiveSearchTXT*:*LiveSearchANY*:*LiveGetLocalhostByAddr*" CMAKE_FLAGS: "-DCMAKE_BUILD_TYPE=DEBUG -DCARES_STATIC=ON -DCMAKE_INSTALL_PREFIX=C:/projects/build-cares/test_install -DCARES_STATIC_PIC=ON -G Ninja" CONFIG_OPTS: "--disable-shared" MAKE: make diff --git a/docs/ares_dns_rr.3 b/docs/ares_dns_rr.3 index 692eb909..4da76865 100644 --- a/docs/ares_dns_rr.3 +++ b/docs/ares_dns_rr.3 @@ -96,6 +96,10 @@ ares_status_t ares_dns_rr_set_opt(ares_dns_rr_t *dns_rr, const unsigned char *val, size_t val_len); +ares_status_t ares_dns_rr_del_opt_byid(ares_dns_rr_t *dns_rr, + ares_dns_rr_key_t key, + unsigned short opt); + const struct in_addr *ares_dns_rr_get_addr(const ares_dns_rr_t *dns_rr, ares_dns_rr_key_t key); @@ -553,7 +557,7 @@ parameter, and the index to remove is provided in the parameter. The \fIares_dns_rr_set_opt(3)\fP function is used to set option/parameter keys and -values for the resource record when the datatype if \fIARES_DATATYPE_OPT\fP. The +values for the resource record when the datatype is \fIARES_DATATYPE_OPT\fP. The resource record to be modified is provided in the .IR dns_rr parameter. They key/parameter is provided in the @@ -568,6 +572,18 @@ enumerations. The value for the option is always provided in binary form in with length provided in .IR val_len. +The \fIares_dns_rr_del_opt_byid(3)\fP function is used to delete option/parameter +keys and values for the resource record when the datatype is +\fIARES_DATATYPE_OPT\fP. The resource record to be modified is provided in the +.IR dns_rr +parameter. They key/parameter is provided in the +.IR key +parameter. The option/parameter value specific to the resource record is provided +in the +.IR opt +parameter. This function returns \fIARES_SUCCESS\fP if the record is successfully +removed, or \fIARES_ENOTFOUND\fP if the record could not be found. + The \fIares_dns_rr_get_addr(3)\fP function is used to retrieve the IPv4 address from the resource record when the datatype is \fIARES_DATATYPE_INADDR\fP. The resource record is provided in the diff --git a/docs/ares_dns_rr_del_opt_byid.3 b/docs/ares_dns_rr_del_opt_byid.3 new file mode 100644 index 00000000..b93e4cd4 --- /dev/null +++ b/docs/ares_dns_rr_del_opt_byid.3 @@ -0,0 +1,3 @@ +.\" Copyright (C) 2023 The c-ares project and its contributors. +.\" SPDX-License-Identifier: MIT +.so man3/ares_dns_rr.3 diff --git a/include/ares_dns_record.h b/include/ares_dns_record.h index ee375fd3..2896eab2 100644 --- a/include/ares_dns_record.h +++ b/include/ares_dns_record.h @@ -918,6 +918,17 @@ CARES_EXTERN ares_status_t ares_dns_rr_set_opt(ares_dns_rr_t *dns_rr, const unsigned char *val, size_t val_len); +/*! Delete the option for the RR by id + * + * \param[in] dns_rr Pointer to resource record + * \param[in] key DNS Resource Record Key + * \param[in] opt Option record key id. + * \return ARES_SUCCESS if removed, ARES_ENOTFOUND if not found + */ +CARES_EXTERN ares_status_t ares_dns_rr_del_opt_byid(ares_dns_rr_t *dns_rr, + ares_dns_rr_key_t key, + unsigned short opt); + /*! Retrieve a pointer to the ipv4 address. Can only be used on keys with * datatype ARES_DATATYPE_INADDR. * diff --git a/src/lib/Makefile.inc b/src/lib/Makefile.inc index abd237c4..31411df4 100644 --- a/src/lib/Makefile.inc +++ b/src/lib/Makefile.inc @@ -10,6 +10,7 @@ CSOURCES = ares__addrinfo2hostent.c \ ares__sortaddrinfo.c \ ares_android.c \ ares_cancel.c \ + ares_cookie.c \ ares_data.c \ ares_destroy.c \ ares_dns_mapping.c \ diff --git a/src/lib/ares__close_sockets.c b/src/lib/ares__close_sockets.c index 814ea10c..84de3ea0 100644 --- a/src/lib/ares__close_sockets.c +++ b/src/lib/ares__close_sockets.c @@ -37,7 +37,7 @@ static void ares__requeue_queries(struct server_connection *conn, ares__tvnow(&now); while ((query = ares__llist_first_val(conn->queries_to_conn)) != NULL) { - ares__requeue_query(query, &now, requeue_status); + ares__requeue_query(query, &now, requeue_status, ARES_TRUE); } } diff --git a/src/lib/ares__socket.c b/src/lib/ares__socket.c index 557f7855..53b30465 100644 --- a/src/lib/ares__socket.c +++ b/src/lib/ares__socket.c @@ -242,6 +242,70 @@ static int configure_socket(ares_socket_t s, struct server_state *server) return 0; } +ares_bool_t ares_sockaddr_to_ares_addr(struct ares_addr *ares_addr, + unsigned short *port, + const struct sockaddr *sockaddr) +{ + if (sockaddr->sa_family == AF_INET) { + /* NOTE: memcpy sockaddr_in due to alignment issues found by UBSAN due to + * dnsinfo packing on MacOS */ + struct sockaddr_in sockaddr_in; + memcpy(&sockaddr_in, sockaddr, sizeof(sockaddr_in)); + + ares_addr->family = AF_INET; + memcpy(&ares_addr->addr.addr4, &(sockaddr_in.sin_addr), + sizeof(ares_addr->addr.addr4)); + + if (port) { + *port = ntohs(sockaddr_in.sin_port); + } + return ARES_TRUE; + } + + if (sockaddr->sa_family == AF_INET6) { + /* NOTE: memcpy sockaddr_in6 due to alignment issues found by UBSAN due to + * dnsinfo packing on MacOS */ + struct sockaddr_in6 sockaddr_in6; + memcpy(&sockaddr_in6, sockaddr, sizeof(sockaddr_in6)); + + ares_addr->family = AF_INET6; + memcpy(&ares_addr->addr.addr6, &(sockaddr_in6.sin6_addr), + sizeof(ares_addr->addr.addr6)); + if (port) { + *port = ntohs(sockaddr_in6.sin6_port); + } + return ARES_TRUE; + } + + return ARES_FALSE; +} + +static ares_status_t ares_conn_set_self_ip(struct server_connection *conn) +{ + /* Some old systems might not have sockaddr_storage, so we make a union + * that's guaranteed to be large enough */ + union { + struct sockaddr sa; + struct sockaddr_in sa4; + struct sockaddr_in6 sa6; + } from; + int rv; + ares_socklen_t len = sizeof(from); + + memset(&from, 0, sizeof(from)); + + rv = getsockname(conn->fd, &from.sa, &len); + if (rv != 0) { + return ARES_ECONNREFUSED; + } + + if (!ares_sockaddr_to_ares_addr(&conn->self_ip, NULL, &from.sa)) { + return ARES_ECONNREFUSED; + } + + return ARES_SUCCESS; +} + ares_status_t ares__open_connection(ares_channel_t *channel, struct server_state *server, ares_bool_t is_tcp) @@ -249,6 +313,7 @@ ares_status_t ares__open_connection(ares_channel_t *channel, ares_socket_t s; int opt; ares_socklen_t salen; + ares_status_t status; union { struct sockaddr_in sa4; @@ -359,6 +424,16 @@ ares_status_t ares__open_connection(ares_channel_t *channel, /* LCOV_EXCL_STOP */ } + /* Need to store our own ip for DNS cookie support */ + status = ares_conn_set_self_ip(conn); + if (status != ARES_SUCCESS) { + /* LCOV_EXCL_START: UntestablePath */ + ares__close_socket(channel, s); + ares_free(conn); + return status; + /* LCOV_EXCL_STOP */ + } + /* TCP connections are thrown to the end as we don't spawn multiple TCP * connections. UDP connections are put on front where the newest connection * can be quickly pulled */ diff --git a/src/lib/ares_cookie.c b/src/lib/ares_cookie.c new file mode 100644 index 00000000..304f15e9 --- /dev/null +++ b/src/lib/ares_cookie.c @@ -0,0 +1,457 @@ +/* MIT License + * + * Copyright (c) 2024 Brad House + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * SPDX-License-Identifier: MIT + */ + +/* DNS cookies are a simple form of learned mutual authentication supported by + * most DNS server implementations these days and can help prevent DNS Cache + * Poisoning attacks for clients and DNS amplification attacks for servers. + * + * A good overview is here: + * https://www.dotmagazine.online/issues/digital-responsibility-and-sustainability/dns-cookies-transaction-mechanism + * + * RFCs used for implementation are + * [RFC7873](https://datatracker.ietf.org/doc/html/rfc7873) which is extended by + * [RFC9018](https://datatracker.ietf.org/doc/html/rfc9018). + * + * Though this could be used on TCP, the likelihood of it being useful is small + * and could cause some issues. TCP is better used as a fallback in case there + * are issues with DNS Cookie support in the upstream servers (e.g. AnyCast + * cluster issues). + * + * While most recursive DNS servers support DNS Cookies, public DNS servers like + * Google (8.8.8.8, 8.8.4.4) and CloudFlare (1.1.1.1, 1.0.0.1) don't seem to + * have this enabled yet for unknown reasons. + * + * The risk to having DNS Cookie support always enabled is nearly zero as there + * is built-in detection support and it will simply bypass using cookies if the + * remote server doesn't support it. The problem arises if a remote server + * supports DNS cookies, then stops supporting them (such as if an administrator + * reconfigured the server, or maybe there are different servers in a cluster + * with different configurations). We need to detect this behavior by tracking + * how much time has gone by since we received our last valid cookie reply, and + * if we exceed the threshold, reset all cookie parameters like we haven't + * attempted a request yet. + * + * ## Implementation Plan + * + * ### Constants: + * - `COOKIE_CLIENT_TIMEOUT`: 86400s (1 day) + * - How often to regenerate the per-server client cookie, even if our + * source ip address hasn't changed. + * - `COOKIE_UNSUPPORTED_TIMEOUT`: 300s (5 minutes) + * - If a server responds without a cookie in the reply, this is how long to + * wait before attempting to send a client cookie again. + * - `COOKIE_REGRESSION_TIMEOUT`: 120s (2 minutes) + * - If a server was once known to return cookies, and all of a sudden stops + * returning cookies (but the reply is otherwise valid), this is how long + * to continue to attempt to use cookies before giving up and resetting. + * Such an event would cause an outage for this duration, but since a + * cache poisoning attack should be dropping invalid replies we should be + * able to still get the valid reply and not assume it is a server + * regression just because we received replies without cookies. + * - `COOKIE_RESEND_MAX`: 3 + * - Maximum times to resend a query to a server due to the server responding + * with `BAD_COOKIE`, after this, we switch to TCP. + * + * ### Per-server variables: + * - `cookie.state`: Known state of cookie support, enumeration. + * - `INITIAL` (0): Initial state, not yet determined. Used during startup. + * - `GENERATED` (1): Cookie has been generated and sent to a server, but no + * validated response yet. + * - `SUPPORTED` (2): Server has been determined to properly support cookies + * - `UNSUPPORTED` (3): Server has been determined to not support cookies + * - `cookie.client` : 8 byte randomly generated client cookie + * - `cookie.client_ts`: Timestamp client cookie was generated + * - `cookie.client_ip`: IP address client used to connect to server + * - `cookie.server`: 8 to 32 byte server cookie + * - `cookie.server_len`: length of server cookie + * - `cookie.unsupported_ts`: Timestamp of last attempt to use a cookies, but + * it was determined that the server didn't support them. + * + * ### Per-query variables: + * - `query.client_cookie`: Duplicate of `cookie.client` at the point in time + * the query is put on the wire. This should be available in the + * `ares_dns_record_t` for the request for verification purposes so we don't + * actually need to duplicate this, just naming it here for the ease of + * documentation below. + * - `query.cookie_try_count`: Number of tries to send a cookie but receive + * `BAD_COOKIE` responses. Used to know when we need to switch to TCP. + * + * ### Procedure: + * **NOTE**: These steps will all be done after obtaining a connection handle as + * some of these steps depend on determining the source ip address for the + * connection. + * + * 1. If the query is not using EDNS, then **skip any remaining processing**. + * 2. If using TCP, ensure there is no EDNS cookie opt (10) set (there may have + * been if this is a resend after upgrade to TCP), then **skip any remaining + * processing**. + * 3. If `cookie.state == SUPPORTED`, `cookie.unsupported_ts` is non-zero, and + * evaluates greater than `COOKIE_REGRESSION_TIMEOUT`, then clear all cookie + * settings, set `cookie.state = INITIAL`. Continue to next step (4) + * 4. If `cookie.state == UNSUPPORTED` + * - If `cookie.unsupported_ts` evaluates less than + * `COOKIE_UNSUPPORTED_TIMEOUT` + * - Ensure there is no EDNS cookie opt (10) set (shouldn't be unless + * requestor had put this themselves), then **skip any remaining + * processing** as we don't want to try to send cookies. + * - Otherwise: + * - clear all cookie settings, set `cookie.state = INITIAL`. + * - Continue to next step (5) which will send a new cookie. + * 5. If `cookie.state == INITIAL`: + * - randomly generate new `cookie.client` + * - set `cookie.client_ts` to the current timestamp. + * - set `cookie.state = GENERATED`. + * - set `cookie.client_ip` to the current source ip address. + * 6. If `cookie.state == GENERATED || cookie.state == SUPPORTED` and + * `cookie.client_ip` does not match the current source ip address: + * - clear `cookie.server` + * - randomly generate new `cookie.client` + * - set `cookie.client_ts` to the current timestamp. + * - set `cookie.client_ip` to the current source ip address. + * - do not change the `cookie.state` + * 7. If `cookie.state == SUPPORTED` and `cookie.client_ts` evaluation exceeds + * `COOKIE_CLIENT_TIMEOUT`: + * - clear `cookie.server` + * - randomly generate new `cookie.client` + * - set `cookie.client_ts` to the current timestamp. + * - set `cookie.client_ip` to the current source ip address. + * - do not change the `cookie.state` + * 8. Generate EDNS OPT record (10) for client cookie. The option value will be + * the `cookie.client` concatenated with the `cookie.server`. If there is no + * known server cookie, it will not be appended. Copy `cookie.client` to + * `query.client_cookie` to handle possible client cookie changes by other + * queries before a reply is received (technically this is in the cached + * `ares_dns_record_t` so no need to manually do this). Send request to + * server. + * 9. Evaluate response: + * 1. If invalid EDNS OPT cookie (10) length sent back in response (valid + * length is 16-40), or bad client cookie value (validate first 8 bytes + * against `query.client_cookie` not `cookie.client`), **drop response** + * as if it hadn't been received. This is likely a spoofing attack. + * Wait for valid response up to normal response timeout. + * 2. If a EDNS OPT cookie (10) server cookie is returned: + * - set `cookie.unsupported_ts` to zero and `cookie.state = SUPPORTED`. + * We can confirm this server supports cookies based on the existence + * of this record. + * - If a new EDNS OPT cookie (10) server cookie is in the response, and + * the `client.cookie` matches the `query.client_cookie` still (hasn't + * been rotated by some other parallel query), save it as + * `cookie.server`. + * 3. If dns response `rcode` is `BAD_COOKIE`: + * - Ensure a EDNS OPT cookie (10) is returned, otherwise **drop + * response**, this is completely invalid and likely an spoof of some + * sort. + * - Otherwise + * - Increment `query.cookie_try_count` + * - If `query.cookie_try_count >= COOKIE_RESEND_MAX`, set + * `query.using_tcp` to force the next attempt to use TCP. + * - **Requeue the query**, but do not increment the normal + * `try_count` as a `BAD_COOKIE` reply isn't a normal try failure. + * This should end up going all the way back to step 1 on the next + * attempt. + * 4. If EDNS OPT cookie (10) is **NOT** returned in the response: + * - If `cookie.state == SUPPORTED` + * - if `cookie.unsupported_ts` is zero, set to the current timestamp. + * - Drop the response, wait for a valid response to be returned + * - if `cookie.state == GENERATED` + * - clear all cookie settings + * - set `cookie.state = UNSUPPORTED` + * - set `cookie.unsupported_ts` to the current time + * - Accept response (state should be `UNSUPPORTED` if we're here) + */ + +#include "ares_private.h" + +/* 1 day */ +#define COOKIE_CLIENT_TIMEOUT_MS (86400 * 1000) + +/* 5 minutes */ +#define COOKIE_UNSUPPORTED_TIMEOUT_MS (300 * 1000) + +/* 2 minutes */ +#define COOKIE_REGRESSION_TIMEOUT_MS (120 * 1000) + +#define COOKIE_RESEND_MAX 3 + +static const unsigned char * + ares_dns_cookie_fetch(const ares_dns_record_t *dnsrec, size_t *len) +{ + const ares_dns_rr_t *rr = ares_dns_get_opt_rr_const(dnsrec); + const unsigned char *val = NULL; + *len = 0; + + if (rr == NULL) { + return NULL; + } + + if (!ares_dns_rr_get_opt_byid(rr, ARES_RR_OPT_OPTIONS, ARES_OPT_PARAM_COOKIE, + &val, len)) { + return NULL; + } + + return val; +} + +static ares_bool_t timeval_is_set(const ares_timeval_t *tv) +{ + if (tv->sec != 0 && tv->usec != 0) { + return ARES_TRUE; + } + return ARES_FALSE; +} + +static ares_bool_t timeval_expired(const ares_timeval_t *tv, + const ares_timeval_t *now, + unsigned long millsecs) +{ + ares_int64_t tvdiff_ms; + ares_timeval_t tvdiff; + ares__timeval_diff(&tvdiff, tv, now); + + tvdiff_ms = tvdiff.sec * 1000 + tvdiff.usec / 1000; + if (tvdiff_ms >= (ares_int64_t)millsecs) { + return ARES_TRUE; + } + return ARES_FALSE; +} + +static void ares_cookie_clear(ares_cookie_t *cookie) +{ + memset(cookie, 0, sizeof(*cookie)); + cookie->state = ARES_COOKIE_INITIAL; +} + +static void ares_cookie_generate(ares_cookie_t *cookie, + struct server_connection *conn, + const ares_timeval_t *now) +{ + ares_channel_t *channel = conn->server->channel; + + ares__rand_bytes(channel->rand_state, cookie->client, sizeof(cookie->client)); + memcpy(&cookie->client_ts, now, sizeof(cookie->client_ts)); + memcpy(&cookie->client_ip, &conn->self_ip, sizeof(cookie->client_ip)); +} + +static void ares_cookie_clear_server(ares_cookie_t *cookie) +{ + memset(cookie->server, 0, sizeof(cookie->server)); + cookie->server_len = 0; +} + +static ares_bool_t ares_addr_equal(const struct ares_addr *addr1, + const struct ares_addr *addr2) +{ + if (addr1->family != addr2->family) { + return ARES_FALSE; + } + + switch (addr1->family) { + case AF_INET: + if (memcmp(&addr1->addr.addr4, &addr2->addr.addr4, + sizeof(addr1->addr.addr4)) == 0) { + return ARES_TRUE; + } + break; + case AF_INET6: + if (memcmp(&addr1->addr.addr6, &addr2->addr.addr6, + sizeof(addr1->addr.addr6)) == 0) { + return ARES_TRUE; + } + break; + default: + break; /* LCOV_EXCL_LINE */ + } + + return ARES_FALSE; +} + +ares_status_t ares_cookie_apply(ares_dns_record_t *dnsrec, + struct server_connection *conn, + const ares_timeval_t *now) +{ + struct server_state *server = conn->server; + ares_cookie_t *cookie = &server->cookie; + ares_dns_rr_t *rr = ares_dns_get_opt_rr(dnsrec); + unsigned char c[40]; + size_t c_len; + + /* If there is no OPT record, then EDNS isn't supported, and therefore + * cookies can't be supported */ + if (rr == NULL) { + return ARES_SUCCESS; + } + + /* No cookies on TCP, make sure we remove one if one is present */ + if (conn->is_tcp) { + ares_dns_rr_del_opt_byid(rr, ARES_RR_OPT_OPTIONS, ARES_OPT_PARAM_COOKIE); + return ARES_SUCCESS; + } + + /* Look for regression */ + if (cookie->state == ARES_COOKIE_SUPPORTED && + timeval_is_set(&cookie->unsupported_ts) && + timeval_expired(&cookie->unsupported_ts, now, + COOKIE_REGRESSION_TIMEOUT_MS)) { + ares_cookie_clear(cookie); + } + + /* Handle unsupported state */ + if (cookie->state == ARES_COOKIE_UNSUPPORTED) { + /* If timer hasn't expired, just delete any possible cookie and return */ + if (!timeval_expired(&cookie->unsupported_ts, now, + COOKIE_REGRESSION_TIMEOUT_MS)) { + ares_dns_rr_del_opt_byid(rr, ARES_RR_OPT_OPTIONS, ARES_OPT_PARAM_COOKIE); + return ARES_SUCCESS; + } + + /* We want to try to "learn" again */ + ares_cookie_clear(cookie); + } + + /* Generate a new cookie */ + if (cookie->state == ARES_COOKIE_INITIAL) { + ares_cookie_generate(cookie, conn, now); + cookie->state = ARES_COOKIE_GENERATED; + } + + /* Regenerate the cookie and clear the server cookie if the client ip has + * changed */ + if ((cookie->state == ARES_COOKIE_GENERATED || + cookie->state == ARES_COOKIE_SUPPORTED) && + !ares_addr_equal(&conn->self_ip, &cookie->client_ip)) { + ares_cookie_clear_server(cookie); + ares_cookie_generate(cookie, conn, now); + } + + /* If the client cookie has reached its maximum time, refresh it */ + if (cookie->state == ARES_COOKIE_SUPPORTED && + timeval_expired(&cookie->client_ts, now, COOKIE_CLIENT_TIMEOUT_MS)) { + ares_cookie_clear_server(cookie); + ares_cookie_generate(cookie, conn, now); + } + + /* Generate the full cookie which is the client cookie concatenated with the + * server cookie (if there is one) and apply it. */ + memcpy(c, cookie->client, sizeof(cookie->client)); + if (cookie->server_len) { + memcpy(c + sizeof(cookie->client), cookie->server, cookie->server_len); + } + c_len = sizeof(cookie->client) + cookie->server_len; + + return ares_dns_rr_set_opt(rr, ARES_RR_OPT_OPTIONS, ARES_OPT_PARAM_COOKIE, c, + c_len); +} + +ares_status_t ares_cookie_validate(struct query *query, + const ares_dns_record_t *dnsresp, + struct server_connection *conn, + const ares_timeval_t *now) +{ + struct server_state *server = conn->server; + ares_cookie_t *cookie = &server->cookie; + const ares_dns_record_t *dnsreq = query->query; + const unsigned char *resp_cookie; + size_t resp_cookie_len; + const unsigned char *req_cookie; + size_t req_cookie_len; + + resp_cookie = ares_dns_cookie_fetch(dnsresp, &resp_cookie_len); + + /* Invalid cookie length, drop */ + if (resp_cookie && (resp_cookie_len < 8 || resp_cookie_len > 40)) { + return ARES_EBADRESP; + } + + req_cookie = ares_dns_cookie_fetch(dnsreq, &req_cookie_len); + + /* Didn't request cookies, so we can stop evaluating */ + if (req_cookie == NULL) { + return ARES_SUCCESS; + } + + /* If 8-byte prefix for returned cookie doesn't match the requested cookie, + * drop for spoofing */ + if (resp_cookie && memcmp(req_cookie, resp_cookie, 8) != 0) { + return ARES_EBADRESP; + } + + if (resp_cookie && resp_cookie_len > 8) { + /* Make sure we record that we successfully received a cookie response */ + cookie->state = ARES_COOKIE_SUPPORTED; + memset(&cookie->unsupported_ts, 0, sizeof(cookie->unsupported_ts)); + + /* If client cookie hasn't been rotated, save the returned server cookie */ + if (memcmp(cookie->client, req_cookie, sizeof(cookie->client)) == 0) { + memcpy(cookie->server, resp_cookie + 8, resp_cookie_len - 8); + } + } + + if (ares_dns_record_get_rcode(dnsresp) == ARES_RCODE_BADCOOKIE) { + /* Illegal to return BADCOOKIE but no cookie, drop */ + if (resp_cookie == NULL) { + return ARES_EBADRESP; + } + + /* If we have too many attempts to send a cookie, we need to requeue as + * tcp */ + query->cookie_try_count++; + if (query->cookie_try_count >= COOKIE_RESEND_MAX) { + query->using_tcp = ARES_TRUE; + } + + /* Resend the request, hopefully it will work the next time as we should + * have recorded a server cookie */ + ares__requeue_query(query, now, ARES_SUCCESS, + ARES_FALSE /* Don't increment try count */); + + /* Parent needs to drop this response */ + return ARES_EBADRESP; + } + + /* We've got a response with a server cookie, and we've done all the + * evaluation we can, return success */ + if (resp_cookie_len > 8) { + return ARES_SUCCESS; + } + + if (cookie->state == ARES_COOKIE_SUPPORTED) { + /* If we're not currently tracking an error time yet, start */ + if (!timeval_is_set(&cookie->unsupported_ts)) { + memcpy(&cookie->unsupported_ts, now, sizeof(cookie->unsupported_ts)); + } + /* Drop it since we expected a cookie */ + return ARES_EBADRESP; + } + + if (cookie->state == ARES_COOKIE_GENERATED) { + ares_cookie_clear(cookie); + cookie->state = ARES_COOKIE_UNSUPPORTED; + memcpy(&cookie->unsupported_ts, now, sizeof(cookie->unsupported_ts)); + } + + /* Cookie state should be UNSUPPORTED if we're here */ + return ARES_SUCCESS; +} diff --git a/src/lib/ares_dns_private.h b/src/lib/ares_dns_private.h index 8ffc7295..bffb1064 100644 --- a/src/lib/ares_dns_private.h +++ b/src/lib/ares_dns_private.h @@ -38,22 +38,23 @@ ares_bool_t ares_dns_class_isvalid(ares_dns_class_t qclass, ares_dns_rec_type_t type, ares_bool_t is_query); ares_bool_t ares_dns_section_isvalid(ares_dns_section_t sect); -ares_status_t ares_dns_rr_set_str_own(ares_dns_rr_t *dns_rr, - ares_dns_rr_key_t key, char *val); -ares_status_t ares_dns_rr_set_bin_own(ares_dns_rr_t *dns_rr, - ares_dns_rr_key_t key, unsigned char *val, - size_t len); -ares_status_t ares_dns_rr_set_abin_own(ares_dns_rr_t *dns_rr, - ares_dns_rr_key_t key, - ares__dns_multistring_t *strs); -ares_status_t ares_dns_rr_set_opt_own(ares_dns_rr_t *dns_rr, - ares_dns_rr_key_t key, unsigned short opt, - unsigned char *val, size_t val_len); -ares_status_t ares_dns_record_rr_prealloc(ares_dns_record_t *dnsrec, - ares_dns_section_t sect, size_t cnt); -ares_bool_t ares_dns_has_opt_rr(const ares_dns_record_t *rec); -void ares_dns_record_write_ttl_decrement(ares_dns_record_t *dnsrec, - unsigned int ttl_decrement); +ares_status_t ares_dns_rr_set_str_own(ares_dns_rr_t *dns_rr, + ares_dns_rr_key_t key, char *val); +ares_status_t ares_dns_rr_set_bin_own(ares_dns_rr_t *dns_rr, + ares_dns_rr_key_t key, unsigned char *val, + size_t len); +ares_status_t ares_dns_rr_set_abin_own(ares_dns_rr_t *dns_rr, + ares_dns_rr_key_t key, + ares__dns_multistring_t *strs); +ares_status_t ares_dns_rr_set_opt_own(ares_dns_rr_t *dns_rr, + ares_dns_rr_key_t key, unsigned short opt, + unsigned char *val, size_t val_len); +ares_status_t ares_dns_record_rr_prealloc(ares_dns_record_t *dnsrec, + ares_dns_section_t sect, size_t cnt); +ares_dns_rr_t *ares_dns_get_opt_rr(ares_dns_record_t *rec); +const ares_dns_rr_t *ares_dns_get_opt_rr_const(const ares_dns_record_t *rec); +void ares_dns_record_write_ttl_decrement(ares_dns_record_t *dnsrec, + unsigned int ttl_decrement); /*! Create a DNS record object for a query. The arguments are the same as * those for ares_create_query(). diff --git a/src/lib/ares_dns_record.c b/src/lib/ares_dns_record.c index d9dcab3a..8667c3c6 100644 --- a/src/lib/ares_dns_record.c +++ b/src/lib/ares_dns_record.c @@ -1460,6 +1460,51 @@ ares_status_t ares_dns_rr_set_opt(ares_dns_rr_t *dns_rr, ares_dns_rr_key_t key, return status; } +ares_status_t ares_dns_rr_del_opt_byid(ares_dns_rr_t *dns_rr, + ares_dns_rr_key_t key, + unsigned short opt) +{ + ares__dns_options_t **options; + size_t idx; + size_t cnt_after; + + if (ares_dns_rr_key_datatype(key) != ARES_DATATYPE_OPT) { + return ARES_EFORMERR; + } + + options = ares_dns_rr_data_ptr(dns_rr, key, NULL); + if (options == NULL) { + return ARES_EFORMERR; + } + + /* No options */ + if (*options == NULL) { + return ARES_SUCCESS; + } + + for (idx = 0; idx < (*options)->cnt; idx++) { + if ((*options)->optval[idx].opt == opt) { + break; + } + } + + /* No matching option */ + if (idx == (*options)->cnt) { + return ARES_ENOTFOUND; + } + + ares_free((*options)->optval[idx].val); + + cnt_after = (*options)->cnt - idx - 1; + if (cnt_after) { + memmove(&(*options)->optval[idx], &(*options)->optval[idx + 1], + sizeof(*(*options)->optval) * cnt_after); + } + + (*options)->cnt--; + return ARES_SUCCESS; +} + char *ares_dns_addr_to_ptr(const struct ares_addr *addr) { ares__buf_t *buf = NULL; @@ -1532,8 +1577,20 @@ fail: return NULL; } -/* search for an OPT RR in the response */ -ares_bool_t ares_dns_has_opt_rr(const ares_dns_record_t *rec) +ares_dns_rr_t *ares_dns_get_opt_rr(ares_dns_record_t *rec) +{ + size_t i; + for (i = 0; i < ares_dns_record_rr_cnt(rec, ARES_SECTION_ADDITIONAL); i++) { + ares_dns_rr_t *rr = ares_dns_record_rr_get(rec, ARES_SECTION_ADDITIONAL, i); + + if (ares_dns_rr_get_type(rr) == ARES_REC_TYPE_OPT) { + return rr; + } + } + return NULL; +} + +const ares_dns_rr_t *ares_dns_get_opt_rr_const(const ares_dns_record_t *rec) { size_t i; for (i = 0; i < ares_dns_record_rr_cnt(rec, ARES_SECTION_ADDITIONAL); i++) { @@ -1541,10 +1598,10 @@ ares_bool_t ares_dns_has_opt_rr(const ares_dns_record_t *rec) ares_dns_record_rr_get_const(rec, ARES_SECTION_ADDITIONAL, i); if (ares_dns_rr_get_type(rr) == ARES_REC_TYPE_OPT) { - return ARES_TRUE; + return rr; } } - return ARES_FALSE; + return NULL; } /* Construct a DNS record for a name with given class and type. Used internally diff --git a/src/lib/ares_dns_write.c b/src/lib/ares_dns_write.c index 60bbd702..ae1df8f9 100644 --- a/src/lib/ares_dns_write.c +++ b/src/lib/ares_dns_write.c @@ -91,7 +91,7 @@ static ares_status_t ares_dns_write_header(const ares_dns_record_t *dnsrec, } /* RCODE */ - if (dnsrec->rcode > 15 && !ares_dns_has_opt_rr(dnsrec)) { + if (dnsrec->rcode > 15 && ares_dns_get_opt_rr_const(dnsrec) == NULL) { /* Must have OPT RR in order to write extended error codes */ rcode = ARES_RCODE_SERVFAIL; } else { diff --git a/src/lib/ares_private.h b/src/lib/ares_private.h index d0c66130..1ccdf127 100644 --- a/src/lib/ares_private.h +++ b/src/lib/ares_private.h @@ -157,6 +157,7 @@ struct server_state; struct server_connection { struct server_state *server; ares_socket_t fd; + struct ares_addr self_ip; ares_bool_t is_tcp; /* total number of queries run on this connection since it was established */ size_t total_queries; @@ -204,6 +205,36 @@ typedef struct { ares_uint64_t prev_total_count; /*!< Previous period bucket query count */ } ares_server_metrics_t; +typedef enum { + ARES_COOKIE_INITIAL = 0, + ARES_COOKIE_GENERATED = 1, + ARES_COOKIE_SUPPORTED = 2, + ARES_COOKIE_UNSUPPORTED = 3 +} ares_cookie_state_t; + +/*! Structure holding tracking data for RFC 7873/9018 DNS cookies. + * Implementation plan for this feature is here: + * https://github.com/c-ares/c-ares/issues/620 + */ +typedef struct { + /*! starts at INITIAL, transitions as needed. */ + ares_cookie_state_t state; + /*! randomly-generate client cookie */ + unsigned char client[8]; + /*! timestamp client cookie was generated, used for rotation purposes */ + ares_timeval_t client_ts; + /*! IP address last used for client to connect to server. If this changes + * The client cookie gets invalidated */ + struct ares_addr client_ip; + /*! Server Cookie last received, 8-32 bytes in length */ + unsigned char server[32]; + /*! Length of server cookie on file. */ + size_t server_len; + /*! Timestamp of last attempt to use cookies, but it was determined that the + * server didn't support them */ + ares_timeval_t unsupported_ts; +} ares_cookie_t; + struct server_state { /* Configuration */ size_t idx; /* index for server in system configuration */ @@ -232,6 +263,9 @@ struct server_state { /*! Buckets for collecting metrics about the server */ ares_server_metrics_t metrics[ARES_METRIC_COUNT]; + /*! RFC 7873/9018 DNS Cookies */ + ares_cookie_t cookie; + /* Link back to owning channel */ ares_channel_t *channel; }; @@ -263,11 +297,12 @@ struct query { /* Query status */ size_t try_count; /* Number of times we tried this query already. */ + size_t cookie_try_count; /* Attempt count for cookie resends */ ares_bool_t using_tcp; ares_status_t error_status; - size_t timeouts; /* number of timeouts we saw for this request */ - ares_bool_t no_retries; /* do not perform any additional retries, this is set - * when a query is to be canceled */ + size_t timeouts; /* number of timeouts we saw for this request */ + ares_bool_t no_retries; /* do not perform any additional retries, this is + * set when a query is to be canceled */ }; struct apattern { @@ -411,7 +446,8 @@ ares_bool_t ares__timedout(const ares_timeval_t *now, ares_status_t ares__send_query(struct query *query, const ares_timeval_t *now); ares_status_t ares__requeue_query(struct query *query, const ares_timeval_t *now, - ares_status_t status); + ares_status_t status, + ares_bool_t inc_try_count); /*! Retrieve a list of names to use for searching. The first successful * query in the list wins. This function also uses the HOSTSALIASES file @@ -553,6 +589,9 @@ ares_status_t ares__addrinfo_localhost(const char *name, unsigned short port, ares_status_t ares__open_connection(ares_channel_t *channel, struct server_state *server, ares_bool_t is_tcp); +ares_bool_t ares_sockaddr_to_ares_addr(struct ares_addr *ares_addr, + unsigned short *port, + const struct sockaddr *sockaddr); ares_socket_t ares__open_socket(ares_channel_t *channel, int af, int type, int protocol); ares_ssize_t ares__socket_write(ares_channel_t *channel, ares_socket_t s, @@ -715,6 +754,14 @@ void ares_metrics_record(const struct query *query, struct server_state *server, size_t ares_metrics_server_timeout(const struct server_state *server, const ares_timeval_t *now); +ares_status_t ares_cookie_apply(ares_dns_record_t *dnsrec, + struct server_connection *conn, + const ares_timeval_t *now); +ares_status_t ares_cookie_validate(struct query *query, + const ares_dns_record_t *dnsresp, + struct server_connection *conn, + const ares_timeval_t *now); + ares_status_t ares__channel_threading_init(ares_channel_t *channel); void ares__channel_threading_destroy(ares_channel_t *channel); void ares__channel_lock(const ares_channel_t *channel); diff --git a/src/lib/ares_process.c b/src/lib/ares_process.c index 532376eb..f40f025f 100644 --- a/src/lib/ares_process.c +++ b/src/lib/ares_process.c @@ -623,7 +623,7 @@ static void process_timeouts(ares_channel_t *channel, const ares_timeval_t *now) conn = query->conn; server_increment_failures(conn->server, query->using_tcp); - ares__requeue_query(query, now, ARES_ETIMEOUT); + ares__requeue_query(query, now, ARES_ETIMEOUT, ARES_TRUE); } } @@ -697,6 +697,14 @@ static ares_status_t process_answer(ares_channel_t *channel, goto cleanup; } + /* Validate DNS cookie in response. This function may need to requeue the + * query. */ + if (ares_cookie_validate(query, rdnsrec, conn, now) != ARES_SUCCESS) { + /* Drop response and return */ + status = ARES_SUCCESS; + goto cleanup; + } + /* At this point we know we've received an answer for this query, so we should * remove it from the connection's queue so we can possibly invalidate the * connection. Delay cleaning up the connection though as we may enqueue @@ -708,7 +716,8 @@ static ares_status_t process_answer(ares_channel_t *channel, * protocol extension is not understood by the responder. We must retry the * query without EDNS enabled. */ if (ares_dns_record_get_rcode(rdnsrec) == ARES_RCODE_FORMERR && - ares_dns_has_opt_rr(query->query) && !ares_dns_has_opt_rr(rdnsrec)) { + ares_dns_get_opt_rr_const(query->query) != NULL && + ares_dns_get_opt_rr_const(rdnsrec) == NULL) { status = rewrite_without_edns(query); if (status != ARES_SUCCESS) { end_query(channel, server, query, status, NULL); @@ -754,7 +763,7 @@ static ares_status_t process_answer(ares_channel_t *channel, } server_increment_failures(server, query->using_tcp); - ares__requeue_query(query, now, status); + ares__requeue_query(query, now, status, ARES_TRUE); /* Should any of these cause a connection termination? * Maybe SERVER_FAILURE? */ @@ -801,7 +810,8 @@ static void handle_conn_error(struct server_connection *conn, ares_status_t ares__requeue_query(struct query *query, const ares_timeval_t *now, - ares_status_t status) + ares_status_t status, + ares_bool_t inc_try_count) { ares_channel_t *channel = query->channel; size_t max_tries = ares__slist_len(channel->servers) * channel->tries; @@ -812,7 +822,10 @@ ares_status_t ares__requeue_query(struct query *query, query->error_status = status; } - query->try_count++; + if (inc_try_count) { + query->try_count++; + } + if (query->try_count < max_tries && !query->no_retries) { return ares__send_query(query, now); } @@ -923,44 +936,57 @@ static struct server_state *ares__failover_server(ares_channel_t *channel) return first_server; } -static ares_status_t ares__append_tcpbuf(struct server_state *server, - const struct query *query) +static ares_status_t ares__append_tcpbuf(struct server_connection *conn, + const struct query *query, + const ares_timeval_t *now) { ares_status_t status; unsigned char *qbuf = NULL; size_t qbuf_len = 0; + status = ares_cookie_apply(query->query, conn, now); + if (status != ARES_SUCCESS) { + goto done; + } + status = ares_dns_write(query->query, &qbuf, &qbuf_len); if (status != ARES_SUCCESS) { goto done; } - status = ares__buf_append_be16(server->tcp_send, (unsigned short)qbuf_len); + status = + ares__buf_append_be16(conn->server->tcp_send, (unsigned short)qbuf_len); if (status != ARES_SUCCESS) { goto done; /* LCOV_EXCL_LINE: OutOfMemory */ } - status = ares__buf_append(server->tcp_send, qbuf, qbuf_len); + status = ares__buf_append(conn->server->tcp_send, qbuf, qbuf_len); done: ares_free(qbuf); return status; } -static ares_status_t ares__write_udpbuf(ares_channel_t *channel, - ares_socket_t fd, - const struct query *query) +static ares_status_t ares__write_udpbuf(struct server_connection *conn, + const struct query *query, + const ares_timeval_t *now) { ares_status_t status; unsigned char *qbuf = NULL; size_t qbuf_len = 0; + status = ares_cookie_apply(query->query, conn, now); + if (status != ARES_SUCCESS) { + goto done; + } + status = ares_dns_write(query->query, &qbuf, &qbuf_len); if (status != ARES_SUCCESS) { goto done; } - if (ares__socket_write(channel, fd, qbuf, qbuf_len) == -1) { + if (ares__socket_write(conn->server->channel, conn->fd, qbuf, qbuf_len) == + -1) { if (try_again(SOCKERRNO)) { status = ARES_ESERVFAIL; } else { @@ -1071,7 +1097,7 @@ ares_status_t ares__send_query(struct query *query, const ares_timeval_t *now) case ARES_ECONNREFUSED: case ARES_EBADFAMILY: server_increment_failures(server, query->using_tcp); - return ares__requeue_query(query, now, status); + return ares__requeue_query(query, now, status, ARES_TRUE); /* Anything else is not retryable, likely ENOMEM */ default: @@ -1084,7 +1110,7 @@ ares_status_t ares__send_query(struct query *query, const ares_timeval_t *now) prior_len = ares__buf_len(server->tcp_send); - status = ares__append_tcpbuf(server, query); + status = ares__append_tcpbuf(conn, query, now); if (status != ARES_SUCCESS) { end_query(channel, server, query, status, NULL); @@ -1129,7 +1155,7 @@ ares_status_t ares__send_query(struct query *query, const ares_timeval_t *now) case ARES_ECONNREFUSED: case ARES_EBADFAMILY: server_increment_failures(server, query->using_tcp); - return ares__requeue_query(query, now, status); + return ares__requeue_query(query, now, status, ARES_TRUE); /* Anything else is not retryable, likely ENOMEM */ default: @@ -1141,7 +1167,7 @@ ares_status_t ares__send_query(struct query *query, const ares_timeval_t *now) conn = ares__llist_node_val(node); - status = ares__write_udpbuf(channel, conn->fd, query); + status = ares__write_udpbuf(conn, query, now); if (status != ARES_SUCCESS) { if (status == ARES_ENOMEM) { /* Not retryable */ @@ -1154,7 +1180,7 @@ ares_status_t ares__send_query(struct query *query, const ares_timeval_t *now) /* This query wasn't yet bound to the connection, need to manually * requeue it and return an appropriate error */ - status = ares__requeue_query(query, now, status); + status = ares__requeue_query(query, now, status, ARES_TRUE); if (status == ARES_ETIMEOUT) { status = ARES_ECONNREFUSED; } @@ -1164,7 +1190,7 @@ ares_status_t ares__send_query(struct query *query, const ares_timeval_t *now) /* FIXME: Handle EAGAIN here since it likely can happen. Right now we * just requeue to a different server/connection. */ server_increment_failures(server, query->using_tcp); - status = ares__requeue_query(query, now, status); + status = ares__requeue_query(query, now, status, ARES_TRUE); /* Only safe to kill connection if it was new, otherwise it should be * cleaned up by another process later */ diff --git a/src/lib/ares_sysconfig_mac.c b/src/lib/ares_sysconfig_mac.c index c04ae709..38ac451c 100644 --- a/src/lib/ares_sysconfig_mac.c +++ b/src/lib/ares_sysconfig_mac.c @@ -274,25 +274,7 @@ static ares_status_t read_resolver(const dns_resolver_t *resolver, /* UBSAN alignment workaround to fetch memory address */ memcpy(&sockaddr, resolver->nameserver + i, sizeof(sockaddr)); - if (sockaddr->sa_family == AF_INET) { - /* NOTE: memcpy sockaddr_in due to alignment issues found by UBSAN due to - * dnsinfo packing */ - struct sockaddr_in addr_in; - memcpy(&addr_in, sockaddr, sizeof(addr_in)); - - addr.family = AF_INET; - memcpy(&addr.addr.addr4, &(addr_in.sin_addr), sizeof(addr.addr.addr4)); - addrport = ntohs(addr_in.sin_port); - } else if (sockaddr->sa_family == AF_INET6) { - /* NOTE: memcpy sockaddr_in6 due to alignment issues found by UBSAN due to - * dnsinfo packing */ - struct sockaddr_in6 addr_in6; - memcpy(&addr_in6, sockaddr, sizeof(addr_in6)); - - addr.family = AF_INET6; - memcpy(&addr.addr.addr6, &(addr_in6.sin6_addr), sizeof(addr.addr.addr6)); - addrport = ntohs(addr_in6.sin6_port); - } else { + if (!ares_sockaddr_to_ares_addr(&addr, &addrport, sockaddr)) { continue; } diff --git a/src/lib/event/ares_event_thread.c b/src/lib/event/ares_event_thread.c index cc96907b..8b332e9b 100644 --- a/src/lib/event/ares_event_thread.c +++ b/src/lib/event/ares_event_thread.c @@ -143,13 +143,13 @@ ares_status_t ares_event_update(ares_event_t **event, ares_event_thread_t *e, ev = ares_malloc_zero(sizeof(*ev)); if (ev == NULL) { status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ - goto done; /* LCOV_EXCL_LINE: OutOfMemory */ + goto done; /* LCOV_EXCL_LINE: OutOfMemory */ } if (ares__llist_insert_last(e->ev_updates, ev) == NULL) { - ares_free(ev); /* LCOV_EXCL_LINE: OutOfMemory */ + ares_free(ev); /* LCOV_EXCL_LINE: OutOfMemory */ status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ - goto done; /* LCOV_EXCL_LINE: OutOfMemory */ + goto done; /* LCOV_EXCL_LINE: OutOfMemory */ } } diff --git a/src/tools/adig.c b/src/tools/adig.c index 65fd8a04..8b2ad2e9 100644 --- a/src/tools/adig.c +++ b/src/tools/adig.c @@ -754,18 +754,31 @@ static void print_section(ares_dns_record_t *dnsrec, ares_dns_section_t section) static void print_opt_psuedosection(ares_dns_record_t *dnsrec) { - const ares_dns_rr_t *rr = has_opt(dnsrec, ARES_SECTION_ADDITIONAL); + const ares_dns_rr_t *rr = has_opt(dnsrec, ARES_SECTION_ADDITIONAL); + const unsigned char *cookie = NULL; + size_t cookie_len = 0; + if (rr == NULL) { return; } + if (!ares_dns_rr_get_opt_byid(rr, ARES_RR_OPT_OPTIONS, ARES_OPT_PARAM_COOKIE, + &cookie, &cookie_len)) { + cookie = NULL; + } + + printf(";; OPT PSEUDOSECTION:\n"); - printf("; EDNS: version: %u, flags: %u; udp: %u\t", + printf("; EDNS: version: %u, flags: %u; udp: %u\n", (unsigned int)ares_dns_rr_get_u8(rr, ARES_RR_OPT_VERSION), (unsigned int)ares_dns_rr_get_u16(rr, ARES_RR_OPT_FLAGS), (unsigned int)ares_dns_rr_get_u16(rr, ARES_RR_OPT_UDP_SIZE)); - printf("\n"); + if (cookie) { + printf("; COOKIE: "); + print_opt_bin(cookie, cookie_len); + printf(" (good)\n"); + } } static void callback(void *arg, int status, int timeouts, unsigned char *abuf, diff --git a/test/ares-test-misc.cc b/test/ares-test-misc.cc index 5d5641a9..13aec90c 100644 --- a/test/ares-test-misc.cc +++ b/test/ares-test-misc.cc @@ -519,7 +519,7 @@ TEST_F(LibraryTest, CreateEDNSQuery) { std::string actual = PacketToString(data); DNSPacket pkt; pkt.set_qid(0x1234).add_question(new DNSQuestion("example.com", T_A)) - .add_additional(new DNSOptRR(0, 1280)); + .add_additional(new DNSOptRR(0, 0, 0, 1280, { }, { } /* No server cookie */)); std::string expected = PacketToString(pkt.data()); EXPECT_EQ(expected, actual); } diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc index 24b5de86..731d7d43 100644 --- a/test/ares-test-mock.cc +++ b/test/ares-test-mock.cc @@ -1005,7 +1005,9 @@ TEST_P(MockChannelTest, SearchHighNdots) { // Test that performing an EDNS search with an OPT RR options value works. The // options value should be included on the requests to the mock server. -TEST_P(MockEDNSChannelTest, SearchOptVal) { +// We are going to do this only via TCP since this won't include the dynamically +// generated DNS cookie that would otherwise mess with this result. +TEST_P(MockTCPChannelTest, SearchOptVal) { /* Define the OPT RR options code and value to use. */ unsigned short opt_opt = 3; unsigned char opt_val[] = { 'c', '-', 'a', 'r', 'e', 's' }; @@ -1511,6 +1513,264 @@ TEST_P(MockChannelTest, GetHostByAddrDestroy) { EXPECT_EQ(0, result.timeouts_); } +static const unsigned char * + fetch_server_cookie(const ares_dns_record_t *dnsrec, size_t *len) +{ + const ares_dns_rr_t *rr = fetch_rr_opt(dnsrec); + const unsigned char *val = NULL; + *len = 0; + + if (rr == NULL) { + return NULL; + } + + if (!ares_dns_rr_get_opt_byid(rr, ARES_RR_OPT_OPTIONS, ARES_OPT_PARAM_COOKIE, + &val, len)) { + return NULL; + } + + if (*len <= 8) { + *len = 0; + return NULL; + } + + *len -= 8; + val += 8; + return val; +} + +static const unsigned char * + fetch_client_cookie(const ares_dns_record_t *dnsrec, size_t *len) +{ + const ares_dns_rr_t *rr = fetch_rr_opt(dnsrec); + const unsigned char *val = NULL; + *len = 0; + + if (rr == NULL) { + return NULL; + } + + if (!ares_dns_rr_get_opt_byid(rr, ARES_RR_OPT_OPTIONS, ARES_OPT_PARAM_COOKIE, + &val, len)) { + return NULL; + } + + if (*len < 8) { + *len = 0; + return NULL; + } + + *len = 8; + return val; +} + +TEST_P(MockUDPChannelTest, DNSCookieSingle) { + DNSPacket reply; + std::vector server_cookie = { 1, 2, 3, 4, 5, 6, 7, 8 }; + reply.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 0x0100, {0x01, 0x02, 0x03, 0x04})) + .add_additional(new DNSOptRR(0, 0, 0, 1280, { }, server_cookie)); + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .WillOnce(SetReply(&server_, &reply)); + + QueryResult result; + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, QueryCallback, &result, NULL); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(0, result.timeouts_); + + size_t len; + const unsigned char *returned_cookie = fetch_server_cookie(result.dnsrec_.dnsrec_, &len); + EXPECT_EQ(len, server_cookie.size()); + EXPECT_TRUE(memcmp(server_cookie.data(), returned_cookie, len) == 0); +} + +TEST_P(MockUDPChannelTest, DNSCookieMissingAfterGood) { + DNSPacket reply; + std::vector server_cookie = { 1, 2, 3, 4, 5, 6, 7, 8 }; + reply.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 0x0100, {0x01, 0x02, 0x03, 0x04})) + .add_additional(new DNSOptRR(0, 0, 0, 1280, { }, server_cookie)); + DNSPacket reply_nocookie; + reply_nocookie.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 0x0100, {0x01, 0x02, 0x03, 0x04})) + .add_additional(new DNSOptRR(0, 0, 0, 1280, { }, { })); + + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .WillOnce(SetReply(&server_, &reply)) + .WillOnce(SetReply(&server_, &reply_nocookie)) + .WillOnce(SetReply(&server_, &reply)); + + /* This test will establish the server supports cookies, then the next reply + * will be missing the server cookie and therefore be rejected and timeout, then + * an internal retry will occur and the cookie will be present again. */ + QueryResult result1; + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, QueryCallback, &result1, NULL); + Process(); + EXPECT_TRUE(result1.done_); + EXPECT_EQ(0, result1.timeouts_); + + QueryResult result2; + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, QueryCallback, &result2, NULL); + Process(); + EXPECT_TRUE(result2.done_); + EXPECT_EQ(1, result2.timeouts_); + + /* Client cookie should NOT have rotated */ + size_t len1; + const unsigned char *client_cookie_1 = fetch_client_cookie(result1.dnsrec_.dnsrec_, &len1); + size_t len2; + const unsigned char *client_cookie_2 = fetch_client_cookie(result2.dnsrec_.dnsrec_, &len2); + EXPECT_EQ(len1, 8); + EXPECT_EQ(len1, len2); + EXPECT_TRUE(memcmp(client_cookie_1, client_cookie_2, len1) == 0); +} + + +TEST_P(MockUDPChannelTest, DNSCookieBadLen) { + std::vector server_cookie = { 1, 2, 3, 4, 5, 6, 7, 8 }; + std::vector server_cookie_bad = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0 }; + DNSPacket reply; + reply.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 0x0100, {0x01, 0x02, 0x03, 0x04})) + .add_additional(new DNSOptRR(0, 0, 0, 1280, { }, server_cookie)); + DNSPacket reply_badcookielen; + reply_badcookielen.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 0x0100, {0x01, 0x02, 0x03, 0x04})) + .add_additional(new DNSOptRR(0, 0, 0, 1280, { }, server_cookie_bad )); + + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .WillOnce(SetReply(&server_, &reply_badcookielen)) + .WillOnce(SetReply(&server_, &reply)); + + /* This test will send back a malformed cookie len, then when it times out and retry occurs will send back a valid cookie. */ + QueryResult result1; + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, QueryCallback, &result1, NULL); + Process(); + EXPECT_TRUE(result1.done_); + EXPECT_EQ(1, result1.timeouts_); +} + + +TEST_P(MockUDPChannelTest, DNSCookieServerRotate) { + std::vector server_cookie = { 1, 2, 3, 4, 5, 6, 7, 8 }; + std::vector server_cookie_rotate = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF }; + + DNSPacket reply_cookie1; + reply_cookie1.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 0x0100, {0x01, 0x02, 0x03, 0x04})) + .add_additional(new DNSOptRR(0, 0, 0, 1280, {}, server_cookie)); + DNSPacket reply_cookie2_badcookie; + reply_cookie2_badcookie.set_response().set_aa().set_rcode(ARES_RCODE_BADCOOKIE & 0xF) + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 0x0100, {0x01, 0x02, 0x03, 0x04})) + .add_additional(new DNSOptRR((ARES_RCODE_BADCOOKIE >> 4) & 0xFF, 0, 0, 1280, { }, server_cookie_rotate)); + DNSPacket reply_cookie2; + reply_cookie2.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 0x0100, {0x01, 0x02, 0x03, 0x04})) + .add_additional(new DNSOptRR(0, 0, 0, 1280, { }, server_cookie_rotate)); + + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .WillOnce(SetReply(&server_, &reply_cookie1)) + .WillOnce(SetReply(&server_, &reply_cookie2_badcookie)) + .WillOnce(SetReply(&server_, &reply_cookie2)); + + /* This test will establish the server supports cookies, then the next reply + * the server returns BADCOOKIE indicating the cookie has rotated and + * returns a new cookie. Then the query will be automatically retried with + * the newly returned cookie. No timeouts should be indicated, and the + * client cookie should not rotate. */ + QueryResult result1; + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, QueryCallback, &result1, NULL); + Process(); + EXPECT_TRUE(result1.done_); + EXPECT_EQ(0, result1.timeouts_); + + QueryResult result2; + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, QueryCallback, &result2, NULL); + Process(); + EXPECT_TRUE(result2.done_); + EXPECT_EQ(0, result2.timeouts_); + + /* Client cookie should NOT have rotated */ + size_t len1; + const unsigned char *client_cookie_1 = fetch_client_cookie(result1.dnsrec_.dnsrec_, &len1); + size_t len2; + const unsigned char *client_cookie_2 = fetch_client_cookie(result2.dnsrec_.dnsrec_, &len2); + EXPECT_EQ(len1, 8); + EXPECT_EQ(len1, len2); + EXPECT_TRUE(memcmp(client_cookie_1, client_cookie_2, len1) == 0); +} + +TEST_P(MockUDPChannelTest, DNSCookieSpoof) { + std::vector client_cookie = { 1, 2, 3, 4, 5, 6, 7, 8 }; + std::vector server_cookie = { 1, 2, 3, 4, 5, 6, 7, 8 }; + + DNSPacket reply_spoof; + reply_spoof.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 0x0100, {0x01, 0x02, 0x03, 0x04})) + .add_additional(new DNSOptRR(0, 0, 0, 1280, client_cookie, server_cookie)); + DNSPacket reply; + reply.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 0x0100, {0x01, 0x02, 0x03, 0x04})) + .add_additional(new DNSOptRR(0, 0, 0, 1280, { }, server_cookie)); + + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .WillOnce(SetReply(&server_, &reply_spoof)) + .WillOnce(SetReply(&server_, &reply)); + + /* This test will return a reply that doesn't have the same client cookie as + * was sent, this should result in a drop of the packet alltogether, then + * the library will retry and a proper result will be sent. */ + QueryResult result; + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, QueryCallback, &result, NULL); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(1, result.timeouts_); +} + +TEST_P(MockUDPChannelTest, DNSCookieTCPUpgrade) { + std::vector server_cookie = { 1, 2, 3, 4, 5, 6, 7, 8 }; + + DNSPacket reply_badcookie; + reply_badcookie.set_response().set_aa().set_rcode(ARES_RCODE_BADCOOKIE & 0xF) + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 0x0100, {0x01, 0x02, 0x03, 0x04})) + .add_additional(new DNSOptRR((ARES_RCODE_BADCOOKIE >> 4) & 0xFF, 0, 0, 1280, { }, server_cookie)); + DNSPacket reply; + reply.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 0x0100, {0x01, 0x02, 0x03, 0x04})) + .add_additional(new DNSOptRR(0, 0, 0, 1280, { }, { })); + + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .WillOnce(SetReply(&server_, &reply_badcookie)) + .WillOnce(SetReply(&server_, &reply_badcookie)) + .WillOnce(SetReply(&server_, &reply_badcookie)) + .WillOnce(SetReply(&server_, &reply)); + + /* This test will establish the server supports cookies, but continuously + * returns BADCOOKIE which is an indicator that there is some form of + * AnyCast issue across servers, so it upgrades to TCP afterwards. No + * timeouts are recorded as the queries are sent back-to-back as immediate + * reattempts after the response. */ + QueryResult result; + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, QueryCallback, &result, NULL); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(0, result.timeouts_); +} + + #ifndef WIN32 TEST_P(MockChannelTest, HostAlias) { DNSPacket reply; diff --git a/test/ares-test-parse-ptr.cc b/test/ares-test-parse-ptr.cc index 2ebe1b9f..9985ef3c 100644 --- a/test/ares-test-parse-ptr.cc +++ b/test/ares-test-parse-ptr.cc @@ -73,8 +73,8 @@ TEST_F(LibraryTest, ParsePtrReplyCname) { struct DNSMalformedCnameRR : public DNSCnameRR { DNSMalformedCnameRR(const std::string& name, int ttl, const std::string& other) : DNSCnameRR(name, ttl, other) {} - std::vector data() const { - std::vector data = DNSRR::data(); + std::vector data(const ares_dns_record_t *dnsrec) const { + std::vector data = DNSRR::data(dnsrec); std::vector encname = EncodeString(other_); encname[0] = encname[0] + 63; // invalid label length int len = (int)encname.size(); diff --git a/test/ares-test.cc b/test/ares-test.cc index 1885a7cf..03c328f8 100644 --- a/test/ares-test.cc +++ b/test/ares-test.cc @@ -596,7 +596,7 @@ void MockServer::ProcessPacket(ares_socket_t fd, struct sockaddr_storage *addr, std::cerr << "ProcessRequest(" << qid << ", '" << name << "', " << RRTypeToString(rrtype) << ")" << std::endl; } - ProcessRequest(fd, addr, addrlen, reqstr, qid, name, rrtype); + ProcessRequest(fd, addr, addrlen, req, reqstr, qid, name, rrtype); ares_free_string(name); } @@ -666,7 +666,8 @@ std::set MockServer::fds() const { } void MockServer::ProcessRequest(ares_socket_t fd, struct sockaddr_storage* addr, - ares_socklen_t addrlen, const std::string &reqstr, + ares_socklen_t addrlen, const std::vector &req, + const std::string &reqstr, int qid, const char *name, int rrtype) { /* DNS 0x20 will mix case, do case-insensitive matching of name in request */ @@ -683,7 +684,17 @@ void MockServer::ProcessRequest(ares_socket_t fd, struct sockaddr_storage* addr, } if (reply_ != nullptr) { - exact_reply_ = reply_->data(name); + ares_dns_record_t *dnsrec = NULL; + /* We will *attempt* to parse the request string. It may be malformed that + * will lead to a parse failure. If so, we just ignore it. We want to + * pass this parsed data structure to the reply generator in case it needs + * to extract metadata (such as a DNS client cookie) from the original + * request. If we can't parse it, oh well, we'll just pass NULL, most + * replies don't need anything from the request other than the name which + * is passed separately. */ + ares_dns_parse(req.data(), req.size(), 0, &dnsrec); + exact_reply_ = reply_->data(name, dnsrec); + ares_dns_record_destroy(dnsrec); } if (exact_reply_.size() == 0) { @@ -1023,6 +1034,44 @@ void HostCallback(void *data, int status, int timeouts, if (verbose) std::cerr << "HostCallback(" << *result << ")" << std::endl; } +std::ostream& operator<<(std::ostream& os, const AresDnsRecord& dnsrec) { + os << "{'"; + /* XXX: Todo */ + os << '}'; + return os; +} + +std::ostream& operator<<(std::ostream& os, const QueryResult& result) { + os << '{'; + if (result.done_) { + os << StatusToString(result.status_); + if (result.dnsrec_.dnsrec_ != nullptr) { + os << " " << result.dnsrec_; + } else { + os << ", (no dnsrec)"; + } + } else { + os << "(incomplete)"; + } + os << '}'; + return os; +} + +void QueryCallback(void *data, ares_status_t status, size_t timeouts, + const ares_dns_record_t *dnsrec) { + EXPECT_NE(nullptr, data); + if (data == nullptr) + return; + + QueryResult* result = reinterpret_cast(data); + result->done_ = true; + result->status_ = status; + result->timeouts_ = timeouts; + if (dnsrec) + result->dnsrec_.SetDnsRecord(dnsrec); + if (verbose) std::cerr << "QueryCallback(" << *result << ")" << std::endl; +} + std::ostream& operator<<(std::ostream& os, const AddrInfoResult& result) { os << '{'; if (result.done_ && result.ai_) { diff --git a/test/ares-test.h b/test/ares-test.h index 26d84845..0f51aced 100644 --- a/test/ares-test.h +++ b/test/ares-test.h @@ -290,7 +290,8 @@ public: private: void ProcessRequest(ares_socket_t fd, struct sockaddr_storage *addr, - ares_socklen_t addrlen, const std::string &reqstr, + ares_socklen_t addrlen, const std::vector &req, + const std::string &reqstr, int qid, const char *name, int rrtype); void ProcessPacket(ares_socket_t fd, struct sockaddr_storage *addr, ares_socklen_t addrlen, byte *data, int len); @@ -496,6 +497,52 @@ struct HostResult { std::ostream &operator<<(std::ostream &os, const HostResult &result); + +// C++ wrapper for ares_dns_record_t. +struct AresDnsRecord { + ~AresDnsRecord() + { + ares_dns_record_destroy(dnsrec_); + dnsrec_ = NULL; + } + + AresDnsRecord() : dnsrec_(NULL) + { + } + + void SetDnsRecord(const ares_dns_record_t *dnsrec) { + if (dnsrec_ != NULL) { + ares_dns_record_destroy(dnsrec_); + } + if (dnsrec == NULL) { + return; + } + dnsrec_ = ares_dns_record_duplicate(dnsrec); + } + + ares_dns_record_t *dnsrec_ = NULL; +}; + +std::ostream &operator<<(std::ostream &os, const AresDnsRecord &result); + +// Structure that describes the result of an ares_host_callback invocation. +struct QueryResult { + QueryResult() : done_(false), status_(ARES_SUCCESS), timeouts_(0) + { + } + + // Whether the callback has been invoked. + bool done_; + // Explicitly provided result information. + ares_status_t status_; + size_t timeouts_; + // Contents of the ares_dns_record_t structure if provided + AresDnsRecord dnsrec_; +}; + +std::ostream &operator<<(std::ostream &os, const QueryResult &result); + + // Structure that describes the result of an ares_callback invocation. struct SearchResult { // Whether the callback has been invoked. @@ -556,6 +603,8 @@ std::ostream &operator<<(std::ostream &os, const AddrInfoResult &result); // structures. void HostCallback(void *data, int status, int timeouts, struct hostent *hostent); +void QueryCallback(void *data, ares_status_t status, size_t timeouts, + const ares_dns_record_t *dnsrec); void SearchCallback(void *data, int status, int timeouts, unsigned char *abuf, int alen); void SearchCallbackDnsRec(void *data, ares_status_t status, size_t timeouts, diff --git a/test/dns-proto.cc b/test/dns-proto.cc index f9c7d4d5..85f79aa9 100644 --- a/test/dns-proto.cc +++ b/test/dns-proto.cc @@ -548,7 +548,7 @@ std::vector EncodeString(const std::string &name) { return data; } -std::vector DNSQuestion::data(const char *request_name) const { +std::vector DNSQuestion::data(const char *request_name, const ares_dns_record_t *dnsrec) const { std::vector data; std::vector encname; if (request_name != nullptr && strcasecmp(request_name, name_.c_str()) == 0) { @@ -562,14 +562,14 @@ std::vector DNSQuestion::data(const char *request_name) const { return data; } -std::vector DNSRR::data() const { - std::vector data = DNSQuestion::data(); +std::vector DNSRR::data(const ares_dns_record_t *dnsrec) const { + std::vector data = DNSQuestion::data(dnsrec); PushInt32(&data, ttl_); return data; } -std::vector DNSSingleNameRR::data() const { - std::vector data = DNSRR::data(); +std::vector DNSSingleNameRR::data(const ares_dns_record_t *dnsrec) const { + std::vector data = DNSRR::data(dnsrec); std::vector encname = EncodeString(other_); int len = (int)encname.size(); PushInt16(&data, len); @@ -577,8 +577,8 @@ std::vector DNSSingleNameRR::data() const { return data; } -std::vector DNSTxtRR::data() const { - std::vector data = DNSRR::data(); +std::vector DNSTxtRR::data(const ares_dns_record_t *dnsrec) const { + std::vector data = DNSRR::data(dnsrec); int len = 0; for (const std::string& txt : txt_) { len += (1 + (int)txt.size()); @@ -591,8 +591,8 @@ std::vector DNSTxtRR::data() const { return data; } -std::vector DNSMxRR::data() const { - std::vector data = DNSRR::data(); +std::vector DNSMxRR::data(const ares_dns_record_t *dnsrec) const { + std::vector data = DNSRR::data(dnsrec); std::vector encname = EncodeString(other_); int len = 2 + (int)encname.size(); PushInt16(&data, len); @@ -601,8 +601,8 @@ std::vector DNSMxRR::data() const { return data; } -std::vector DNSSrvRR::data() const { - std::vector data = DNSRR::data(); +std::vector DNSSrvRR::data(const ares_dns_record_t *dnsrec) const { + std::vector data = DNSRR::data(dnsrec); std::vector encname = EncodeString(target_); int len = 6 + (int)encname.size(); PushInt16(&data, len); @@ -613,8 +613,8 @@ std::vector DNSSrvRR::data() const { return data; } -std::vector DNSUriRR::data() const { - std::vector data = DNSRR::data(); +std::vector DNSUriRR::data(const ares_dns_record_t *dnsrec) const { + std::vector data = DNSRR::data(dnsrec); int len = 4 + (int)target_.size(); PushInt16(&data, len); PushInt16(&data, prio_); @@ -623,16 +623,16 @@ std::vector DNSUriRR::data() const { return data; } -std::vector DNSAddressRR::data() const { - std::vector data = DNSRR::data(); +std::vector DNSAddressRR::data(const ares_dns_record_t *dnsrec) const { + std::vector data = DNSRR::data(dnsrec); int len = (int)addr_.size(); PushInt16(&data, len); data.insert(data.end(), addr_.begin(), addr_.end()); return data; } -std::vector DNSSoaRR::data() const { - std::vector data = DNSRR::data(); +std::vector DNSSoaRR::data(const ares_dns_record_t *dnsrec) const { + std::vector data = DNSRR::data(dnsrec); std::vector encname1 = EncodeString(nsname_); std::vector encname2 = EncodeString(rname_); int len = (int)encname1.size() + (int)encname2.size() + 5*4; @@ -647,23 +647,70 @@ std::vector DNSSoaRR::data() const { return data; } -std::vector DNSOptRR::data() const { - std::vector data = DNSRR::data(); - int len = 0; +const ares_dns_rr_t *fetch_rr_opt(const ares_dns_record_t *rec) +{ + size_t i; + for (i = 0; i < ares_dns_record_rr_cnt(rec, ARES_SECTION_ADDITIONAL); i++) { + const ares_dns_rr_t *rr = + ares_dns_record_rr_get_const(rec, ARES_SECTION_ADDITIONAL, i); + + if (ares_dns_rr_get_type(rr) == ARES_REC_TYPE_OPT) { + return rr; + } + } + return NULL; +} + +std::vector DNSOptRR::data(const ares_dns_record_t *dnsrec) const { + std::vector data = DNSRR::data(dnsrec); + int len = 0; + std::vector cookie; + + /* See if we should be applying a server cookie */ + if (server_cookie_.size()) { + const ares_dns_rr_t *rr = fetch_rr_opt(dnsrec); + const unsigned char *val = NULL; + size_t len = 0; + + if (ares_dns_rr_get_opt_byid(rr, ARES_RR_OPT_OPTIONS, ARES_OPT_PARAM_COOKIE, + &val, &len)) { + /* If client cookie was provided to test framework, we are overwriting + * the one received from the client. This is likely to test failure + * scenarios */ + if (client_cookie_.size()) { + cookie.insert(cookie.end(), client_cookie_.begin(), client_cookie_.end()); + } else { + cookie.insert(cookie.end(), val, val+8); + } + cookie.insert(cookie.end(), server_cookie_.begin(), server_cookie_.end()); + } + } + + if (cookie.size()) { + len += 4 + (int)cookie.size(); + } for (const DNSOption& opt : opts_) { len += (4 + (int)opt.data_.size()); } + PushInt16(&data, len); for (const DNSOption& opt : opts_) { PushInt16(&data, opt.code_); PushInt16(&data, (int)opt.data_.size()); data.insert(data.end(), opt.data_.begin(), opt.data_.end()); } + + if (cookie.size()) { + PushInt16(&data, ARES_OPT_PARAM_COOKIE); + PushInt16(&data, (int)cookie.size()); + data.insert(data.end(), cookie.begin(), cookie.end()); + } + return data; } -std::vector DNSNaptrRR::data() const { - std::vector data = DNSRR::data(); +std::vector DNSNaptrRR::data(const ares_dns_record_t *dnsrec) const { + std::vector data = DNSRR::data(dnsrec); std::vector encname = EncodeString(replacement_); int len = (4 + 1 + (int)flags_.size() + 1 + (int)service_.size() + 1 + (int)regexp_.size() + (int)encname.size()); PushInt16(&data, len); @@ -679,7 +726,7 @@ std::vector DNSNaptrRR::data() const { return data; } -std::vector DNSPacket::data(const char *request_name) const { +std::vector DNSPacket::data(const char *request_name, const ares_dns_record_t *dnsrec) const { std::vector data; PushInt16(&data, qid_); byte b = 0x00; @@ -707,19 +754,19 @@ std::vector DNSPacket::data(const char *request_name) const { PushInt16(&data, count); for (const std::unique_ptr& question : questions_) { - std::vector qdata = question->data(request_name); + std::vector qdata = question->data(request_name, dnsrec); data.insert(data.end(), qdata.begin(), qdata.end()); } for (const std::unique_ptr& rr : answers_) { - std::vector rrdata = rr->data(); + std::vector rrdata = rr->data(dnsrec); data.insert(data.end(), rrdata.begin(), rrdata.end()); } for (const std::unique_ptr& rr : auths_) { - std::vector rrdata = rr->data(); + std::vector rrdata = rr->data(dnsrec); data.insert(data.end(), rrdata.begin(), rrdata.end()); } for (const std::unique_ptr& rr : adds_) { - std::vector rrdata = rr->data(); + std::vector rrdata = rr->data(dnsrec); data.insert(data.end(), rrdata.begin(), rrdata.end()); } return data; diff --git a/test/dns-proto.h b/test/dns-proto.h index 3fe95c41..af8f1cc5 100644 --- a/test/dns-proto.h +++ b/test/dns-proto.h @@ -53,6 +53,8 @@ std::string RRTypeToString(int rrtype); std::string ClassToString(int qclass); std::string AddressToString(const void *addr, int len); +const ares_dns_rr_t *fetch_rr_opt(const ares_dns_record_t *rec); + // Convert DNS protocol data to strings. // Note that these functions are not defensive; they assume // a validly formatted input, and so should not be used on @@ -84,11 +86,16 @@ struct DNSQuestion { { } - virtual std::vector data(const char *request_name) const; + virtual std::vector data(const char *request_name, const ares_dns_record_t *dnsrec) const; + + virtual std::vector data(const ares_dns_record_t *dnsrec) const + { + return data(nullptr, dnsrec); + } virtual std::vector data() const { - return data(nullptr); + return data(nullptr, nullptr); } std::string name_; @@ -111,7 +118,7 @@ struct DNSRR : public DNSQuestion { { } - virtual std::vector data() const = 0; + virtual std::vector data(const ares_dns_record_t *dnsrec) const = 0; int ttl_; }; @@ -128,7 +135,7 @@ struct DNSAddressRR : public DNSRR { { } - virtual std::vector data() const; + virtual std::vector data(const ares_dns_record_t *dnsrec) const; std::vector addr_; }; @@ -163,7 +170,7 @@ struct DNSSingleNameRR : public DNSRR { { } - virtual std::vector data() const; + virtual std::vector data(const ares_dns_record_t *dnsrec) const; std::string other_; }; @@ -195,7 +202,7 @@ struct DNSTxtRR : public DNSRR { { } - virtual std::vector data() const; + virtual std::vector data(const ares_dns_record_t *dnsrec) const; std::vector txt_; }; @@ -205,7 +212,7 @@ struct DNSMxRR : public DNSRR { { } - virtual std::vector data() const; + virtual std::vector data(const ares_dns_record_t *dnsrec) const; int pref_; std::string other_; }; @@ -218,7 +225,7 @@ struct DNSSrvRR : public DNSRR { { } - virtual std::vector data() const; + virtual std::vector data(const ares_dns_record_t *dnsrec) const; int prio_; int weight_; int port_; @@ -232,7 +239,7 @@ struct DNSUriRR : public DNSRR { { } - virtual std::vector data() const; + virtual std::vector data(const ares_dns_record_t *dnsrec) const; int prio_; int weight_; std::string target_; @@ -247,7 +254,7 @@ struct DNSSoaRR : public DNSRR { { } - virtual std::vector data() const; + virtual std::vector data(const ares_dns_record_t *dnsrec) const; std::string nsname_; std::string rname_; int serial_; @@ -266,7 +273,7 @@ struct DNSNaptrRR : public DNSRR { { } - virtual std::vector data() const; + virtual std::vector data(const ares_dns_record_t *dnsrec) const; int order_; int pref_; std::string flags_; @@ -281,13 +288,17 @@ struct DNSOption { }; struct DNSOptRR : public DNSRR { - DNSOptRR(int extrcode, int udpsize) - : DNSRR("", T_OPT, static_cast(udpsize), extrcode) + DNSOptRR(unsigned char extrcode, unsigned char version, unsigned short flags, int udpsize, std::vector client_cookie, std::vector server_cookie) + : DNSRR("", T_OPT, static_cast(udpsize), ((int)extrcode) << 24 | ((int)version) << 16 | ((int)flags)/* ttl */) { + client_cookie_ = client_cookie; + server_cookie_ = server_cookie; } - virtual std::vector data() const; + virtual std::vector data(const ares_dns_record_t *dnsrec) const; std::vector opts_; + std::vector client_cookie_; + std::vector server_cookie_; }; struct DNSPacket { @@ -384,11 +395,11 @@ struct DNSPacket { } // Return the encoded packet. - std::vector data(const char *request_name) const; + std::vector data(const char *request_name, const ares_dns_record_t *dnsrec) const; std::vector data() const { - return data(nullptr); + return data(nullptr, nullptr); } int qid_;