quickjs-tart

quickjs-based runtime for wallet-core logic
Log | Files | Refs | README | LICENSE

ares_cookie.c (19045B)


      1 /* MIT License
      2  *
      3  * Copyright (c) 2024 Brad House
      4  *
      5  * Permission is hereby granted, free of charge, to any person obtaining a copy
      6  * of this software and associated documentation files (the "Software"), to deal
      7  * in the Software without restriction, including without limitation the rights
      8  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
      9  * copies of the Software, and to permit persons to whom the Software is
     10  * furnished to do so, subject to the following conditions:
     11  *
     12  * The above copyright notice and this permission notice (including the next
     13  * paragraph) shall be included in all copies or substantial portions of the
     14  * Software.
     15  *
     16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     17  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     18  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     19  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     20  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     21  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     22  * SOFTWARE.
     23  *
     24  * SPDX-License-Identifier: MIT
     25  */
     26 
     27 /* DNS cookies are a simple form of learned mutual authentication supported by
     28  * most DNS server implementations these days and can help prevent DNS Cache
     29  * Poisoning attacks for clients and DNS amplification attacks for servers.
     30  *
     31  * A good overview is here:
     32  * https://www.dotmagazine.online/issues/digital-responsibility-and-sustainability/dns-cookies-transaction-mechanism
     33  *
     34  * RFCs used for implementation are
     35  * [RFC7873](https://datatracker.ietf.org/doc/html/rfc7873) which is extended by
     36  * [RFC9018](https://datatracker.ietf.org/doc/html/rfc9018).
     37  *
     38  * Though this could be used on TCP, the likelihood of it being useful is small
     39  * and could cause some issues.  TCP is better used as a fallback in case there
     40  * are issues with DNS Cookie support in the upstream servers (e.g. AnyCast
     41  * cluster issues).
     42  *
     43  * While most recursive DNS servers support DNS Cookies, public DNS servers like
     44  * Google (8.8.8.8, 8.8.4.4) and CloudFlare (1.1.1.1, 1.0.0.1) don't seem to
     45  * have this enabled yet for unknown reasons.
     46  *
     47  * The risk to having DNS Cookie support always enabled is nearly zero as there
     48  * is built-in detection support and it will simply bypass using cookies if the
     49  * remote server doesn't support it.  The problem arises if a remote server
     50  * supports DNS cookies, then stops supporting them (such as if an administrator
     51  * reconfigured the server, or maybe there are different servers in a cluster
     52  * with different configurations).  We need to detect this behavior by tracking
     53  * how much time has gone by since we received our last valid cookie reply, and
     54  * if we exceed the threshold, reset all cookie parameters like we haven't
     55  * attempted a request yet.
     56  *
     57  * ## Implementation Plan
     58  *
     59  * ### Constants:
     60  *  - `COOKIE_CLIENT_TIMEOUT`: 86400s (1 day)
     61  *     - How often to regenerate the per-server client cookie, even if our
     62  *       source ip address hasn't changed.
     63  *  - `COOKIE_UNSUPPORTED_TIMEOUT`: 300s (5 minutes)
     64  *     - If a server responds without a cookie in the reply, this is how long to
     65  *       wait before attempting to send a client cookie again.
     66  *  - `COOKIE_REGRESSION_TIMEOUT`: 120s (2 minutes)
     67  *     - If a server was once known to return cookies, and all of a sudden stops
     68  *       returning cookies (but the reply is otherwise valid), this is how long
     69  *       to continue to attempt to use cookies before giving up and resetting.
     70  *       Such an event would cause an outage for this duration, but since a
     71  *       cache poisoning attack should be dropping invalid replies we should be
     72  *       able to still get the valid reply and not assume it is a server
     73  *       regression just because we received replies without cookies.
     74  *  - `COOKIE_RESEND_MAX`: 3
     75  *    - Maximum times to resend a query to a server due to the server responding
     76  *      with `BAD_COOKIE`, after this, we switch to TCP.
     77  *
     78  * ### Per-server variables:
     79  *  - `cookie.state`: Known state of cookie support, enumeration.
     80  *    - `INITIAL` (0): Initial state, not yet determined. Used during startup.
     81  *    - `GENERATED` (1): Cookie has been generated and sent to a server, but no
     82  *      validated response yet.
     83  *    - `SUPPORTED` (2):  Server has been determined to properly support cookies
     84  *    - `UNSUPPORTED` (3): Server has been determined to not support cookies
     85  *  - `cookie.client` : 8 byte randomly generated client cookie
     86  *  - `cookie.client_ts`: Timestamp client cookie was generated
     87  *  - `cookie.client_ip`: IP address client used to connect to server
     88  *  - `cookie.server`: 8 to 32 byte server cookie
     89  *  - `cookie.server_len`: length of server cookie
     90  *  - `cookie.unsupported_ts`: Timestamp of last attempt to use a cookies, but
     91  *    it was determined that the server didn't support them.
     92  *
     93  * ### Per-query variables:
     94  *  - `query.client_cookie`: Duplicate of `cookie.client` at the point in time
     95  *    the query is put on the wire.  This should be available in the
     96  *    `ares_dns_record_t` for the request for verification purposes so we don't
     97  *    actually need to duplicate this, just naming it here for the ease of
     98  *    documentation below.
     99  * - `query.cookie_try_count`: Number of tries to send a cookie but receive
    100  *   `BAD_COOKIE` responses.  Used to know when we need to switch to TCP.
    101  *
    102  * ### Procedure:
    103  * **NOTE**: These steps will all be done after obtaining a connection handle as
    104  * some of these steps depend on determining the source ip address for the
    105  * connection.
    106  *
    107  * 1. If the query is not using EDNS, then **skip any remaining processing**.
    108  * 2. If using TCP, ensure there is no EDNS cookie opt (10) set (there may have
    109  *    been if this is a resend after upgrade to TCP), then **skip any remaining
    110  *    processing**.
    111  * 3. If `cookie.state == SUPPORTED`, `cookie.unsupported_ts` is non-zero, and
    112  *    evaluates greater than `COOKIE_REGRESSION_TIMEOUT`, then clear all cookie
    113  *    settings, set `cookie.state = INITIAL`. Continue to next step (4)
    114  * 4. If `cookie.state == UNSUPPORTED`
    115  *     - If `cookie.unsupported_ts` evaluates less than
    116  *       `COOKIE_UNSUPPORTED_TIMEOUT`
    117  *        - Ensure there is no EDNS cookie opt (10) set (shouldn't be unless
    118  *          requester had put this themselves), then **skip any remaining
    119  *          processing** as we don't want to try to send cookies.
    120  *     - Otherwise:
    121  *       - clear all cookie settings, set `cookie.state = INITIAL`.
    122  *       - Continue to next step (5) which will send a new cookie.
    123  * 5. If `cookie.state == INITIAL`:
    124  *    - randomly generate new `cookie.client`
    125  *    - set `cookie.client_ts` to the current timestamp.
    126  *    - set `cookie.state = GENERATED`.
    127  *    - set `cookie.client_ip` to the current source ip address.
    128  * 6. If `cookie.state == GENERATED || cookie.state == SUPPORTED` and
    129  *    `cookie.client_ip` does not match the current source ip address:
    130  *    - clear `cookie.server`
    131  *    - randomly generate new `cookie.client`
    132  *    - set `cookie.client_ts` to the current timestamp.
    133  *    - set `cookie.client_ip` to the current source ip address.
    134  *    - do not change the `cookie.state`
    135  * 7. If `cookie.state == SUPPORTED` and `cookie.client_ts` evaluation exceeds
    136  *    `COOKIE_CLIENT_TIMEOUT`:
    137  *    - clear `cookie.server`
    138  *    - randomly generate new `cookie.client`
    139  *    - set `cookie.client_ts` to the current timestamp.
    140  *    - set `cookie.client_ip` to the current source ip address.
    141  *    - do not change the `cookie.state`
    142  * 8. Generate EDNS OPT record (10) for client cookie.  The option value will be
    143  *    the `cookie.client` concatenated with the `cookie.server`.  If there is no
    144  *    known server cookie, it will not be appended. Copy `cookie.client` to
    145  *    `query.client_cookie` to handle possible client cookie changes by other
    146  *    queries before a reply is received (technically this is in the cached
    147  *    `ares_dns_record_t` so no need to manually do this). Send request to
    148  *    server.
    149  * 9. Evaluate response:
    150  *     1. If invalid EDNS OPT cookie (10) length sent back in response (valid
    151  *        length is 16-40), or bad client cookie value (validate first 8 bytes
    152  *        against `query.client_cookie` not `cookie.client`), **drop response**
    153  *        as if it hadn't been received.  This is likely a spoofing attack.
    154  *        Wait for valid response up to normal response timeout.
    155  *     2. If a EDNS OPT cookie (10) server cookie is returned:
    156  *         - set `cookie.unsupported_ts` to zero and `cookie.state = SUPPORTED`.
    157  *           We can confirm this server supports cookies based on the existence
    158  *           of this record.
    159  *         - If a new EDNS OPT cookie (10) server cookie is in the response, and
    160  *           the `client.cookie` matches the `query.client_cookie` still (hasn't
    161  *           been rotated by some other parallel query), save it as
    162  *           `cookie.server`.
    163  *     3. If dns response `rcode` is `BAD_COOKIE`:
    164  *         - Ensure a EDNS OPT cookie (10) is returned, otherwise **drop
    165  *           response**, this is completely invalid and likely an spoof of some
    166  *           sort.
    167  *         - Otherwise
    168  *           - Increment `query.cookie_try_count`
    169  *           - If `query.cookie_try_count >= COOKIE_RESEND_MAX`, set
    170  *             `query.using_tcp` to force the next attempt to use TCP.
    171  *           - **Requeue the query**, but do not increment the normal
    172  *             `try_count` as a `BAD_COOKIE` reply isn't a normal try failure.
    173  *             This should end up going all the way back to step 1 on the next
    174  *             attempt.
    175  *     4. If EDNS OPT cookie (10) is **NOT** returned in the response:
    176  *         - If `cookie.state == SUPPORTED`
    177  *           - if `cookie.unsupported_ts` is zero, set to the current timestamp.
    178  *           - Drop the response, wait for a valid response to be returned
    179  *         - if `cookie.state == GENERATED`
    180  *           - clear all cookie settings
    181  *           - set `cookie.state = UNSUPPORTED`
    182  *           - set `cookie.unsupported_ts` to the current time
    183  *         - Accept response (state should be `UNSUPPORTED` if we're here)
    184  */
    185 
    186 #include "ares_private.h"
    187 
    188 /* 1 day */
    189 #define COOKIE_CLIENT_TIMEOUT_MS (86400 * 1000)
    190 
    191 /* 5 minutes */
    192 #define COOKIE_UNSUPPORTED_TIMEOUT_MS (300 * 1000)
    193 
    194 /* 2 minutes */
    195 #define COOKIE_REGRESSION_TIMEOUT_MS (120 * 1000)
    196 
    197 #define COOKIE_RESEND_MAX 3
    198 
    199 static const unsigned char *
    200   ares_dns_cookie_fetch(const ares_dns_record_t *dnsrec, size_t *len)
    201 {
    202   const ares_dns_rr_t *rr  = ares_dns_get_opt_rr_const(dnsrec);
    203   const unsigned char *val = NULL;
    204   *len                     = 0;
    205 
    206   if (rr == NULL) {
    207     return NULL;
    208   }
    209 
    210   if (!ares_dns_rr_get_opt_byid(rr, ARES_RR_OPT_OPTIONS, ARES_OPT_PARAM_COOKIE,
    211                                 &val, len)) {
    212     return NULL;
    213   }
    214 
    215   return val;
    216 }
    217 
    218 static ares_bool_t timeval_is_set(const ares_timeval_t *tv)
    219 {
    220   if (tv->sec != 0 && tv->usec != 0) {
    221     return ARES_TRUE;
    222   }
    223   return ARES_FALSE;
    224 }
    225 
    226 static ares_bool_t timeval_expired(const ares_timeval_t *tv,
    227                                    const ares_timeval_t *now,
    228                                    unsigned long         millsecs)
    229 {
    230   ares_int64_t   tvdiff_ms;
    231   ares_timeval_t tvdiff;
    232   ares_timeval_diff(&tvdiff, tv, now);
    233 
    234   tvdiff_ms = tvdiff.sec * 1000 + tvdiff.usec / 1000;
    235   if (tvdiff_ms >= (ares_int64_t)millsecs) {
    236     return ARES_TRUE;
    237   }
    238   return ARES_FALSE;
    239 }
    240 
    241 static void ares_cookie_clear(ares_cookie_t *cookie)
    242 {
    243   memset(cookie, 0, sizeof(*cookie));
    244   cookie->state = ARES_COOKIE_INITIAL;
    245 }
    246 
    247 static void ares_cookie_generate(ares_cookie_t *cookie, ares_conn_t *conn,
    248                                  const ares_timeval_t *now)
    249 {
    250   ares_channel_t *channel = conn->server->channel;
    251 
    252   ares_rand_bytes(channel->rand_state, cookie->client, sizeof(cookie->client));
    253   memcpy(&cookie->client_ts, now, sizeof(cookie->client_ts));
    254   memcpy(&cookie->client_ip, &conn->self_ip, sizeof(cookie->client_ip));
    255 }
    256 
    257 static void ares_cookie_clear_server(ares_cookie_t *cookie)
    258 {
    259   memset(cookie->server, 0, sizeof(cookie->server));
    260   cookie->server_len = 0;
    261 }
    262 
    263 static ares_bool_t ares_addr_equal(const struct ares_addr *addr1,
    264                                    const struct ares_addr *addr2)
    265 {
    266   if (addr1->family != addr2->family) {
    267     return ARES_FALSE;
    268   }
    269 
    270   switch (addr1->family) {
    271     case AF_INET:
    272       if (memcmp(&addr1->addr.addr4, &addr2->addr.addr4,
    273                  sizeof(addr1->addr.addr4)) == 0) {
    274         return ARES_TRUE;
    275       }
    276       break;
    277     case AF_INET6:
    278       /* This structure is weird, and due to padding SonarCloud complains if
    279        * you don't punch all the way down.  At some point we should rework
    280        * this structure */
    281       if (memcmp(&addr1->addr.addr6._S6_un._S6_u8,
    282                  &addr2->addr.addr6._S6_un._S6_u8,
    283                  sizeof(addr1->addr.addr6._S6_un._S6_u8)) == 0) {
    284         return ARES_TRUE;
    285       }
    286       break;
    287     default:
    288       break; /* LCOV_EXCL_LINE */
    289   }
    290 
    291   return ARES_FALSE;
    292 }
    293 
    294 ares_status_t ares_cookie_apply(ares_dns_record_t *dnsrec, ares_conn_t *conn,
    295                                 const ares_timeval_t *now)
    296 {
    297   ares_server_t *server = conn->server;
    298   ares_cookie_t *cookie = &server->cookie;
    299   ares_dns_rr_t *rr     = ares_dns_get_opt_rr(dnsrec);
    300   unsigned char  c[40];
    301   size_t         c_len;
    302 
    303   /* If there is no OPT record, then EDNS isn't supported, and therefore
    304    * cookies can't be supported */
    305   if (rr == NULL) {
    306     return ARES_SUCCESS;
    307   }
    308 
    309   /* No cookies on TCP, make sure we remove one if one is present */
    310   if (conn->flags & ARES_CONN_FLAG_TCP) {
    311     ares_dns_rr_del_opt_byid(rr, ARES_RR_OPT_OPTIONS, ARES_OPT_PARAM_COOKIE);
    312     return ARES_SUCCESS;
    313   }
    314 
    315   /* Look for regression */
    316   if (cookie->state == ARES_COOKIE_SUPPORTED &&
    317       timeval_is_set(&cookie->unsupported_ts) &&
    318       timeval_expired(&cookie->unsupported_ts, now,
    319                       COOKIE_REGRESSION_TIMEOUT_MS)) {
    320     ares_cookie_clear(cookie);
    321   }
    322 
    323   /* Handle unsupported state */
    324   if (cookie->state == ARES_COOKIE_UNSUPPORTED) {
    325     /* If timer hasn't expired, just delete any possible cookie and return */
    326     if (!timeval_expired(&cookie->unsupported_ts, now,
    327                          COOKIE_REGRESSION_TIMEOUT_MS)) {
    328       ares_dns_rr_del_opt_byid(rr, ARES_RR_OPT_OPTIONS, ARES_OPT_PARAM_COOKIE);
    329       return ARES_SUCCESS;
    330     }
    331 
    332     /* We want to try to "learn" again */
    333     ares_cookie_clear(cookie);
    334   }
    335 
    336   /* Generate a new cookie */
    337   if (cookie->state == ARES_COOKIE_INITIAL) {
    338     ares_cookie_generate(cookie, conn, now);
    339     cookie->state = ARES_COOKIE_GENERATED;
    340   }
    341 
    342   /* Regenerate the cookie and clear the server cookie if the client ip has
    343    * changed */
    344   if ((cookie->state == ARES_COOKIE_GENERATED ||
    345        cookie->state == ARES_COOKIE_SUPPORTED) &&
    346       !ares_addr_equal(&conn->self_ip, &cookie->client_ip)) {
    347     ares_cookie_clear_server(cookie);
    348     ares_cookie_generate(cookie, conn, now);
    349   }
    350 
    351   /* If the client cookie has reached its maximum time, refresh it */
    352   if (cookie->state == ARES_COOKIE_SUPPORTED &&
    353       timeval_expired(&cookie->client_ts, now, COOKIE_CLIENT_TIMEOUT_MS)) {
    354     ares_cookie_clear_server(cookie);
    355     ares_cookie_generate(cookie, conn, now);
    356   }
    357 
    358   /* Generate the full cookie which is the client cookie concatenated with the
    359    * server cookie (if there is one) and apply it. */
    360   memcpy(c, cookie->client, sizeof(cookie->client));
    361   if (cookie->server_len) {
    362     memcpy(c + sizeof(cookie->client), cookie->server, cookie->server_len);
    363   }
    364   c_len = sizeof(cookie->client) + cookie->server_len;
    365 
    366   return ares_dns_rr_set_opt(rr, ARES_RR_OPT_OPTIONS, ARES_OPT_PARAM_COOKIE, c,
    367                              c_len);
    368 }
    369 
    370 ares_status_t ares_cookie_validate(ares_query_t            *query,
    371                                    const ares_dns_record_t *dnsresp,
    372                                    ares_conn_t *conn, const ares_timeval_t *now,
    373                                    ares_array_t **requeue)
    374 {
    375   ares_server_t           *server = conn->server;
    376   ares_cookie_t           *cookie = &server->cookie;
    377   const ares_dns_record_t *dnsreq = query->query;
    378   const unsigned char     *resp_cookie;
    379   size_t                   resp_cookie_len;
    380   const unsigned char     *req_cookie;
    381   size_t                   req_cookie_len;
    382 
    383   resp_cookie = ares_dns_cookie_fetch(dnsresp, &resp_cookie_len);
    384 
    385   /* Invalid cookie length, drop */
    386   if (resp_cookie && (resp_cookie_len < 8 || resp_cookie_len > 40)) {
    387     return ARES_EBADRESP;
    388   }
    389 
    390   req_cookie = ares_dns_cookie_fetch(dnsreq, &req_cookie_len);
    391 
    392   /* Didn't request cookies, so we can stop evaluating */
    393   if (req_cookie == NULL) {
    394     return ARES_SUCCESS;
    395   }
    396 
    397   /* If 8-byte prefix for returned cookie doesn't match the requested cookie,
    398    * drop for spoofing */
    399   if (resp_cookie && memcmp(req_cookie, resp_cookie, 8) != 0) {
    400     return ARES_EBADRESP;
    401   }
    402 
    403   if (resp_cookie && resp_cookie_len > 8) {
    404     /* Make sure we record that we successfully received a cookie response */
    405     cookie->state = ARES_COOKIE_SUPPORTED;
    406     memset(&cookie->unsupported_ts, 0, sizeof(cookie->unsupported_ts));
    407 
    408     /* If client cookie hasn't been rotated, save the returned server cookie */
    409     if (memcmp(cookie->client, req_cookie, sizeof(cookie->client)) == 0) {
    410       cookie->server_len = resp_cookie_len - 8;
    411       memcpy(cookie->server, resp_cookie + 8, cookie->server_len);
    412     }
    413   }
    414 
    415   if (ares_dns_record_get_rcode(dnsresp) == ARES_RCODE_BADCOOKIE) {
    416     /* Illegal to return BADCOOKIE but no cookie, drop */
    417     if (resp_cookie == NULL) {
    418       return ARES_EBADRESP;
    419     }
    420 
    421     /* If we have too many attempts to send a cookie, we need to requeue as
    422      * tcp */
    423     query->cookie_try_count++;
    424     if (query->cookie_try_count >= COOKIE_RESEND_MAX) {
    425       query->using_tcp = ARES_TRUE;
    426     }
    427 
    428     /* Resend the request, hopefully it will work the next time as we should
    429      * have recorded a server cookie */
    430     ares_requeue_query(query, now, ARES_SUCCESS,
    431                        ARES_FALSE /* Don't increment try count */, NULL,
    432                        requeue);
    433 
    434     /* Parent needs to drop this response */
    435     return ARES_EBADRESP;
    436   }
    437 
    438   /* We've got a response with a server cookie, and we've done all the
    439    * evaluation we can, return success */
    440   if (resp_cookie_len > 8) {
    441     return ARES_SUCCESS;
    442   }
    443 
    444   if (cookie->state == ARES_COOKIE_SUPPORTED) {
    445     /* If we're not currently tracking an error time yet, start */
    446     if (!timeval_is_set(&cookie->unsupported_ts)) {
    447       memcpy(&cookie->unsupported_ts, now, sizeof(cookie->unsupported_ts));
    448     }
    449     /* Drop it since we expected a cookie */
    450     return ARES_EBADRESP;
    451   }
    452 
    453   if (cookie->state == ARES_COOKIE_GENERATED) {
    454     ares_cookie_clear(cookie);
    455     cookie->state = ARES_COOKIE_UNSUPPORTED;
    456     memcpy(&cookie->unsupported_ts, now, sizeof(cookie->unsupported_ts));
    457   }
    458 
    459   /* Cookie state should be UNSUPPORTED if we're here */
    460   return ARES_SUCCESS;
    461 }