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)
pull/730/head
Oliver Welsh 8 months ago committed by GitHub
parent a2a8578ee0
commit fab65acae9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      docs/Makefile.inc
  2. 4
      docs/ares_dns_record.3
  3. 3
      docs/ares_dns_record_rr_get_const.3
  4. 34
      docs/ares_dns_rr.3
  5. 25
      docs/ares_search.3
  6. 3
      docs/ares_search_dnsrec.3
  7. 65
      include/ares.h
  8. 16
      include/ares_dns_record.h
  9. 51
      src/lib/ares_create_query.c
  10. 34
      src/lib/ares_dns_mapping.c
  11. 43
      src/lib/ares_dns_private.h
  12. 80
      src/lib/ares_dns_record.c
  13. 33
      src/lib/ares_dns_write.c
  14. 46
      src/lib/ares_query.c
  15. 298
      src/lib/ares_search.c
  16. 100
      test/ares-test-mock.cc
  17. 32
      test/ares-test.cc
  18. 23
      test/ares-test.h

@ -32,6 +32,7 @@ MANPAGES = ares_cancel.3 \
ares_dns_record_rr_cnt.3 \ ares_dns_record_rr_cnt.3 \
ares_dns_record_rr_del.3 \ ares_dns_record_rr_del.3 \
ares_dns_record_rr_get.3 \ ares_dns_record_rr_get.3 \
ares_dns_record_rr_get_const.3 \
ares_dns_rec_type_fromstr.3 \ ares_dns_rec_type_fromstr.3 \
ares_dns_rec_type_t.3 \ ares_dns_rec_type_t.3 \
ares_dns_rr.3 \ ares_dns_rr.3 \
@ -111,6 +112,7 @@ MANPAGES = ares_cancel.3 \
ares_reinit.3 \ ares_reinit.3 \
ares_save_options.3 \ ares_save_options.3 \
ares_search.3 \ ares_search.3 \
ares_search_dnsrec.3 \
ares_send.3 \ ares_send.3 \
ares_set_local_dev.3 \ ares_set_local_dev.3 \
ares_set_local_ip4.3 \ ares_set_local_ip4.3 \

@ -19,7 +19,7 @@ ares_status_t ares_dns_parse(const unsigned char *buf,
size_t buf_len, unsigned int flags, size_t buf_len, unsigned int flags,
ares_dns_record_t **dnsrec); 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); unsigned char **buf, size_t *buf_len);
ares_status_t ares_dns_record_create(ares_dns_record_t **dnsrec, 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 .B ARES_REC_TYPE_SOA
- Start of authority zone - Start of authority zone
.br .br
.B ARES_REC_TYPE_PTR .B ARES_REC_TYPE_PTR
- Domain name pointer - Domain name pointer
.br .br
.B ARES_REC_TYPE_HINFO .B ARES_REC_TYPE_HINFO

@ -0,0 +1,3 @@
.\" Copyright (C) 2023 The c-ares project and its contributors.
.\" SPDX-License-Identifier: MIT
.so man3/ares_dns_rr.3

@ -4,14 +4,15 @@
.TH ARES_DNS_RR 3 "12 November 2023" .TH ARES_DNS_RR 3 "12 November 2023"
.SH NAME .SH NAME
ares_dns_record_rr_add, ares_dns_record_rr_cnt, ares_dns_record_rr_del, 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_record_rr_get, ares_dns_record_rr_get_const, ares_dns_rr_get_addr,
ares_dns_rr_get_bin, ares_dns_rr_get_class, ares_dns_rr_get_name, ares_dns_rr_get_addr6, ares_dns_rr_get_bin, ares_dns_rr_get_class,
ares_dns_rr_get_opt, ares_dns_rr_get_opt_byid, ares_dns_rr_get_opt_cnt, ares_dns_rr_get_name, ares_dns_rr_get_opt, ares_dns_rr_get_opt_byid,
ares_dns_rr_get_str, ares_dns_rr_get_ttl, ares_dns_rr_get_type, ares_dns_rr_get_opt_cnt, ares_dns_rr_get_str, ares_dns_rr_get_ttl,
ares_dns_rr_get_u16, ares_dns_rr_get_u32, ares_dns_rr_get_u8, ares_dns_rr_key_t, ares_dns_rr_get_type, ares_dns_rr_get_u16, ares_dns_rr_get_u32,
ares_dns_rr_set_addr, ares_dns_rr_set_addr6, ares_dns_rr_set_bin, ares_dns_rr_get_u8, ares_dns_rr_key_t, ares_dns_rr_set_addr,
ares_dns_rr_set_opt, ares_dns_rr_set_str, ares_dns_rr_set_u16, ares_dns_rr_set_addr6, ares_dns_rr_set_bin, ares_dns_rr_set_opt,
ares_dns_rr_set_u32, ares_dns_rr_set_u8, ares_dns_section_t, ares_tlsa_match_t, 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 \- ares_tlsa_selector_t, ares_tlsa_usage_t \-
DNS Resource Record creating, reading, and writing functions. DNS Resource Record creating, reading, and writing functions.
.SH SYNOPSIS .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, ares_dns_section_t sect,
size_t idx); 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_status_t ares_dns_record_rr_del(ares_dns_record_t *dnsrec,
ares_dns_section_t sect, ares_dns_section_t sect,
size_t idx); size_t idx);
@ -357,14 +362,17 @@ parameter, and the Time To Live (TTL) in the
parameter. parameter.
The \fIares_dns_record_rr_get(3)\fP function is used to retrieve the resource The \fIares_dns_record_rr_get(3)\fP and \fIares_dns_record_rr_get_const(3)\fP
record pointer from the DNS record provided in the functions are used to retrieve the resource record pointer from the DNS record
provided in the
.IR dnsrec .IR dnsrec
parameter, for the resource record section provided in the parameter, for the resource record section provided in the
.IR sect .IR sect
parameter, for the specified index in the parameter, for the specified index in the
.IR idx .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 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 in which a 0 (or NULL) may be returned, however 0 can also be a valid return
value for most of these functions. value for most of these functions.
\fIares_dns_record_rr_get(3)\fP will return the requested resource record \fIares_dns_record_rr_get(3)\fP and \fIares_dns_record_rr_get_const(3)\fP will
pointer or NULL on failure (misuse). 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 \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). found, otherwise ARES_FALSE if not found (or misuse).

@ -13,9 +13,18 @@ typedef void (*ares_callback)(void *\fIarg\fP, int \fIstatus\fP,
int \fItimeouts\fP, unsigned char *\fIabuf\fP, int \fItimeouts\fP, unsigned char *\fIabuf\fP,
int \fIalen\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, void ares_search(ares_channel_t *\fIchannel\fP, const char *\fIname\fP,
int \fIdnsclass\fP, int \fItype\fP, int \fIdnsclass\fP, int \fItype\fP,
ares_callback \fIcallback\fP, void *\fIarg\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 .fi
.SH DESCRIPTION .SH DESCRIPTION
The The
@ -142,6 +151,22 @@ will usually be NULL and
will usually be 0, but in some cases an unsuccessful query result may will usually be 0, but in some cases an unsuccessful query result may
be placed in be placed in
.IR abuf . .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 .SH SEE ALSO
.BR ares_process (3), .BR ares_process (3),
.BR ares_dns_record (3) .BR ares_dns_record (3)

@ -0,0 +1,3 @@
.\" Copyright (C) 2023 The c-ares project and its contributors.
.\" SPDX-License-Identifier: MIT
.so man3/ares_search.3

@ -352,10 +352,38 @@ typedef struct ares_channeldata *ares_channel;
/* Current main channel typedef */ /* Current main channel typedef */
typedef struct ares_channeldata ares_channel_t; 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, typedef void (*ares_callback)(void *arg, int status, int timeouts,
unsigned char *abuf, int alen); 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, typedef void (*ares_host_callback)(void *arg, int status, int timeouts,
struct hostent *hostent); 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, int dnsclass, int type, ares_callback callback,
void *arg); 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, CARES_EXTERN void ares_gethostbyname(ares_channel_t *channel, const char *name,
int family, ares_host_callback callback, int family, ares_host_callback callback,
void *arg); void *arg);
@ -528,28 +568,6 @@ CARES_EXTERN int ares_expand_string(const unsigned char *encoded,
const unsigned char *abuf, int alen, const unsigned char *abuf, int alen,
unsigned char **s, long *enclen); 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 ares_addrttl {
struct in_addr ipaddr; struct in_addr ipaddr;
int ttl; int ttl;
@ -803,7 +821,4 @@ CARES_EXTERN size_t ares_queue_active_queries(ares_channel_t *channel);
} }
#endif #endif
/* DNS record parser, writer, and helpers */
#include "ares_dns_record.h"
#endif /* ARES__H */ #endif /* ARES__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, const char *name, ares_dns_rec_type_t type, ares_dns_class_t rclass,
unsigned int ttl); 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] dnsrec Initialized record object
* \param[in] sect Section for resource record * \param[in] sect Section for resource record
* \param[in] idx Index of resource record in section * \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, CARES_EXTERN ares_dns_rr_t *ares_dns_record_rr_get(ares_dns_record_t *dnsrec,
ares_dns_section_t sect, ares_dns_section_t sect,
size_t idx); 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 /*! 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. * \param[out] buf_len Length of returned buffer containing DNS message.
* \return ARES_SUCCESS on success * \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); unsigned char **buf, size_t *buf_len);
/*! @} */ /*! @} */

@ -35,6 +35,7 @@ int ares_create_query(const char *name, int dnsclass, int type,
ares_status_t status; ares_status_t status;
ares_dns_record_t *dnsrec = NULL; ares_dns_record_t *dnsrec = NULL;
size_t len; size_t len;
ares_dns_flags_t rd_flag = rd ? ARES_FLAG_RD : 0;
if (name == NULL || bufp == NULL || buflenp == NULL) { if (name == NULL || bufp == NULL || buflenp == NULL) {
status = ARES_EFORMERR; status = ARES_EFORMERR;
@ -44,56 +45,14 @@ int ares_create_query(const char *name, int dnsclass, int type,
*bufp = NULL; *bufp = NULL;
*buflenp = 0; *buflenp = 0;
/* Per RFC 7686, reject queries for ".onion" domain names with NXDOMAIN. */ status = ares_dns_record_create_query(&dnsrec, name,
if (ares__is_onion_domain(name)) { (ares_dns_class_t)dnsclass,
status = ARES_ENOTFOUND; (ares_dns_rec_type_t)type,
goto done; id, rd_flag, (size_t)max_udp_size);
}
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);
if (status != ARES_SUCCESS) { if (status != ARES_SUCCESS) {
goto done; 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); status = ares_dns_write(dnsrec, bufp, &len);
if (status != ARES_SUCCESS) { if (status != ARES_SUCCESS) {
goto done; goto done;

@ -883,3 +883,37 @@ const char *ares_dns_rcode_tostr(ares_dns_rcode_t rcode)
return "UNKNOWN"; 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;
}

@ -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, void ares_dns_record_write_ttl_decrement(ares_dns_record_t *dnsrec,
unsigned int ttl_decrement); 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 { struct ares_dns_qd {
char *name; char *name;
ares_dns_rec_type_t qtype; ares_dns_rec_type_t qtype;

@ -499,7 +499,7 @@ ares_dns_rr_t *ares_dns_record_rr_get(ares_dns_record_t *dnsrec,
return &rr_ptr[idx]; 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_record_rr_get_const(const ares_dns_record_t *dnsrec,
ares_dns_section_t sect, size_t idx) 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; 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;
}

@ -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); 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__llist_t **namelist,
ares_dns_section_t section, ares_dns_section_t section,
ares__buf_t *buf) 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; size_t end_length;
unsigned int ttl; 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) { if (rr == NULL) {
return ARES_EFORMERR; return ARES_EFORMERR;
} }
@ -988,7 +988,8 @@ static ares_status_t ares_dns_write_rr(ares_dns_record_t *dnsrec,
return ARES_SUCCESS; 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) size_t *buf_len)
{ {
ares__buf_t *b = NULL; 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; 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;
}

@ -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, static void qcallback(void *arg, int status, int timeouts, unsigned char *abuf,
int alen) int alen)
{ {
struct qquery *qquery = (struct qquery *)arg; struct qquery *qquery = (struct qquery *)arg;
size_t ancount; ares_dns_record_t *dnsrep = NULL;
int rcode; size_t ancount;
ares_dns_rcode_t rcode;
if (status != ARES_SUCCESS) { if (status != ARES_SUCCESS) {
qquery->callback(qquery->arg, status, timeouts, abuf, alen); qquery->callback(qquery->arg, status, timeouts, abuf, alen);
} else { } else {
/* Pull the response code and answer count from the packet. */ /* Pull the response code and answer count from the packet and convert any
rcode = DNS_HEADER_RCODE(abuf); * errors.
ancount = DNS_HEADER_ANCOUNT(abuf); */
status = (int)ares_dns_parse(abuf, (size_t)alen, 0, &dnsrep);
/* Convert errors. */ if (status != ARES_SUCCESS) {
switch (rcode) { qquery->callback(qquery->arg, status, timeouts, abuf, alen);
case NOERROR: } else {
status = (ancount > 0) ? ARES_SUCCESS : ARES_ENODATA; rcode = ares_dns_record_get_rcode(dnsrep);
break; ancount = ares_dns_record_rr_cnt(dnsrep, ARES_SECTION_ANSWER);
case FORMERR: ares_dns_record_destroy(dnsrep);
status = ARES_EFORMERR; status = (int)ares_dns_query_reply_tostatus(rcode, ancount);
break; qquery->callback(qquery->arg, status, timeouts, abuf, alen);
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;
} }
qquery->callback(qquery->arg, status, timeouts, abuf, alen);
} }
ares_free(qquery); ares_free(qquery);
} }

@ -33,40 +33,71 @@
#include "ares.h" #include "ares.h"
#include "ares_private.h" #include "ares_private.h"
#include "ares_dns.h"
struct search_query { struct search_query {
/* Arguments passed to ares_search */ /* Arguments passed to ares_search() */
ares_channel_t *channel; ares_channel_t *channel;
char *name; /* copied into an allocated buffer */
int dnsclass;
int type;
ares_callback callback; ares_callback callback;
void *arg; 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; size_t ndomains;
int status_as_is; /* error status from trying as-is */ /* State tracking progress through the search query */
size_t next_domain; /* next search domain to try */ int status_as_is; /* error status from trying as-is */
ares_bool_t trying_as_is; /* current query is for name as-is */ size_t next_domain; /* next search domain to try */
size_t timeouts; /* number of timeouts we saw for this request */ ares_bool_t trying_as_is; /* current query is for name as-is */
ares_bool_t ever_got_nodata; /* did we ever get ARES_ENODATA along the way? */ 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, static void search_callback(void *arg, int status, int timeouts,
unsigned char *abuf, int alen); 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, static void end_squery(struct search_query *squery, ares_status_t status,
unsigned char *abuf, size_t alen); 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, static void ares_search_int(ares_channel_t *channel, ares_dns_record_t *dnsrec,
int dnsclass, int type, ares_callback callback, ares_callback callback, void *arg)
void *arg)
{ {
struct search_query *squery; struct search_query *squery;
char *s; const char *name;
char *s = NULL;
const char *p; const char *p;
ares_status_t status; ares_status_t status;
size_t ndots; 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. */ /* Per RFC 7686, reject queries for ".onion" domain names with NXDOMAIN. */
if (ares__is_onion_domain(name)) { if (ares__is_onion_domain(name)) {
callback(arg, ARES_ENOTFOUND, 0, NULL, 0); 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 /* 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); status = ares__single_domain(channel, name, &s);
if (status != ARES_SUCCESS) { if (status != ARES_SUCCESS) {
callback(arg, (int)status, 0, NULL, 0); callback(arg, (int)status, 0, NULL, 0);
return; return;
} } else if (s != NULL) {
if (s) { /* We only have a single domain to search, so do it here. */
ares_query(channel, s, dnsclass, type, callback, arg); status = ares__write_and_send_query(channel, dnsrec, s, callback, arg);
ares_free(s); ares_free(s);
if (status != ARES_SUCCESS) {
callback(arg, (int)status, 0, NULL, 0);
}
return; return;
} }
@ -96,10 +130,14 @@ static void ares_search_int(ares_channel_t *channel, const char *name,
return; return;
} }
squery->channel = channel; 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); ares_free(squery);
callback(arg, ARES_ENOMEM, 0, NULL, 0); callback(arg, (int)status, 0, NULL, 0);
return; return;
} }
@ -108,7 +146,7 @@ static void ares_search_int(ares_channel_t *channel, const char *name,
squery->domains = squery->domains =
ares__strsplit_duplicate(channel->domains, channel->ndomains); ares__strsplit_duplicate(channel->domains, channel->ndomains);
if (squery->domains == NULL) { if (squery->domains == NULL) {
ares_free(squery->name); ares_free(squery->buf);
ares_free(squery); ares_free(squery);
callback(arg, ARES_ENOMEM, 0, NULL, 0); callback(arg, ARES_ENOMEM, 0, NULL, 0);
return; return;
@ -116,8 +154,6 @@ static void ares_search_int(ares_channel_t *channel, const char *name,
squery->ndomains = channel->ndomains; squery->ndomains = channel->ndomains;
} }
squery->dnsclass = dnsclass;
squery->type = type;
squery->status_as_is = -1; squery->status_as_is = -1;
squery->callback = callback; squery->callback = callback;
squery->arg = arg; 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. */ /* Try the name as-is first. */
squery->next_domain = 0; squery->next_domain = 0;
squery->trying_as_is = ARES_TRUE; 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 { } else {
/* Try the name as-is last; start with the first search domain. */ /* Try the name as-is last; start with the first search domain. */
squery->next_domain = 1; status = ares__cat_domain(name, squery->domains[0], &s);
squery->trying_as_is = ARES_FALSE;
status = ares__cat_domain(name, squery->domains[0], &s);
if (status == ARES_SUCCESS) { 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); ares_free(s);
} else { }
/* failed, free the malloc()ed memory */ /* Handle any errors. */
ares_free(squery->name); if (status != ARES_SUCCESS) {
ares_free(squery); end_squery(squery, status, NULL, 0);
callback(arg, (int)status, 0, 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, void ares_search(ares_channel_t *channel, const char *name, int dnsclass,
int type, ares_callback callback, void *arg) 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; return;
} }
carg->callback = callback;
carg->arg = arg;
ares__channel_lock(channel); 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); 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; struct search_query *squery = (struct search_query *)arg;
ares_channel_t *channel = squery->channel; 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; squery->timeouts += (size_t)timeouts;
/* Stop searching unless we got a non-fatal error. */ if (status != ARES_SUCCESS) {
if (status != ARES_ENODATA && status != ARES_ESERVFAIL &&
status != ARES_ENOTFOUND) {
end_squery(squery, (ares_status_t)status, abuf, (size_t)alen); 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 { } else {
/* Save the status if we were trying as-is. */ /* Save the status if we were trying as-is. */
if (squery->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, * 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 * 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 * even if the last domain we queried for T_AAAA resource records
* returned ARES_ENOTFOUND. * returned ARES_ENOTFOUND.
*/ */
if (status == ARES_ENODATA) { if (mystatus == ARES_ENODATA) {
squery->ever_got_nodata = ARES_TRUE; squery->ever_got_nodata = ARES_TRUE;
} }
if (squery->next_domain < squery->ndomains) { if (squery->next_domain < squery->ndomains) {
ares_status_t mystatus; /* Try the next domain.
/* Try the next domain. */ *
mystatus = ares__cat_domain(squery->name, * First parse the encoded DNS record in the search_query structure, so
squery->domains[squery->next_domain], &s); * that we can append the next domain to it.
*/
mystatus = ares_dns_parse(squery->buf, squery->buflen, 0, &dnsrec);
if (mystatus != ARES_SUCCESS) { if (mystatus != ARES_SUCCESS) {
end_squery(squery, mystatus, NULL, 0); end_squery(squery, mystatus, NULL, 0);
} else { } else {
squery->trying_as_is = ARES_FALSE; /* Concatenate the name with the search domain and query using that. */
squery->next_domain++; if (ares_dns_record_query_cnt(dnsrec) != 1) {
ares_query(channel, s, squery->dnsclass, squery->type, search_callback, mystatus = ARES_EBADQUERY;
squery); } else {
ares_free(s); 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) { } else if (squery->status_as_is == -1) {
/* Try the name as-is at the end. */ /* Try the name as-is at the end. */
squery->trying_as_is = ARES_TRUE; squery->trying_as_is = ARES_TRUE;
ares_query(channel, squery->name, squery->dnsclass, squery->type, ares_send(channel, squery->buf, (int)squery->buflen, search_callback,
search_callback, squery); squery);
} else { } else {
/* We have no more domains to search, return an appropriate response. */
if (squery->status_as_is == ARES_ENOTFOUND && squery->ever_got_nodata) { if (squery->status_as_is == ARES_ENOTFOUND && squery->ever_got_nodata) {
end_squery(squery, ARES_ENODATA, NULL, 0); end_squery(squery, ARES_ENODATA, NULL, 0);
} else { } 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, static void end_squery(struct search_query *squery, ares_status_t status,
unsigned char *abuf, size_t alen) unsigned char *abuf, size_t alen)
{ {
squery->callback(squery->arg, (int)status, (int)squery->timeouts, abuf, squery->callback(squery->arg, (int)status, (int)squery->timeouts, abuf,
(int)alen); (int)alen);
ares__strsplit_free(squery->domains, squery->ndomains); ares__strsplit_free(squery->domains, squery->ndomains);
ares_free(squery->name); ares_free(squery->buf);
ares_free(squery); ares_free(squery);
} }
@ -391,3 +551,29 @@ ares_status_t ares__single_domain(const ares_channel_t *channel,
*s = NULL; *s = NULL;
return ARES_SUCCESS; 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);
}

@ -803,6 +803,104 @@ TEST_P(MockChannelTest, SearchHighNdots) {
ss.str()); 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_, &nothird_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) { TEST_P(MockChannelTest, V4WorksV6Timeout) {
std::vector<byte> nothing; std::vector<byte> nothing;
DNSPacket reply; DNSPacket reply;
@ -1118,7 +1216,7 @@ TEST_P(MockChannelTest, CancelImmediateGetHostByAddr) {
HostResult result; HostResult result;
struct in_addr addr; struct in_addr addr;
addr.s_addr = htonl(0x08080808); addr.s_addr = htonl(0x08080808);
ares_gethostbyaddr(channel_, &addr, sizeof(addr), AF_INET, HostCallback, &result); ares_gethostbyaddr(channel_, &addr, sizeof(addr), AF_INET, HostCallback, &result);
ares_cancel(channel_); ares_cancel(channel_);
EXPECT_TRUE(result.done_); EXPECT_TRUE(result.done_);

32
test/ares-test.cc vendored

@ -574,15 +574,16 @@ void MockServer::ProcessPacket(ares_socket_t fd, struct sockaddr_storage *addr,
} }
int rrtype = DNS_QUESTION_TYPE(question); int rrtype = DNS_QUESTION_TYPE(question);
std::vector<byte> req(data, data + len);
std::string reqstr = PacketToString(req);
if (verbose) { if (verbose) {
std::vector<byte> req(data, data + len); std::cerr << "received " << (fd == udpfd_ ? "UDP" : "TCP") << " request " << reqstr
std::cerr << "received " << (fd == udpfd_ ? "UDP" : "TCP") << " request " << PacketToString(req)
<< " on port " << (fd == udpfd_ ? udpport_ : tcpport_) << " on port " << (fd == udpfd_ ? udpport_ : tcpport_)
<< ":" << getaddrport(addr) << std::endl; << ":" << getaddrport(addr) << std::endl;
std::cerr << "ProcessRequest(" << qid << ", '" << namestr std::cerr << "ProcessRequest(" << qid << ", '" << namestr
<< "', " << RRTypeToString(rrtype) << ")" << std::endl; << "', " << 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<ares_socket_t> 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) { int qid, const std::string& name, int rrtype) {
// Before processing, let gMock know the request is happening. // Before processing, let gMock know the request is happening.
OnRequest(name, rrtype); 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) { if (reply_.size() == 0) {
return; return;
} }
@ -1076,6 +1083,23 @@ void SearchCallback(void *data, int status, int timeouts,
if (verbose) std::cerr << "SearchCallback(" << *result << ")" << std::endl; 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<SearchResult*>(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) { std::ostream& operator<<(std::ostream& os, const NameInfoResult& result) {
os << '{'; os << '{';
if (result.done_) { if (result.done_) {

23
test/ares-test.h vendored

@ -243,6 +243,15 @@ public:
SetReplyData(reply->data()); 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) void SetReplyQID(int qid)
{ {
qid_ = qid; qid_ = qid;
@ -278,8 +287,8 @@ public:
private: private:
void ProcessRequest(ares_socket_t fd, struct sockaddr_storage *addr, void ProcessRequest(ares_socket_t fd, struct sockaddr_storage *addr,
ares_socklen_t addrlen, int qid, const std::string &name, ares_socklen_t addrlen, const std::string &reqstr,
int rrtype); int qid, const std::string &name, int rrtype);
void ProcessPacket(ares_socket_t fd, struct sockaddr_storage *addr, void ProcessPacket(ares_socket_t fd, struct sockaddr_storage *addr,
ares_socklen_t addrlen, byte *data, int len); ares_socklen_t addrlen, byte *data, int len);
unsigned short udpport_; unsigned short udpport_;
@ -288,6 +297,7 @@ private:
ares_socket_t tcpfd_; ares_socket_t tcpfd_;
std::set<ares_socket_t> connfds_; std::set<ares_socket_t> connfds_;
std::vector<byte> reply_; std::vector<byte> reply_;
std::string expected_request_;
int qid_; int qid_;
unsigned char *tcp_data_; unsigned char *tcp_data_;
size_t tcp_data_len_; size_t tcp_data_len_;
@ -444,6 +454,13 @@ ACTION_P2(SetReply, mockserver, reply)
mockserver->SetReply(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) ACTION_P2(SetReplyQID, mockserver, qid)
{ {
mockserver->SetReplyQID(qid); mockserver->SetReplyQID(qid);
@ -555,6 +572,8 @@ void HostCallback(void *data, int status, int timeouts,
struct hostent *hostent); struct hostent *hostent);
void SearchCallback(void *data, int status, int timeouts, unsigned char *abuf, void SearchCallback(void *data, int status, int timeouts, unsigned char *abuf,
int alen); 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, void NameInfoCallback(void *data, int status, int timeouts, char *node,
char *service); char *service);
void AddrInfoCallback(void *data, int status, int timeouts, void AddrInfoCallback(void *data, int status, int timeouts,

Loading…
Cancel
Save