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,