From abf3e40902abe9a3b32aba0d1691b209b4d32e66 Mon Sep 17 00:00:00 2001 From: Joshua Colp Date: Wed, 25 Mar 2015 12:32:26 +0000 Subject: dns: Add core DNS API + unit tests and res_resolver_unbound module + unit tests. This change adds an abstracted core DNS API which resembles the API described here[1]. The API provides a pluggable mechanism for resolvers and also a consistent view for records. Both synchronous and asynchronous queries are supported. This change also adds a res_resolver_unbound module which uses the libunbound library to provide resolution. Unit tests have also been written for all of the above to confirm the API and functionality. ASTERISK-24834 #close Reported by: Matt Jordan ASTERISK-24836 #close Reported by: Matt Jordan Review: https://reviewboard.asterisk.org/r/4474/ Review: https://reviewboard.asterisk.org/r/4512/ [1] https://wiki.asterisk.org/wiki/display/AST/Asterisk+DNS+API git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@433370 65c4cc65-6c06-0410-ace0-fbb531ad65f3 --- build_tools/menuselect-deps.in | 1 + configs/samples/resolver_unbound.conf.sample | 24 + configure | 145 ++- configure.ac | 3 + include/asterisk/autoconfig.h.in | 3 + include/asterisk/dns_core.h | 267 +++++ include/asterisk/dns_internal.h | 145 +++ include/asterisk/dns_naptr.h | 89 ++ include/asterisk/dns_query_set.h | 136 +++ include/asterisk/dns_recurring.h | 78 ++ include/asterisk/dns_resolver.h | 142 +++ include/asterisk/dns_srv.h | 71 ++ include/asterisk/dns_tlsa.h | 72 ++ main/dns_core.c | 566 +++++++++++ main/dns_naptr.c | 65 ++ main/dns_query_set.c | 93 ++ main/dns_recurring.c | 149 +++ main/dns_srv.c | 55 ++ main/dns_tlsa.c | 55 ++ makeopts.in | 3 + res/res_resolver_unbound.c | 1271 ++++++++++++++++++++++++ tests/test_dns.c | 1355 ++++++++++++++++++++++++++ tests/test_dns_recurring.c | 648 ++++++++++++ 23 files changed, 5435 insertions(+), 1 deletion(-) create mode 100644 configs/samples/resolver_unbound.conf.sample create mode 100644 include/asterisk/dns_core.h create mode 100644 include/asterisk/dns_internal.h create mode 100644 include/asterisk/dns_naptr.h create mode 100644 include/asterisk/dns_query_set.h create mode 100644 include/asterisk/dns_recurring.h create mode 100644 include/asterisk/dns_resolver.h create mode 100644 include/asterisk/dns_srv.h create mode 100644 include/asterisk/dns_tlsa.h create mode 100644 main/dns_core.c create mode 100644 main/dns_naptr.c create mode 100644 main/dns_query_set.c create mode 100644 main/dns_recurring.c create mode 100644 main/dns_srv.c create mode 100644 main/dns_tlsa.c create mode 100644 res/res_resolver_unbound.c create mode 100644 tests/test_dns.c create mode 100644 tests/test_dns_recurring.c diff --git a/build_tools/menuselect-deps.in b/build_tools/menuselect-deps.in index a77530b78..edeb84877 100644 --- a/build_tools/menuselect-deps.in +++ b/build_tools/menuselect-deps.in @@ -65,6 +65,7 @@ OPENSSL=@PBX_OPENSSL@ SUPPSERV=@PBX_SUPPSERV@ SYSLOG=@PBX_SYSLOG@ TONEZONE=@PBX_TONEZONE@ +UNBOUND=@PBX_UNBOUND@ UNIXODBC=@PBX_UNIXODBC@ VORBIS=@PBX_VORBIS@ VPB=@PBX_VPB@ diff --git a/configs/samples/resolver_unbound.conf.sample b/configs/samples/resolver_unbound.conf.sample new file mode 100644 index 000000000..97361525e --- /dev/null +++ b/configs/samples/resolver_unbound.conf.sample @@ -0,0 +1,24 @@ +; Unbound DNS Resolver Configuration +; +; This file serves as a reference for the configurable options within the +; unbound DNS resolver. + +[general] +;hosts = /etc/hosts ; Full path to a hosts file which contains a mapping of +; ; hostnames to addresses. If "system" is specified then +; ; the system specific hosts file will be used. (default: system) +;resolv = /etc/resolv.conf ; Full path to a resolv.conf which contains the nameservers +; ; to use for resolution. If "system" is specified then the +; ; system specific resolv.conf file will be used. (default: system) +;nameserver = 127.0.0.1 ; An explicit nameserver to use for queries. If this option +; ; is specified multiple times the first configured one will +; ; be treated as the primary with each subsequent one being +; ; a backup. If the resolv options is also specified the +; ; nameservers from it will be tried after all nameserver +; ; options. +;debug = 99 ; The debug level to run the unbound resolver at. While +; ; there is no explicit range the higher the number the more +; ; debug is output. +;ta_file = /etc/asterisk/dnssec_keys ; Full path to a trusted anchors key file. These keys are +; ; used to verify DNSSEC signed results. + diff --git a/configure b/configure index be5062007..9a2630dc1 100755 --- a/configure +++ b/configure @@ -1,5 +1,5 @@ #! /bin/sh -# From configure.ac Revision: 432282 . +# From configure.ac Revision: 432815 . # Guess values for system-dependent variables and create Makefiles. # Generated by GNU Autoconf 2.69 for asterisk trunk. # @@ -731,6 +731,10 @@ PBX_UNIXODBC UNIXODBC_DIR UNIXODBC_INCLUDE UNIXODBC_LIB +PBX_UNBOUND +UNBOUND_DIR +UNBOUND_INCLUDE +UNBOUND_LIB PBX_TONEZONE TONEZONE_DIR TONEZONE_INCLUDE @@ -1357,6 +1361,7 @@ with_termcap with_timerfd with_tinfo with_tonezone +with_unbound with_unixodbc with_vorbis with_vpb @@ -2097,6 +2102,7 @@ Optional Packages: --with-timerfd=PATH use timerfd files in PATH --with-tinfo=PATH use Term Info files in PATH --with-tonezone=PATH use tonezone files in PATH + --with-unbound=PATH use unbound files in PATH --with-unixodbc=PATH use unixODBC files in PATH --with-vorbis=PATH use Vorbis files in PATH --with-vpb=PATH use Voicetronix API files in PATH @@ -11299,6 +11305,38 @@ fi + UNBOUND_DESCRIP="unbound" + UNBOUND_OPTION="unbound" + PBX_UNBOUND=0 + +# Check whether --with-unbound was given. +if test "${with_unbound+set}" = set; then : + withval=$with_unbound; + case ${withval} in + n|no) + USE_UNBOUND=no + # -1 is a magic value used by menuselect to know that the package + # was disabled, other than 'not found' + PBX_UNBOUND=-1 + ;; + y|ye|yes) + ac_mandatory_list="${ac_mandatory_list} UNBOUND" + ;; + *) + UNBOUND_DIR="${withval}" + ac_mandatory_list="${ac_mandatory_list} UNBOUND" + ;; + esac + +fi + + + + + + + + UNIXODBC_DESCRIP="unixODBC" UNIXODBC_OPTION="unixodbc" PBX_UNIXODBC=0 @@ -22704,6 +22742,111 @@ fi +if test "x${PBX_UNBOUND}" != "x1" -a "${USE_UNBOUND}" != "no"; then + pbxlibdir="" + # if --with-UNBOUND=DIR has been specified, use it. + if test "x${UNBOUND_DIR}" != "x"; then + if test -d ${UNBOUND_DIR}/lib; then + pbxlibdir="-L${UNBOUND_DIR}/lib" + else + pbxlibdir="-L${UNBOUND_DIR}" + fi + fi + pbxfuncname="ub_ctx_create" + if test "x${pbxfuncname}" = "x" ; then # empty lib, assume only headers + AST_UNBOUND_FOUND=yes + else + ast_ext_lib_check_save_CFLAGS="${CFLAGS}" + CFLAGS="${CFLAGS} " + as_ac_Lib=`$as_echo "ac_cv_lib_unbound_${pbxfuncname}" | $as_tr_sh` +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for ${pbxfuncname} in -lunbound" >&5 +$as_echo_n "checking for ${pbxfuncname} in -lunbound... " >&6; } +if eval \${$as_ac_Lib+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_check_lib_save_LIBS=$LIBS +LIBS="-lunbound ${pbxlibdir} $LIBS" +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. */ +#ifdef __cplusplus +extern "C" +#endif +char ${pbxfuncname} (); +int +main () +{ +return ${pbxfuncname} (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + eval "$as_ac_Lib=yes" +else + eval "$as_ac_Lib=no" +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +eval ac_res=\$$as_ac_Lib + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +$as_echo "$ac_res" >&6; } +if eval test \"x\$"$as_ac_Lib"\" = x"yes"; then : + AST_UNBOUND_FOUND=yes +else + AST_UNBOUND_FOUND=no +fi + + CFLAGS="${ast_ext_lib_check_save_CFLAGS}" + fi + + # now check for the header. + if test "${AST_UNBOUND_FOUND}" = "yes"; then + UNBOUND_LIB="${pbxlibdir} -lunbound " + # if --with-UNBOUND=DIR has been specified, use it. + if test "x${UNBOUND_DIR}" != "x"; then + UNBOUND_INCLUDE="-I${UNBOUND_DIR}/include" + fi + UNBOUND_INCLUDE="${UNBOUND_INCLUDE} " + if test "xunbound.h" = "x" ; then # no header, assume found + UNBOUND_HEADER_FOUND="1" + else # check for the header + ast_ext_lib_check_saved_CPPFLAGS="${CPPFLAGS}" + CPPFLAGS="${CPPFLAGS} ${UNBOUND_INCLUDE}" + ac_fn_c_check_header_mongrel "$LINENO" "unbound.h" "ac_cv_header_unbound_h" "$ac_includes_default" +if test "x$ac_cv_header_unbound_h" = xyes; then : + UNBOUND_HEADER_FOUND=1 +else + UNBOUND_HEADER_FOUND=0 +fi + + + CPPFLAGS="${ast_ext_lib_check_saved_CPPFLAGS}" + fi + if test "x${UNBOUND_HEADER_FOUND}" = "x0" ; then + UNBOUND_LIB="" + UNBOUND_INCLUDE="" + else + if test "x${pbxfuncname}" = "x" ; then # only checking headers -> no library + UNBOUND_LIB="" + fi + PBX_UNBOUND=1 + cat >>confdefs.h <<_ACEOF +#define HAVE_UNBOUND 1 +_ACEOF + + fi + fi +fi + + + + if test "x${PBX_UNIXODBC}" != "x1" -a "${USE_UNIXODBC}" != "no"; then pbxlibdir="" # if --with-UNIXODBC=DIR has been specified, use it. diff --git a/configure.ac b/configure.ac index edb4322b1..afbb5afc1 100644 --- a/configure.ac +++ b/configure.ac @@ -507,6 +507,7 @@ AST_EXT_LIB_SETUP([TERMCAP], [Termcap], [termcap]) AST_EXT_LIB_SETUP([TIMERFD], [timerfd], [timerfd]) AST_EXT_LIB_SETUP([TINFO], [Term Info], [tinfo]) AST_EXT_LIB_SETUP([TONEZONE], [tonezone], [tonezone]) +AST_EXT_LIB_SETUP([UNBOUND], [unbound], [unbound]) AST_EXT_LIB_SETUP([UNIXODBC], [unixODBC], [unixodbc]) AST_EXT_LIB_SETUP([VORBIS], [Vorbis], [vorbis]) AST_EXT_LIB_SETUP([VPB], [Voicetronix API], [vpb]) @@ -2032,6 +2033,8 @@ AST_EXT_TOOL_CHECK([NETSNMP], [net-snmp-config], , [--agent-libs], AST_EXT_LIB_CHECK([NEWT], [newt], [newtBell], [newt.h]) +AST_EXT_LIB_CHECK([UNBOUND], [unbound], [ub_ctx_create], [unbound.h], []) + AST_EXT_LIB_CHECK([UNIXODBC], [odbc], [SQLConnect], [sql.h], []) AST_EXT_LIB_CHECK([OGG], [ogg], [ogg_sync_init], []) diff --git a/include/asterisk/autoconfig.h.in b/include/asterisk/autoconfig.h.in index a8293a2fe..8c7ead499 100644 --- a/include/asterisk/autoconfig.h.in +++ b/include/asterisk/autoconfig.h.in @@ -1065,6 +1065,9 @@ /* Define to 1 if you have the `truncl' function. */ #undef HAVE_TRUNCL +/* Define to 1 if you have the unbound library. */ +#undef HAVE_UNBOUND + /* Define to 1 if you have the header file. */ #undef HAVE_UNISTD_H diff --git a/include/asterisk/dns_core.h b/include/asterisk/dns_core.h new file mode 100644 index 000000000..1f67bb803 --- /dev/null +++ b/include/asterisk/dns_core.h @@ -0,0 +1,267 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Digium, Inc. + * + * Joshua Colp + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * \brief Core DNS API + * \author Joshua Colp + */ + +#ifndef _ASTERISK_DNS_CORE_H +#define _ASTERISK_DNS_CORE_H + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/*! \brief Opaque structure for an active DNS query */ +struct ast_dns_query_active; + +/*! \brief Opaque structure for a DNS query */ +struct ast_dns_query; + +/*! + * \brief Get the name queried in a DNS query + * + * \param query The DNS query + * + * \return the name queried + */ +const char *ast_dns_query_get_name(const struct ast_dns_query *query); + +/*! + * \brief Get the record resource type of a DNS query + * + * \param query The DNS query + * + * \return the record resource type + */ +int ast_dns_query_get_rr_type(const struct ast_dns_query *query); + +/*! + * \brief Get the record resource class of a DNS query + * + * \param query The DNS query + * + * \return the record resource class + */ +int ast_dns_query_get_rr_class(const struct ast_dns_query *query); + +/*! + * \brief Get the user specific data of a DNS query + * + * \param query The DNS query + * + * \return the user specific data + * + * \note The reference count of the data is NOT incremented on return + */ +void *ast_dns_query_get_data(const struct ast_dns_query *query); + +/*! \brief Opaque structure for a DNS query result, guaranteed to be immutable */ +struct ast_dns_result; + +/*! + * \brief Get the result information for a DNS query + * + * \param query The DNS query + * + * \return the DNS result information + * + * \note The result is NOT ao2 allocated + */ +struct ast_dns_result *ast_dns_query_get_result(const struct ast_dns_query *query); + +/*! + * \brief Get whether the result is secure or not + * + * \param result The DNS result + * + * \return whether the result is secure or not + */ +unsigned int ast_dns_result_get_secure(const struct ast_dns_result *result); + +/*! + * \brief Get whether the result is bogus or not + * + * \param result The DNS result + * + * \return whether the result is bogus or not + */ +unsigned int ast_dns_result_get_bogus(const struct ast_dns_result *result); + +/*! + * \brief Get the error rcode of a DN result + * + * \param query The DNS result + * + * \return the DNS rcode + */ +unsigned int ast_dns_result_get_rcode(const struct ast_dns_result *result); + +/*! + * \brief Get the canonical name of the result + * + * \param result The DNS result + * + * \return the canonical name + */ +const char *ast_dns_result_get_canonical(const struct ast_dns_result *result); + +/*! + * \brief Get the first record of a DNS Result + * + * \param result The DNS result + * + * \return first DNS record + */ +const struct ast_dns_record *ast_dns_result_get_records(const struct ast_dns_result *result); + +/*! + * \brief Get the raw DNS answer from a DNS result + * + * \param result The DNS result + * + * \return The DNS result + */ +const char *ast_dns_result_get_answer(const struct ast_dns_result *result); + +/*! + * \brief Retrieve the lowest TTL from a result + * + * \param result The DNS result + * + * \return the lowest TTL + * + * \note If no records exist this function will return a TTL of 0 + */ +int ast_dns_result_get_lowest_ttl(const struct ast_dns_result *result); + +/*! + * \brief Free the DNS result information + * + * \param result The DNS result + */ +void ast_dns_result_free(struct ast_dns_result *result); + +/*! \brief Opaque structure for a DNS record */ +struct ast_dns_record; + +/*! + * \brief Callback invoked when a query completes + * + * \param query The DNS query that was invoked + */ +typedef void (*ast_dns_resolve_callback)(const struct ast_dns_query *query); + +/*! + * \brief Get the resource record type of a DNS record + * + * \param record The DNS record + * + * \return the resource record type + */ +int ast_dns_record_get_rr_type(const struct ast_dns_record *record); + +/*! + * \brief Get the resource record class of a DNS record + * + * \param record The DNS record + * + * \return the resource record class + */ +int ast_dns_record_get_rr_class(const struct ast_dns_record *record); + +/*! + * \brief Get the TTL of a DNS record + * + * \param record The DNS record + * + * \return the TTL + */ +int ast_dns_record_get_ttl(const struct ast_dns_record *record); + +/*! + * \brief Retrieve the raw DNS record + * + * \param record The DNS record + * + * \return the raw DNS record + */ +const char *ast_dns_record_get_data(const struct ast_dns_record *record); + +/*! + * \brief Get the next DNS record + * + * \param record The current DNS record + * + * \return the next DNS record + */ +const struct ast_dns_record *ast_dns_record_get_next(const struct ast_dns_record *record); + +/*! + * \brief Asynchronously resolve a DNS query + * + * \param name The name of what to resolve + * \param rr_type Resource record type + * \param rr_class Resource record class + * \param callback The callback to invoke upon completion + * \param data User data to make available on the query + * + * \retval non-NULL success - query has been sent for resolution + * \retval NULL failure + * + * \note The result passed to the callback does not need to be freed + * + * \note The user data MUST be an ao2 object + * + * \note This function increments the reference count of the user data, it does NOT steal + * + * \note The active query must be released upon completion or cancellation using ao2_ref + */ +struct ast_dns_query_active *ast_dns_resolve_async(const char *name, int rr_type, int rr_class, ast_dns_resolve_callback callback, void *data); + +/*! + * \brief Cancel an asynchronous DNS resolution + * + * \param active The active DNS query returned from ast_dns_resolve_async + * + * \retval 0 success + * \retval -1 failure + * + * \note If successfully cancelled the callback will not be invoked + */ +int ast_dns_resolve_cancel(struct ast_dns_query_active *active); + +/*! + * \brief Synchronously resolve a DNS query + * + * \param name The name of what to resolve + * \param rr_type Resource record type + * \param rr_class Resource record class + * \param result A pointer to hold the DNS result + * + * \retval 0 success - query was completed and result is available + * \retval -1 failure + */ +int ast_dns_resolve(const char *name, int rr_type, int rr_class, struct ast_dns_result **result); + +#if defined(__cplusplus) || defined(c_plusplus) +} +#endif + +#endif /* _ASTERISK_DNS_CORE_H */ diff --git a/include/asterisk/dns_internal.h b/include/asterisk/dns_internal.h new file mode 100644 index 000000000..48fa26471 --- /dev/null +++ b/include/asterisk/dns_internal.h @@ -0,0 +1,145 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Digium, Inc. + * + * Joshua Colp + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief Internal DNS structure definitions + * + * \author Joshua Colp + */ + +/*! \brief Generic DNS record information */ +struct ast_dns_record { + /*! \brief Resource record type */ + int rr_type; + /*! \brief Resource record class */ + int rr_class; + /*! \brief Time-to-live of the record */ + int ttl; + /*! \brief The size of the raw DNS record */ + size_t data_len; + /*! \brief Linked list information */ + AST_LIST_ENTRY(ast_dns_record) list; + /*! \brief The raw DNS record */ + char data[0]; +}; + +/*! \brief An SRV record */ +struct ast_dns_srv_record { + /*! \brief Generic DNS record information */ + struct ast_dns_record generic; + /*! \brief The hostname in the SRV record */ + const char *host; + /*! \brief The priority of the SRV record */ + unsigned short priority; + /*! \brief The weight of the SRV record */ + unsigned short weight; + /*! \brief The port in the SRV record */ + unsigned short port; +}; + +/*! \brief A NAPTR record */ +struct ast_dns_naptr_record { + /*! \brief Generic DNS record information */ + struct ast_dns_record generic; + /*! \brief The flags from the NAPTR record */ + const char *flags; + /*! \brief The service from the NAPTR record */ + const char *service; + /*! \brief The regular expression from the NAPTR record */ + const char *regexp; + /*! \brief The replacement from the NAPTR record */ + const char *replacement; + /*! \brief The order for the NAPTR record */ + unsigned short order; + /*! \brief The preference of the NAPTR record */ + unsigned short preference; +}; + +/*! \brief The result of a DNS query */ +struct ast_dns_result { + /*! \brief Whether the result is secure */ + unsigned int secure; + /*! \brief Whether the result is bogus */ + unsigned int bogus; + /*! \brief Optional rcode, set if an error occurred */ + unsigned int rcode; + /*! \brief Records returned */ + AST_LIST_HEAD_NOLOCK(, ast_dns_record) records; + /*! \brief The canonical name */ + const char *canonical; + /*! \brief The raw DNS answer */ + const char *answer; + /*! \brief Buffer for dynamic data */ + char buf[0]; +}; + +/*! \brief A DNS query */ +struct ast_dns_query { + /*! \brief Callback to invoke upon completion */ + ast_dns_resolve_callback callback; + /*! \brief User-specific data */ + void *user_data; + /*! \brief The resolver in use for this query */ + struct ast_dns_resolver *resolver; + /*! \brief Resolver-specific data */ + void *resolver_data; + /*! \brief Result of the DNS query */ + struct ast_dns_result *result; + /*! \brief Resource record type */ + int rr_type; + /*! \brief Resource record class */ + int rr_class; + /*! \brief The name of what is being resolved */ + char name[0]; +}; + +/*! \brief A recurring DNS query */ +struct ast_dns_query_recurring { + /*! \brief Callback to invoke upon completion */ + ast_dns_resolve_callback callback; + /*! \brief User-specific data */ + void *user_data; + /*! \brief Current active query */ + struct ast_dns_query_active *active; + /*! \brief The recurring query has been cancelled */ + unsigned int cancelled; + /*! \brief Scheduled timer for next resolution */ + int timer; + /*! \brief Resource record type */ + int rr_type; + /*! \brief Resource record class */ + int rr_class; + /*! \brief The name of what is being resolved */ + char name[0]; +}; + +/*! \brief An active DNS query */ +struct ast_dns_query_active { + /*! \brief The underlying DNS query */ + struct ast_dns_query *query; +}; + +struct ast_sched_context; + +/*! + * \brief Retrieve the DNS scheduler context + * + * \return scheduler context + */ +struct ast_sched_context *ast_dns_get_sched(void); diff --git a/include/asterisk/dns_naptr.h b/include/asterisk/dns_naptr.h new file mode 100644 index 000000000..5d7541aa5 --- /dev/null +++ b/include/asterisk/dns_naptr.h @@ -0,0 +1,89 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Digium, Inc. + * + * Joshua Colp + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * \brief DNS NAPTR Record Parsing API + * \author Joshua Colp + */ + +#ifndef _ASTERISK_DNS_NAPTR_H +#define _ASTERISK_DNS_NAPTR_H + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/*! + * \brief Get the flags from a NAPTR record + * + * \param record The DNS record + * + * \return the flags + */ +const char *ast_dns_naptr_get_flags(const struct ast_dns_record *record); + +/*! + * \brief Get the service from a NAPTR record + * + * \param record The DNS record + * + * \return the service + */ +const char *ast_dns_naptr_get_service(const struct ast_dns_record *record); + +/*! + * \brief Get the regular expression from a NAPTR record + * + * \param record The DNS record + * + * \return the regular expression + */ +const char *ast_dns_naptr_get_regexp(const struct ast_dns_record *record); + +/*! + * \brief Get the replacement value from a NAPTR record + * + * \param record The DNS record + * + * \return the replacement value + */ +const char *ast_dns_naptr_get_replacement(const struct ast_dns_record *record); + +/*! + * \brief Get the order from a NAPTR record + * + * \param record The DNS record + * + * \return the order + */ +unsigned short ast_dns_naptr_get_order(const struct ast_dns_record *record); + +/*! + * \brief Get the preference from a NAPTR record + * + * \param record The DNS record + * + * \return the preference + */ +unsigned short ast_dns_naptr_get_preference(const struct ast_dns_record *record); + +#if defined(__cplusplus) || defined(c_plusplus) +} +#endif + +#endif /* _ASTERISK_DNS_NAPTR_H */ diff --git a/include/asterisk/dns_query_set.h b/include/asterisk/dns_query_set.h new file mode 100644 index 000000000..c89fdfde7 --- /dev/null +++ b/include/asterisk/dns_query_set.h @@ -0,0 +1,136 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Digium, Inc. + * + * Joshua Colp + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * \brief DNS Query Set API + * \author Joshua Colp + */ + +#ifndef _ASTERISK_DNS_QUERY_SET_H +#define _ASTERISK_DNS_QUERY_SET_H + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/*! \brief Opaque structure for a set of DNS queries */ +struct ast_dns_query_set; + +/*! + * \brief Callback invoked when a query set completes + * + * \param query_set The DNS query set that was invoked + */ +typedef void (*ast_dns_query_set_callback)(const struct ast_dns_query_set *query_set); + +/*! + * \brief Create a query set to hold queries + * + * \retval non-NULL success + * \retval NULL failure + */ +struct ast_dns_query_set *ast_dns_query_set_create(void); + +/*! + * \brief Add a query to a query set + * + * \param query_set A DNS query set + * \param name The name of what to resolve + * \param rr_type Resource record type + * \param rr_class Resource record class + * + * \retval 0 success + * \retval -1 failure + */ +int ast_dns_query_set_add(struct ast_dns_query_set *query_set, const char *name, int rr_type, int rr_class); + +/*! + * \brief Retrieve the number of queries in a query set + * + * \param query_set A DNS query set + * + * \return the number of queries + */ +size_t ast_dns_query_set_num_queries(const struct ast_dns_query_set *query_set); + +/*! + * \brief Retrieve a query from a query set + * + * \param query_set A DNS query set + * \param index The index of the query to retrieve + * + * \retval non-NULL success + * \retval NULL failure + */ +struct ast_dns_query *ast_dns_query_set_get(const struct ast_dns_query_set *query_set, unsigned int index); + +/*! + * \brief Retrieve user specific data from a query set + * + * \param query_set A DNS query set + * + * \return user specific data + */ +void *ast_dns_query_set_get_data(const struct ast_dns_query_set *query_set); + +/*! + * \brief Asynchronously resolve queries in a query set + * + * \param query_set The query set + * \param callback The callback to invoke upon completion + * \param data User data to make available on the query set + * + * \note The callback will be invoked when all queries have completed + * + * \note The user data passed in to this function must be ao2 allocated + */ +void ast_dns_query_set_resolve_async(struct ast_dns_query_set *query_set, ast_dns_query_set_callback callback, void *data); + +/*! + * \brief Synchronously resolve queries in a query set + * + * \param query_set The query set + * + * \note This function will return when all queries have been completed + */ +void ast_query_set_resolve(struct ast_dns_query_set *query_set); + +/*! + * \brief Cancel an asynchronous DNS query set resolution + * + * \param query_set The DNS query set + * + * \retval 0 success + * \retval -1 failure + * + * \note If successfully cancelled the callback will not be invoked + */ +int ast_dns_query_set_resolve_cancel(struct ast_dns_query_set *query_set); + +/*! + * \brief Free a query set + * + * \param query_set A DNS query set + */ +void ast_dns_query_set_free(struct ast_dns_query_set *query_set); + +#if defined(__cplusplus) || defined(c_plusplus) +} +#endif + +#endif /* _ASTERISK_DNS_QUERY_SET_H */ diff --git a/include/asterisk/dns_recurring.h b/include/asterisk/dns_recurring.h new file mode 100644 index 000000000..c5c1af1ac --- /dev/null +++ b/include/asterisk/dns_recurring.h @@ -0,0 +1,78 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Digium, Inc. + * + * Joshua Colp + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * \brief DNS Recurring Resolution API + * \author Joshua Colp + */ + +#ifndef _ASTERISK_DNS_RECURRING_H +#define _ASTERISK_DNS_RECURRING_H + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/*! \brief Opaque structure for a recurring DNS query */ +struct ast_dns_query_recurring; + +/*! + * \brief Asynchronously resolve a DNS query, and continue resolving it according to the lowest TTL available + * + * \param name The name of what to resolve + * \param rr_type Resource record type + * \param rr_class Resource record class + * \param callback The callback to invoke upon completion + * \param data User data to make available on the query + * + * \retval non-NULL success - query has been sent for resolution + * \retval NULL failure + * + * \note The user data passed in to this function must be ao2 allocated + * + * \note This query will continue to happen according to the lowest TTL unless cancelled using ast_dns_resolve_recurring_cancel + * + * \note It is NOT possible for the callback to be invoked concurrently for the query multiple times + * + * \note The query will occur when the TTL expires, not before. This means that there is a period of time where the previous + * information can be considered stale. + * + * \note If the TTL is determined to be 0 (the record specifies 0, or no records exist) this will cease doing a recurring query. + * It is the responsibility of the caller to resume querying at an interval they determine. + */ +struct ast_dns_query_recurring *ast_dns_resolve_recurring(const char *name, int rr_type, int rr_class, ast_dns_resolve_callback callback, void *data); + +/*! + * \brief Cancel an asynchronous recurring DNS resolution + * + * \param query The DNS query returned from ast_dns_resolve_recurring + * + * \retval 0 success - any active query has been cancelled and the query will no longer occur + * \retval -1 failure - an active query was in progress and could not be cancelled + * + * \note If successfully cancelled the callback will not be invoked + * + * \note This function does NOT drop your reference to the recurring query, this should be dropped using ao2_ref + */ +int ast_dns_resolve_recurring_cancel(struct ast_dns_query_recurring *recurring); + +#if defined(__cplusplus) || defined(c_plusplus) +} +#endif + +#endif /* _ASTERISK_DNS_RECURRING_H */ diff --git a/include/asterisk/dns_resolver.h b/include/asterisk/dns_resolver.h new file mode 100644 index 000000000..819ce7a59 --- /dev/null +++ b/include/asterisk/dns_resolver.h @@ -0,0 +1,142 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Digium, Inc. + * + * Joshua Colp + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * \brief DNS Resolver API + * \author Joshua Colp + */ + +#ifndef _ASTERISK_DNS_RESOLVER_H +#define _ASTERISK_DNS_RESOLVER_H + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/*! \brief DNS resolver implementation */ +struct ast_dns_resolver { + /*! \brief The name of the resolver implementation */ + const char *name; + + /*! \brief Priority for this resolver if multiple exist, lower being higher priority */ + unsigned int priority; + + /*! + * \brief Perform resolution of a DNS query + * + * \note The reference count of the query should be increased and released + * upon the query completing or being successfully cancelled + */ + int (*resolve)(struct ast_dns_query *query); + + /*! \brief Cancel resolution of a DNS query */ + int (*cancel)(struct ast_dns_query *query); + + /*! \brief Linked list information */ + AST_RWLIST_ENTRY(ast_dns_resolver) next; +}; + +/*! + * \brief Set resolver specific data on a query + * + * \param query The DNS query + * \param data The resolver specific data + * + * \note The resolver data MUST be an ao2 object + * + * \note This function increments the reference count of the resolver data, it does NOT steal + * + * \note Once resolver specific data has been set it can not be changed + * + * \retval 0 success + * \retval -1 failure, resolver data is already set + */ +int ast_dns_resolver_set_data(struct ast_dns_query *query, void *data); + +/*! + * \brief Retrieve resolver specific data + * + * \param query The DNS query + * + * \return the resolver specific data + * + * \note The reference count of the resolver data is NOT incremented on return + */ +void *ast_dns_resolver_get_data(const struct ast_dns_query *query); + +/*! + * \brief Set result information for a DNS query + * + * \param query The DNS query + * \param result Whether the result is secured or not + * \param bogus Whether the result is bogus or not + * \param rcode Optional response code + * \param canonical The canonical name + * \param answer The raw DNS answer + * \param answer_size The size of the raw DNS answer + * + * \retval 0 success + * \retval -1 failure + */ +int ast_dns_resolver_set_result(struct ast_dns_query *query, unsigned int secure, unsigned int bogus, + unsigned int rcode, const char *canonical, const char *answer, size_t answer_size); + +/*! + * \brief Add a DNS record to the result of a DNS query + * + * \param query The DNS query + * \param rr_type Resource record type + * \param rr_class Resource record class + * \param ttl TTL of the record + * \param data The raw DNS record + * \param size The size of the raw DNS record + * + * \retval 0 success + * \retval -1 failure + */ +int ast_dns_resolver_add_record(struct ast_dns_query *query, int rr_type, int rr_class, int ttl, const char *data, const size_t size); + +/*! + * \brief Mark a DNS query as having been completed + * + * \param query The DNS query + */ +void ast_dns_resolver_completed(struct ast_dns_query *query); + +/*! + * \brief Register a DNS resolver + * + * \param resolver A DNS resolver implementation + * + * \retval 0 success + * \retval -1 failure + */ +int ast_dns_resolver_register(struct ast_dns_resolver *resolver); + +/*! + * \brief Unregister a DNS resolver + * + * \param resolver A DNS resolver implementation + */ +void ast_dns_resolver_unregister(struct ast_dns_resolver *resolver); + +#if defined(__cplusplus) || defined(c_plusplus) +} +#endif + +#endif /* _ASTERISK_DNS_RESOLVER_H */ diff --git a/include/asterisk/dns_srv.h b/include/asterisk/dns_srv.h new file mode 100644 index 000000000..ef25a908b --- /dev/null +++ b/include/asterisk/dns_srv.h @@ -0,0 +1,71 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Digium, Inc. + * + * Joshua Colp + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * \brief DNS SRV Record Parsing API + * \author Joshua Colp + */ + +#ifndef _ASTERISK_DNS_SRV_H +#define _ASTERISK_DNS_SRV_H + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/*! + * \brief Get the hostname from an SRV record + * + * \param record The DNS record + * + * \return the hostname + */ +const char *ast_dns_srv_get_host(const struct ast_dns_record *record); + +/*! + * \brief Get the priority from an SRV record + * + * \param record The DNS record + * + * \return the priority + */ +unsigned short ast_dns_srv_get_priority(const struct ast_dns_record *record); + +/*! + * \brief Get the weight from an SRV record + * + * \param record The DNS record + * + * \return the weight + */ +unsigned short ast_dns_srv_get_weight(const struct ast_dns_record *record); + +/*! + * \brief Get the port from an SRV record + * + * \param record The DNS record + * + * \return the port + */ +unsigned short ast_dns_srv_get_port(const struct ast_dns_record *record); + +#if defined(__cplusplus) || defined(c_plusplus) +} +#endif + +#endif /* _ASTERISK_DNS_SRV_H */ diff --git a/include/asterisk/dns_tlsa.h b/include/asterisk/dns_tlsa.h new file mode 100644 index 000000000..736c85ee2 --- /dev/null +++ b/include/asterisk/dns_tlsa.h @@ -0,0 +1,72 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Digium, Inc. + * + * Joshua Colp + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * \brief DNS TLSA Record Parsing API + * \author Joshua Colp + */ + +#ifndef _ASTERISK_DNS_TLSA_H +#define _ASTERISK_DNS_TLSA_H + +#if defined(__cplusplus) || defined(c_plusplus) +extern "C" { +#endif + +/*! + * \brief Get the certificate usage field from a TLSA record + * + * \param record The DNS record + * + * \return the certificate usage field + */ + +unsigned int ast_dns_tlsa_get_usage(const struct ast_dns_record *record); + +/*! + * \brief Get the selector field from a TLSA record + * + * \param record The DNS record + * + * \return the selector field + */ +unsigned int ast_dns_tlsa_get_selector(const struct ast_dns_record *record); + +/*! + * \brief Get the matching type field from a TLSA record + * + * \param record The DNS record + * + * \return the matching type field + */ +unsigned int ast_dns_tlsa_get_matching_type(const struct ast_dns_record *record); + +/*! + * \brief Get the certificate association data from a TLSA record + * + * \param record The DNS record + * + * \return the certificate association data + */ +const char *ast_dns_tlsa_get_association_data(const struct ast_dns_record *record); + +#if defined(__cplusplus) || defined(c_plusplus) +} +#endif + +#endif /* _ASTERISK_DNS_TLSA_H */ diff --git a/main/dns_core.c b/main/dns_core.c new file mode 100644 index 000000000..394eaa514 --- /dev/null +++ b/main/dns_core.c @@ -0,0 +1,566 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Digium, Inc. + * + * Joshua Colp + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief Core DNS Functionality + * + * \author Joshua Colp + */ + +/*** MODULEINFO + core + ***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include "asterisk/linkedlists.h" +#include "asterisk/vector.h" +#include "asterisk/astobj2.h" +#include "asterisk/strings.h" +#include "asterisk/sched.h" +#include "asterisk/dns_core.h" +#include "asterisk/dns_srv.h" +#include "asterisk/dns_tlsa.h" +#include "asterisk/dns_recurring.h" +#include "asterisk/dns_resolver.h" +#include "asterisk/dns_internal.h" + +#include + +AST_RWLIST_HEAD_STATIC(resolvers, ast_dns_resolver); + +static struct ast_sched_context *sched; + +struct ast_sched_context *ast_dns_get_sched(void) +{ + return sched; +} + +const char *ast_dns_query_get_name(const struct ast_dns_query *query) +{ + return query->name; +} + +int ast_dns_query_get_rr_type(const struct ast_dns_query *query) +{ + return query->rr_type; +} + +int ast_dns_query_get_rr_class(const struct ast_dns_query *query) +{ + return query->rr_class; +} + +void *ast_dns_query_get_data(const struct ast_dns_query *query) +{ + return query->user_data; +} + +struct ast_dns_result *ast_dns_query_get_result(const struct ast_dns_query *query) +{ + return query->result; +} + +unsigned int ast_dns_result_get_secure(const struct ast_dns_result *result) +{ + return result->secure; +} + +unsigned int ast_dns_result_get_bogus(const struct ast_dns_result *result) +{ + return result->bogus; +} + +unsigned int ast_dns_result_get_rcode(const struct ast_dns_result *result) +{ + return result->rcode; +} + +const char *ast_dns_result_get_canonical(const struct ast_dns_result *result) +{ + return result->canonical; +} + +const struct ast_dns_record *ast_dns_result_get_records(const struct ast_dns_result *result) +{ + return AST_LIST_FIRST(&result->records); +} + +const char *ast_dns_result_get_answer(const struct ast_dns_result *result) +{ + return result->answer; +} + +int ast_dns_result_get_lowest_ttl(const struct ast_dns_result *result) +{ + int ttl = 0; + const struct ast_dns_record *record; + + if (ast_dns_result_get_rcode(result) == ns_r_nxdomain) { + return 0; + } + + for (record = ast_dns_result_get_records(result); record; record = ast_dns_record_get_next(record)) { + if (!ttl || (ast_dns_record_get_ttl(record) && (ast_dns_record_get_ttl(record) < ttl))) { + ttl = ast_dns_record_get_ttl(record); + } + } + + return ttl; +} + +void ast_dns_result_free(struct ast_dns_result *result) +{ + struct ast_dns_record *record; + + if (!result) { + return; + } + + while ((record = AST_LIST_REMOVE_HEAD(&result->records, list))) { + ast_free(record); + } + + ast_free(result); +} + +int ast_dns_record_get_rr_type(const struct ast_dns_record *record) +{ + return record->rr_type; +} + +int ast_dns_record_get_rr_class(const struct ast_dns_record *record) +{ + return record->rr_class; +} + +int ast_dns_record_get_ttl(const struct ast_dns_record *record) +{ + return record->ttl; +} + +const char *ast_dns_record_get_data(const struct ast_dns_record *record) +{ + return record->data; +} + +const struct ast_dns_record *ast_dns_record_get_next(const struct ast_dns_record *record) +{ + return AST_LIST_NEXT(record, list); +} + +/*! \brief Destructor for an active DNS query */ +static void dns_query_active_destroy(void *data) +{ + struct ast_dns_query_active *active = data; + + ao2_cleanup(active->query); +} + +/*! \brief \brief Destructor for a DNS query */ +static void dns_query_destroy(void *data) +{ + struct ast_dns_query *query = data; + + ao2_cleanup(query->user_data); + ao2_cleanup(query->resolver_data); + ast_dns_result_free(query->result); +} + +struct ast_dns_query_active *ast_dns_resolve_async(const char *name, int rr_type, int rr_class, ast_dns_resolve_callback callback, void *data) +{ + struct ast_dns_query_active *active; + + if (ast_strlen_zero(name)) { + ast_log(LOG_WARNING, "Could not perform asynchronous resolution, no name provided\n"); + return NULL; + } else if (rr_type > ns_t_max) { + ast_log(LOG_WARNING, "Could not perform asynchronous resolution of '%s', resource record type '%d' exceeds maximum\n", + name, rr_type); + return NULL; + } else if (rr_type < 0) { + ast_log(LOG_WARNING, "Could not perform asynchronous resolution of '%s', invalid resource record type '%d'\n", + name, rr_type); + return NULL; + } else if (rr_class > ns_c_max) { + ast_log(LOG_WARNING, "Could not perform asynchronous resolution of '%s', resource record class '%d' exceeds maximum\n", + name, rr_class); + return NULL; + } else if (rr_class < 0) { + ast_log(LOG_WARNING, "Could not perform asynchronous resolution of '%s', invalid resource class '%d'\n", + name, rr_class); + return NULL; + } else if (!callback) { + ast_log(LOG_WARNING, "Could not perform asynchronous resolution of '%s', no callback provided\n", + name); + return NULL; + } + + active = ao2_alloc_options(sizeof(*active), dns_query_active_destroy, AO2_ALLOC_OPT_LOCK_NOLOCK); + if (!active) { + return NULL; + } + + active->query = ao2_alloc_options(sizeof(*active->query) + strlen(name) + 1, dns_query_destroy, AO2_ALLOC_OPT_LOCK_NOLOCK); + if (!active->query) { + ao2_ref(active, -1); + return NULL; + } + + active->query->callback = callback; + active->query->user_data = ao2_bump(data); + active->query->rr_type = rr_type; + active->query->rr_class = rr_class; + strcpy(active->query->name, name); /* SAFE */ + + AST_RWLIST_RDLOCK(&resolvers); + active->query->resolver = AST_RWLIST_FIRST(&resolvers); + AST_RWLIST_UNLOCK(&resolvers); + + if (!active->query->resolver) { + ast_log(LOG_ERROR, "Attempted to do a DNS query for '%s' of class '%d' and type '%d' but no resolver is available\n", + name, rr_class, rr_type); + ao2_ref(active, -1); + return NULL; + } + + if (active->query->resolver->resolve(active->query)) { + ast_log(LOG_ERROR, "Resolver '%s' returned an error when resolving '%s' of class '%d' and type '%d'\n", + active->query->resolver->name, name, rr_class, rr_type); + ao2_ref(active, -1); + return NULL; + } + + return active; +} + +int ast_dns_resolve_cancel(struct ast_dns_query_active *active) +{ + return active->query->resolver->cancel(active->query); +} + +/*! \brief Structure used for signaling back for synchronous resolution completion */ +struct dns_synchronous_resolve { + /*! \brief Lock used for signaling */ + ast_mutex_t lock; + /*! \brief Condition used for signaling */ + ast_cond_t cond; + /*! \brief Whether the query has completed */ + unsigned int completed; + /*! \brief The result from the query */ + struct ast_dns_result *result; +}; + +/*! \brief Destructor for synchronous resolution structure */ +static void dns_synchronous_resolve_destroy(void *data) +{ + struct dns_synchronous_resolve *synchronous = data; + + ast_mutex_destroy(&synchronous->lock); + ast_cond_destroy(&synchronous->cond); + + /* This purposely does not unref result as it has been passed to the caller */ +} + +/*! \brief Callback used to implement synchronous resolution */ +static void dns_synchronous_resolve_callback(const struct ast_dns_query *query) +{ + struct dns_synchronous_resolve *synchronous = ast_dns_query_get_data(query); + + synchronous->result = query->result; + ((struct ast_dns_query *)query)->result = NULL; + + ast_mutex_lock(&synchronous->lock); + synchronous->completed = 1; + ast_cond_signal(&synchronous->cond); + ast_mutex_unlock(&synchronous->lock); +} + +int ast_dns_resolve(const char *name, int rr_type, int rr_class, struct ast_dns_result **result) +{ + struct dns_synchronous_resolve *synchronous; + struct ast_dns_query_active *active; + + if (ast_strlen_zero(name)) { + ast_log(LOG_WARNING, "Could not perform synchronous resolution, no name provided\n"); + return -1; + } else if (rr_type > ns_t_max) { + ast_log(LOG_WARNING, "Could not perform synchronous resolution of '%s', resource record type '%d' exceeds maximum\n", + name, rr_type); + return -1; + } else if (rr_type < 0) { + ast_log(LOG_WARNING, "Could not perform synchronous resolution of '%s', invalid resource record type '%d'\n", + name, rr_type); + return -1; + } else if (rr_class > ns_c_max) { + ast_log(LOG_WARNING, "Could not perform synchronous resolution of '%s', resource record class '%d' exceeds maximum\n", + name, rr_class); + return -1; + } else if (rr_class < 0) { + ast_log(LOG_WARNING, "Could not perform synchronous resolution of '%s', invalid resource class '%d'\n", + name, rr_class); + return -1; + } else if (!result) { + ast_log(LOG_WARNING, "Could not perform synchronous resolution of '%s', no result pointer provided for storing results\n", + name); + return -1; + } + + synchronous = ao2_alloc_options(sizeof(*synchronous), dns_synchronous_resolve_destroy, AO2_ALLOC_OPT_LOCK_NOLOCK); + if (!synchronous) { + return -1; + } + + ast_mutex_init(&synchronous->lock); + ast_cond_init(&synchronous->cond, NULL); + + active = ast_dns_resolve_async(name, rr_type, rr_class, dns_synchronous_resolve_callback, synchronous); + if (active) { + /* Wait for resolution to complete */ + ast_mutex_lock(&synchronous->lock); + while (!synchronous->completed) { + ast_cond_wait(&synchronous->cond, &synchronous->lock); + } + ast_mutex_unlock(&synchronous->lock); + ao2_ref(active, -1); + } + + *result = synchronous->result; + ao2_ref(synchronous, -1); + + return *result ? 0 : -1; +} + +int ast_dns_resolver_set_data(struct ast_dns_query *query, void *data) +{ + if (query->resolver_data) { + return -1; + } + + query->resolver_data = ao2_bump(data); + + return 0; +} + +void *ast_dns_resolver_get_data(const struct ast_dns_query *query) +{ + return query->resolver_data; +} + +int ast_dns_resolver_set_result(struct ast_dns_query *query, unsigned int secure, unsigned int bogus, + unsigned int rcode, const char *canonical, const char *answer, size_t answer_size) +{ + char *buf_ptr; + + if (secure && bogus) { + ast_debug(2, "Query '%p': Could not set result information, it can not be both secure and bogus\n", + query); + return -1; + } + + if (ast_strlen_zero(canonical)) { + ast_debug(2, "Query '%p': Could not set result information since no canonical name was provided\n", + query); + return -1; + } + + if (!answer || answer_size == 0) { + ast_debug(2, "Query '%p': Could not set result information since no DNS answer was provided\n", + query); + return -1; + } + + ast_dns_result_free(query->result); + + query->result = ast_calloc(1, sizeof(*query->result) + strlen(canonical) + 1 + answer_size); + if (!query->result) { + return -1; + } + + query->result->secure = secure; + query->result->bogus = bogus; + query->result->rcode = rcode; + + buf_ptr = query->result->buf; + strcpy(buf_ptr, canonical); /* SAFE */ + query->result->canonical = buf_ptr; + + buf_ptr += strlen(canonical) + 1; + memcpy(buf_ptr, answer, answer_size); /* SAFE */ + query->result->answer = buf_ptr; + + return 0; +} + +int ast_dns_resolver_add_record(struct ast_dns_query *query, int rr_type, int rr_class, int ttl, const char *data, const size_t size) +{ + struct ast_dns_record *record; + + if (rr_type < 0) { + ast_debug(2, "Query '%p': Could not add record, invalid resource record type '%d'\n", + query, rr_type); + return -1; + } else if (rr_type > ns_t_max) { + ast_debug(2, "Query '%p': Could not add record, resource record type '%d' exceeds maximum\n", + query, rr_type); + return -1; + } else if (rr_class < 0) { + ast_debug(2, "Query '%p': Could not add record, invalid resource record class '%d'\n", + query, rr_class); + return -1; + } else if (rr_class > ns_c_max) { + ast_debug(2, "Query '%p': Could not add record, resource record class '%d' exceeds maximum\n", + query, rr_class); + return -1; + } else if (ttl < 0) { + ast_debug(2, "Query '%p': Could not add record, invalid TTL '%d'\n", + query, ttl); + return -1; + } else if (!data || !size) { + ast_debug(2, "Query '%p': Could not add record, no data specified\n", + query); + return -1; + } else if (!query->result) { + ast_debug(2, "Query '%p': No result was set on the query, thus records can not be added\n", + query); + return -1; + } + + record = ast_calloc(1, sizeof(*record) + size); + if (!record) { + return -1; + } + + record->rr_type = rr_type; + record->rr_class = rr_class; + record->ttl = ttl; + memcpy(record->data, data, size); + record->data_len = size; + + AST_LIST_INSERT_TAIL(&query->result->records, record, list); + + return 0; +} + +void ast_dns_resolver_completed(struct ast_dns_query *query) +{ + query->callback(query); +} + +static void dns_shutdown(void) +{ + if (sched) { + ast_sched_context_destroy(sched); + sched = NULL; + } +} + +int ast_dns_resolver_register(struct ast_dns_resolver *resolver) +{ + struct ast_dns_resolver *iter; + int inserted = 0; + + if (!resolver) { + return -1; + } else if (ast_strlen_zero(resolver->name)) { + ast_log(LOG_ERROR, "Registration of DNS resolver failed as it does not have a name\n"); + return -1; + } else if (!resolver->resolve) { + ast_log(LOG_ERROR, "DNS resolver '%s' does not implement the resolve callback which is required\n", + resolver->name); + return -1; + } else if (!resolver->cancel) { + ast_log(LOG_ERROR, "DNS resolver '%s' does not implement the cancel callback which is required\n", + resolver->name); + return -1; + } + + AST_RWLIST_WRLOCK(&resolvers); + + /* On the first registration of a resolver start a scheduler for recurring queries */ + if (AST_LIST_EMPTY(&resolvers) && !sched) { + sched = ast_sched_context_create(); + if (!sched) { + ast_log(LOG_ERROR, "DNS resolver '%s' could not be registered: Failed to create scheduler for recurring DNS queries\n", + resolver->name); + AST_RWLIST_UNLOCK(&resolvers); + return -1; + } + + if (ast_sched_start_thread(sched)) { + ast_log(LOG_ERROR, "DNS resolver '%s' could not be registered: Failed to start thread for recurring DNS queries\n", + resolver->name); + dns_shutdown(); + AST_RWLIST_UNLOCK(&resolvers); + return -1; + } + + ast_register_cleanup(dns_shutdown); + } + + AST_LIST_TRAVERSE(&resolvers, iter, next) { + if (!strcmp(iter->name, resolver->name)) { + ast_log(LOG_ERROR, "A DNS resolver with the name '%s' is already registered\n", resolver->name); + AST_RWLIST_UNLOCK(&resolvers); + return -1; + } + } + + AST_RWLIST_TRAVERSE_SAFE_BEGIN(&resolvers, iter, next) { + if (iter->priority > resolver->priority) { + AST_RWLIST_INSERT_BEFORE_CURRENT(resolver, next); + inserted = 1; + break; + } + } + AST_RWLIST_TRAVERSE_SAFE_END; + + if (!inserted) { + AST_RWLIST_INSERT_TAIL(&resolvers, resolver, next); + } + + AST_RWLIST_UNLOCK(&resolvers); + + ast_verb(2, "Registered DNS resolver '%s' with priority '%d'\n", resolver->name, resolver->priority); + + return 0; +} + +void ast_dns_resolver_unregister(struct ast_dns_resolver *resolver) +{ + struct ast_dns_resolver *iter; + + if (!resolver) { + return; + } + + AST_RWLIST_WRLOCK(&resolvers); + AST_RWLIST_TRAVERSE_SAFE_BEGIN(&resolvers, iter, next) { + if (resolver == iter) { + AST_RWLIST_REMOVE_CURRENT(next); + break; + } + } + AST_RWLIST_TRAVERSE_SAFE_END; + AST_RWLIST_UNLOCK(&resolvers); + + ast_verb(2, "Unregistered DNS resolver '%s'\n", resolver->name); +} diff --git a/main/dns_naptr.c b/main/dns_naptr.c new file mode 100644 index 000000000..f4facddae --- /dev/null +++ b/main/dns_naptr.c @@ -0,0 +1,65 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Digium, Inc. + * + * Joshua Colp + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief DNS NAPTR Record Support + * + * \author Joshua Colp + */ + +/*** MODULEINFO + core + ***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include "asterisk/dns_core.h" +#include "asterisk/dns_naptr.h" + +const char *ast_dns_naptr_get_flags(const struct ast_dns_record *record) +{ + return NULL; +} + +const char *ast_dns_naptr_get_service(const struct ast_dns_record *record) +{ + return NULL; +} + +const char *ast_dns_naptr_get_regexp(const struct ast_dns_record *record) +{ + return NULL; +} + +const char *ast_dns_naptr_get_replacement(const struct ast_dns_record *record) +{ + return NULL; +} + +unsigned short ast_dns_naptr_get_order(const struct ast_dns_record *record) +{ + return 0; +} + +unsigned short ast_dns_naptr_get_preference(const struct ast_dns_record *record) +{ + return 0; +} \ No newline at end of file diff --git a/main/dns_query_set.c b/main/dns_query_set.c new file mode 100644 index 000000000..45626d1b9 --- /dev/null +++ b/main/dns_query_set.c @@ -0,0 +1,93 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Digium, Inc. + * + * Joshua Colp + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief DNS Query Set API + * + * \author Joshua Colp + */ + +/*** MODULEINFO + core + ***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include "asterisk/vector.h" +#include "asterisk/astobj2.h" +#include "asterisk/dns_core.h" +#include "asterisk/dns_query_set.h" + +/*! \brief A set of DNS queries */ +struct ast_dns_query_set { + /*! \brief DNS queries */ + AST_VECTOR(, struct ast_dns_query *) queries; + /*! \brief The total number of completed queries */ + unsigned int queries_completed; + /*! \brief Callback to invoke upon completion */ + ast_dns_query_set_callback callback; + /*! \brief User-specific data */ + void *user_data; +}; + +struct ast_dns_query_set *ast_dns_query_set_create(void) +{ + return NULL; +} + +int ast_dns_query_set_add(struct ast_dns_query_set *query_set, const char *name, int rr_type, int rr_class) +{ + return -1; +} + +size_t ast_dns_query_set_num_queries(const struct ast_dns_query_set *query_set) +{ + return 0; +} + +struct ast_dns_query *ast_dns_query_set_get(const struct ast_dns_query_set *query_set, unsigned int index) +{ + return NULL; +} + +void *ast_dns_query_set_get_data(const struct ast_dns_query_set *query_set) +{ + return query_set->user_data; +} + +void ast_dns_query_set_resolve_async(struct ast_dns_query_set *query_set, ast_dns_query_set_callback callback, void *data) +{ + query_set->callback = callback; + query_set->user_data = ao2_bump(data); +} + +void ast_query_set_resolve(struct ast_dns_query_set *query_set) +{ +} + +int ast_dns_query_set_resolve_cancel(struct ast_dns_query_set *query_set) +{ + return -1; +} + +void ast_dns_query_set_free(struct ast_dns_query_set *query_set) +{ +} diff --git a/main/dns_recurring.c b/main/dns_recurring.c new file mode 100644 index 000000000..3ebbab070 --- /dev/null +++ b/main/dns_recurring.c @@ -0,0 +1,149 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Digium, Inc. + * + * Joshua Colp + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief DNS Recurring Query Support + * + * \author Joshua Colp + */ + +/*** MODULEINFO + core + ***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include "asterisk/astobj2.h" +#include "asterisk/linkedlists.h" +#include "asterisk/sched.h" +#include "asterisk/strings.h" +#include "asterisk/dns_core.h" +#include "asterisk/dns_recurring.h" +#include "asterisk/dns_internal.h" + +#include + +/*! \brief Destructor for a DNS query */ +static void dns_query_recurring_destroy(void *data) +{ + struct ast_dns_query_recurring *recurring = data; + + ao2_cleanup(recurring->user_data); +} + +static void dns_query_recurring_resolution_callback(const struct ast_dns_query *query); + +/*! \brief Scheduled recurring query callback */ +static int dns_query_recurring_scheduled_callback(const void *data) +{ + struct ast_dns_query_recurring *recurring = (struct ast_dns_query_recurring *)data; + + ao2_lock(recurring); + recurring->timer = -1; + if (!recurring->cancelled) { + recurring->active = ast_dns_resolve_async(recurring->name, recurring->rr_type, recurring->rr_class, dns_query_recurring_resolution_callback, + recurring); + } + ao2_unlock(recurring); + + ao2_ref(recurring, -1); + + return 0; +} + +/*! \brief Query resolution callback */ +static void dns_query_recurring_resolution_callback(const struct ast_dns_query *query) +{ + struct ast_dns_query_recurring *recurring = ast_dns_query_get_data(query); + + /* Replace the user data so the actual callback sees what it provided */ + ((struct ast_dns_query*)query)->user_data = ao2_bump(recurring->user_data); + recurring->callback(query); + + ao2_lock(recurring); + /* So.. if something has not externally cancelled this we can reschedule based on the TTL */ + if (!recurring->cancelled) { + const struct ast_dns_result *result = ast_dns_query_get_result(query); + int ttl = MIN(ast_dns_result_get_lowest_ttl(result), INT_MAX / 1000); + + if (ttl) { + recurring->timer = ast_sched_add(ast_dns_get_sched(), ttl * 1000, dns_query_recurring_scheduled_callback, ao2_bump(recurring)); + if (recurring->timer < 0) { + /* It is impossible for this to be the last reference as this callback function holds a reference itself */ + ao2_ref(recurring, -1); + } + } + } + + ao2_replace(recurring->active, NULL); + ao2_unlock(recurring); + + /* Since we stole the reference from the query we need to drop it ourselves */ + ao2_ref(recurring, -1); +} + +struct ast_dns_query_recurring *ast_dns_resolve_recurring(const char *name, int rr_type, int rr_class, ast_dns_resolve_callback callback, void *data) +{ + struct ast_dns_query_recurring *recurring; + + if (ast_strlen_zero(name) || !callback || !ast_dns_get_sched()) { + return NULL; + } + + recurring = ao2_alloc(sizeof(*recurring) + strlen(name) + 1, dns_query_recurring_destroy); + if (!recurring) { + return NULL; + } + + recurring->callback = callback; + recurring->user_data = ao2_bump(data); + recurring->timer = -1; + recurring->rr_type = rr_type; + recurring->rr_class = rr_class; + strcpy(recurring->name, name); /* SAFE */ + + recurring->active = ast_dns_resolve_async(name, rr_type, rr_class, dns_query_recurring_resolution_callback, recurring); + if (!recurring->active) { + ao2_ref(recurring, -1); + return NULL; + } + + return recurring; +} + +int ast_dns_resolve_recurring_cancel(struct ast_dns_query_recurring *recurring) +{ + int res = 0; + + ao2_lock(recurring); + + recurring->cancelled = 1; + AST_SCHED_DEL_UNREF(ast_dns_get_sched(), recurring->timer, ao2_ref(recurring, -1)); + + if (recurring->active) { + res = ast_dns_resolve_cancel(recurring->active); + ao2_replace(recurring->active, NULL); + } + + ao2_unlock(recurring); + + return res; +} diff --git a/main/dns_srv.c b/main/dns_srv.c new file mode 100644 index 000000000..eeba9a679 --- /dev/null +++ b/main/dns_srv.c @@ -0,0 +1,55 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Digium, Inc. + * + * Joshua Colp + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief DNS SRV Record Support + * + * \author Joshua Colp + */ + +/*** MODULEINFO + core + ***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include "asterisk/dns_core.h" +#include "asterisk/dns_srv.h" + +const char *ast_dns_srv_get_host(const struct ast_dns_record *record) +{ + return NULL; +} + +unsigned short ast_dns_srv_get_priority(const struct ast_dns_record *record) +{ + return 0; +} + +unsigned short ast_dns_srv_get_weight(const struct ast_dns_record *record) +{ + return 0; +} + +unsigned short ast_dns_srv_get_port(const struct ast_dns_record *record) +{ + return 0; +} \ No newline at end of file diff --git a/main/dns_tlsa.c b/main/dns_tlsa.c new file mode 100644 index 000000000..aa6f5308f --- /dev/null +++ b/main/dns_tlsa.c @@ -0,0 +1,55 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Digium, Inc. + * + * Joshua Colp + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief DNS TLSA Record Support + * + * \author Joshua Colp + */ + +/*** MODULEINFO + core + ***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include "asterisk/dns_core.h" +#include "asterisk/dns_tlsa.h" + +unsigned int ast_dns_tlsa_get_usage(const struct ast_dns_record *record) +{ + return 0; +} + +unsigned int ast_dns_tlsa_get_selector(const struct ast_dns_record *record) +{ + return 0; +} + +unsigned int ast_dns_tlsa_get_matching_type(const struct ast_dns_record *record) +{ + return 0; +} + +const char *ast_dns_tlsa_get_association_data(const struct ast_dns_record *record) +{ + return NULL; +} \ No newline at end of file diff --git a/makeopts.in b/makeopts.in index 96b031bdd..cc233109d 100644 --- a/makeopts.in +++ b/makeopts.in @@ -299,6 +299,9 @@ CRYPTO_LIB=@CRYPTO_LIB@ TONEZONE_INCLUDE=@TONEZONE_INCLUDE@ TONEZONE_LIB=@TONEZONE_LIB@ +UNBOUND_INCLUDE=@UNBOUND_INCLUDE@ +UNBOUND_LIB=@UNBOUND_LIB@ + UNIXODBC_INCLUDE=@UNIXODBC_INCLUDE@ UNIXODBC_LIB=@UNIXODBC_LIB@ diff --git a/res/res_resolver_unbound.c b/res/res_resolver_unbound.c new file mode 100644 index 000000000..43f2acdfd --- /dev/null +++ b/res/res_resolver_unbound.c @@ -0,0 +1,1271 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Digium, Inc. + * + * Joshua Colp + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*** MODULEINFO + unbound + core + ***/ + +#include "asterisk.h" + +ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + +#include +#include + +#include "asterisk/module.h" +#include "asterisk/linkedlists.h" +#include "asterisk/dns_core.h" +#include "asterisk/dns_resolver.h" +#include "asterisk/config.h" +#include "asterisk/config_options.h" +#include "asterisk/test.h" + +/*** DOCUMENTATION + + + + Options that apply globally to res_resolver_unbound + + Full path to an optional hosts file + Hosts specified in a hosts file will be resolved within the resolver itself. If a value + of system is provided the system-specific file will be used. + + + Full path to an optional resolv.conf file + The resolv.conf file specifies the nameservers to contact when resolving queries. If a + value of system is provided the system-specific file will be used. If provided alongside explicit nameservers the + nameservers contained within the resolv.conf file will be used after all others. + + + Nameserver to use for queries + An explicit nameserver can be specified which is used for resolving queries. If multiple + nameserver lines are specified the first will be the primary with failover occurring, in order, to the other + nameservers as backups. If provided alongside a resolv.conf file the nameservers explicitly specified will be + used before all others. + + + Unbound debug level + The debugging level for the unbound resolver. While there is no explicit range generally + the higher the number the more debug is output. + + + Trust anchor file + Full path to a file with DS and DNSKEY records in zone file format. This file is provided + to unbound and is used as a source for trust anchors. + + + + + ***/ + +/*! \brief Structure for an unbound resolver */ +struct unbound_resolver { + /*! \brief Resolver context itself */ + struct ub_ctx *context; + /*! \brief Thread handling the resolver */ + pthread_t thread; +}; + +/*! \brief Structure for query resolver data */ +struct unbound_resolver_data { + /*! \brief ID for the specific query */ + int id; + /*! \brief The resolver in use for the query */ + struct unbound_resolver *resolver; +}; + +/*! \brief Unbound configuration state information */ +struct unbound_config_state { + /*! \brief The configured resolver */ + struct unbound_resolver *resolver; +}; + +/*! \brief A structure to hold global configuration-related options */ +struct unbound_global_config { + AST_DECLARE_STRING_FIELDS( + AST_STRING_FIELD(hosts); /*!< Optional hosts file */ + AST_STRING_FIELD(resolv); /*!< Optional resolv.conf file */ + AST_STRING_FIELD(ta_file); /*!< Optional trust anchor file */ + ); + /*! \brief List of nameservers (in order) to use for queries */ + struct ao2_container *nameservers; + /*! \brief Debug level for the resolver */ + unsigned int debug; + /*! \brief State information */ + struct unbound_config_state *state; +}; + +/*! \brief A container for config related information */ +struct unbound_config { + struct unbound_global_config *global; +}; + +/*! + * \brief Allocate a unbound_config to hold a snapshot of the complete results of parsing a config + * \internal + * \returns A void pointer to a newly allocated unbound_config + */ +static void *unbound_config_alloc(void); + +/*! \brief An aco_type structure to link the "general" category to the unbound_global_config type */ +static struct aco_type global_option = { + .type = ACO_GLOBAL, + .name = "globals", + .item_offset = offsetof(struct unbound_config, global), + .category_match = ACO_WHITELIST, + .category = "^general$", +}; + +static struct aco_type *global_options[] = ACO_TYPES(&global_option); + +static struct aco_file resolver_unbound_conf = { + .filename = "resolver_unbound.conf", + .types = ACO_TYPES(&global_option), +}; + +/*! \brief A global object container that will contain the global_config that gets swapped out on reloads */ +static AO2_GLOBAL_OBJ_STATIC(globals); + +/*! + * \brief Finish initializing new configuration + * \internal + */ +static int unbound_config_preapply_callback(void); + +/*! \brief Register information about the configs being processed by this module */ +CONFIG_INFO_STANDARD(cfg_info, globals, unbound_config_alloc, + .files = ACO_FILES(&resolver_unbound_conf), + .pre_apply_config = unbound_config_preapply_callback, +); + +/*! \brief Destructor for unbound resolver */ +static void unbound_resolver_destroy(void *obj) +{ + struct unbound_resolver *resolver = obj; + + if (resolver->context) { + ub_ctx_delete(resolver->context); + } +} + +/*! \brief Allocator for unbound resolver */ +static struct unbound_resolver *unbound_resolver_alloc(void) +{ + struct unbound_resolver *resolver; + + resolver = ao2_alloc_options(sizeof(*resolver), unbound_resolver_destroy, AO2_ALLOC_OPT_LOCK_NOLOCK); + if (!resolver) { + return NULL; + } + + resolver->thread = AST_PTHREADT_NULL; + + resolver->context = ub_ctx_create(); + if (!resolver->context) { + ao2_ref(resolver, -1); + return NULL; + } + + /* Each async result should be invoked in a separate thread so others are not blocked */ + ub_ctx_async(resolver->context, 1); + + return resolver; +} + +/*! \brief Resolver thread which waits and handles results */ +static void *unbound_resolver_thread(void *data) +{ + struct unbound_resolver *resolver = data; + + ast_debug(1, "Starting processing for unbound resolver\n"); + + while (resolver->thread != AST_PTHREADT_STOP) { + /* Wait for any results to come in */ + ast_wait_for_input(ub_fd(resolver->context), -1); + + /* Finally process any results */ + ub_process(resolver->context); + } + + ast_debug(1, "Terminating processing for unbound resolver\n"); + + ao2_ref(resolver, -1); + + return NULL; +} + +/*! \brief Start function for the unbound resolver */ +static int unbound_resolver_start(struct unbound_resolver *resolver) +{ + int res; + + if (resolver->thread != AST_PTHREADT_NULL) { + return 0; + } + + ast_debug(1, "Starting thread for unbound resolver\n"); + + res = ast_pthread_create(&resolver->thread, NULL, unbound_resolver_thread, ao2_bump(resolver)); + if (res) { + ast_debug(1, "Could not start thread for unbound resolver\n"); + ao2_ref(resolver, -1); + } + + return res; +} + +/*! \brief Stop function for the unbound resolver */ +static void unbound_resolver_stop(struct unbound_resolver *resolver) +{ + pthread_t thread; + + if (resolver->thread == AST_PTHREADT_NULL) { + return; + } + + ast_debug(1, "Stopping processing thread for unbound resolver\n"); + + thread = resolver->thread; + resolver->thread = AST_PTHREADT_STOP; + pthread_kill(thread, SIGURG); + pthread_join(thread, NULL); + + ast_debug(1, "Stopped processing thread for unbound resolver\n"); +} + +/*! \brief Callback invoked when resolution completes on a query */ +static void unbound_resolver_callback(void *data, int err, struct ub_result *ub_result) +{ + struct ast_dns_query *query = data; + + if (!ast_dns_resolver_set_result(query, ub_result->secure, ub_result->bogus, ub_result->rcode, + S_OR(ub_result->canonname, ast_dns_query_get_name(query)), ub_result->answer_packet, ub_result->answer_len)) { + int i; + char *data; + + for (i = 0; (data = ub_result->data[i]); i++) { + if (ast_dns_resolver_add_record(query, ub_result->qtype, ub_result->qclass, ub_result->ttl, + data, ub_result->len[i])) { + break; + } + } + } + + ast_dns_resolver_completed(query); + ao2_ref(query, -1); + ub_resolve_free(ub_result); +} + +static int unbound_resolver_resolve(struct ast_dns_query *query) +{ + struct unbound_config *cfg = ao2_global_obj_ref(globals); + struct unbound_resolver_data *data; + int res; + + data = ao2_alloc_options(sizeof(*data), NULL, AO2_ALLOC_OPT_LOCK_NOLOCK); + if (!data) { + ast_log(LOG_ERROR, "Failed to allocate resolver data for resolution of '%s'\n", + ast_dns_query_get_name(query)); + return -1; + } + data->resolver = ao2_bump(cfg->global->state->resolver); + ast_dns_resolver_set_data(query, data); + + res = ub_resolve_async(data->resolver->context, ast_dns_query_get_name(query), + ast_dns_query_get_rr_type(query), ast_dns_query_get_rr_class(query), + ao2_bump(query), unbound_resolver_callback, &data->id); + + if (res) { + ast_log(LOG_ERROR, "Failed to perform async DNS resolution of '%s'\n", + ast_dns_query_get_name(query)); + ao2_ref(query, -1); + } + + ao2_ref(data, -1); + ao2_ref(cfg, -1); + + return res; +} + +static int unbound_resolver_cancel(struct ast_dns_query *query) +{ + struct unbound_resolver_data *data = ast_dns_resolver_get_data(query); + int res; + + res = ub_cancel(data->resolver->context, data->id); + if (!res) { + /* When this query was started we bumped the ref, now that it has been cancelled we have ownership and + * need to drop it + */ + ao2_ref(query, -1); + } + + return res; +} + +struct ast_dns_resolver unbound_resolver = { + .name = "unbound", + .priority = 100, + .resolve = unbound_resolver_resolve, + .cancel = unbound_resolver_cancel, +}; + +static void unbound_config_destructor(void *obj) +{ + struct unbound_config *cfg = obj; + + ao2_cleanup(cfg->global); +} + +static void unbound_global_config_destructor(void *obj) +{ + struct unbound_global_config *global = obj; + + ast_string_field_free_memory(global); + ao2_cleanup(global->nameservers); + ao2_cleanup(global->state); +} + +static void unbound_config_state_destructor(void *obj) +{ + struct unbound_config_state *state = obj; + + if (state->resolver) { + unbound_resolver_stop(state->resolver); + ao2_ref(state->resolver, -1); + } +} + +static void *unbound_config_alloc(void) +{ + struct unbound_config *cfg; + + cfg = ao2_alloc_options(sizeof(*cfg), unbound_config_destructor, AO2_ALLOC_OPT_LOCK_NOLOCK); + if (!cfg) { + return NULL; + } + + /* Allocate/initialize memory */ + cfg->global = ao2_alloc_options(sizeof(*cfg->global), unbound_global_config_destructor, + AO2_ALLOC_OPT_LOCK_NOLOCK); + if (!cfg->global) { + goto error; + } + + if (ast_string_field_init(cfg->global, 128)) { + goto error; + } + + return cfg; +error: + ao2_ref(cfg, -1); + return NULL; +} + +static int unbound_config_preapply(struct unbound_config *cfg) +{ + int res = 0; + + cfg->global->state = ao2_alloc_options(sizeof(*cfg->global->state), unbound_config_state_destructor, + AO2_ALLOC_OPT_LOCK_NOLOCK); + if (!cfg->global->state) { + ast_log(LOG_ERROR, "Could not allocate unbound resolver state structure\n"); + return -1; + } + + cfg->global->state->resolver = unbound_resolver_alloc(); + if (!cfg->global->state->resolver) { + ast_log(LOG_ERROR, "Could not create an unbound resolver\n"); + return -1; + } + + ub_ctx_debuglevel(cfg->global->state->resolver->context, cfg->global->debug); + + if (!strcmp(cfg->global->hosts, "system")) { + res = ub_ctx_hosts(cfg->global->state->resolver->context, NULL); + } else if (!ast_strlen_zero(cfg->global->hosts)) { + res = ub_ctx_hosts(cfg->global->state->resolver->context, cfg->global->hosts); + } + + if (res) { + ast_log(LOG_ERROR, "Failed to set hosts file to '%s' in unbound resolver: %s\n", + cfg->global->hosts, ub_strerror(res)); + return -1; + } + + if (cfg->global->nameservers) { + struct ao2_iterator it_nameservers; + const char *nameserver; + + it_nameservers = ao2_iterator_init(cfg->global->nameservers, 0); + while ((nameserver = ao2_iterator_next(&it_nameservers))) { + res = ub_ctx_set_fwd(cfg->global->state->resolver->context, nameserver); + + if (res) { + ast_log(LOG_ERROR, "Failed to add nameserver '%s' to unbound resolver: %s\n", + nameserver, ub_strerror(res)); + ao2_iterator_destroy(&it_nameservers); + return -1; + } + } + ao2_iterator_destroy(&it_nameservers); + } + + if (!strcmp(cfg->global->resolv, "system")) { + res = ub_ctx_resolvconf(cfg->global->state->resolver->context, NULL); + } else if (!ast_strlen_zero(cfg->global->resolv)) { + res = ub_ctx_resolvconf(cfg->global->state->resolver->context, cfg->global->resolv); + } + + if (res) { + ast_log(LOG_ERROR, "Failed to set resolv.conf file to '%s' in unbound resolver: %s\n", + cfg->global->resolv, ub_strerror(res)); + return -1; + } + + if (!ast_strlen_zero(cfg->global->ta_file)) { + res = ub_ctx_add_ta_file(cfg->global->state->resolver->context, cfg->global->ta_file); + + if (res) { + ast_log(LOG_ERROR, "Failed to set trusted anchor file to '%s' in unbound resolver: %s\n", + cfg->global->ta_file, ub_strerror(res)); + return -1; + } + } + + if (unbound_resolver_start(cfg->global->state->resolver)) { + ast_log(LOG_ERROR, "Could not start unbound resolver thread\n"); + return -1; + } + + return 0; +} + +static int unbound_config_apply_default(void) +{ + struct unbound_config *cfg; + + cfg = unbound_config_alloc(); + if (!cfg) { + ast_log(LOG_ERROR, "Could not create default configuration for unbound resolver\n"); + return -1; + } + + aco_set_defaults(&global_option, "general", cfg->global); + + if (unbound_config_preapply(cfg)) { + ao2_ref(cfg, -1); + return -1; + } + + ast_verb(1, "Starting unbound resolver using default configuration\n"); + + ao2_global_obj_replace_unref(globals, cfg); + ao2_ref(cfg, -1); + + return 0; +} + +static int unbound_config_preapply_callback(void) +{ + return unbound_config_preapply(aco_pending_config(&cfg_info)); +} + +#ifdef TEST_FRAMEWORK + +/*! + * \brief A DNS record to be used during a test + */ +struct dns_record { + /*! String representation of the record, as would be found in a file */ + const char *as_string; + /*! The domain this record belongs to */ + const char *domain; + /*! The type of the record */ + int rr_type; + /*! The class of the record */ + int rr_class; + /*! The TTL of the record, in seconds */ + int ttl; + /*! The RDATA of the DNS record */ + const char *buf; + /*! The size of the RDATA */ + const size_t bufsize; + /*! Whether a record checker has visited this record */ + int visited; +}; + +/*! + * \brief Resolution function for tests. + * + * Several tests will have similar setups but will want to make use of a different + * means of actually making queries and checking their results. This pluggable + * function pointer allows for similar tests to be operated in different ways. + * + * \param test The test being run + * \param domain The domain to look up + * \param rr_type The record type to look up + * \param rr_class The class of record to look up + * \param records All records that exist for the test. + * \param num_records Number of records in the records array. + * + * \retval 0 The test has passed thus far. + * \retval -1 The test has failed. + */ +typedef int (*resolve_fn)(struct ast_test *test, const char *domain, int rr_type, + int rr_class, struct dns_record *records, size_t num_records); + +/*! + * \brief Pluggable function for running a synchronous query and checking its results + */ +static int nominal_sync_run(struct ast_test *test, const char *domain, int rr_type, + int rr_class, struct dns_record *records, size_t num_records) +{ + RAII_VAR(struct ast_dns_result *, result, NULL, ast_dns_result_free); + const struct ast_dns_record *record; + int i; + + /* Start by making sure no records have been visited */ + for (i = 0; i < num_records; ++i) { + records[i].visited = 0; + } + + ast_test_status_update(test, "Performing DNS query '%s', type %d\n", domain, rr_type); + + if (ast_dns_resolve(domain, rr_type, rr_class, &result)) { + ast_test_status_update(test, "Failed to perform synchronous resolution of domain %s\n", domain); + return -1; + } + + if (!result) { + ast_test_status_update(test, "Successful synchronous resolution of domain %s gave NULL result\n", domain); + return -1; + } + + for (record = ast_dns_result_get_records(result); record; record = ast_dns_record_get_next(record)) { + int match = 0; + + /* Let's make sure this matches one of our known records */ + for (i = 0; i < num_records; ++i) { + if (ast_dns_record_get_rr_type(record) == records[i].rr_type && + ast_dns_record_get_rr_class(record) == records[i].rr_class && + ast_dns_record_get_ttl(record) == records[i].ttl && + !memcmp(ast_dns_record_get_data(record), records[i].buf, records[i].bufsize)) { + match = 1; + records[i].visited = 1; + break; + } + } + + if (!match) { + ast_test_status_update(test, "Unknown DNS record returned from domain %s\n", domain); + return -1; + } + } + + return 0; +} + +/*! + * \brief Data required for an asynchronous callback + */ +struct async_data { + /*! The set of DNS records on a test */ + struct dns_record *records; + /*! The number of DNS records on the test */ + size_t num_records; + /*! Whether an asynchronous query failed */ + int failed; + /*! Indicates the asynchronous query is complete */ + int complete; + ast_mutex_t lock; + ast_cond_t cond; +}; + +static void async_data_destructor(void *obj) +{ + struct async_data *adata = obj; + + ast_mutex_destroy(&adata->lock); + ast_cond_destroy(&adata->cond); +} + +static struct async_data *async_data_alloc(struct dns_record *records, size_t num_records) +{ + struct async_data *adata; + + adata = ao2_alloc(sizeof(*adata), async_data_destructor); + if (!adata) { + return NULL; + } + + ast_mutex_init(&adata->lock); + ast_cond_init(&adata->cond, NULL); + adata->records = records; + adata->num_records = num_records; + + return adata; +} + +/*! + * \brief Callback for asynchronous queries + * + * This query will check that the records in the DNS result match + * records that the test has created. The success or failure of the + * query is indicated through the async_data failed field. + * + * \param query The DNS query that has been resolved + */ +static void async_callback(const struct ast_dns_query *query) +{ + struct async_data *adata = ast_dns_query_get_data(query); + struct ast_dns_result *result = ast_dns_query_get_result(query); + const struct ast_dns_record *record; + int i; + + if (!result) { + adata->failed = -1; + goto end; + } + + for (record = ast_dns_result_get_records(result); record; record = ast_dns_record_get_next(record)) { + int match = 0; + + /* Let's make sure this matches one of our known records */ + for (i = 0; i < adata->num_records; ++i) { + if (ast_dns_record_get_rr_type(record) == adata->records[i].rr_type && + ast_dns_record_get_rr_class(record) == adata->records[i].rr_class && + ast_dns_record_get_ttl(record) == adata->records[i].ttl && + !memcmp(ast_dns_record_get_data(record), adata->records[i].buf, adata->records[i].bufsize)) { + match = 1; + adata->records[i].visited = 1; + break; + } + } + + if (!match) { + adata->failed = -1; + goto end; + } + } + +end: + ast_mutex_lock(&adata->lock); + adata->complete = 1; + ast_cond_signal(&adata->cond); + ast_mutex_unlock(&adata->lock); +} + +/*! + * \brief Pluggable function for performing an asynchronous query during a test + * + * Unlike the synchronous version, this does not check the records, instead leaving + * that to be done in the asynchronous callback. + */ +static int nominal_async_run(struct ast_test *test, const char *domain, int rr_type, + int rr_class, struct dns_record *records, size_t num_records) +{ + RAII_VAR(struct ast_dns_query_active *, active, NULL, ao2_cleanup); + RAII_VAR(struct async_data *, adata, NULL, ao2_cleanup); + int i; + + adata = async_data_alloc(records, num_records); + if (!adata) { + ast_test_status_update(test, "Unable to allocate data for async query\n"); + return -1; + } + + /* Start by making sure no records have been visited */ + for (i = 0; i < num_records; ++i) { + records[i].visited = 0; + } + + ast_test_status_update(test, "Performing DNS query '%s', type %d\n", domain, rr_type); + + active = ast_dns_resolve_async(domain, rr_type, rr_class, async_callback, adata); + if (!active) { + ast_test_status_update(test, "Failed to perform asynchronous resolution of domain %s\n", domain); + return -1; + } + + ast_mutex_lock(&adata->lock); + while (!adata->complete) { + ast_cond_wait(&adata->cond, &adata->lock); + } + ast_mutex_unlock(&adata->lock); + + if (adata->failed) { + ast_test_status_update(test, "Unknown DNS record returned from domain %s\n", domain); + } + return adata->failed; +} + +/*! + * \brief Framework for running a nominal DNS test + * + * Synchronous and asynchronous tests mostly have the same setup, so this function + * serves as a common way to set up both types of tests by accepting a pluggable + * function to determine which type of lookup is used + * + * \param test The test being run + * \param runner The method for resolving queries on this test + */ +static enum ast_test_result_state nominal_test(struct ast_test *test, resolve_fn runner) +{ + RAII_VAR(struct unbound_resolver *, resolver, NULL, ao2_cleanup); + RAII_VAR(struct unbound_config *, cfg, NULL, ao2_cleanup); + + static const size_t V4_SIZE = sizeof(struct in_addr); + static const size_t V6_SIZE = sizeof(struct in6_addr); + + static const char *DOMAIN1 = "goose.feathers"; + static const char *DOMAIN2 = "duck.feathers"; + + static const char *ADDR1 = "127.0.0.2"; + static const char *ADDR2 = "127.0.0.3"; + static const char *ADDR3 = "::1"; + static const char *ADDR4 = "127.0.0.4"; + + char addr1_buf[V4_SIZE]; + char addr2_buf[V4_SIZE]; + char addr3_buf[V6_SIZE]; + char addr4_buf[V4_SIZE]; + + struct dns_record records [] = { + { "goose.feathers 12345 IN A 127.0.0.2", DOMAIN1, ns_t_a, ns_c_in, 12345, addr1_buf, V4_SIZE, 0 }, + { "goose.feathers 12345 IN A 127.0.0.3", DOMAIN1, ns_t_a, ns_c_in, 12345, addr2_buf, V4_SIZE, 0 }, + { "goose.feathers 12345 IN AAAA ::1", DOMAIN1, ns_t_aaaa, ns_c_in, 12345, addr3_buf, V6_SIZE, 0 }, + { "duck.feathers 12345 IN A 127.0.0.4", DOMAIN2, ns_t_a, ns_c_in, 12345, addr4_buf, V4_SIZE, 0 }, + }; + + struct { + const char *domain; + int rr_type; + int rr_class; + int visited[ARRAY_LEN(records)]; + } runs [] = { + { DOMAIN1, ns_t_a, ns_c_in, { 1, 1, 0, 0 } }, + { DOMAIN1, ns_t_aaaa, ns_c_in, { 0, 0, 1, 0 } }, + { DOMAIN2, ns_t_a, ns_c_in, { 0, 0, 0, 1 } }, + }; + + int i; + enum ast_test_result_state res = AST_TEST_PASS; + + inet_pton(AF_INET, ADDR1, addr1_buf); + inet_pton(AF_INET, ADDR2, addr2_buf); + inet_pton(AF_INET6, ADDR3, addr3_buf); + inet_pton(AF_INET, ADDR4, addr4_buf); + + cfg = ao2_global_obj_ref(globals); + resolver = ao2_bump(cfg->global->state->resolver); + + ub_ctx_zone_add(resolver->context, DOMAIN1, "static"); + ub_ctx_zone_add(resolver->context, DOMAIN2, "static"); + + for (i = 0; i < ARRAY_LEN(records); ++i) { + ub_ctx_data_add(resolver->context, records[i].as_string); + } + + for (i = 0; i < ARRAY_LEN(runs); ++i) { + int j; + + if (runner(test, runs[i].domain, runs[i].rr_type, runs[i].rr_class, records, ARRAY_LEN(records))) { + res = AST_TEST_FAIL; + goto cleanup; + } + + for (j = 0; j < ARRAY_LEN(records); ++j) { + if (records[j].visited != runs[i].visited[j]) { + ast_test_status_update(test, "DNS results match unexpected records\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + } + } + +cleanup: + for (i = 0; i < ARRAY_LEN(records); ++i) { + ub_ctx_data_remove(resolver->context, records[i].as_string); + } + ub_ctx_zone_remove(resolver->context, DOMAIN1); + ub_ctx_zone_remove(resolver->context, DOMAIN2); + + return res; +} + +AST_TEST_DEFINE(resolve_sync) +{ + + switch (cmd) { + case TEST_INIT: + info->name = "resolve_sync"; + info->category = "/res/res_resolver_unbound/"; + info->summary = "Test nominal synchronous resolution using libunbound\n"; + info->description = "This test performs the following:\n" + "\t* Set two static A records and one static AAAA record on one domain\n" + "\t* Set an A record for a second domain\n" + "\t* Perform an A record lookup on the first domain\n" + "\t* Ensure that both A records are returned and no AAAA record is returned\n" + "\t* Perform an AAAA record lookup on the first domain\n" + "\t* Ensure that the AAAA record is returned and no A record is returned\n" + "\t* Perform an A record lookup on the second domain\n" + "\t* Ensure that the A record from the second domain is returned\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + return nominal_test(test, nominal_sync_run); +} + +AST_TEST_DEFINE(resolve_async) +{ + switch (cmd) { + case TEST_INIT: + info->name = "resolve_async"; + info->category = "/res/res_resolver_unbound/"; + info->summary = "Test nominal asynchronous resolution using libunbound\n"; + info->description = "This test performs the following:\n" + "\t* Set two static A records and one static AAAA record on one domain\n" + "\t* Set an A record for a second domain\n" + "\t* Perform an A record lookup on the first domain\n" + "\t* Ensure that both A records are returned and no AAAA record is returned\n" + "\t* Perform an AAAA record lookup on the first domain\n" + "\t* Ensure that the AAAA record is returned and no A record is returned\n" + "\t* Perform an A record lookup on the second domain\n" + "\t* Ensure that the A record from the second domain is returned\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + return nominal_test(test, nominal_async_run); +} + +typedef int (*off_nominal_resolve_fn)(struct ast_test *test, const char *domain, int rr_type, + int rr_class, int expected_rcode); + +static int off_nominal_sync_run(struct ast_test *test, const char *domain, int rr_type, + int rr_class, int expected_rcode) +{ + struct ast_dns_result *result; + int res = 0; + + if (ast_dns_resolve(domain, rr_type, rr_class, &result)) { + ast_test_status_update(test, "Failed to perform resolution :(\n"); + return -1; + } + + if (!result) { + ast_test_status_update(test, "Resolution returned no result\n"); + return -1; + } + + if (ast_dns_result_get_rcode(result) != expected_rcode) { + ast_test_status_update(test, "Unexpected rcode from DNS resolution\n"); + res = -1; + } + + if (ast_dns_result_get_records(result)) { + ast_test_status_update(test, "DNS resolution returned records unexpectedly\n"); + res = -1; + } + + ast_dns_result_free(result); + return res; +} + +/*! + * \brief User data for off-nominal async resolution test + */ +struct off_nominal_async_data { + /*! The DNS result's expected rcode */ + int expected_rcode; + /*! Whether an asynchronous query failed */ + int failed; + /*! Indicates the asynchronous query is complete */ + int complete; + ast_mutex_t lock; + ast_cond_t cond; +}; + +static void off_nominal_async_data_destructor(void *obj) +{ + struct off_nominal_async_data *adata = obj; + + ast_mutex_destroy(&adata->lock); + ast_cond_destroy(&adata->cond); +} + +static struct off_nominal_async_data *off_nominal_async_data_alloc(int expected_rcode) +{ + struct off_nominal_async_data *adata; + + adata = ao2_alloc(sizeof(*adata), off_nominal_async_data_destructor); + if (!adata) { + return NULL; + } + + ast_mutex_init(&adata->lock); + ast_cond_init(&adata->cond, NULL); + + adata->expected_rcode = expected_rcode; + + return adata; +} + +/*! + * \brief Async callback for off-nominal async test + * + * This test ensures that there is a result present on the query, then it checks + * that the rcode on the result is the expected value and that there are no + * records on the result. + * + * Once completed, the testing thread is signaled that the async query has + * completed. + */ +static void off_nominal_async_callback(const struct ast_dns_query *query) +{ + struct off_nominal_async_data *adata = ast_dns_query_get_data(query); + struct ast_dns_result *result = ast_dns_query_get_result(query); + + if (!result) { + adata->failed = -1; + goto end; + } + + if (ast_dns_result_get_rcode(result) != adata->expected_rcode) { + adata->failed = -1; + } + + if (ast_dns_result_get_records(result)) { + adata->failed = -1; + } + +end: + ast_mutex_lock(&adata->lock); + adata->complete = 1; + ast_cond_signal(&adata->cond); + ast_mutex_unlock(&adata->lock); +} + +static int off_nominal_async_run(struct ast_test *test, const char *domain, int rr_type, + int rr_class, int expected_rcode) +{ + RAII_VAR(struct ast_dns_query_active *, active, NULL, ao2_cleanup); + RAII_VAR(struct off_nominal_async_data *, adata, NULL, ao2_cleanup); + + adata = off_nominal_async_data_alloc(expected_rcode); + if (!adata) { + ast_test_status_update(test, "Unable to allocate data for async query\n"); + return -1; + } + + ast_test_status_update(test, "Performing DNS query '%s', type %d\n", domain, rr_type); + + active = ast_dns_resolve_async(domain, rr_type, rr_class, off_nominal_async_callback, adata); + if (!active) { + ast_test_status_update(test, "Failed to perform asynchronous resolution of domain %s\n", domain); + return -1; + } + + ast_mutex_lock(&adata->lock); + while (!adata->complete) { + ast_cond_wait(&adata->cond, &adata->lock); + } + ast_mutex_unlock(&adata->lock); + + if (adata->failed) { + ast_test_status_update(test, "Asynchronous resolution failure %s\n", domain); + } + return adata->failed; +} + +static enum ast_test_result_state off_nominal_test(struct ast_test *test, + off_nominal_resolve_fn runner) +{ + RAII_VAR(struct unbound_resolver *, resolver, NULL, ao2_cleanup); + RAII_VAR(struct unbound_config *, cfg, NULL, ao2_cleanup); + + static const size_t V4_SIZE = sizeof(struct in_addr); + + static const char *DOMAIN1 = "goose.feathers"; + static const char *DOMAIN2 = "duck.feathers"; + + static const char *ADDR1 = "127.0.0.2"; + + char addr1_buf[V4_SIZE]; + + struct dns_record records [] = { + { "goose.feathers 12345 IN A 127.0.0.2", DOMAIN1, ns_t_a, ns_c_in, 12345, addr1_buf, V4_SIZE, 0, }, + }; + + int i; + enum ast_test_result_state res = AST_TEST_PASS; + + struct { + const char *domain; + int rr_type; + int rr_class; + int rcode; + } runs [] = { + { DOMAIN2, ns_t_a, ns_c_in, ns_r_nxdomain }, + { DOMAIN1, ns_t_aaaa, ns_c_in, ns_r_noerror }, + { DOMAIN1, ns_t_a, ns_c_chaos, ns_r_refused }, + }; + + inet_pton(AF_INET, ADDR1, addr1_buf); + + cfg = ao2_global_obj_ref(globals); + resolver = ao2_bump(cfg->global->state->resolver); + + ub_ctx_zone_add(resolver->context, DOMAIN1, "static"); + ub_ctx_zone_add(resolver->context, DOMAIN2, "static"); + + for (i = 0; i < ARRAY_LEN(records); ++i) { + ub_ctx_data_add(resolver->context, records[i].as_string); + } + + for (i = 0; i < ARRAY_LEN(runs); ++i) { + if (runner(test, runs[i].domain, runs[i].rr_type, runs[i].rr_class, runs[i].rcode)) { + res = AST_TEST_FAIL; + } + } + + return res; +} + +AST_TEST_DEFINE(resolve_sync_off_nominal) +{ + switch (cmd) { + case TEST_INIT: + info->name = "resolve_sync_off_nominal"; + info->category = "/res/res_resolver_unbound/"; + info->summary = "Test off-nominal synchronous resolution using libunbound\n"; + info->description = "This test performs the following:\n" + "\t* Attempt a lookup of a non-existent domain\n" + "\t* Attempt a lookup of a AAAA record on a domain that contains only A records\n" + "\t* Attempt a lookup of an A record on Chaos-net\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + return off_nominal_test(test, off_nominal_sync_run); +} + +AST_TEST_DEFINE(resolve_async_off_nominal) +{ + switch (cmd) { + case TEST_INIT: + info->name = "resolve_async_off_nominal"; + info->category = "/res/res_resolver_unbound/"; + info->summary = "Test off-nominal synchronous resolution using libunbound\n"; + info->description = "This test performs the following:\n" + "\t* Attempt a lookup of a non-existent domain\n" + "\t* Attempt a lookup of a AAAA record on a domain that contains only A records\n" + "\t* Attempt a lookup of an A record on Chaos-net\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + return off_nominal_test(test, off_nominal_async_run); +} + +/*! + * \brief Minimal data required to signal the completion of an async resolve + */ +struct async_minimal_data { + int complete; + ast_mutex_t lock; + ast_cond_t cond; +}; + +static void async_minimal_data_destructor(void *obj) +{ + struct async_minimal_data *adata = obj; + + ast_mutex_destroy(&adata->lock); + ast_cond_destroy(&adata->cond); +} + +static struct async_minimal_data *async_minimal_data_alloc(void) +{ + struct async_minimal_data *adata; + + adata = ao2_alloc(sizeof(*adata), async_minimal_data_destructor); + if (!adata) { + return NULL; + } + + ast_mutex_init(&adata->lock); + ast_cond_init(&adata->cond, NULL); + + return adata; +} + +/*! + * \brief Async callback for off-nominal cancellation test. + * + * This simply signals the testing thread that the query completed + */ +static void minimal_callback(const struct ast_dns_query *query) +{ + struct async_minimal_data *adata = ast_dns_query_get_data(query); + + ast_mutex_lock(&adata->lock); + adata->complete = 1; + ast_cond_signal(&adata->cond); + ast_mutex_unlock(&adata->lock); +} + +AST_TEST_DEFINE(resolve_cancel_off_nominal) +{ + RAII_VAR(struct ast_dns_query_active *, active, NULL, ao2_cleanup); + RAII_VAR(struct async_minimal_data *, adata, NULL, ao2_cleanup); + + switch (cmd) { + case TEST_INIT: + info->name = "resolve_cancel_off_nominal"; + info->category = "/res/res_resolver_unbound/"; + info->summary = "Off nominal cancellation test using libunbound\n"; + info->description = "This test does the following:\n" + "\t* Perform an asynchronous query\n" + "\t* Once the query has completed, attempt to cancel it\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + adata = async_minimal_data_alloc(); + if (!adata) { + ast_test_status_update(test, "Failed to allocate necessary data for test\n"); + return AST_TEST_FAIL; + } + + active = ast_dns_resolve_async("crunchy.peanut.butter", ns_t_a, ns_c_in, minimal_callback, adata); + if (!active) { + ast_test_status_update(test, "Failed to perform asynchronous query\n"); + return AST_TEST_FAIL; + } + + /* Wait for async query to complete */ + ast_mutex_lock(&adata->lock); + while (!adata->complete) { + ast_cond_wait(&adata->cond, &adata->lock); + } + ast_mutex_unlock(&adata->lock); + + if (!ast_dns_resolve_cancel(active)) { + ast_test_status_update(test, "Successfully canceled completed query\n"); + return AST_TEST_FAIL; + } + + return AST_TEST_PASS; +} +#endif + +static int reload_module(void) +{ + if (aco_process_config(&cfg_info, 1) == ACO_PROCESS_ERROR) { + return AST_MODULE_RELOAD_ERROR; + } + + return 0; +} + +static int unload_module(void) +{ + aco_info_destroy(&cfg_info); + ao2_global_obj_release(globals); + + AST_TEST_UNREGISTER(resolve_sync); + AST_TEST_UNREGISTER(resolve_async); + AST_TEST_UNREGISTER(resolve_sync_off_nominal); + AST_TEST_UNREGISTER(resolve_sync_off_nominal); + AST_TEST_UNREGISTER(resolve_cancel_off_nominal); + return 0; +} + +static int custom_nameserver_handler(const struct aco_option *opt, struct ast_variable *var, void *obj) +{ + struct unbound_global_config *global = obj; + + if (!global->nameservers) { + global->nameservers = ast_str_container_alloc_options(AO2_ALLOC_OPT_LOCK_NOLOCK, 1); + if (!global->nameservers) { + return -1; + } + } + + return ast_str_container_add(global->nameservers, var->value); +} + +static int load_module(void) +{ + struct ast_config *cfg; + struct ast_flags cfg_flags = { 0, }; + + if (aco_info_init(&cfg_info)) { + return AST_MODULE_LOAD_DECLINE; + } + + aco_option_register(&cfg_info, "hosts", ACO_EXACT, global_options, "system", OPT_STRINGFIELD_T, 0, STRFLDSET(struct unbound_global_config, hosts)); + aco_option_register(&cfg_info, "resolv", ACO_EXACT, global_options, "system", OPT_STRINGFIELD_T, 0, STRFLDSET(struct unbound_global_config, resolv)); + aco_option_register_custom(&cfg_info, "nameserver", ACO_EXACT, global_options, "", custom_nameserver_handler, 0); + aco_option_register(&cfg_info, "debug", ACO_EXACT, global_options, "0", OPT_UINT_T, 0, FLDSET(struct unbound_global_config, debug)); + aco_option_register(&cfg_info, "ta_file", ACO_EXACT, global_options, "", OPT_STRINGFIELD_T, 0, STRFLDSET(struct unbound_global_config, ta_file)); + + /* This purposely checks for a configuration file so we don't output an error message in ACO if one is not present */ + cfg = ast_config_load(resolver_unbound_conf.filename, cfg_flags); + if (!cfg) { + if (unbound_config_apply_default()) { + unload_module(); + return AST_MODULE_LOAD_DECLINE; + } + } else { + ast_config_destroy(cfg); + if (aco_process_config(&cfg_info, 0) == ACO_PROCESS_ERROR) { + unload_module(); + return AST_MODULE_LOAD_DECLINE; + } + } + + ast_dns_resolver_register(&unbound_resolver); + + ast_module_shutdown_ref(ast_module_info->self); + + AST_TEST_REGISTER(resolve_sync); + AST_TEST_REGISTER(resolve_async); + AST_TEST_REGISTER(resolve_sync_off_nominal); + AST_TEST_REGISTER(resolve_async_off_nominal); + AST_TEST_REGISTER(resolve_cancel_off_nominal); + + return AST_MODULE_LOAD_SUCCESS; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "Unbound DNS Resolver Support", + .support_level = AST_MODULE_SUPPORT_CORE, + .load = load_module, + .unload = unload_module, + .reload = reload_module, + .load_pri = AST_MODPRI_CHANNEL_DEPEND - 4, + ); diff --git a/tests/test_dns.c b/tests/test_dns.c new file mode 100644 index 000000000..13306adc8 --- /dev/null +++ b/tests/test_dns.c @@ -0,0 +1,1355 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Mark Michelson + * + * Mark Michelson + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*** MODULEINFO + TEST_FRAMEWORK + core + ***/ + +#include "asterisk.h" + +#include +#include + +#include "asterisk/test.h" +#include "asterisk/module.h" +#include "asterisk/dns_core.h" +#include "asterisk/dns_resolver.h" +#include "asterisk/dns_internal.h" + +/* Used when a stub is needed for certain tests */ +static int stub_resolve(struct ast_dns_query *query) +{ + return 0; +} + +/* Used when a stub is needed for certain tests */ +static int stub_cancel(struct ast_dns_query *query) +{ + return 0; +} + +AST_TEST_DEFINE(resolver_register_unregister) +{ + struct ast_dns_resolver cool_guy_resolver = { + .name = "A snake that swallowed a deer", + .priority = 19890504, + .resolve = stub_resolve, + .cancel = stub_cancel, + }; + + switch (cmd) { + case TEST_INIT: + info->name = "resolver_register_unregister"; + info->category = "/main/dns/"; + info->summary = "Test nominal resolver registration and unregistration"; + info->description = + "The test performs the following steps:\n" + "\t* Register a valid resolver.\n" + "\t* Unregister the resolver.\n" + "If either step fails, the test fails\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + if (ast_dns_resolver_register(&cool_guy_resolver)) { + ast_test_status_update(test, "Unable to register a perfectly good resolver\n"); + return AST_TEST_FAIL; + } + + ast_dns_resolver_unregister(&cool_guy_resolver); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(resolver_register_off_nominal) +{ + struct ast_dns_resolver valid = { + .name = "valid", + .resolve = stub_resolve, + .cancel = stub_cancel, + }; + + struct ast_dns_resolver incomplete1 = { + .name = NULL, + .resolve = stub_resolve, + .cancel = stub_cancel, + }; + + struct ast_dns_resolver incomplete2 = { + .name = "incomplete2", + .resolve = NULL, + .cancel = stub_cancel, + }; + + struct ast_dns_resolver incomplete3 = { + .name = "incomplete3", + .resolve = stub_resolve, + .cancel = NULL, + }; + + switch (cmd) { + case TEST_INIT: + info->name = "resolver_register_off_nominal"; + info->category = "/main/dns/"; + info->summary = "Test off-nominal resolver registration"; + info->description = + "Test off-nominal resolver registration:\n" + "\t* Register a duplicate resolver\n" + "\t* Register a resolver without a name\n" + "\t* Register a resolver without a resolve() method\n" + "\t* Register a resolver without a cancel() method\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + if (ast_dns_resolver_register(&valid)) { + ast_test_status_update(test, "Failed to register valid resolver\n"); + return AST_TEST_FAIL; + } + + if (!ast_dns_resolver_register(&valid)) { + ast_test_status_update(test, "Successfully registered the same resolver multiple times\n"); + return AST_TEST_FAIL; + } + + ast_dns_resolver_unregister(&valid); + + if (!ast_dns_resolver_register(NULL)) { + ast_test_status_update(test, "Successfully registered a NULL resolver\n"); + return AST_TEST_FAIL; + } + + if (!ast_dns_resolver_register(&incomplete1)) { + ast_test_status_update(test, "Successfully registered a DNS resolver with no name\n"); + return AST_TEST_FAIL; + } + + if (!ast_dns_resolver_register(&incomplete2)) { + ast_test_status_update(test, "Successfully registered a DNS resolver with no resolve() method\n"); + return AST_TEST_FAIL; + } + + if (!ast_dns_resolver_register(&incomplete3)) { + ast_test_status_update(test, "Successfully registered a DNS resolver with no cancel() method\n"); + return AST_TEST_FAIL; + } + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(resolver_unregister_off_nominal) +{ + struct ast_dns_resolver non_existent = { + .name = "I do not exist", + .priority = 20141004, + .resolve = stub_resolve, + .cancel = stub_cancel, + }; + + switch (cmd) { + case TEST_INIT: + info->name = "resolver_unregister_off_nominal"; + info->category = "/main/dns/"; + info->summary = "Test off-nominal DNS resolver unregister"; + info->description = + "The test attempts the following:\n" + "\t* Unregister a resolver that is not registered.\n" + "\t* Unregister a NULL pointer.\n" + "Because unregistering a resolver does not return an indicator of success, the best\n" + "this test can do is verify that nothing blows up when this is attempted.\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + ast_dns_resolver_unregister(&non_existent); + ast_dns_resolver_unregister(NULL); + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(resolver_data) +{ + struct ast_dns_query some_query; + + struct digits { + int fingers; + int toes; + }; + + RAII_VAR(struct digits *, average, NULL, ao2_cleanup); + RAII_VAR(struct digits *, polydactyl, NULL, ao2_cleanup); + + struct digits *data_ptr; + + switch (cmd) { + case TEST_INIT: + info->name = "resolver_data"; + info->category = "/main/dns/"; + info->summary = "Test getting and setting data on a DNS resolver"; + info->description = "This test does the following:\n" + "\t* Ensure that requesting resolver data results in a NULL return if no data has been set.\n" + "\t* Ensure that setting resolver data does not result in an error.\n" + "\t* Ensure that retrieving the set resolver data returns the data we expect\n" + "\t* Ensure that setting new resolver data on the query does not result in an error\n" + "\t* Ensure that retrieving the resolver data returns the new data that we set\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + memset(&some_query, 0, sizeof(some_query)); + + average = ao2_alloc(sizeof(*average), NULL); + polydactyl = ao2_alloc(sizeof(*average), NULL); + + if (!average || !polydactyl) { + ast_test_status_update(test, "Allocation failure during unit test\n"); + return AST_TEST_FAIL; + } + + /* Ensure that NULL is retrieved if we haven't set anything on the query */ + data_ptr = ast_dns_resolver_get_data(&some_query); + if (data_ptr) { + ast_test_status_update(test, "Retrieved non-NULL resolver data from query unexpectedly\n"); + return AST_TEST_FAIL; + } + + if (ast_dns_resolver_set_data(&some_query, average)) { + ast_test_status_update(test, "Failed to set resolver data on query\n"); + return AST_TEST_FAIL; + } + + /* Go ahead now and remove the query's reference to the resolver data to prevent memory leaks */ + ao2_ref(average, -1); + + /* Ensure that data can be set and retrieved */ + data_ptr = ast_dns_resolver_get_data(&some_query); + if (!data_ptr) { + ast_test_status_update(test, "Unable to retrieve resolver data from DNS query\n"); + return AST_TEST_FAIL; + } + + if (data_ptr != average) { + ast_test_status_update(test, "Unexpected resolver data retrieved from DNS query\n"); + return AST_TEST_FAIL; + } + + /* Ensure that attempting to set new resolver data on the query fails */ + if (!ast_dns_resolver_set_data(&some_query, polydactyl)) { + ast_test_status_update(test, "Successfully overwrote resolver data on a query. We shouldn't be able to do that\n"); + return AST_TEST_FAIL; + } + + return AST_TEST_PASS; +} + +static int test_results(struct ast_test *test, const struct ast_dns_query *query, + unsigned int expected_secure, unsigned int expected_bogus, + unsigned int expected_rcode, const char *expected_canonical, + const char *expected_answer, size_t answer_size) +{ + struct ast_dns_result *result; + + result = ast_dns_query_get_result(query); + if (!result) { + ast_test_status_update(test, "Unable to retrieve result from query\n"); + return -1; + } + + if (ast_dns_result_get_secure(result) != expected_secure || + ast_dns_result_get_bogus(result) != expected_bogus || + ast_dns_result_get_rcode(result) != expected_rcode || + strcmp(ast_dns_result_get_canonical(result), expected_canonical) || + memcmp(ast_dns_result_get_answer(result), expected_answer, answer_size)) { + ast_test_status_update(test, "Unexpected values in result from query\n"); + return -1; + } + + return 0; +} + +/* When setting a DNS result, we have to provide the raw DNS answer. This + * is not happening. Sorry. Instead, we provide a dummy string and call it + * a day + */ +#define DNS_ANSWER "Grumble Grumble" +#define DNS_ANSWER_SIZE strlen(DNS_ANSWER) + +AST_TEST_DEFINE(resolver_set_result) +{ + struct ast_dns_query some_query; + struct ast_dns_result *result; + + struct dns_result { + unsigned int secure; + unsigned int bogus; + unsigned int rcode; + } results[] = { + { 0, 0, ns_r_noerror, }, + { 0, 1, ns_r_noerror, }, + { 1, 0, ns_r_noerror, }, + { 0, 0, ns_r_nxdomain, }, + }; + int i; + enum ast_test_result_state res = AST_TEST_PASS; + + switch (cmd) { + case TEST_INIT: + info->name = "resolver_set_result"; + info->category = "/main/dns/"; + info->summary = "Test setting and getting results on DNS queries"; + info->description = + "This test performs the following:\n" + "\t* Sets a result that is not secure, bogus, and has rcode 0\n" + "\t* Sets a result that is not secure, has rcode 0, but is secure\n" + "\t* Sets a result that is not bogus, has rcode 0, but is secure\n" + "\t* Sets a result that is not secure or bogus, but has rcode NXDOMAIN\n" + "After each result is set, we ensure that parameters retrieved from\n" + "the result have the expected values."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + memset(&some_query, 0, sizeof(some_query)); + + for (i = 0; i < ARRAY_LEN(results); ++i) { + if (ast_dns_resolver_set_result(&some_query, results[i].secure, results[i].bogus, + results[i].rcode, "asterisk.org", DNS_ANSWER, DNS_ANSWER_SIZE)) { + ast_test_status_update(test, "Unable to add DNS result to query\n"); + res = AST_TEST_FAIL; + } + + if (test_results(test, &some_query, results[i].secure, results[i].bogus, + results[i].rcode, "asterisk.org", DNS_ANSWER, DNS_ANSWER_SIZE)) { + res = AST_TEST_FAIL; + } + } + + /* The final result we set needs to be freed */ + result = ast_dns_query_get_result(&some_query); + ast_dns_result_free(result); + + return res; +} + +AST_TEST_DEFINE(resolver_set_result_off_nominal) +{ + struct ast_dns_query some_query; + struct ast_dns_result *result; + + switch (cmd) { + case TEST_INIT: + info->name = "resolver_set_result_off_nominal"; + info->category = "/main/dns/"; + info->summary = "Test setting off-nominal DNS results\n"; + info->description = + "This test performs the following:\n" + "\t* Attempt to add a DNS result that is both bogus and secure\n" + "\t* Attempt to add a DNS result that has no canonical name\n" + "\t* Attempt to add a DNS result that has no answer\n" + "\t* Attempt to add a DNS result with a zero answer size\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + memset(&some_query, 0, sizeof(some_query)); + + if (!ast_dns_resolver_set_result(&some_query, 1, 1, ns_r_noerror, "asterisk.org", + DNS_ANSWER, DNS_ANSWER_SIZE)) { + ast_test_status_update(test, "Successfully added a result that was both secure and bogus\n"); + result = ast_dns_query_get_result(&some_query); + ast_dns_result_free(result); + return AST_TEST_FAIL; + } + + if (!ast_dns_resolver_set_result(&some_query, 0, 0, ns_r_noerror, NULL, + DNS_ANSWER, DNS_ANSWER_SIZE)) { + ast_test_status_update(test, "Successfully added result with no canonical name\n"); + result = ast_dns_query_get_result(&some_query); + ast_dns_result_free(result); + return AST_TEST_FAIL; + } + + if (!ast_dns_resolver_set_result(&some_query, 0, 0, ns_r_noerror, NULL, + NULL, DNS_ANSWER_SIZE)) { + ast_test_status_update(test, "Successfully added result with no answer\n"); + result = ast_dns_query_get_result(&some_query); + ast_dns_result_free(result); + return AST_TEST_FAIL; + } + + if (!ast_dns_resolver_set_result(&some_query, 0, 0, ns_r_noerror, NULL, + DNS_ANSWER, 0)) { + ast_test_status_update(test, "Successfully added result with answer size of zero\n"); + result = ast_dns_query_get_result(&some_query); + ast_dns_result_free(result); + return AST_TEST_FAIL; + } + + return AST_TEST_PASS; +} + +static int test_record(struct ast_test *test, const struct ast_dns_record *record, + int rr_type, int rr_class, int ttl, const char *data, const size_t size) +{ + if (ast_dns_record_get_rr_type(record) != rr_type) { + ast_test_status_update(test, "Unexpected rr_type from DNS record\n"); + return -1; + } + + if (ast_dns_record_get_rr_class(record) != rr_class) { + ast_test_status_update(test, "Unexpected rr_class from DNS record\n"); + return -1; + } + + if (ast_dns_record_get_ttl(record) != ttl) { + ast_test_status_update(test, "Unexpected ttl from DNS record\n"); + return -1; + } + + if (memcmp(ast_dns_record_get_data(record), data, size)) { + ast_test_status_update(test, "Unexpected data in DNS record\n"); + return -1; + } + + return 0; +} + +AST_TEST_DEFINE(resolver_add_record) +{ + RAII_VAR(struct ast_dns_result *, result, NULL, ast_dns_result_free); + struct ast_dns_query some_query; + const struct ast_dns_record *record; + + static const char *V4 = "127.0.0.1"; + static const size_t V4_BUFSIZE = sizeof(struct in_addr); + char v4_buf[V4_BUFSIZE]; + + static const char *V6 = "::1"; + static const size_t V6_BUFSIZE = sizeof(struct in6_addr); + char v6_buf[V6_BUFSIZE]; + + struct dns_record_details { + int type; + int class; + int ttl; + const char *data; + const size_t size; + int visited; + } records[] = { + { ns_t_a, ns_c_in, 12345, v4_buf, V4_BUFSIZE, 0, }, + { ns_t_aaaa, ns_c_in, 12345, v6_buf, V6_BUFSIZE, 0, }, + }; + + int num_records_visited = 0; + + switch (cmd) { + case TEST_INIT: + info->name = "resolver_add_record"; + info->category = "/main/dns/"; + info->summary = "Test adding DNS records to a query"; + info->description = + "This test performs the following:\n" + "\t* Ensure a nominal A record can be added to a query result\n" + "\t* Ensures that the record can be retrieved\n" + "\t* Ensure that a second record can be added to the query result\n" + "\t* Ensures that both records can be retrieved\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + memset(&some_query, 0, sizeof(some_query)); + + if (ast_dns_resolver_set_result(&some_query, 0, 0, ns_r_noerror, "asterisk.org", + DNS_ANSWER, DNS_ANSWER_SIZE)) { + ast_test_status_update(test, "Unable to set result for DNS query\n"); + return AST_TEST_FAIL; + } + + result = ast_dns_query_get_result(&some_query); + if (!result) { + ast_test_status_update(test, "Unable to retrieve result from query\n"); + return AST_TEST_FAIL; + } + + inet_pton(AF_INET, V4, v4_buf); + + /* Nominal Record */ + if (ast_dns_resolver_add_record(&some_query, records[0].type, records[0].class, + records[0].ttl, records[0].data, records[0].size)) { + ast_test_status_update(test, "Unable to add nominal record to query result\n"); + return AST_TEST_FAIL; + } + + /* I should only be able to retrieve one record */ + record = ast_dns_result_get_records(result); + if (!record) { + ast_test_status_update(test, "Unable to retrieve record from result\n"); + return AST_TEST_FAIL; + } + + if (test_record(test, record, records[0].type, records[0].class, records[0].ttl, + records[0].data, records[0].size)) { + return AST_TEST_FAIL; + } + + if (ast_dns_record_get_next(record)) { + ast_test_status_update(test, "Multiple records returned when only one was expected\n"); + return AST_TEST_FAIL; + } + + inet_pton(AF_INET6, V6, v6_buf); + + if (ast_dns_resolver_add_record(&some_query, records[1].type, records[1].class, + records[1].ttl, records[1].data, records[1].size)) { + ast_test_status_update(test, "Unable to add second record to query result\n"); + return AST_TEST_FAIL; + } + + for (record = ast_dns_result_get_records(result); record; record = ast_dns_record_get_next(record)) { + int res; + + /* The order of returned records is not specified by the API. We use the record type + * as the discriminator to determine which record data to expect. + */ + if (ast_dns_record_get_rr_type(record) == records[0].type) { + res = test_record(test, record, records[0].type, records[0].class, records[0].ttl, records[0].data, records[0].size); + records[0].visited = 1; + } else if (ast_dns_record_get_rr_type(record) == records[1].type) { + res = test_record(test, record, records[1].type, records[1].class, records[1].ttl, records[1].data, records[1].size); + records[1].visited = 1; + } else { + ast_test_status_update(test, "Unknown record type found in DNS results\n"); + return AST_TEST_FAIL; + } + + if (res) { + return AST_TEST_FAIL; + } + + ++num_records_visited; + } + + if (!records[0].visited || !records[1].visited) { + ast_test_status_update(test, "Did not visit all added DNS records\n"); + return AST_TEST_FAIL; + } + + if (num_records_visited != ARRAY_LEN(records)) { + ast_test_status_update(test, "Did not visit the expected number of DNS records\n"); + return AST_TEST_FAIL; + } + + return AST_TEST_PASS; +} + +AST_TEST_DEFINE(resolver_add_record_off_nominal) +{ + RAII_VAR(struct ast_dns_result *, result, NULL, ast_dns_result_free); + struct ast_dns_query some_query; + static const char *V4 = "127.0.0.1"; + static const size_t V4_BUFSIZE = sizeof(struct in_addr); + char v4_buf[V4_BUFSIZE]; + + switch (cmd) { + case TEST_INIT: + info->name = "resolver_add_record_off_nominal"; + info->category = "/main/dns/"; + info->summary = "Test adding off-nominal DNS records to a query"; + info->description = + "This test performs the following:\n" + "\t* Ensure a nominal A record cannot be added if no result has been set.\n" + "\t* Ensure that an A record with invalid RR types cannot be added to a query\n" + "\t* Ensure that an A record with invalid RR classes cannot be added to a query\n" + "\t* Ensure that an A record with invalid TTL cannot be added to a query\n" + "\t* Ensure that an A record with NULL data cannot be added to a query\n" + "\t* Ensure that an A record with invalid length cannot be added to a query\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + memset(&some_query, 0, sizeof(some_query)); + + inet_ntop(AF_INET, V4, v4_buf, V4_BUFSIZE); + + /* Add record before setting result */ + if (!ast_dns_resolver_add_record(&some_query, ns_t_a, ns_c_in, 12345, v4_buf, V4_BUFSIZE)) { + ast_test_status_update(test, "Successfully added DNS record to query before setting a result\n"); + return AST_TEST_FAIL; + } + + if (ast_dns_resolver_set_result(&some_query, 0, 0, ns_r_noerror, "asterisk.org", + DNS_ANSWER, DNS_ANSWER_SIZE)) { + ast_test_status_update(test, "Unable to set result for DNS query\n"); + return AST_TEST_FAIL; + } + + /* We get the result so it will be cleaned up when the function exits */ + result = ast_dns_query_get_result(&some_query); + + /* Invalid RR types */ + if (!ast_dns_resolver_add_record(&some_query, -1, ns_c_in, 12345, v4_buf, V4_BUFSIZE)) { + ast_test_status_update(test, "Successfully added DNS record with negative RR type\n"); + return AST_TEST_FAIL; + } + + if (!ast_dns_resolver_add_record(&some_query, ns_t_max + 1, ns_c_in, 12345, v4_buf, V4_BUFSIZE)) { + ast_test_status_update(test, "Successfully added DNS record with too large RR type\n"); + return AST_TEST_FAIL; + } + + /* Invalid RR classes */ + if (!ast_dns_resolver_add_record(&some_query, ns_t_a, -1, 12345, v4_buf, V4_BUFSIZE)) { + ast_test_status_update(test, "Successfully added DNS record with negative RR class\n"); + return AST_TEST_FAIL; + } + + if (!ast_dns_resolver_add_record(&some_query, ns_t_a, ns_c_max + 1, 12345, v4_buf, V4_BUFSIZE)) { + ast_test_status_update(test, "Successfully added DNS record with too large RR class\n"); + return AST_TEST_FAIL; + } + + /* Invalid TTL */ + if (!ast_dns_resolver_add_record(&some_query, ns_t_a, ns_c_in, -1, v4_buf, V4_BUFSIZE)) { + ast_test_status_update(test, "Successfully added DNS record with negative TTL\n"); + return AST_TEST_FAIL; + } + + /* No data */ + if (!ast_dns_resolver_add_record(&some_query, ns_t_a, ns_c_in, 12345, NULL, 0)) { + ast_test_status_update(test, "Successfully added a DNS record with no data\n"); + return AST_TEST_FAIL; + } + + /* Lie about the length */ + if (!ast_dns_resolver_add_record(&some_query, ns_t_a, ns_c_in, 12345, v4_buf, 0)) { + ast_test_status_update(test, "Successfully added a DNS record with length zero\n"); + return AST_TEST_FAIL; + } + + return AST_TEST_PASS; +} + +/*! + * \brief File-scoped data used during resolver tests + * + * This data has to live at file-scope since it needs to be + * accessible by multiple threads. + */ +static struct resolver_data { + /*! True if the resolver's resolve() method has been called */ + int resolve_called; + /*! True if the resolver's cancel() method has been called */ + int canceled; + /*! True if resolution successfully completed. This is mutually exclusive with \ref canceled */ + int resolution_complete; + /*! Lock used for protecting \ref cancel_cond */ + ast_mutex_t lock; + /*! Condition variable used to coordinate canceling a query */ + ast_cond_t cancel_cond; +} test_resolver_data; + +/*! + * \brief Thread spawned by the mock resolver + * + * All DNS resolvers are required to be asynchronous. The mock resolver + * spawns this thread for every DNS query that is executed. + * + * This thread waits for 5 seconds and then returns the same A record + * every time. The 5 second wait is to allow for the query to be + * canceled if desired + * + * \param dns_query The ast_dns_query that is being resolved + * \return NULL + */ +static void *resolution_thread(void *dns_query) +{ + struct ast_dns_query *query = dns_query; + struct timespec timeout; + + static const char *V4 = "127.0.0.1"; + static const size_t V4_BUFSIZE = sizeof(struct in_addr); + char v4_buf[V4_BUFSIZE]; + + clock_gettime(CLOCK_REALTIME, &timeout); + timeout.tv_sec += 5; + + ast_mutex_lock(&test_resolver_data.lock); + while (!test_resolver_data.canceled) { + if (ast_cond_timedwait(&test_resolver_data.cancel_cond, &test_resolver_data.lock, &timeout) == ETIMEDOUT) { + break; + } + } + ast_mutex_unlock(&test_resolver_data.lock); + + if (test_resolver_data.canceled) { + ast_dns_resolver_completed(query); + ao2_ref(query, -1); + return NULL; + } + + ast_dns_resolver_set_result(query, 0, 0, ns_r_noerror, "asterisk.org", DNS_ANSWER, DNS_ANSWER_SIZE); + + inet_pton(AF_INET, V4, v4_buf); + ast_dns_resolver_add_record(query, ns_t_a, ns_c_in, 12345, v4_buf, V4_BUFSIZE); + + test_resolver_data.resolution_complete = 1; + ast_dns_resolver_completed(query); + + ao2_ref(query, -1); + return NULL; +} + +/*! + * \brief Mock resolver's resolve method + * + * \param query The query to resolve + * \retval 0 Successfully spawned resolution thread + * \retval non-zero Failed to spawn the resolution thread + */ +static int test_resolve(struct ast_dns_query *query) +{ + pthread_t resolver_thread; + + test_resolver_data.resolve_called = 1; + return ast_pthread_create_detached(&resolver_thread, NULL, resolution_thread, ao2_bump(query)); +} + +/*! + * \brief Mock resolver's cancel method + * + * This signals the resolution thread not to return any DNS results. + * + * \param query DNS query to cancel + * \return 0 + */ +static int test_cancel(struct ast_dns_query *query) +{ + ast_mutex_lock(&test_resolver_data.lock); + test_resolver_data.canceled = 1; + ast_cond_signal(&test_resolver_data.cancel_cond); + ast_mutex_unlock(&test_resolver_data.lock); + + return 0; +} + +/*! + * \brief Initialize global mock resolver data. + * + * This must be called at the beginning of tests that use the mock resolver + */ +static void resolver_data_init(void) +{ + test_resolver_data.resolve_called = 0; + test_resolver_data.canceled = 0; + test_resolver_data.resolution_complete = 0; + + ast_mutex_init(&test_resolver_data.lock); + ast_cond_init(&test_resolver_data.cancel_cond, NULL); +} + +/*! + * \brief Cleanup global mock resolver data + * + * This must be called at the end of tests that use the mock resolver + */ +static void resolver_data_cleanup(void) +{ + ast_mutex_destroy(&test_resolver_data.lock); + ast_cond_destroy(&test_resolver_data.cancel_cond); +} + +/*! + * \brief The mock resolver + * + * The mock resolver does not care about the DNS query that is + * actually being made on it. It simply regurgitates the same + * DNS record no matter what. + */ +static struct ast_dns_resolver test_resolver = { + .name = "test", + .priority = 0, + .resolve = test_resolve, + .cancel = test_cancel, +}; + +AST_TEST_DEFINE(resolver_resolve_sync) +{ + RAII_VAR(struct ast_dns_result *, result, NULL, ast_dns_result_free); + enum ast_test_result_state res = AST_TEST_PASS; + + switch (cmd) { + case TEST_INIT: + info->name = "resolver_resolve_sync"; + info->category = "/main/dns/"; + info->summary = "Test a nominal synchronous DNS resolution"; + info->description = + "This test performs a synchronous DNS resolution of a domain. The goal of this\n" + "test is not to check the records for accuracy. Rather, the goal is to ensure that\n" + "the resolver is called into as expected, that the query completes entirely before\n" + "returning from the synchronous resolution, that nothing tried to cancel the resolution\n," + "and that some records were returned."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + if (ast_dns_resolver_register(&test_resolver)) { + ast_test_status_update(test, "Unable to register test resolver\n"); + return AST_TEST_FAIL; + } + + resolver_data_init(); + + if (ast_dns_resolve("asterisk.org", ns_t_a, ns_c_in, &result)) { + ast_test_status_update(test, "Resolution of address failed\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + if (!result) { + ast_test_status_update(test, "DNS resolution returned a NULL result\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + if (!test_resolver_data.resolve_called) { + ast_test_status_update(test, "DNS resolution did not call resolver's resolve() method\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + if (test_resolver_data.canceled) { + ast_test_status_update(test, "Resolver's cancel() method called for no reason\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + if (!test_resolver_data.resolution_complete) { + ast_test_status_update(test, "Synchronous resolution completed early?\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + if (!ast_dns_result_get_records(result)) { + ast_test_status_update(test, "Synchronous resolution yielded no records.\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + +cleanup: + ast_dns_resolver_unregister(&test_resolver); + resolver_data_cleanup(); + return res; +} + +/*! + * \brief A resolve() method that simply fails + * + * \param query The DNS query to resolve. This is ignored. + * \return -1 + */ +static int fail_resolve(struct ast_dns_query *query) +{ + return -1; +} + +AST_TEST_DEFINE(resolver_resolve_sync_off_nominal) +{ + struct ast_dns_resolver terrible_resolver = { + .name = "Uwe Boll's Filmography", + .priority = 0, + .resolve = fail_resolve, + .cancel = stub_cancel, + }; + + struct ast_dns_result *result = NULL; + + struct dns_resolve_data { + const char *name; + int rr_type; + int rr_class; + struct ast_dns_result **result; + } resolves [] = { + { NULL, ns_t_a, ns_c_in, &result }, + { "asterisk.org", -1, ns_c_in, &result }, + { "asterisk.org", ns_t_max + 1, ns_c_in, &result }, + { "asterisk.org", ns_t_a, -1, &result }, + { "asterisk.org", ns_t_a, ns_c_max + 1, &result }, + { "asterisk.org", ns_t_a, ns_c_in, NULL }, + }; + + int i; + + enum ast_test_result_state res = AST_TEST_PASS; + + switch (cmd) { + case TEST_INIT: + info->name = "resolver_resolve_sync_off_nominal"; + info->category = "/main/dns/"; + info->summary = "Test off-nominal synchronous DNS resolution"; + info->description = + "This test performs several off-nominal synchronous DNS resolutions:\n" + "\t* Attempt resolution with NULL name\n", + "\t* Attempt resolution with invalid RR type\n", + "\t* Attempt resolution with invalid RR class\n", + "\t* Attempt resolution with NULL result pointer\n", + "\t* Attempt resolution with resolver that returns an error\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + if (ast_dns_resolver_register(&test_resolver)) { + ast_test_status_update(test, "Failed to register test resolver\n"); + return AST_TEST_FAIL; + } + + for (i = 0; i < ARRAY_LEN(resolves); ++i) { + if (!ast_dns_resolve(resolves[i].name, resolves[i].rr_type, resolves[i].rr_class, resolves[i].result)) { + ast_test_status_update(test, "Successfully resolved DNS query with invalid parameters\n"); + res = AST_TEST_FAIL; + } else if (result) { + ast_test_status_update(test, "Failed resolution set a non-NULL result\n"); + ast_dns_result_free(result); + res = AST_TEST_FAIL; + } + } + + ast_dns_resolver_unregister(&test_resolver); + + /* As a final test, try a legitimate query with a bad resolver */ + if (ast_dns_resolver_register(&terrible_resolver)) { + ast_test_status_update(test, "Failed to register the terrible resolver\n"); + return AST_TEST_FAIL; + } + + if (!ast_dns_resolve("asterisk.org", ns_t_a, ns_c_in, &result)) { + ast_test_status_update(test, "DNS resolution succeeded when we expected it not to\n"); + ast_dns_resolver_unregister(&terrible_resolver); + return AST_TEST_FAIL; + } + + ast_dns_resolver_unregister(&terrible_resolver); + + if (result) { + ast_test_status_update(test, "Failed DNS resolution set the result to something non-NULL\n"); + ast_dns_result_free(result); + return AST_TEST_FAIL; + } + + return res; +} + +/*! + * \brief Data used by async result callback + * + * This is the typical combination of boolean, lock, and condition + * used to synchronize the activities of two threads. In this case, + * the testing thread waits on the condition, and the async callback + * signals the condition when the asynchronous callback is complete. + */ +struct async_resolution_data { + int complete; + ast_mutex_t lock; + ast_cond_t cond; +}; + +/*! + * \brief Destructor for async_resolution_data + */ +static void async_data_destructor(void *obj) +{ + struct async_resolution_data *async_data = obj; + + ast_mutex_destroy(&async_data->lock); + ast_cond_destroy(&async_data->cond); +} + +/*! + * \brief Allocation/initialization for async_resolution_data + * + * The DNS core mandates that a query's user data has to be ao2 allocated, + * so this is a helper method for doing that. + * + * \retval NULL Failed allocation + * \retval non-NULL Newly allocated async_resolution_data + */ +static struct async_resolution_data *async_data_alloc(void) +{ + struct async_resolution_data *async_data; + + async_data = ao2_alloc(sizeof(*async_data), async_data_destructor); + if (!async_data) { + return NULL; + } + + async_data->complete = 0; + ast_mutex_init(&async_data->lock); + ast_cond_init(&async_data->cond, NULL); + + return async_data; +} + +/*! + * \brief Async DNS callback + * + * This is called when an async query completes, either because it resolved or + * because it was canceled. In our case, this callback is used to signal to the + * test that it can continue + * + * \param query The DNS query that has completed + */ +static void async_callback(const struct ast_dns_query *query) +{ + struct async_resolution_data *async_data = ast_dns_query_get_data(query); + + ast_mutex_lock(&async_data->lock); + async_data->complete = 1; + ast_cond_signal(&async_data->cond); + ast_mutex_unlock(&async_data->lock); +} + +AST_TEST_DEFINE(resolver_resolve_async) +{ + RAII_VAR(struct async_resolution_data *, async_data, NULL, ao2_cleanup); + RAII_VAR(struct ast_dns_query_active *, active, NULL, ao2_cleanup); + struct ast_dns_result *result; + enum ast_test_result_state res = AST_TEST_PASS; + struct timespec timeout; + + switch (cmd) { + case TEST_INIT: + info->name = "resolver_resolve_async"; + info->category = "/main/dns/"; + info->summary = "Test a nominal asynchronous DNS resolution"; + info->description = + "This test performs an asynchronous DNS resolution of a domain. The goal of this\n" + "test is not to check the records for accuracy. Rather, the goal is to ensure that\n" + "the resolver is called into as expected, that we regain control before the query\n" + "is completed, and to ensure that nothing tried to cancel the resolution."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + if (ast_dns_resolver_register(&test_resolver)) { + ast_test_status_update(test, "Unable to register test resolver\n"); + return AST_TEST_FAIL; + } + + resolver_data_init(); + + async_data = async_data_alloc(); + if (!async_data) { + ast_test_status_update(test, "Failed to allocate asynchronous data\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + active = ast_dns_resolve_async("asterisk.org", ns_t_a, ns_c_in, async_callback, async_data); + if (!active) { + ast_test_status_update(test, "Asynchronous resolution of address failed\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + if (!test_resolver_data.resolve_called) { + ast_test_status_update(test, "DNS resolution did not call resolver's resolve() method\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + if (test_resolver_data.canceled) { + ast_test_status_update(test, "Resolver's cancel() method called for no reason\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + clock_gettime(CLOCK_REALTIME, &timeout); + timeout.tv_sec += 10; + ast_mutex_lock(&async_data->lock); + while (!async_data->complete) { + if (ast_cond_timedwait(&async_data->cond, &async_data->lock, &timeout) == ETIMEDOUT) { + break; + } + } + ast_mutex_unlock(&async_data->lock); + + if (!async_data->complete) { + ast_test_status_update(test, "Asynchronous resolution timed out\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + if (!test_resolver_data.resolution_complete) { + ast_test_status_update(test, "Asynchronous resolution completed early?\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + result = ast_dns_query_get_result(active->query); + if (!result) { + ast_test_status_update(test, "Asynchronous resolution yielded no result\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + if (!ast_dns_result_get_records(result)) { + ast_test_status_update(test, "Asynchronous result had no records\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + +cleanup: + ast_dns_resolver_unregister(&test_resolver); + resolver_data_cleanup(); + return res; +} + +/*! Stub async resolution callback */ +static void stub_callback(const struct ast_dns_query *query) +{ + return; +} + +AST_TEST_DEFINE(resolver_resolve_async_off_nominal) +{ + struct ast_dns_resolver terrible_resolver = { + .name = "Ed Wood's Filmography", + .priority = 0, + .resolve = fail_resolve, + .cancel = stub_cancel, + }; + + struct dns_resolve_data { + const char *name; + int rr_type; + int rr_class; + ast_dns_resolve_callback callback; + } resolves [] = { + { NULL, ns_t_a, ns_c_in, stub_callback }, + { "asterisk.org", -1, ns_c_in, stub_callback }, + { "asterisk.org", ns_t_max + 1, ns_c_in, stub_callback }, + { "asterisk.org", ns_t_a, -1, stub_callback }, + { "asterisk.org", ns_t_a, ns_c_max + 1, stub_callback }, + { "asterisk.org", ns_t_a, ns_c_in, NULL }, + }; + + struct ast_dns_query_active *active; + enum ast_test_result_state res = AST_TEST_PASS; + int i; + + switch (cmd) { + case TEST_INIT: + info->name = "resolver_resolve_async_off_nominal"; + info->category = "/main/dns/"; + info->summary = "Test off-nominal asynchronous DNS resolution"; + info->description = + "This test performs several off-nominal asynchronous DNS resolutions:\n" + "\t* Attempt resolution with NULL name\n", + "\t* Attempt resolution with invalid RR type\n", + "\t* Attempt resolution with invalid RR class\n", + "\t* Attempt resolution with NULL callback pointer\n", + "\t* Attempt resolution with resolver that returns an error\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + if (ast_dns_resolver_register(&test_resolver)) { + ast_test_status_update(test, "Failed to register test resolver\n"); + return AST_TEST_FAIL; + } + + for (i = 0; i < ARRAY_LEN(resolves); ++i) { + active = ast_dns_resolve_async(resolves[i].name, resolves[i].rr_type, resolves[i].rr_class, + resolves[i].callback, NULL); + if (active) { + ast_test_status_update(test, "Successfully performed asynchronous resolution with invalid data\n"); + ao2_ref(active, -1); + res = AST_TEST_FAIL; + } + } + + ast_dns_resolver_unregister(&test_resolver); + + if (ast_dns_resolver_register(&terrible_resolver)) { + ast_test_status_update(test, "Failed to register the DNS resolver\n"); + return AST_TEST_FAIL; + } + + active = ast_dns_resolve_async("asterisk.org", ns_t_a, ns_c_in, stub_callback, NULL); + + ast_dns_resolver_unregister(&terrible_resolver); + + if (active) { + ast_test_status_update(test, "Successfully performed asynchronous resolution with invalid data\n"); + ao2_ref(active, -1); + return AST_TEST_FAIL; + } + + return res; +} + +AST_TEST_DEFINE(resolver_resolve_async_cancel) +{ + RAII_VAR(struct async_resolution_data *, async_data, NULL, ao2_cleanup); + RAII_VAR(struct ast_dns_query_active *, active, NULL, ao2_cleanup); + struct ast_dns_result *result; + enum ast_test_result_state res = AST_TEST_PASS; + struct timespec timeout; + + switch (cmd) { + case TEST_INIT: + info->name = "resolver_resolve_async_cancel"; + info->category = "/main/dns/"; + info->summary = "Test canceling an asynchronous DNS resolution"; + info->description = + "This test performs an asynchronous DNS resolution of a domain and then cancels\n" + "the resolution. The goal of this test is to ensure that the cancel() callback of\n" + "the resolver is called and that it properly interrupts the resolution such that no\n" + "records are returned.\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + if (ast_dns_resolver_register(&test_resolver)) { + ast_test_status_update(test, "Unable to register test resolver\n"); + return AST_TEST_FAIL; + } + + resolver_data_init(); + + async_data = async_data_alloc(); + if (!async_data) { + ast_test_status_update(test, "Failed to allocate asynchronous data\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + active = ast_dns_resolve_async("asterisk.org", ns_t_a, ns_c_in, async_callback, async_data); + if (!active) { + ast_test_status_update(test, "Asynchronous resolution of address failed\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + if (!test_resolver_data.resolve_called) { + ast_test_status_update(test, "DNS resolution did not call resolver's resolve() method\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + if (test_resolver_data.canceled) { + ast_test_status_update(test, "Resolver's cancel() method called for no reason\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + ast_dns_resolve_cancel(active); + + if (!test_resolver_data.canceled) { + ast_test_status_update(test, "Resolver's cancel() method was not called\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + clock_gettime(CLOCK_REALTIME, &timeout); + timeout.tv_sec += 10; + ast_mutex_lock(&async_data->lock); + while (!async_data->complete) { + if (ast_cond_timedwait(&async_data->cond, &async_data->lock, &timeout) == ETIMEDOUT) { + break; + } + } + ast_mutex_unlock(&async_data->lock); + + if (!async_data->complete) { + ast_test_status_update(test, "Asynchronous resolution timed out\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + if (test_resolver_data.resolution_complete) { + ast_test_status_update(test, "Resolution completed without cancelation\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + result = ast_dns_query_get_result(active->query); + if (result) { + ast_test_status_update(test, "Canceled resolution had a result\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + +cleanup: + ast_dns_resolver_unregister(&test_resolver); + resolver_data_cleanup(); + return res; +} + +static int unload_module(void) +{ + AST_TEST_UNREGISTER(resolver_register_unregister); + AST_TEST_UNREGISTER(resolver_register_off_nominal); + AST_TEST_UNREGISTER(resolver_unregister_off_nominal); + AST_TEST_UNREGISTER(resolver_data); + AST_TEST_UNREGISTER(resolver_set_result); + AST_TEST_UNREGISTER(resolver_set_result_off_nominal); + AST_TEST_UNREGISTER(resolver_add_record); + AST_TEST_UNREGISTER(resolver_add_record_off_nominal); + AST_TEST_UNREGISTER(resolver_resolve_sync); + AST_TEST_UNREGISTER(resolver_resolve_sync_off_nominal); + AST_TEST_UNREGISTER(resolver_resolve_async); + AST_TEST_UNREGISTER(resolver_resolve_async_off_nominal); + AST_TEST_UNREGISTER(resolver_resolve_async_cancel); + + return 0; +} + +static int load_module(void) +{ + AST_TEST_REGISTER(resolver_register_unregister); + AST_TEST_REGISTER(resolver_register_off_nominal); + AST_TEST_REGISTER(resolver_unregister_off_nominal); + AST_TEST_REGISTER(resolver_data); + AST_TEST_REGISTER(resolver_set_result); + AST_TEST_REGISTER(resolver_set_result_off_nominal); + AST_TEST_REGISTER(resolver_add_record); + AST_TEST_REGISTER(resolver_add_record_off_nominal); + AST_TEST_REGISTER(resolver_resolve_sync); + AST_TEST_REGISTER(resolver_resolve_sync_off_nominal); + AST_TEST_REGISTER(resolver_resolve_async); + AST_TEST_REGISTER(resolver_resolve_async_off_nominal); + AST_TEST_REGISTER(resolver_resolve_async_cancel); + + return AST_MODULE_LOAD_SUCCESS; +} + +AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "DNS API Tests"); diff --git a/tests/test_dns_recurring.c b/tests/test_dns_recurring.c new file mode 100644 index 000000000..3d38c645b --- /dev/null +++ b/tests/test_dns_recurring.c @@ -0,0 +1,648 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Mark Michelson + * + * Mark Michelson + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*** MODULEINFO + TEST_FRAMEWORK + core + ***/ + +#include "asterisk.h" + +#include +#include + +#include "asterisk/test.h" +#include "asterisk/module.h" +#include "asterisk/dns_core.h" +#include "asterisk/dns_resolver.h" +#include "asterisk/dns_recurring.h" +#include "asterisk/dns_internal.h" + +struct recurring_data { + /*! TTL to place in first returned result */ + int ttl1; + /*! TTL to place in second returned result */ + int ttl2; + /*! Boolean indicator if query has completed */ + int query_complete; + /*! Number of times recurring resolution has completed */ + int complete_resolutions; + /*! Number of times resolve() method has been called */ + int resolves; + /*! Indicates that the query is expected to be canceled */ + int cancel_expected; + /*! Indicates that the query is ready to be canceled */ + int cancel_ready; + /*! Indicates that the query has been canceled */ + int canceled; + ast_mutex_t lock; + ast_cond_t cond; +}; + +static void recurring_data_destructor(void *obj) +{ + struct recurring_data *rdata = obj; + + ast_mutex_destroy(&rdata->lock); + ast_cond_destroy(&rdata->cond); +} + +static struct recurring_data *recurring_data_alloc(void) +{ + struct recurring_data *rdata; + + rdata = ao2_alloc(sizeof(*rdata), recurring_data_destructor); + if (!rdata) { + return NULL; + } + + ast_mutex_init(&rdata->lock); + ast_cond_init(&rdata->cond, NULL); + + return rdata; +} + +#define DNS_ANSWER "Yes sirree" +#define DNS_ANSWER_SIZE strlen(DNS_ANSWER) + +/*! + * \brief Thread that performs asynchronous resolution. + * + * This thread uses the query's user data to determine how to + * perform the resolution. The query may either be canceled or + * it may be completed with records. + * + * \param dns_query The ast_dns_query that is being performed + * \return NULL + */ +static void *resolution_thread(void *dns_query) +{ + struct ast_dns_query *query = dns_query; + + static const char *ADDR1 = "127.0.0.1"; + static const size_t ADDR1_BUFSIZE = sizeof(struct in_addr); + char addr1_buf[ADDR1_BUFSIZE]; + + static const char *ADDR2 = "192.168.0.1"; + static const size_t ADDR2_BUFSIZE = sizeof(struct in_addr); + char addr2_buf[ADDR2_BUFSIZE]; + + struct ast_dns_query_recurring *recurring = ast_dns_query_get_data(query); + struct recurring_data *rdata = recurring->user_data; + + ast_assert(rdata != NULL); + + /* Canceling is an interesting dance. This thread needs to signal that it is + * ready to be canceled. Then it needs to wait until the query is actually canceled. + */ + if (rdata->cancel_expected) { + ast_mutex_lock(&rdata->lock); + rdata->cancel_ready = 1; + ast_cond_signal(&rdata->cond); + + while (!rdata->canceled) { + ast_cond_wait(&rdata->cond, &rdata->lock); + } + ast_mutex_unlock(&rdata->lock); + + ast_dns_resolver_completed(query); + ao2_ref(query, -1); + + return NULL; + } + + /* When the query isn't canceled, we set the TTL of the results based on what + * we've been told to set it to + */ + ast_dns_resolver_set_result(query, 0, 0, ns_r_noerror, "asterisk.org", DNS_ANSWER, DNS_ANSWER_SIZE); + + inet_pton(AF_INET, ADDR1, addr1_buf); + ast_dns_resolver_add_record(query, ns_t_a, ns_c_in, rdata->ttl1, addr1_buf, ADDR1_BUFSIZE); + + inet_pton(AF_INET, ADDR2, addr2_buf); + ast_dns_resolver_add_record(query, ns_t_a, ns_c_in, rdata->ttl2, addr2_buf, ADDR2_BUFSIZE); + + ++rdata->complete_resolutions; + + ast_dns_resolver_completed(query); + + ao2_ref(query, -1); + return NULL; +} + +/*! + * \brief Resolver's resolve() method + * + * \param query The query that is to be resolved + * \retval 0 Successfully created thread to perform the resolution + * \retval non-zero Failed to create resolution thread + */ +static int recurring_resolve(struct ast_dns_query *query) +{ + struct ast_dns_query_recurring *recurring = ast_dns_query_get_data(query); + struct recurring_data *rdata = recurring->user_data; + pthread_t resolver_thread; + + ast_assert(rdata != NULL); + ++rdata->resolves; + return ast_pthread_create_detached(&resolver_thread, NULL, resolution_thread, ao2_bump(query)); +} + +/*! + * \brief Resolver's cancel() method + * + * \param query The query to cancel + * \return 0 + */ +static int recurring_cancel(struct ast_dns_query *query) +{ + struct ast_dns_query_recurring *recurring = ast_dns_query_get_data(query); + struct recurring_data *rdata = recurring->user_data; + + ast_mutex_lock(&rdata->lock); + rdata->canceled = 1; + ast_cond_signal(&rdata->cond); + ast_mutex_unlock(&rdata->lock); + + return 0; +} + +static struct ast_dns_resolver recurring_resolver = { + .name = "test_recurring", + .priority = 0, + .resolve = recurring_resolve, + .cancel = recurring_cancel, +}; + +/*! + * \brief Wait for a successful resolution to complete + * + * This is called whenever a successful DNS resolution occurs. This function + * serves to ensure that parameters are as we expect them to be. + * + * \param test The test being executed + * \param rdata DNS query user data + * \param expected_lapse The amount of time we expect to wait for the query to complete + * \param num_resolves The number of DNS resolutions that have been executed + * \param num_completed The number of DNS resolutions we expect to have completed successfully + * \param canceled Whether the query is expected to have been canceled + */ +static int wait_for_resolution(struct ast_test *test, struct recurring_data *rdata, + int expected_lapse, int num_resolves, int num_completed, int canceled) +{ + struct timespec begin; + struct timespec end; + struct timespec timeout; + int secdiff; + + clock_gettime(CLOCK_REALTIME, &begin); + + timeout.tv_sec = begin.tv_sec + 20; + timeout.tv_nsec = begin.tv_nsec; + + ast_mutex_lock(&rdata->lock); + while (!rdata->query_complete) { + if (ast_cond_timedwait(&rdata->cond, &rdata->lock, &timeout) == ETIMEDOUT) { + break; + } + } + ast_mutex_unlock(&rdata->lock); + + if (!rdata->query_complete) { + ast_test_status_update(test, "Query timed out\n"); + return -1; + } + + rdata->query_complete = 0; + clock_gettime(CLOCK_REALTIME, &end); + + secdiff = end.tv_sec - begin.tv_sec; + + /* Give ourselves some wiggle room */ + if (secdiff < expected_lapse - 2 || secdiff > expected_lapse + 2) { + ast_test_status_update(test, "Query did not complete in expected time\n"); + return -1; + } + + if (rdata->resolves != num_resolves || rdata->complete_resolutions != num_completed) { + ast_test_status_update(test, "Query has not undergone expected number of resolutions\n"); + return -1; + } + + if (rdata->canceled != canceled) { + ast_test_status_update(test, "Query was canceled unexpectedly\n"); + return -1; + } + + ast_test_status_update(test, "Query completed in expected time frame\n"); + + return 0; +} + +static void async_callback(const struct ast_dns_query *query) +{ + struct recurring_data *rdata = ast_dns_query_get_data(query); + + ast_assert(rdata != NULL); + + ast_mutex_lock(&rdata->lock); + rdata->query_complete = 1; + ast_cond_signal(&rdata->cond); + ast_mutex_unlock(&rdata->lock); +} + +AST_TEST_DEFINE(recurring_query) +{ + RAII_VAR(struct ast_dns_query_recurring *, recurring_query, NULL, ao2_cleanup); + RAII_VAR(struct recurring_data *, rdata, NULL, ao2_cleanup); + + enum ast_test_result_state res = AST_TEST_PASS; + int expected_lapse; + + switch (cmd) { + case TEST_INIT: + info->name = "recurring_query"; + info->category = "/main/dns/recurring/"; + info->summary = "Test nominal asynchronous recurring DNS queries\n"; + info->description = + "This tests nominal recurring queries in the following ways:\n" + "\t* An asynchronous query is sent to a mock resolver\n" + "\t* The mock resolver returns two records with different TTLs\n" + "\t* We ensure that the query re-occurs according to the lower of the TTLs\n" + "\t* The mock resolver returns two records, this time with different TTLs\n" + "\t from the first time the query was resolved\n" + "\t* We ensure that the query re-occurs according to the new lower TTL\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + if (ast_dns_resolver_register(&recurring_resolver)) { + ast_test_status_update(test, "Failed to register recurring DNS resolver\n"); + return AST_TEST_FAIL; + } + + rdata = recurring_data_alloc(); + if (!rdata) { + ast_test_status_update(test, "Failed to allocate data necessary for recurring test\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + expected_lapse = 0; + rdata->ttl1 = 5; + rdata->ttl2 = 20; + + recurring_query = ast_dns_resolve_recurring("asterisk.org", ns_t_a, ns_c_in, async_callback, rdata); + if (!recurring_query) { + ast_test_status_update(test, "Failed to create recurring DNS query\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + /* This should be near instantaneous */ + if (wait_for_resolution(test, rdata, expected_lapse, 1, 1, 0)) { + res = AST_TEST_FAIL; + goto cleanup; + } + + expected_lapse = rdata->ttl1; + rdata->ttl1 = 45; + rdata->ttl2 = 10; + + /* This should take approximately 5 seconds */ + if (wait_for_resolution(test, rdata, expected_lapse, 2, 2, 0)) { + res = AST_TEST_FAIL; + goto cleanup; + } + + expected_lapse = rdata->ttl2; + + /* This should take approximately 10 seconds */ + if (wait_for_resolution(test, rdata, expected_lapse, 3, 3, 0)) { + res = AST_TEST_FAIL; + goto cleanup; + } + +cleanup: + if (recurring_query) { + /* XXX I don't like calling this here since I'm not testing + * canceling recurring queries, but I'm forced into it here + */ + ast_dns_resolve_recurring_cancel(recurring_query); + } + ast_dns_resolver_unregister(&recurring_resolver); + return res; +} + +static int fail_resolve(struct ast_dns_query *query) +{ + return -1; +} + +static int stub_cancel(struct ast_dns_query *query) +{ + return 0; +} + +static void stub_callback(const struct ast_dns_query *query) +{ + return; +} + +AST_TEST_DEFINE(recurring_query_off_nominal) +{ + struct ast_dns_resolver terrible_resolver = { + .name = "Harold P. Warren's Filmography", + .priority = 0, + .resolve = fail_resolve, + .cancel = stub_cancel, + }; + + struct ast_dns_query_recurring *recurring; + + struct dns_resolve_data { + const char *name; + int rr_type; + int rr_class; + ast_dns_resolve_callback callback; + } resolves [] = { + { NULL, ns_t_a, ns_c_in, stub_callback }, + { "asterisk.org", -1, ns_c_in, stub_callback }, + { "asterisk.org", ns_t_max + 1, ns_c_in, stub_callback }, + { "asterisk.org", ns_t_a, -1, stub_callback }, + { "asterisk.org", ns_t_a, ns_c_max + 1, stub_callback }, + { "asterisk.org", ns_t_a, ns_c_in, NULL }, + }; + int i; + enum ast_test_result_state res = AST_TEST_PASS; + + switch (cmd) { + case TEST_INIT: + info->name = "recurring_query_off_nominal"; + info->category = "/main/dns/recurring/"; + info->summary = "Test off-nominal recurring DNS resolution"; + info->description = + "This test performs several off-nominal recurring DNS resolutions:\n" + "\t* Attempt resolution with NULL name\n", + "\t* Attempt resolution with invalid RR type\n", + "\t* Attempt resolution with invalid RR class\n", + "\t* Attempt resolution with NULL callback pointer\n", + "\t* Attempt resolution with resolver that returns an error\n"; + + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + if (ast_dns_resolver_register(&recurring_resolver)) { + ast_test_status_update(test, "Failed to register test resolver\n"); + return AST_TEST_FAIL; + } + + for (i = 0; i < ARRAY_LEN(resolves); ++i) { + recurring = ast_dns_resolve_recurring(resolves[i].name, resolves[i].rr_type, resolves[i].rr_class, + resolves[i].callback, NULL); + if (recurring) { + ast_test_status_update(test, "Successfully performed recurring resolution with invalid data\n"); + ast_dns_resolve_recurring_cancel(recurring); + ao2_ref(recurring, -1); + res = AST_TEST_FAIL; + } + } + + ast_dns_resolver_unregister(&recurring_resolver); + + if (ast_dns_resolver_register(&terrible_resolver)) { + ast_test_status_update(test, "Failed to register the DNS resolver\n"); + return AST_TEST_FAIL; + } + + recurring = ast_dns_resolve_recurring("asterisk.org", ns_t_a, ns_c_in, stub_callback, NULL); + + ast_dns_resolver_unregister(&terrible_resolver); + + if (recurring) { + ast_test_status_update(test, "Successfully performed recurring resolution with invalid data\n"); + ast_dns_resolve_recurring_cancel(recurring); + ao2_ref(recurring, -1); + return AST_TEST_FAIL; + } + + return res; +} + +AST_TEST_DEFINE(recurring_query_cancel_between) +{ + RAII_VAR(struct ast_dns_query_recurring *, recurring_query, NULL, ao2_cleanup); + RAII_VAR(struct recurring_data *, rdata, NULL, ao2_cleanup); + + enum ast_test_result_state res = AST_TEST_PASS; + struct timespec timeout; + + switch (cmd) { + case TEST_INIT: + info->name = "recurring_query_cancel_between"; + info->category = "/main/dns/recurring/"; + info->summary = "Test canceling a recurring DNS query during the downtime between queries\n"; + info->description = "This test does the following:\n" + "\t* Issue a recurring DNS query.\n" + "\t* Once results have been returned, cancel the recurring query.\n" + "\t* Wait a while to ensure that no more queries are occurring.\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + if (ast_dns_resolver_register(&recurring_resolver)) { + ast_test_status_update(test, "Failed to register recurring DNS resolver\n"); + return AST_TEST_FAIL; + } + + rdata = recurring_data_alloc(); + if (!rdata) { + ast_test_status_update(test, "Failed to allocate data necessary for recurring test\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + rdata->ttl1 = 5; + rdata->ttl2 = 20; + + recurring_query = ast_dns_resolve_recurring("asterisk.org", ns_t_a, ns_c_in, async_callback, rdata); + if (!recurring_query) { + ast_test_status_update(test, "Unable to make recurring query\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + if (wait_for_resolution(test, rdata, 0, 1, 1, 0)) { + res = AST_TEST_FAIL; + goto cleanup; + } + + if (ast_dns_resolve_recurring_cancel(recurring_query)) { + ast_test_status_update(test, "Failed to cancel recurring query\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + /* Query has been canceled, so let's wait to make sure that we don't get + * told another query has occurred. + */ + clock_gettime(CLOCK_REALTIME, &timeout); + timeout.tv_sec += 10; + + ast_mutex_lock(&rdata->lock); + while (!rdata->query_complete) { + if (ast_cond_timedwait(&rdata->cond, &rdata->lock, &timeout) == ETIMEDOUT) { + break; + } + } + ast_mutex_unlock(&rdata->lock); + + if (rdata->query_complete) { + ast_test_status_update(test, "Recurring query occurred after cancellation\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + +cleanup: + ast_dns_resolver_unregister(&recurring_resolver); + return res; +} + +AST_TEST_DEFINE(recurring_query_cancel_during) +{ + + RAII_VAR(struct ast_dns_query_recurring *, recurring_query, NULL, ao2_cleanup); + RAII_VAR(struct recurring_data *, rdata, NULL, ao2_cleanup); + + enum ast_test_result_state res = AST_TEST_PASS; + struct timespec timeout; + + switch (cmd) { + case TEST_INIT: + info->name = "recurring_query_cancel_during"; + info->category = "/main/dns/recurring/"; + info->summary = "Cancel a recurring DNS query while a query is actually happening\n"; + info->description = "This test does the following:\n" + "\t* Initiate a recurring DNS query.\n" + "\t* Allow the initial query to complete, and a second query to start\n" + "\t* Cancel the recurring query while the second query is executing\n" + "\t* Ensure that the resolver's cancel() method was called\n" + "\t* Wait a while to make sure that recurring queries are no longer occurring\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + if (ast_dns_resolver_register(&recurring_resolver)) { + ast_test_status_update(test, "Failed to register recurring DNS resolver\n"); + return AST_TEST_FAIL; + } + + rdata = recurring_data_alloc(); + if (!rdata) { + ast_test_status_update(test, "Failed to allocate data necessary for recurring test\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + rdata->ttl1 = 5; + rdata->ttl2 = 20; + + recurring_query = ast_dns_resolve_recurring("asterisk.org", ns_t_a, ns_c_in, async_callback, rdata); + if (!recurring_query) { + ast_test_status_update(test, "Failed to make recurring DNS query\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + if (wait_for_resolution(test, rdata, 0, 1, 1, 0)) { + res = AST_TEST_FAIL; + goto cleanup; + } + + /* Initial query has completed. Now let's make the next query expect a cancelation */ + rdata->cancel_expected = 1; + + /* Wait to be told that the query should be canceled */ + ast_mutex_lock(&rdata->lock); + while (!rdata->cancel_ready) { + ast_cond_wait(&rdata->cond, &rdata->lock); + } + rdata->cancel_expected = 0; + ast_mutex_unlock(&rdata->lock); + + if (ast_dns_resolve_recurring_cancel(recurring_query)) { + ast_test_status_update(test, "Failed to cancel recurring DNS query\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + /* Query has been canceled. We'll be told that the query in flight has completed. */ + if (wait_for_resolution(test, rdata, 0, 2, 1, 1)) { + res = AST_TEST_FAIL; + goto cleanup; + } + + /* Now ensure that no more queries get completed after cancellation. */ + clock_gettime(CLOCK_REALTIME, &timeout); + timeout.tv_sec += 10; + + ast_mutex_lock(&rdata->lock); + while (!rdata->query_complete) { + if (ast_cond_timedwait(&rdata->cond, &rdata->lock, &timeout) == ETIMEDOUT) { + break; + } + } + ast_mutex_unlock(&rdata->lock); + + if (rdata->query_complete) { + ast_test_status_update(test, "Recurring query occurred after cancellation\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + +cleanup: + ast_dns_resolver_unregister(&recurring_resolver); + return res; +} + +static int unload_module(void) +{ + AST_TEST_UNREGISTER(recurring_query); + AST_TEST_UNREGISTER(recurring_query_off_nominal); + AST_TEST_UNREGISTER(recurring_query_cancel_between); + AST_TEST_UNREGISTER(recurring_query_cancel_during); + + return 0; +} + +static int load_module(void) +{ + AST_TEST_REGISTER(recurring_query); + AST_TEST_REGISTER(recurring_query_off_nominal); + AST_TEST_REGISTER(recurring_query_cancel_between); + AST_TEST_REGISTER(recurring_query_cancel_during); + + return AST_MODULE_LOAD_SUCCESS; +} + +AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Recurring DNS query tests"); -- cgit v1.2.3