From fab65acae92da6c2bed9cb07f4d166bfb139258f Mon Sep 17 00:00:00 2001 From: Oliver Welsh <160491261+oliverwelsh@users.noreply.github.com> Date: Wed, 20 Mar 2024 00:25:51 +0000 Subject: [PATCH] Add function ares_search_dnrec() to search for records using the new DNS record parser (#719) This PR adds a new function `ares_search_dnsrec()` to search for records using the new DNS record parser. The function takes an arbitrary DNS record object to search (that must represent a query for a single name). The function takes a new callback type, `ares_callback_dnsrec`, that is invoked with a parsed DNS record object rather than the raw buffer(+length). The original motivation for this change is to provide support for [draft-kaplan-enum-sip-routing-04](https://datatracker.ietf.org/doc/html/draft-kaplan-enum-sip-routing-04); when routing phone calls using an ENUM server, it can be useful to include identifying source information in an OPT RR options value, to help select the appropriate route for the call. The new function allows for more customisable searches like this. **Summary of code changes** A new function `ares_search_dnsrec()` has been added and exposed. Moreover, the entire `ares_search_int()` internal code flow has been refactored to use parsed DNS record objects and the new DNS record parser. The DNS record object is passed through the `search_query` structure by encoding/decoding to/from a buffer (if multiple search domains are used). A helper function `ares_dns_write_query_altname()` is used to re-write the DNS record object with a new query name (used to append search domains). `ares_search()` is now a wrapper around the new internal code, where the DNS record object is created based on the name, class and type parameters. The new function uses a new callback type, `ares_callback_dnsrec`. This is invoked with a parsed DNS record object. For now, we convert from `ares_callback` to this new type using `ares__dnsrec_convert_cb()`. Some functions that are common to both `ares_query()` and `ares_search()` have been refactored using the new DNS record parser. See `ares_dns_record_create_query()` and `ares_dns_query_reply_tostatus()`. **Testing** A new FV has been added to test the new function, which searches for a DNS record containing an OPT RR with custom options value. As part of this, I needed to enhance the mock DNS server to expect request text (and assert that it matches actual request text). This is because the FV needs to check that the request contains the correct OPT RR. **Documentation** The man page docs have been updated to describe the new feature. **Futures** In the future, a new variant of `ares_send()` could be introduced in the same vein (`ares_send_dnsrec()`). This could be used by `ares_search_dnsrec()`. Moreover, we could migrate internal code to use `ares_callback_dnsrec` as the default callback. This will help to make the new DNS record parser the norm in C-Ares. --------- Co-authored-by: Oliver Welsh (@oliverwelsh) --- docs/Makefile.inc | 2 + docs/ares_dns_record.3 | 4 +- docs/ares_dns_record_rr_get_const.3 | 3 + docs/ares_dns_rr.3 | 34 ++-- docs/ares_search.3 | 25 +++ docs/ares_search_dnsrec.3 | 3 + include/ares.h | 65 +++--- include/ares_dns_record.h | 16 +- src/lib/ares_create_query.c | 51 +---- src/lib/ares_dns_mapping.c | 34 ++++ src/lib/ares_dns_private.h | 43 ++++ src/lib/ares_dns_record.c | 80 +++++++- src/lib/ares_dns_write.c | 33 ++- src/lib/ares_query.c | 46 ++--- src/lib/ares_search.c | 298 ++++++++++++++++++++++------ test/ares-test-mock.cc | 100 +++++++++- test/ares-test.cc | 32 ++- test/ares-test.h | 23 ++- 18 files changed, 706 insertions(+), 186 deletions(-) create mode 100644 docs/ares_dns_record_rr_get_const.3 create mode 100644 docs/ares_search_dnsrec.3 diff --git a/docs/Makefile.inc b/docs/Makefile.inc index 3645a7fc..a66791a7 100644 --- a/docs/Makefile.inc +++ b/docs/Makefile.inc @@ -32,6 +32,7 @@ MANPAGES = ares_cancel.3 \ ares_dns_record_rr_cnt.3 \ ares_dns_record_rr_del.3 \ ares_dns_record_rr_get.3 \ + ares_dns_record_rr_get_const.3 \ ares_dns_rec_type_fromstr.3 \ ares_dns_rec_type_t.3 \ ares_dns_rr.3 \ @@ -111,6 +112,7 @@ MANPAGES = ares_cancel.3 \ ares_reinit.3 \ ares_save_options.3 \ ares_search.3 \ + ares_search_dnsrec.3 \ ares_send.3 \ ares_set_local_dev.3 \ ares_set_local_ip4.3 \ diff --git a/docs/ares_dns_record.3 b/docs/ares_dns_record.3 index fe23b5ee..0bbe5008 100644 --- a/docs/ares_dns_record.3 +++ b/docs/ares_dns_record.3 @@ -19,7 +19,7 @@ ares_status_t ares_dns_parse(const unsigned char *buf, size_t buf_len, unsigned int flags, ares_dns_record_t **dnsrec); -ares_status_t ares_dns_write(ares_dns_record_t *dnsrec, +ares_status_t ares_dns_write(const ares_dns_record_t *dnsrec, unsigned char **buf, size_t *buf_len); ares_status_t ares_dns_record_create(ares_dns_record_t **dnsrec, @@ -67,7 +67,7 @@ on requests, and some may only be valid on responses: .B ARES_REC_TYPE_SOA - Start of authority zone .br -.B ARES_REC_TYPE_PTR +.B ARES_REC_TYPE_PTR - Domain name pointer .br .B ARES_REC_TYPE_HINFO diff --git a/docs/ares_dns_record_rr_get_const.3 b/docs/ares_dns_record_rr_get_const.3 new file mode 100644 index 00000000..b93e4cd4 --- /dev/null +++ b/docs/ares_dns_record_rr_get_const.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/docs/ares_dns_rr.3 b/docs/ares_dns_rr.3 index 2999d18e..290859e8 100644 --- a/docs/ares_dns_rr.3 +++ b/docs/ares_dns_rr.3 @@ -4,14 +4,15 @@ .TH ARES_DNS_RR 3 "12 November 2023" .SH NAME ares_dns_record_rr_add, ares_dns_record_rr_cnt, ares_dns_record_rr_del, -ares_dns_record_rr_get, ares_dns_rr_get_addr, ares_dns_rr_get_addr6, -ares_dns_rr_get_bin, ares_dns_rr_get_class, ares_dns_rr_get_name, -ares_dns_rr_get_opt, ares_dns_rr_get_opt_byid, ares_dns_rr_get_opt_cnt, -ares_dns_rr_get_str, ares_dns_rr_get_ttl, ares_dns_rr_get_type, -ares_dns_rr_get_u16, ares_dns_rr_get_u32, ares_dns_rr_get_u8, ares_dns_rr_key_t, -ares_dns_rr_set_addr, ares_dns_rr_set_addr6, ares_dns_rr_set_bin, -ares_dns_rr_set_opt, ares_dns_rr_set_str, ares_dns_rr_set_u16, -ares_dns_rr_set_u32, ares_dns_rr_set_u8, ares_dns_section_t, ares_tlsa_match_t, +ares_dns_record_rr_get, ares_dns_record_rr_get_const, ares_dns_rr_get_addr, +ares_dns_rr_get_addr6, ares_dns_rr_get_bin, ares_dns_rr_get_class, +ares_dns_rr_get_name, ares_dns_rr_get_opt, ares_dns_rr_get_opt_byid, +ares_dns_rr_get_opt_cnt, ares_dns_rr_get_str, ares_dns_rr_get_ttl, +ares_dns_rr_get_type, ares_dns_rr_get_u16, ares_dns_rr_get_u32, +ares_dns_rr_get_u8, ares_dns_rr_key_t, ares_dns_rr_set_addr, +ares_dns_rr_set_addr6, ares_dns_rr_set_bin, ares_dns_rr_set_opt, +ares_dns_rr_set_str, ares_dns_rr_set_u16, ares_dns_rr_set_u32, +ares_dns_rr_set_u8, ares_dns_section_t, ares_tlsa_match_t, ares_tlsa_selector_t, ares_tlsa_usage_t \- DNS Resource Record creating, reading, and writing functions. .SH SYNOPSIS @@ -33,6 +34,10 @@ ares_dns_rr_t *ares_dns_record_rr_get(ares_dns_record_t *dnsrec, ares_dns_section_t sect, size_t idx); +const ares_dns_rr_t *ares_dns_record_rr_get_const(const ares_dns_record_t *dnsrec, + ares_dns_section_t sect, + size_t idx); + ares_status_t ares_dns_record_rr_del(ares_dns_record_t *dnsrec, ares_dns_section_t sect, size_t idx); @@ -357,14 +362,17 @@ parameter, and the Time To Live (TTL) in the parameter. -The \fIares_dns_record_rr_get(3)\fP function is used to retrieve the resource -record pointer from the DNS record provided in the +The \fIares_dns_record_rr_get(3)\fP and \fIares_dns_record_rr_get_const(3)\fP +functions are used to retrieve the resource record pointer from the DNS record +provided in the .IR dnsrec parameter, for the resource record section provided in the .IR sect parameter, for the specified index in the .IR idx -parameter. The index must be less than \fIares_dns_record_rr_cnt(3)\fP. +parameter. The index must be less than \fIares_dns_record_rr_cnt(3)\fP. The +former returns a writable pointer to the resource record, while the latter +returns a read-only pointer to the resource record. The \fIares_dns_record_rr_del(3)\fP is used to delete a resource record from @@ -615,8 +623,8 @@ prescribed datatype values and in general can't fail except for misuse cases, in which a 0 (or NULL) may be returned, however 0 can also be a valid return value for most of these functions. -\fIares_dns_record_rr_get(3)\fP will return the requested resource record -pointer or NULL on failure (misuse). +\fIares_dns_record_rr_get(3)\fP and \fIares_dns_record_rr_get_const(3)\fP will +return the requested resource record pointer or NULL on failure (misuse). \fIares_dns_rr_get_opt_byid(3)\fP will return ARES_TRUE if the option was found, otherwise ARES_FALSE if not found (or misuse). diff --git a/docs/ares_search.3 b/docs/ares_search.3 index 08246d34..58a0bbf2 100644 --- a/docs/ares_search.3 +++ b/docs/ares_search.3 @@ -13,9 +13,18 @@ typedef void (*ares_callback)(void *\fIarg\fP, int \fIstatus\fP, int \fItimeouts\fP, unsigned char *\fIabuf\fP, int \fIalen\fP) +typedef void (*ares_callback_dnsrec)(void *\fIarg\fP, + ares_status_t \fIstatus\fP, + size_t \fItimeouts\fP, + const ares_dns_record_t *\fIdnsrec\fP) + void ares_search(ares_channel_t *\fIchannel\fP, const char *\fIname\fP, int \fIdnsclass\fP, int \fItype\fP, ares_callback \fIcallback\fP, void *\fIarg\fP) + +void ares_search_dnsrec(ares_channel_t *\fIchannel\fP, + ares_dns_record_t *\fIdnsrec\fP, + ares_callback_dnsrec \fIcallback\fP, void *\fIarg\fP) .fi .SH DESCRIPTION The @@ -142,6 +151,22 @@ will usually be NULL and will usually be 0, but in some cases an unsuccessful query result may be placed in .IR abuf . + +The \fIares_search_dnsrec(3)\fP function behaves identically to +\fIares_search(3)\fP, but takes an initialized and filled DNS record object to +use for queries as the second argument +.I dnsrec +instead of a name, class and type. This object is used as the base for the +queries and must itself represent a valid query for a single name. Note that +the search domains will only be appended to the name in the question section; +RRs on the DNS record object will not be affected. Moreover, the +.I callback +argument is of type \fIares_callback_dnsrec\fP. This callback behaves +identically to \fIares_callback\fP, but is invoked with a parsed DNS record +object +.I dnsrec +rather than a raw buffer with length. Note that this object is read-only. + .SH SEE ALSO .BR ares_process (3), .BR ares_dns_record (3) diff --git a/docs/ares_search_dnsrec.3 b/docs/ares_search_dnsrec.3 new file mode 100644 index 00000000..86c2317c --- /dev/null +++ b/docs/ares_search_dnsrec.3 @@ -0,0 +1,3 @@ +.\" Copyright (C) 2023 The c-ares project and its contributors. +.\" SPDX-License-Identifier: MIT +.so man3/ares_search.3 diff --git a/include/ares.h b/include/ares.h index acbd6583..770d680d 100644 --- a/include/ares.h +++ b/include/ares.h @@ -352,10 +352,38 @@ typedef struct ares_channeldata *ares_channel; /* Current main channel typedef */ typedef struct ares_channeldata ares_channel_t; +/* + * NOTE: before c-ares 1.7.0 we would most often use the system in6_addr + * struct below when ares itself was built, but many apps would use this + * private version since the header checked a HAVE_* define for it. Starting + * with 1.7.0 we always declare and use our own to stop relying on the + * system's one. + */ +struct ares_in6_addr { + union { + unsigned char _S6_u8[16]; + } _S6_un; +}; + +struct ares_addr { + int family; + + union { + struct in_addr addr4; + struct ares_in6_addr addr6; + } addr; +}; + +/* DNS record parser, writer, and helpers */ +#include "ares_dns_record.h" typedef void (*ares_callback)(void *arg, int status, int timeouts, unsigned char *abuf, int alen); +typedef void (*ares_callback_dnsrec)(void *arg, ares_status_t status, + size_t timeouts, + const ares_dns_record_t *dnsrec); + typedef void (*ares_host_callback)(void *arg, int status, int timeouts, struct hostent *hostent); @@ -477,6 +505,18 @@ CARES_EXTERN void ares_search(ares_channel_t *channel, const char *name, int dnsclass, int type, ares_callback callback, void *arg); +/*! Search for a complete DNS message. + * + * \param[in] channel Pointer to channel on which queries will be sent. + * \param[in] dnsrec Pointer to initialized and filled DNS record object. + * \param[in] callback Callback function invoked on completion or failure of + * the query sequence. + * \param[in] arg Additional argument passed to the callback function. + */ +CARES_EXTERN void ares_search_dnsrec(ares_channel_t *channel, + ares_dns_record_t *dnsrec, + ares_callback_dnsrec callback, void *arg); + CARES_EXTERN void ares_gethostbyname(ares_channel_t *channel, const char *name, int family, ares_host_callback callback, void *arg); @@ -528,28 +568,6 @@ CARES_EXTERN int ares_expand_string(const unsigned char *encoded, const unsigned char *abuf, int alen, unsigned char **s, long *enclen); -/* - * NOTE: before c-ares 1.7.0 we would most often use the system in6_addr - * struct below when ares itself was built, but many apps would use this - * private version since the header checked a HAVE_* define for it. Starting - * with 1.7.0 we always declare and use our own to stop relying on the - * system's one. - */ -struct ares_in6_addr { - union { - unsigned char _S6_u8[16]; - } _S6_un; -}; - -struct ares_addr { - int family; - - union { - struct in_addr addr4; - struct ares_in6_addr addr6; - } addr; -}; - struct ares_addrttl { struct in_addr ipaddr; int ttl; @@ -803,7 +821,4 @@ CARES_EXTERN size_t ares_queue_active_queries(ares_channel_t *channel); } #endif -/* DNS record parser, writer, and helpers */ -#include "ares_dns_record.h" - #endif /* ARES__H */ diff --git a/include/ares_dns_record.h b/include/ares_dns_record.h index 7ae2d2f9..110f629d 100644 --- a/include/ares_dns_record.h +++ b/include/ares_dns_record.h @@ -667,17 +667,27 @@ CARES_EXTERN ares_status_t ares_dns_record_rr_add( const char *name, ares_dns_rec_type_t type, ares_dns_class_t rclass, unsigned int ttl); -/*! Fetch a resource record based on the section and index. +/*! Fetch a writable resource record based on the section and index. * * \param[in] dnsrec Initialized record object * \param[in] sect Section for resource record * \param[in] idx Index of resource record in section - * \return NULL on misuse, otherwise a pointer to the resource record + * \return NULL on misuse, otherwise a writable pointer to the resource record */ CARES_EXTERN ares_dns_rr_t *ares_dns_record_rr_get(ares_dns_record_t *dnsrec, ares_dns_section_t sect, size_t idx); +/*! Fetch a non-writeable resource record based on the section and index. + * + * \param[in] dnsrec Initialized record object + * \param[in] sect Section for resource record + * \param[in] idx Index of resource record in section + * \return NULL on misuse, otherwise a const pointer to the resource record + */ +CARES_EXTERN const ares_dns_rr_t *ares_dns_record_rr_get_const( + const ares_dns_record_t *dnsrec, ares_dns_section_t sect, size_t idx); + /*! Remove the resource record based on the section and index * @@ -959,7 +969,7 @@ CARES_EXTERN ares_status_t ares_dns_parse(const unsigned char *buf, * \param[out] buf_len Length of returned buffer containing DNS message. * \return ARES_SUCCESS on success */ -CARES_EXTERN ares_status_t ares_dns_write(ares_dns_record_t *dnsrec, +CARES_EXTERN ares_status_t ares_dns_write(const ares_dns_record_t *dnsrec, unsigned char **buf, size_t *buf_len); /*! @} */ diff --git a/src/lib/ares_create_query.c b/src/lib/ares_create_query.c index f66b0ff6..51470e90 100644 --- a/src/lib/ares_create_query.c +++ b/src/lib/ares_create_query.c @@ -35,6 +35,7 @@ int ares_create_query(const char *name, int dnsclass, int type, ares_status_t status; ares_dns_record_t *dnsrec = NULL; size_t len; + ares_dns_flags_t rd_flag = rd ? ARES_FLAG_RD : 0; if (name == NULL || bufp == NULL || buflenp == NULL) { status = ARES_EFORMERR; @@ -44,56 +45,14 @@ int ares_create_query(const char *name, int dnsclass, int type, *bufp = NULL; *buflenp = 0; - /* Per RFC 7686, reject queries for ".onion" domain names with NXDOMAIN. */ - if (ares__is_onion_domain(name)) { - status = ARES_ENOTFOUND; - goto done; - } - - status = ares_dns_record_create(&dnsrec, id, rd ? ARES_FLAG_RD : 0, - ARES_OPCODE_QUERY, ARES_RCODE_NOERROR); - if (status != ARES_SUCCESS) { - goto done; - } - - status = ares_dns_record_query_add(dnsrec, name, (ares_dns_rec_type_t)type, - (ares_dns_class_t)dnsclass); + status = ares_dns_record_create_query(&dnsrec, name, + (ares_dns_class_t)dnsclass, + (ares_dns_rec_type_t)type, + id, rd_flag, (size_t)max_udp_size); if (status != ARES_SUCCESS) { goto done; } - /* max_udp_size > 0 indicates EDNS, so send OPT RR as an additional record */ - if (max_udp_size > 0) { - ares_dns_rr_t *rr = NULL; - - status = ares_dns_record_rr_add(&rr, dnsrec, ARES_SECTION_ADDITIONAL, "", - ARES_REC_TYPE_OPT, ARES_CLASS_IN, 0); - if (status != ARES_SUCCESS) { - goto done; - } - - if (max_udp_size > 65535) { - status = ARES_EFORMERR; - goto done; - } - - status = ares_dns_rr_set_u16(rr, ARES_RR_OPT_UDP_SIZE, - (unsigned short)max_udp_size); - if (status != ARES_SUCCESS) { - goto done; - } - - status = ares_dns_rr_set_u8(rr, ARES_RR_OPT_VERSION, 0); - if (status != ARES_SUCCESS) { - goto done; - } - - status = ares_dns_rr_set_u16(rr, ARES_RR_OPT_FLAGS, 0); - if (status != ARES_SUCCESS) { - goto done; - } - } - status = ares_dns_write(dnsrec, bufp, &len); if (status != ARES_SUCCESS) { goto done; diff --git a/src/lib/ares_dns_mapping.c b/src/lib/ares_dns_mapping.c index 55f1af79..8f06a44c 100644 --- a/src/lib/ares_dns_mapping.c +++ b/src/lib/ares_dns_mapping.c @@ -883,3 +883,37 @@ const char *ares_dns_rcode_tostr(ares_dns_rcode_t rcode) return "UNKNOWN"; } + +/* Convert an rcode and ancount from a query reply into an ares_status_t + * value. Used internally by ares_search() and ares_query(). + */ +ares_status_t ares_dns_query_reply_tostatus(ares_dns_rcode_t rcode, + size_t ancount) +{ + ares_status_t status = ARES_SUCCESS; + + switch (rcode) { + case ARES_RCODE_NOERROR: + status = (ancount > 0) ? ARES_SUCCESS : ARES_ENODATA; + break; + case ARES_RCODE_FORMERR: + status = ARES_EFORMERR; + break; + case ARES_RCODE_SERVFAIL: + status = ARES_ESERVFAIL; + break; + case ARES_RCODE_NXDOMAIN: + status = ARES_ENOTFOUND; + break; + case ARES_RCODE_NOTIMP: + status = ARES_ENOTIMP; + break; + case ARES_RCODE_REFUSED: + status = ARES_EREFUSED; + break; + default: + break; + } + + return status; +} diff --git a/src/lib/ares_dns_private.h b/src/lib/ares_dns_private.h index 91635e74..07339d3f 100644 --- a/src/lib/ares_dns_private.h +++ b/src/lib/ares_dns_private.h @@ -49,6 +49,49 @@ 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); +/*! Create a DNS record object for a query. The arguments are the same as + * those for ares_create_query(). + * + * \param[out] dnsrec DNS record object to create. + * \param[in] name NUL-terminated name for the query. + * \param[in] dnsclass Class for the query. + * \param[in] type Type for the query. + * \param[in] id Identifier for the query. + * \param[in] flags Flags for the query. + * \param[in] max_udp_size Maximum size of a UDP packet for EDNS. + * \return ARES_SUCCESS on success, otherwise an error code. + */ +ares_status_t ares_dns_record_create_query(ares_dns_record_t **dnsrec, + const char *name, + ares_dns_class_t dnsclass, + ares_dns_rec_type_t type, + unsigned short id, + ares_dns_flags_t flags, + size_t max_udp_size); + +/*! Convert the RCODE and ANCOUNT from a DNS query reply into a status code. + * + * \param[in] rcode The RCODE from the reply. + * \param[in] ancount The ANCOUNT from the reply. + * \return An appropriate status code. + */ +ares_status_t ares_dns_query_reply_tostatus(ares_dns_rcode_t rcode, + size_t ancount); + +/*! Write a DNS record representing a query for a single name into a buffer. + * An alternative name can be specified to temporarily overwrite the name + * in the query. Note that this only affects the name in the question section; + * RRs are not affected. + * + * \param[in] dnsrec DNS record object to write. + * \param[in] altname Alternative name to use in the query. + * \param[out] buf Buffer to write the query into. + * \param[out] buflen Length of the buffer. + * \return ARES_SUCCESS on success, otherwise an error code. + */ +ares_status_t ares_dns_write_query_altname(ares_dns_record_t *dnsrec, + char *altname, unsigned char **buf, + size_t *buflen); struct ares_dns_qd { char *name; ares_dns_rec_type_t qtype; diff --git a/src/lib/ares_dns_record.c b/src/lib/ares_dns_record.c index 30219003..90e3af8b 100644 --- a/src/lib/ares_dns_record.c +++ b/src/lib/ares_dns_record.c @@ -499,7 +499,7 @@ ares_dns_rr_t *ares_dns_record_rr_get(ares_dns_record_t *dnsrec, return &rr_ptr[idx]; } -static const ares_dns_rr_t * +const ares_dns_rr_t * ares_dns_record_rr_get_const(const ares_dns_record_t *dnsrec, ares_dns_section_t sect, size_t idx) { @@ -1314,3 +1314,81 @@ ares_bool_t ares_dns_has_opt_rr(const ares_dns_record_t *rec) } return ARES_FALSE; } + +/* Construct a DNS record for a name with given class and type. Used internally + * by ares_search() and ares_create_query(). + */ +ares_status_t ares_dns_record_create_query(ares_dns_record_t **dnsrec, + const char *name, + ares_dns_class_t dnsclass, + ares_dns_rec_type_t type, + unsigned short id, + ares_dns_flags_t flags, + size_t max_udp_size) +{ + ares_status_t status; + ares_dns_rr_t *rr = NULL; + + if (dnsrec == NULL) { + return ARES_EFORMERR; + } + + *dnsrec = NULL; + + /* Per RFC 7686, reject queries for ".onion" domain names with NXDOMAIN */ + if (ares__is_onion_domain(name)) { + status = ARES_ENOTFOUND; + goto done; + } + + status = ares_dns_record_create(dnsrec, id, (unsigned short)flags, + ARES_OPCODE_QUERY, ARES_RCODE_NOERROR); + if (status != ARES_SUCCESS) { + goto done; + } + + status = ares_dns_record_query_add(*dnsrec, name, type, dnsclass); + if (status != ARES_SUCCESS) { + goto done; + } + + /* max_udp_size > 0 indicates EDNS, so send OPT RR as an additional record */ + if (max_udp_size > 0) { + /* max_udp_size must fit into a 16 bit unsigned integer field on the OPT + * RR, so check here that it fits + */ + if (max_udp_size > 65535) { + status = ARES_EFORMERR; + goto done; + } + + status = ares_dns_record_rr_add(&rr, *dnsrec, ARES_SECTION_ADDITIONAL, "", + ARES_REC_TYPE_OPT, ARES_CLASS_IN, 0); + if (status != ARES_SUCCESS) { + goto done; + } + + status = ares_dns_rr_set_u16(rr, ARES_RR_OPT_UDP_SIZE, + (unsigned short)max_udp_size); + if (status != ARES_SUCCESS) { + goto done; + } + + status = ares_dns_rr_set_u8(rr, ARES_RR_OPT_VERSION, 0); + if (status != ARES_SUCCESS) { + goto done; + } + + status = ares_dns_rr_set_u16(rr, ARES_RR_OPT_FLAGS, 0); + if (status != ARES_SUCCESS) { + goto done; + } + } + +done: + if (status != ARES_SUCCESS) { + ares_dns_record_destroy(*dnsrec); + *dnsrec = NULL; + } + return status; +} diff --git a/src/lib/ares_dns_write.c b/src/lib/ares_dns_write.c index 2e99c5ba..761f2c16 100644 --- a/src/lib/ares_dns_write.c +++ b/src/lib/ares_dns_write.c @@ -831,7 +831,7 @@ static ares_status_t ares_dns_write_rr_raw_rr(ares__buf_t *buf, return ares__buf_append(buf, data, data_len); } -static ares_status_t ares_dns_write_rr(ares_dns_record_t *dnsrec, +static ares_status_t ares_dns_write_rr(const ares_dns_record_t *dnsrec, ares__llist_t **namelist, ares_dns_section_t section, ares__buf_t *buf) @@ -849,7 +849,7 @@ static ares_status_t ares_dns_write_rr(ares_dns_record_t *dnsrec, size_t end_length; unsigned int ttl; - rr = ares_dns_record_rr_get(dnsrec, section, i); + rr = ares_dns_record_rr_get_const(dnsrec, section, i); if (rr == NULL) { return ARES_EFORMERR; } @@ -988,7 +988,8 @@ static ares_status_t ares_dns_write_rr(ares_dns_record_t *dnsrec, return ARES_SUCCESS; } -ares_status_t ares_dns_write(ares_dns_record_t *dnsrec, unsigned char **buf, +ares_status_t ares_dns_write(const ares_dns_record_t *dnsrec, + unsigned char **buf, size_t *buf_len) { ares__buf_t *b = NULL; @@ -1052,3 +1053,29 @@ void ares_dns_record_write_ttl_decrement(ares_dns_record_t *dnsrec, } dnsrec->ttl_decrement = ttl_decrement; } + +/* Write a DNS record representing a query for a single name, but temporarily + * overwrite the name with an alternative name before doing so. This is used + * as a helper function in ares_search(). Note that this only affects the name + * in the question section; RRs are not affected. + */ +ares_status_t ares_dns_write_query_altname(ares_dns_record_t *dnsrec, + char *altname, unsigned char **buf, + size_t *buflen) +{ + char *qname; + ares_status_t status; + + if (ares_dns_record_query_cnt(dnsrec) != 1) { + return ARES_EBADQUERY; + } + + qname = dnsrec->qd[0].name; + if (altname != NULL) { + dnsrec->qd[0].name = altname; + } + status = ares_dns_write(dnsrec, buf, buflen); + dnsrec->qd[0].name = qname; + + return status; +} diff --git a/src/lib/ares_query.c b/src/lib/ares_query.c index 098e6789..4c5be945 100644 --- a/src/lib/ares_query.c +++ b/src/lib/ares_query.c @@ -99,41 +99,27 @@ void ares_query(ares_channel_t *channel, const char *name, int dnsclass, static void qcallback(void *arg, int status, int timeouts, unsigned char *abuf, int alen) { - struct qquery *qquery = (struct qquery *)arg; - size_t ancount; - int rcode; + struct qquery *qquery = (struct qquery *)arg; + ares_dns_record_t *dnsrep = NULL; + size_t ancount; + ares_dns_rcode_t rcode; if (status != ARES_SUCCESS) { qquery->callback(qquery->arg, status, timeouts, abuf, alen); } else { - /* Pull the response code and answer count from the packet. */ - rcode = DNS_HEADER_RCODE(abuf); - ancount = DNS_HEADER_ANCOUNT(abuf); - - /* Convert errors. */ - switch (rcode) { - case NOERROR: - status = (ancount > 0) ? ARES_SUCCESS : ARES_ENODATA; - break; - case FORMERR: - status = ARES_EFORMERR; - break; - case SERVFAIL: - status = ARES_ESERVFAIL; - break; - case NXDOMAIN: - status = ARES_ENOTFOUND; - break; - case NOTIMP: - status = ARES_ENOTIMP; - break; - case REFUSED: - status = ARES_EREFUSED; - break; - default: - break; + /* Pull the response code and answer count from the packet and convert any + * errors. + */ + status = (int)ares_dns_parse(abuf, (size_t)alen, 0, &dnsrep); + if (status != ARES_SUCCESS) { + qquery->callback(qquery->arg, status, timeouts, abuf, alen); + } else { + rcode = ares_dns_record_get_rcode(dnsrep); + ancount = ares_dns_record_rr_cnt(dnsrep, ARES_SECTION_ANSWER); + ares_dns_record_destroy(dnsrep); + status = (int)ares_dns_query_reply_tostatus(rcode, ancount); + qquery->callback(qquery->arg, status, timeouts, abuf, alen); } - qquery->callback(qquery->arg, status, timeouts, abuf, alen); } ares_free(qquery); } diff --git a/src/lib/ares_search.c b/src/lib/ares_search.c index 34d52587..18d667df 100644 --- a/src/lib/ares_search.c +++ b/src/lib/ares_search.c @@ -33,40 +33,71 @@ #include "ares.h" #include "ares_private.h" +#include "ares_dns.h" struct search_query { - /* Arguments passed to ares_search */ + /* Arguments passed to ares_search() */ ares_channel_t *channel; - char *name; /* copied into an allocated buffer */ - int dnsclass; - int type; ares_callback callback; void *arg; - char **domains; /* duplicate for ares_reinit() safety */ + + /* DNS record passed to ares_search(), encoded in string format */ + unsigned char *buf; + size_t buflen; + + /* Duplicate of channel domains for ares_reinit() safety */ + char **domains; size_t ndomains; - int status_as_is; /* error status from trying as-is */ - size_t next_domain; /* next search domain to try */ - ares_bool_t trying_as_is; /* current query is for name as-is */ - size_t timeouts; /* number of timeouts we saw for this request */ - ares_bool_t ever_got_nodata; /* did we ever get ARES_ENODATA along the way? */ + /* State tracking progress through the search query */ + int status_as_is; /* error status from trying as-is */ + size_t next_domain; /* next search domain to try */ + ares_bool_t trying_as_is; /* current query is for name as-is */ + size_t timeouts; /* number of timeouts we saw for this request */ + ares_bool_t ever_got_nodata; /* did we ever get ARES_ENODATA along the way? */ +}; + +/* Callback argument structure passed to ares__dnsrec_convert_cb(). */ +struct dnsrec_convert_arg { + ares_callback_dnsrec callback; + void *arg; }; static void search_callback(void *arg, int status, int timeouts, unsigned char *abuf, int alen); +static ares_status_t ares__write_and_send_query(ares_channel_t *channel, + ares_dns_record_t *dnsrec, + char *altname, + ares_callback callback, + void *arg); static void end_squery(struct search_query *squery, ares_status_t status, unsigned char *abuf, size_t alen); +static void ares__dnsrec_convert_cb(void *arg, int status, int timeouts, + unsigned char *abuf, int alen); -static void ares_search_int(ares_channel_t *channel, const char *name, - int dnsclass, int type, ares_callback callback, - void *arg) +static void ares_search_int(ares_channel_t *channel, ares_dns_record_t *dnsrec, + ares_callback callback, void *arg) { struct search_query *squery; - char *s; + const char *name; + char *s = NULL; const char *p; ares_status_t status; size_t ndots; + /* Extract the name for the search. Note that searches are only supported for + * DNS records containing a single query. + */ + if (ares_dns_record_query_cnt(dnsrec) != 1) { + callback(arg, ARES_EBADQUERY, 0, NULL, 0); + return; + } + status = ares_dns_record_query_get(dnsrec, 0, &name, NULL, NULL); + if (status != ARES_SUCCESS) { + callback(arg, (int)status, 0, NULL, 0); + return; + } + /* Per RFC 7686, reject queries for ".onion" domain names with NXDOMAIN. */ if (ares__is_onion_domain(name)) { callback(arg, ARES_ENOTFOUND, 0, NULL, 0); @@ -74,16 +105,19 @@ static void ares_search_int(ares_channel_t *channel, const char *name, } /* If name only yields one domain to search, then we don't have - * to keep extra state, so just do an ares_query(). + * to keep extra state, so just do an ares_send(). */ status = ares__single_domain(channel, name, &s); if (status != ARES_SUCCESS) { callback(arg, (int)status, 0, NULL, 0); return; - } - if (s) { - ares_query(channel, s, dnsclass, type, callback, arg); + } else if (s != NULL) { + /* We only have a single domain to search, so do it here. */ + status = ares__write_and_send_query(channel, dnsrec, s, callback, arg); ares_free(s); + if (status != ARES_SUCCESS) { + callback(arg, (int)status, 0, NULL, 0); + } return; } @@ -96,10 +130,14 @@ static void ares_search_int(ares_channel_t *channel, const char *name, return; } squery->channel = channel; - squery->name = ares_strdup(name); - if (!squery->name) { + + /* We pass the DNS record through the search_query structure by encoding it + * into a buffer and then later decoding it back. + */ + status = ares_dns_write(dnsrec, &squery->buf, &squery->buflen); + if (status != ARES_SUCCESS) { ares_free(squery); - callback(arg, ARES_ENOMEM, 0, NULL, 0); + callback(arg, (int)status, 0, NULL, 0); return; } @@ -108,7 +146,7 @@ static void ares_search_int(ares_channel_t *channel, const char *name, squery->domains = ares__strsplit_duplicate(channel->domains, channel->ndomains); if (squery->domains == NULL) { - ares_free(squery->name); + ares_free(squery->buf); ares_free(squery); callback(arg, ARES_ENOMEM, 0, NULL, 0); return; @@ -116,8 +154,6 @@ static void ares_search_int(ares_channel_t *channel, const char *name, squery->ndomains = channel->ndomains; } - squery->dnsclass = dnsclass; - squery->type = type; squery->status_as_is = -1; squery->callback = callback; squery->arg = arg; @@ -140,32 +176,83 @@ static void ares_search_int(ares_channel_t *channel, const char *name, /* Try the name as-is first. */ squery->next_domain = 0; squery->trying_as_is = ARES_TRUE; - ares_query(channel, name, dnsclass, type, search_callback, squery); + ares_send(channel, squery->buf, (int)squery->buflen, search_callback, + squery); } else { /* Try the name as-is last; start with the first search domain. */ - squery->next_domain = 1; - squery->trying_as_is = ARES_FALSE; - status = ares__cat_domain(name, squery->domains[0], &s); + status = ares__cat_domain(name, squery->domains[0], &s); if (status == ARES_SUCCESS) { - ares_query(channel, s, dnsclass, type, search_callback, squery); + squery->next_domain = 1; + squery->trying_as_is = ARES_FALSE; + status = ares__write_and_send_query(channel, dnsrec, s, search_callback, + squery); ares_free(s); - } else { - /* failed, free the malloc()ed memory */ - ares_free(squery->name); - ares_free(squery); - callback(arg, (int)status, 0, NULL, 0); + } + /* Handle any errors. */ + if (status != ARES_SUCCESS) { + end_squery(squery, status, NULL, 0); } } } +/* Search for a DNS name with given class and type. Wrapper around + * ares_search_int() where the DNS record to search is first constructed. + */ void ares_search(ares_channel_t *channel, const char *name, int dnsclass, int type, ares_callback callback, void *arg) { - if (channel == NULL) { + ares_status_t status; + ares_dns_record_t *dnsrec = NULL; + size_t max_udp_size; + ares_dns_flags_t rd_flag; + + if ((channel == NULL) || (name == NULL)) { + return; + } + + rd_flag = !(channel->flags & ARES_FLAG_NORECURSE) ? ARES_FLAG_RD: 0; + max_udp_size = (channel->flags & ARES_FLAG_EDNS) ? channel->ednspsz : 0; + status = ares_dns_record_create_query(&dnsrec, name, + (ares_dns_class_t)dnsclass, + (ares_dns_rec_type_t)type, + 0, rd_flag, max_udp_size); + if (status != ARES_SUCCESS) { + callback(arg, (int)status, 0, NULL, 0); + return; + } + + ares__channel_lock(channel); + ares_search_int(channel, dnsrec, callback, arg); + ares__channel_unlock(channel); + + ares_dns_record_destroy(dnsrec); +} + +/* Search for a DNS record. Wrapper around ares_search_int(). */ +void ares_search_dnsrec(ares_channel_t *channel, ares_dns_record_t *dnsrec, + ares_callback_dnsrec callback, void *arg) +{ + struct dnsrec_convert_arg *carg; + + if ((channel == NULL) || (dnsrec == NULL)) { + return; + } + + /* For now, ares_search_int() uses the ares_callback prototype. We need to + * wrap the callback passed to this function in ares__dnsrec_convert_cb, to + * convert from ares_callback_dnsrec to ares_callback. Allocate the convert + * arg structure here. + */ + carg = ares_malloc_zero(sizeof(*carg)); + if (carg == NULL) { + callback(arg, ARES_ENOMEM, 0, NULL); return; } + carg->callback = callback; + carg->arg = arg; + ares__channel_lock(channel); - ares_search_int(channel, name, dnsclass, type, callback, arg); + ares_search_int(channel, dnsrec, ares__dnsrec_convert_cb, carg); ares__channel_unlock(channel); } @@ -174,51 +261,94 @@ static void search_callback(void *arg, int status, int timeouts, { struct search_query *squery = (struct search_query *)arg; ares_channel_t *channel = squery->channel; - char *s; + ares_dns_record_t *dnsrep = NULL; + ares_dns_rcode_t rcode; + size_t ancount; + ares_dns_record_t *dnsrec = NULL; + const char *name; + char *s = NULL; + ares_status_t mystatus; squery->timeouts += (size_t)timeouts; - /* Stop searching unless we got a non-fatal error. */ - if (status != ARES_ENODATA && status != ARES_ESERVFAIL && - status != ARES_ENOTFOUND) { + if (status != ARES_SUCCESS) { end_squery(squery, (ares_status_t)status, abuf, (size_t)alen); + return; + } + + /* Convert the rcode and ancount from the response into an ares_status_t + * value. Stop searching unless we got a non-fatal error. + */ + mystatus = ares_dns_parse(abuf, (size_t)alen, 0, &dnsrep); + if (mystatus != ARES_SUCCESS) { + end_squery(squery, mystatus, abuf, (size_t)alen); + return; + } + rcode = ares_dns_record_get_rcode(dnsrep); + ancount = ares_dns_record_rr_cnt(dnsrep, ARES_SECTION_ANSWER); + ares_dns_record_destroy(dnsrep); + mystatus = ares_dns_query_reply_tostatus(rcode, ancount); + + if ((mystatus != ARES_ENODATA) && (mystatus != ARES_ESERVFAIL) && + (mystatus != ARES_ENOTFOUND)) { + end_squery(squery, mystatus, abuf, (size_t)alen); } else { /* Save the status if we were trying as-is. */ if (squery->trying_as_is) { - squery->status_as_is = status; + squery->status_as_is = (int)mystatus; } - /* - * If we ever get ARES_ENODATA along the way, record that; if the search + /* If we ever get ARES_ENODATA along the way, record that; if the search * should run to the very end and we got at least one ARES_ENODATA, * then callers like ares_gethostbyname() may want to try a T_A search * even if the last domain we queried for T_AAAA resource records * returned ARES_ENOTFOUND. */ - if (status == ARES_ENODATA) { + if (mystatus == ARES_ENODATA) { squery->ever_got_nodata = ARES_TRUE; } if (squery->next_domain < squery->ndomains) { - ares_status_t mystatus; - /* Try the next domain. */ - mystatus = ares__cat_domain(squery->name, - squery->domains[squery->next_domain], &s); + /* Try the next domain. + * + * First parse the encoded DNS record in the search_query structure, so + * that we can append the next domain to it. + */ + mystatus = ares_dns_parse(squery->buf, squery->buflen, 0, &dnsrec); if (mystatus != ARES_SUCCESS) { end_squery(squery, mystatus, NULL, 0); } else { - squery->trying_as_is = ARES_FALSE; - squery->next_domain++; - ares_query(channel, s, squery->dnsclass, squery->type, search_callback, - squery); - ares_free(s); + /* Concatenate the name with the search domain and query using that. */ + if (ares_dns_record_query_cnt(dnsrec) != 1) { + mystatus = ARES_EBADQUERY; + } else { + mystatus = ares_dns_record_query_get(dnsrec, 0, &name, NULL, NULL); + if (mystatus == ARES_SUCCESS) { + mystatus = ares__cat_domain(name, + squery->domains[squery->next_domain], + &s); + if (mystatus == ARES_SUCCESS) { + squery->trying_as_is = ARES_FALSE; + squery->next_domain++; + mystatus = ares__write_and_send_query(channel, dnsrec, s, + search_callback, arg); + ares_free(s); + } + } + } + /* Clean up the DNS record object and handle any errors. */ + ares_dns_record_destroy(dnsrec); + if (mystatus != ARES_SUCCESS) { + end_squery(squery, mystatus, NULL, 0); + } } } else if (squery->status_as_is == -1) { /* Try the name as-is at the end. */ squery->trying_as_is = ARES_TRUE; - ares_query(channel, squery->name, squery->dnsclass, squery->type, - search_callback, squery); + ares_send(channel, squery->buf, (int)squery->buflen, search_callback, + squery); } else { + /* We have no more domains to search, return an appropriate response. */ if (squery->status_as_is == ARES_ENOTFOUND && squery->ever_got_nodata) { end_squery(squery, ARES_ENODATA, NULL, 0); } else { @@ -228,13 +358,43 @@ static void search_callback(void *arg, int status, int timeouts, } } +/* Write and send a DNS record on a channel. The DNS record must represent a + * query for a single name. An alternative name can be specified to temporarily + * overwrite the name on the DNS record before doing so. Note that this only + * affects the name in the question section; RRs are not affected. + * This is used as a helper function in ares_search(). + */ +static ares_status_t ares__write_and_send_query(ares_channel_t *channel, + ares_dns_record_t *dnsrec, + char *altname, + ares_callback callback, + void *arg) +{ + ares_status_t status; + unsigned char *buf; + size_t buflen; + + status = ares_dns_write_query_altname(dnsrec, altname, &buf, &buflen); + if (status != ARES_SUCCESS) { + return status; + } + + ares_send(channel, buf, (int)buflen, callback, arg); + ares_free(buf); + return ARES_SUCCESS; +} + + +/* End a search query by invoking the user callback and freeing the + * search_query structure. + */ static void end_squery(struct search_query *squery, ares_status_t status, unsigned char *abuf, size_t alen) { squery->callback(squery->arg, (int)status, (int)squery->timeouts, abuf, (int)alen); ares__strsplit_free(squery->domains, squery->ndomains); - ares_free(squery->name); + ares_free(squery->buf); ares_free(squery); } @@ -391,3 +551,29 @@ ares_status_t ares__single_domain(const ares_channel_t *channel, *s = NULL; return ARES_SUCCESS; } + +/* Callback function used to convert from the ares_callback prototype to the + * ares_callback_dnsrec prototype, by parsing the result and passing that to + * the inner callback. + */ +static void ares__dnsrec_convert_cb(void *arg, int status, int timeouts, + unsigned char *abuf, int alen) +{ + struct dnsrec_convert_arg *carg = (struct dnsrec_convert_arg *)arg; + ares_dns_record_t *dnsrec = NULL; + ares_status_t mystatus; + + if (status != ARES_SUCCESS) { + carg->callback(carg->arg, (ares_status_t)status, (size_t)timeouts, NULL); + } else { + /* Parse the result. */ + mystatus = ares_dns_parse(abuf, (size_t)alen, 0, &dnsrec); + if (mystatus != ARES_SUCCESS) { + carg->callback(carg->arg, mystatus, (size_t)timeouts, NULL); + } else { + carg->callback(carg->arg, ARES_SUCCESS, (size_t)timeouts, dnsrec); + ares_dns_record_destroy(dnsrec); + } + } + ares_free(carg); +} diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc index 687d308f..e344154e 100644 --- a/test/ares-test-mock.cc +++ b/test/ares-test-mock.cc @@ -803,6 +803,104 @@ TEST_P(MockChannelTest, SearchHighNdots) { ss.str()); } +// 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) { + /* Define the OPT RR options code and value to use. */ + unsigned short opt_opt = 3; + unsigned char opt_val[] = { 'c', '-', 'a', 'r', 'e', 's' }; + + /* Set up the expected request and reply on the mock server for the first, + * second and third domains. The expected requests contain the OPT RR options + * value defined above. + */ + std::string nofirst_req = "REQ QRY RD Q:{'example.first.com' IN A} " + "ADD:{'' MAXUDP=1232 OPT RCODE2=0 " + "0003" // opt_opt + "0006" // length of opt_val + "632d61726573" // opt_val in hex + "}"; + DNSPacket nofirst_rep; + nofirst_rep.set_response().set_aa().set_rcode(NXDOMAIN) + .add_question(new DNSQuestion("example.first.com", T_A)); + ON_CALL(server_, OnRequest("example.first.com", T_A)) + .WillByDefault(SetReplyExpRequest(&server_, &nofirst_rep, nofirst_req)); + + std::string nosecond_req = "REQ QRY RD Q:{'example.second.org' IN A} " + "ADD:{'' MAXUDP=1232 OPT RCODE2=0 " + "0003" // opt_opt + "0006" // length of opt_val + "632d61726573" // opt_val in hex + "}"; + DNSPacket nosecond_rep; + nosecond_rep.set_response().set_aa().set_rcode(NXDOMAIN) + .add_question(new DNSQuestion("example.second.org", T_A)); + ON_CALL(server_, OnRequest("example.second.org", T_A)) + .WillByDefault(SetReplyExpRequest(&server_, &nosecond_rep, nosecond_req)); + + std::string nothird_req = "REQ QRY RD Q:{'example.third.gov' IN A} " + "ADD:{'' MAXUDP=1232 OPT RCODE2=0 " + "0003" // opt_opt + "0006" // length of opt_val + "632d61726573" // opt_val in hex + "}"; + DNSPacket nothird_rep; + nothird_rep.set_response().set_aa().set_rcode(NXDOMAIN) + .add_question(new DNSQuestion("example.third.gov", T_A)); + ON_CALL(server_, OnRequest("example.third.gov", T_A)) + .WillByDefault(SetReplyExpRequest(&server_, ¬hird_rep, nothird_req)); + + /* Set up the expected request and reply on the mock server for the bare + * domain. The expected request contains the OPT RR options value defined + * above. + */ + std::string yesbare_req = "REQ QRY RD Q:{'example' IN A} " + "ADD:{'' MAXUDP=1232 OPT RCODE2=0 " + "0003" // opt_opt + "0006" // length of opt_val + "632d61726573" // opt_val in hex + "}"; + DNSPacket yesbare_rep; + yesbare_rep.set_response().set_aa() + .add_question(new DNSQuestion("example", T_A)) + .add_answer(new DNSARR("example", 0x0200, {2, 3, 4, 5})); + ON_CALL(server_, OnRequest("example", T_A)) + .WillByDefault(SetReplyExpRequest(&server_, &yesbare_rep, yesbare_req)); + + /* Construct the DNS record to search. */ + ares_dns_record_t *dnsrec = NULL; + ares_dns_rr_t *rr = NULL; + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_create(&dnsrec, 0, ARES_FLAG_RD, ARES_OPCODE_QUERY, + ARES_RCODE_NOERROR)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_query_add(dnsrec, "example", (ares_dns_rec_type_t)T_A, + (ares_dns_class_t)C_IN)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_rr_add(&rr, dnsrec, ARES_SECTION_ADDITIONAL, "", + ARES_REC_TYPE_OPT, ARES_CLASS_IN, 0)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_rr_set_u16(rr, ARES_RR_OPT_UDP_SIZE, 1232)); + EXPECT_EQ(ARES_SUCCESS, ares_dns_rr_set_u8(rr, ARES_RR_OPT_VERSION, 0)); + EXPECT_EQ(ARES_SUCCESS, ares_dns_rr_set_u16(rr, ARES_RR_OPT_FLAGS, 0)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_rr_set_opt(rr, ARES_RR_OPT_OPTIONS, opt_opt, opt_val, + sizeof(opt_val))); + + /* Perform the search. Check that it succeeds with the expected response. */ + SearchResult result; + ares_search_dnsrec(channel_, dnsrec, SearchCallbackDnsRec, &result); + ares_dns_record_destroy(dnsrec); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_SUCCESS, result.status_); + std::stringstream ss; + ss << PacketToString(result.data_); + EXPECT_EQ("RSP QRY AA NOERROR Q:{'example' IN A} " + "A:{'example' IN A TTL=512 2.3.4.5}", + ss.str()); +} + TEST_P(MockChannelTest, V4WorksV6Timeout) { std::vector nothing; DNSPacket reply; @@ -1118,7 +1216,7 @@ TEST_P(MockChannelTest, CancelImmediateGetHostByAddr) { HostResult result; struct in_addr addr; addr.s_addr = htonl(0x08080808); - + ares_gethostbyaddr(channel_, &addr, sizeof(addr), AF_INET, HostCallback, &result); ares_cancel(channel_); EXPECT_TRUE(result.done_); diff --git a/test/ares-test.cc b/test/ares-test.cc index 53a299e0..758a5366 100644 --- a/test/ares-test.cc +++ b/test/ares-test.cc @@ -574,15 +574,16 @@ void MockServer::ProcessPacket(ares_socket_t fd, struct sockaddr_storage *addr, } int rrtype = DNS_QUESTION_TYPE(question); + std::vector req(data, data + len); + std::string reqstr = PacketToString(req); if (verbose) { - std::vector req(data, data + len); - std::cerr << "received " << (fd == udpfd_ ? "UDP" : "TCP") << " request " << PacketToString(req) + std::cerr << "received " << (fd == udpfd_ ? "UDP" : "TCP") << " request " << reqstr << " on port " << (fd == udpfd_ ? udpport_ : tcpport_) << ":" << getaddrport(addr) << std::endl; std::cerr << "ProcessRequest(" << qid << ", '" << namestr << "', " << RRTypeToString(rrtype) << ")" << std::endl; } - ProcessRequest(fd, addr, addrlen, qid, namestr, rrtype); + ProcessRequest(fd, addr, addrlen, reqstr, qid, namestr, rrtype); } @@ -651,11 +652,17 @@ std::set MockServer::fds() const { } -void MockServer::ProcessRequest(ares_socket_t fd, struct sockaddr_storage* addr, ares_socklen_t addrlen, +void MockServer::ProcessRequest(ares_socket_t fd, struct sockaddr_storage* addr, + ares_socklen_t addrlen, const std::string &reqstr, int qid, const std::string& name, int rrtype) { // Before processing, let gMock know the request is happening. OnRequest(name, rrtype); + // If we are expecting a specific request then check it matches here. + if (expected_request_.length() > 0) { + ASSERT_EQ(expected_request_, reqstr); + } + if (reply_.size() == 0) { return; } @@ -1076,6 +1083,23 @@ void SearchCallback(void *data, int status, int timeouts, if (verbose) std::cerr << "SearchCallback(" << *result << ")" << std::endl; } +void SearchCallbackDnsRec(void *data, ares_status_t status, size_t timeouts, + const ares_dns_record_t *dnsrec) { + EXPECT_NE(nullptr, data); + SearchResult* result = reinterpret_cast(data); + unsigned char *abuf = NULL; + size_t alen = 0; + result->done_ = true; + result->status_ = (int)status; + result->timeouts_ = (int)timeouts; + if (dnsrec != NULL) { + ares_dns_write(dnsrec, &abuf, &alen); + } + result->data_.assign(abuf, abuf + alen); + ares_free_string(abuf); + if (verbose) std::cerr << "SearchCallbackDnsRec(" << *result << ")" << std::endl; +} + std::ostream& operator<<(std::ostream& os, const NameInfoResult& result) { os << '{'; if (result.done_) { diff --git a/test/ares-test.h b/test/ares-test.h index 812f61c3..949cf337 100644 --- a/test/ares-test.h +++ b/test/ares-test.h @@ -243,6 +243,15 @@ public: SetReplyData(reply->data()); } + // Set the reply to be sent next as well as the request (in string form) that + // the server should expect to receive; the query ID field in the reply will + // be overwritten with the value from the request. + void SetReplyExpRequest(const DNSPacket *reply, const std::string &request) + { + expected_request_ = request; + SetReply(reply); + } + void SetReplyQID(int qid) { qid_ = qid; @@ -278,8 +287,8 @@ public: private: void ProcessRequest(ares_socket_t fd, struct sockaddr_storage *addr, - ares_socklen_t addrlen, int qid, const std::string &name, - int rrtype); + ares_socklen_t addrlen, const std::string &reqstr, + int qid, const std::string &name, int rrtype); void ProcessPacket(ares_socket_t fd, struct sockaddr_storage *addr, ares_socklen_t addrlen, byte *data, int len); unsigned short udpport_; @@ -288,6 +297,7 @@ private: ares_socket_t tcpfd_; std::set connfds_; std::vector reply_; + std::string expected_request_; int qid_; unsigned char *tcp_data_; size_t tcp_data_len_; @@ -444,6 +454,13 @@ ACTION_P2(SetReply, mockserver, reply) mockserver->SetReply(reply); } +// gMock action to set the reply for a mock server, as well as the request (in +// string form) that the server should expect to receive. +ACTION_P3(SetReplyExpRequest, mockserver, reply, request) +{ + mockserver->SetReplyExpRequest(reply, request); +} + ACTION_P2(SetReplyQID, mockserver, qid) { mockserver->SetReplyQID(qid); @@ -555,6 +572,8 @@ void HostCallback(void *data, int status, int timeouts, struct hostent *hostent); void SearchCallback(void *data, int status, int timeouts, unsigned char *abuf, int alen); +void SearchCallbackDnsRec(void *data, ares_status_t status, size_t timeouts, + const ares_dns_record_t *dnsrec); void NameInfoCallback(void *data, int status, int timeouts, char *node, char *service); void AddrInfoCallback(void *data, int status, int timeouts,