dns: Add core DNS API + unit tests and res_resolver_unbound module + unit tests.
authorJoshua Colp <jcolp@digium.com>
Wed, 25 Mar 2015 12:32:26 +0000 (12:32 +0000)
committerJoshua Colp <jcolp@digium.com>
Wed, 25 Mar 2015 12:32:26 +0000 (12:32 +0000)
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

23 files changed:
build_tools/menuselect-deps.in
configs/samples/resolver_unbound.conf.sample [new file with mode: 0644]
configure
configure.ac
include/asterisk/autoconfig.h.in
include/asterisk/dns_core.h [new file with mode: 0644]
include/asterisk/dns_internal.h [new file with mode: 0644]
include/asterisk/dns_naptr.h [new file with mode: 0644]
include/asterisk/dns_query_set.h [new file with mode: 0644]
include/asterisk/dns_recurring.h [new file with mode: 0644]
include/asterisk/dns_resolver.h [new file with mode: 0644]
include/asterisk/dns_srv.h [new file with mode: 0644]
include/asterisk/dns_tlsa.h [new file with mode: 0644]
main/dns_core.c [new file with mode: 0644]
main/dns_naptr.c [new file with mode: 0644]
main/dns_query_set.c [new file with mode: 0644]
main/dns_recurring.c [new file with mode: 0644]
main/dns_srv.c [new file with mode: 0644]
main/dns_tlsa.c [new file with mode: 0644]
makeopts.in
res/res_resolver_unbound.c [new file with mode: 0644]
tests/test_dns.c [new file with mode: 0644]
tests/test_dns_recurring.c [new file with mode: 0644]

index a77530b..edeb848 100644 (file)
@@ -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 (file)
index 0000000..9736152
--- /dev/null
@@ -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.
+
index be50620..9a2630d 100755 (executable)
--- 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
 
 
 
+    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
 
 
 
+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.
index edb4322..afbb5af 100644 (file)
@@ -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], [])
index a8293a2..8c7ead4 100644 (file)
 /* 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 <unistd.h> header file. */
 #undef HAVE_UNISTD_H
 
diff --git a/include/asterisk/dns_core.h b/include/asterisk/dns_core.h
new file mode 100644 (file)
index 0000000..1f67bb8
--- /dev/null
@@ -0,0 +1,267 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <jcolp@digium.com>
+ *
+ * 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 <jcolp@digium.com>
+ */
+
+#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 (file)
index 0000000..48fa264
--- /dev/null
@@ -0,0 +1,145 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <jcolp@digium.com>
+ *
+ * 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 <jcolp@digium.com>
+ */
+
+/*! \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 (file)
index 0000000..5d7541a
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <jcolp@digium.com>
+ *
+ * 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 <jcolp@digium.com>
+ */
+
+#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 (file)
index 0000000..c89fdfd
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <jcolp@digium.com>
+ *
+ * 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 <jcolp@digium.com>
+ */
+
+#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 (file)
index 0000000..c5c1af1
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <jcolp@digium.com>
+ *
+ * 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 <jcolp@digium.com>
+ */
+
+#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 (file)
index 0000000..819ce7a
--- /dev/null
@@ -0,0 +1,142 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <jcolp@digium.com>
+ *
+ * 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 <jcolp@digium.com>
+ */
+
+#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 (file)
index 0000000..ef25a90
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <jcolp@digium.com>
+ *
+ * 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 <jcolp@digium.com>
+ */
+
+#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 (file)
index 0000000..736c85e
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <jcolp@digium.com>
+ *
+ * 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 <jcolp@digium.com>
+ */
+
+#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 (file)
index 0000000..394eaa5
--- /dev/null
@@ -0,0 +1,566 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <jcolp@digium.com>
+ *
+ * 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 <jcolp@digium.com>
+ */
+
+/*** MODULEINFO
+       <support_level>core</support_level>
+ ***/
+
+#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 <arpa/nameser.h>
+
+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 (file)
index 0000000..f4facdd
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <jcolp@digium.com>
+ *
+ * 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 <jcolp@digium.com>
+ */
+
+/*** MODULEINFO
+       <support_level>core</support_level>
+ ***/
+
+#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 (file)
index 0000000..45626d1
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <jcolp@digium.com>
+ *
+ * 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 <jcolp@digium.com>
+ */
+
+/*** MODULEINFO
+       <support_level>core</support_level>
+ ***/
+
+#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 (file)
index 0000000..3ebbab0
--- /dev/null
@@ -0,0 +1,149 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <jcolp@digium.com>
+ *
+ * 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 <jcolp@digium.com>
+ */
+
+/*** MODULEINFO
+       <support_level>core</support_level>
+ ***/
+
+#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 <arpa/nameser.h>
+
+/*! \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 (file)
index 0000000..eeba9a6
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <jcolp@digium.com>
+ *
+ * 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 <jcolp@digium.com>
+ */
+
+/*** MODULEINFO
+       <support_level>core</support_level>
+ ***/
+
+#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 (file)
index 0000000..aa6f530
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <jcolp@digium.com>
+ *
+ * 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 <jcolp@digium.com>
+ */
+
+/*** MODULEINFO
+       <support_level>core</support_level>
+ ***/
+
+#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
index 96b031b..cc23310 100644 (file)
@@ -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 (file)
index 0000000..43f2acd
--- /dev/null
@@ -0,0 +1,1271 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <jcolp@digium.com>
+ *
+ * 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
+       <depend>unbound</depend>
+       <support_level>core</support_level>
+ ***/
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
+
+#include <unbound.h>
+#include <arpa/nameser.h>
+
+#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
+       <configInfo name="res_resolver_unbound" language="en_US">
+               <configFile name="resolver_unbound.conf">
+                       <configObject name="globals">
+                               <synopsis>Options that apply globally to res_resolver_unbound</synopsis>
+                               <configOption name="hosts">
+                                       <synopsis>Full path to an optional hosts file</synopsis>
+                                       <description><para>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.</para></description>
+                               </configOption>
+                               <configOption name="resolv">
+                                       <synopsis>Full path to an optional resolv.conf file</synopsis>
+                                       <description><para>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.</para></description>
+                               </configOption>
+                               <configOption name="nameserver">
+                                       <synopsis>Nameserver to use for queries</synopsis>
+                                       <description><para>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.</para></description>
+                               </configOption>
+                               <configOption name="debug">
+                                       <synopsis>Unbound debug level</synopsis>
+                                       <description><para>The debugging level for the unbound resolver. While there is no explicit range generally
+                                       the higher the number the more debug is output.</para></description>
+                               </configOption>
+                               <configOption name="ta_file">
+                                       <synopsis>Trust anchor file</synopsis>
+                                       <description><para>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.</para></description>
+                               </configOption>
+                       </configObject>
+               </configFile>
+       </configInfo>
+ ***/
+
+/*! \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 (file)
index 0000000..13306ad
--- /dev/null
@@ -0,0 +1,1355 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Mark Michelson
+ *
+ * Mark Michelson <mmichelson@digium.com>
+ *
+ * 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
+       <depend>TEST_FRAMEWORK</depend>
+       <support_level>core</support_level>
+ ***/
+
+#include "asterisk.h"
+
+#include <arpa/nameser.h>
+#include <arpa/inet.h>
+
+#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 (file)
index 0000000..3d38c64
--- /dev/null
@@ -0,0 +1,648 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Mark Michelson
+ *
+ * Mark Michelson <mmichelson@digium.com>
+ *
+ * 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
+       <depend>TEST_FRAMEWORK</depend>
+       <support_level>core</support_level>
+ ***/
+
+#include "asterisk.h"
+
+#include <arpa/nameser.h>
+#include <arpa/inet.h>
+
+#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");