summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build_tools/menuselect-deps.in1
-rw-r--r--configs/samples/resolver_unbound.conf.sample24
-rwxr-xr-xconfigure145
-rw-r--r--configure.ac3
-rw-r--r--include/asterisk/autoconfig.h.in3
-rw-r--r--include/asterisk/dns_core.h267
-rw-r--r--include/asterisk/dns_internal.h145
-rw-r--r--include/asterisk/dns_naptr.h89
-rw-r--r--include/asterisk/dns_query_set.h136
-rw-r--r--include/asterisk/dns_recurring.h78
-rw-r--r--include/asterisk/dns_resolver.h142
-rw-r--r--include/asterisk/dns_srv.h71
-rw-r--r--include/asterisk/dns_tlsa.h72
-rw-r--r--main/dns_core.c566
-rw-r--r--main/dns_naptr.c65
-rw-r--r--main/dns_query_set.c93
-rw-r--r--main/dns_recurring.c149
-rw-r--r--main/dns_srv.c55
-rw-r--r--main/dns_tlsa.c55
-rw-r--r--makeopts.in3
-rw-r--r--res/res_resolver_unbound.c1271
-rw-r--r--tests/test_dns.c1355
-rw-r--r--tests/test_dns_recurring.c648
23 files changed, 5435 insertions, 1 deletions
diff --git a/build_tools/menuselect-deps.in b/build_tools/menuselect-deps.in
index a77530b78..edeb84877 100644
--- a/build_tools/menuselect-deps.in
+++ b/build_tools/menuselect-deps.in
@@ -65,6 +65,7 @@ OPENSSL=@PBX_OPENSSL@
SUPPSERV=@PBX_SUPPSERV@
SYSLOG=@PBX_SYSLOG@
TONEZONE=@PBX_TONEZONE@
+UNBOUND=@PBX_UNBOUND@
UNIXODBC=@PBX_UNIXODBC@
VORBIS=@PBX_VORBIS@
VPB=@PBX_VPB@
diff --git a/configs/samples/resolver_unbound.conf.sample b/configs/samples/resolver_unbound.conf.sample
new file mode 100644
index 000000000..97361525e
--- /dev/null
+++ b/configs/samples/resolver_unbound.conf.sample
@@ -0,0 +1,24 @@
+; Unbound DNS Resolver Configuration
+;
+; This file serves as a reference for the configurable options within the
+; unbound DNS resolver.
+
+[general]
+;hosts = /etc/hosts ; Full path to a hosts file which contains a mapping of
+; ; hostnames to addresses. If "system" is specified then
+; ; the system specific hosts file will be used. (default: system)
+;resolv = /etc/resolv.conf ; Full path to a resolv.conf which contains the nameservers
+; ; to use for resolution. If "system" is specified then the
+; ; system specific resolv.conf file will be used. (default: system)
+;nameserver = 127.0.0.1 ; An explicit nameserver to use for queries. If this option
+; ; is specified multiple times the first configured one will
+; ; be treated as the primary with each subsequent one being
+; ; a backup. If the resolv options is also specified the
+; ; nameservers from it will be tried after all nameserver
+; ; options.
+;debug = 99 ; The debug level to run the unbound resolver at. While
+; ; there is no explicit range the higher the number the more
+; ; debug is output.
+;ta_file = /etc/asterisk/dnssec_keys ; Full path to a trusted anchors key file. These keys are
+; ; used to verify DNSSEC signed results.
+
diff --git a/configure b/configure
index be5062007..9a2630dc1 100755
--- a/configure
+++ b/configure
@@ -1,5 +1,5 @@
#! /bin/sh
-# From configure.ac Revision: 432282 .
+# From configure.ac Revision: 432815 .
# Guess values for system-dependent variables and create Makefiles.
# Generated by GNU Autoconf 2.69 for asterisk trunk.
#
@@ -731,6 +731,10 @@ PBX_UNIXODBC
UNIXODBC_DIR
UNIXODBC_INCLUDE
UNIXODBC_LIB
+PBX_UNBOUND
+UNBOUND_DIR
+UNBOUND_INCLUDE
+UNBOUND_LIB
PBX_TONEZONE
TONEZONE_DIR
TONEZONE_INCLUDE
@@ -1357,6 +1361,7 @@ with_termcap
with_timerfd
with_tinfo
with_tonezone
+with_unbound
with_unixodbc
with_vorbis
with_vpb
@@ -2097,6 +2102,7 @@ Optional Packages:
--with-timerfd=PATH use timerfd files in PATH
--with-tinfo=PATH use Term Info files in PATH
--with-tonezone=PATH use tonezone files in PATH
+ --with-unbound=PATH use unbound files in PATH
--with-unixodbc=PATH use unixODBC files in PATH
--with-vorbis=PATH use Vorbis files in PATH
--with-vpb=PATH use Voicetronix API files in PATH
@@ -11299,6 +11305,38 @@ fi
+ UNBOUND_DESCRIP="unbound"
+ UNBOUND_OPTION="unbound"
+ PBX_UNBOUND=0
+
+# Check whether --with-unbound was given.
+if test "${with_unbound+set}" = set; then :
+ withval=$with_unbound;
+ case ${withval} in
+ n|no)
+ USE_UNBOUND=no
+ # -1 is a magic value used by menuselect to know that the package
+ # was disabled, other than 'not found'
+ PBX_UNBOUND=-1
+ ;;
+ y|ye|yes)
+ ac_mandatory_list="${ac_mandatory_list} UNBOUND"
+ ;;
+ *)
+ UNBOUND_DIR="${withval}"
+ ac_mandatory_list="${ac_mandatory_list} UNBOUND"
+ ;;
+ esac
+
+fi
+
+
+
+
+
+
+
+
UNIXODBC_DESCRIP="unixODBC"
UNIXODBC_OPTION="unixodbc"
PBX_UNIXODBC=0
@@ -22704,6 +22742,111 @@ fi
+if test "x${PBX_UNBOUND}" != "x1" -a "${USE_UNBOUND}" != "no"; then
+ pbxlibdir=""
+ # if --with-UNBOUND=DIR has been specified, use it.
+ if test "x${UNBOUND_DIR}" != "x"; then
+ if test -d ${UNBOUND_DIR}/lib; then
+ pbxlibdir="-L${UNBOUND_DIR}/lib"
+ else
+ pbxlibdir="-L${UNBOUND_DIR}"
+ fi
+ fi
+ pbxfuncname="ub_ctx_create"
+ if test "x${pbxfuncname}" = "x" ; then # empty lib, assume only headers
+ AST_UNBOUND_FOUND=yes
+ else
+ ast_ext_lib_check_save_CFLAGS="${CFLAGS}"
+ CFLAGS="${CFLAGS} "
+ as_ac_Lib=`$as_echo "ac_cv_lib_unbound_${pbxfuncname}" | $as_tr_sh`
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for ${pbxfuncname} in -lunbound" >&5
+$as_echo_n "checking for ${pbxfuncname} in -lunbound... " >&6; }
+if eval \${$as_ac_Lib+:} false; then :
+ $as_echo_n "(cached) " >&6
+else
+ ac_check_lib_save_LIBS=$LIBS
+LIBS="-lunbound ${pbxlibdir} $LIBS"
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+
+/* Override any GCC internal prototype to avoid an error.
+ Use char because int might match the return type of a GCC
+ builtin and then its argument prototype would still apply. */
+#ifdef __cplusplus
+extern "C"
+#endif
+char ${pbxfuncname} ();
+int
+main ()
+{
+return ${pbxfuncname} ();
+ ;
+ return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"; then :
+ eval "$as_ac_Lib=yes"
+else
+ eval "$as_ac_Lib=no"
+fi
+rm -f core conftest.err conftest.$ac_objext \
+ conftest$ac_exeext conftest.$ac_ext
+LIBS=$ac_check_lib_save_LIBS
+fi
+eval ac_res=\$$as_ac_Lib
+ { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5
+$as_echo "$ac_res" >&6; }
+if eval test \"x\$"$as_ac_Lib"\" = x"yes"; then :
+ AST_UNBOUND_FOUND=yes
+else
+ AST_UNBOUND_FOUND=no
+fi
+
+ CFLAGS="${ast_ext_lib_check_save_CFLAGS}"
+ fi
+
+ # now check for the header.
+ if test "${AST_UNBOUND_FOUND}" = "yes"; then
+ UNBOUND_LIB="${pbxlibdir} -lunbound "
+ # if --with-UNBOUND=DIR has been specified, use it.
+ if test "x${UNBOUND_DIR}" != "x"; then
+ UNBOUND_INCLUDE="-I${UNBOUND_DIR}/include"
+ fi
+ UNBOUND_INCLUDE="${UNBOUND_INCLUDE} "
+ if test "xunbound.h" = "x" ; then # no header, assume found
+ UNBOUND_HEADER_FOUND="1"
+ else # check for the header
+ ast_ext_lib_check_saved_CPPFLAGS="${CPPFLAGS}"
+ CPPFLAGS="${CPPFLAGS} ${UNBOUND_INCLUDE}"
+ ac_fn_c_check_header_mongrel "$LINENO" "unbound.h" "ac_cv_header_unbound_h" "$ac_includes_default"
+if test "x$ac_cv_header_unbound_h" = xyes; then :
+ UNBOUND_HEADER_FOUND=1
+else
+ UNBOUND_HEADER_FOUND=0
+fi
+
+
+ CPPFLAGS="${ast_ext_lib_check_saved_CPPFLAGS}"
+ fi
+ if test "x${UNBOUND_HEADER_FOUND}" = "x0" ; then
+ UNBOUND_LIB=""
+ UNBOUND_INCLUDE=""
+ else
+ if test "x${pbxfuncname}" = "x" ; then # only checking headers -> no library
+ UNBOUND_LIB=""
+ fi
+ PBX_UNBOUND=1
+ cat >>confdefs.h <<_ACEOF
+#define HAVE_UNBOUND 1
+_ACEOF
+
+ fi
+ fi
+fi
+
+
+
+
if test "x${PBX_UNIXODBC}" != "x1" -a "${USE_UNIXODBC}" != "no"; then
pbxlibdir=""
# if --with-UNIXODBC=DIR has been specified, use it.
diff --git a/configure.ac b/configure.ac
index edb4322b1..afbb5afc1 100644
--- a/configure.ac
+++ b/configure.ac
@@ -507,6 +507,7 @@ AST_EXT_LIB_SETUP([TERMCAP], [Termcap], [termcap])
AST_EXT_LIB_SETUP([TIMERFD], [timerfd], [timerfd])
AST_EXT_LIB_SETUP([TINFO], [Term Info], [tinfo])
AST_EXT_LIB_SETUP([TONEZONE], [tonezone], [tonezone])
+AST_EXT_LIB_SETUP([UNBOUND], [unbound], [unbound])
AST_EXT_LIB_SETUP([UNIXODBC], [unixODBC], [unixodbc])
AST_EXT_LIB_SETUP([VORBIS], [Vorbis], [vorbis])
AST_EXT_LIB_SETUP([VPB], [Voicetronix API], [vpb])
@@ -2032,6 +2033,8 @@ AST_EXT_TOOL_CHECK([NETSNMP], [net-snmp-config], , [--agent-libs],
AST_EXT_LIB_CHECK([NEWT], [newt], [newtBell], [newt.h])
+AST_EXT_LIB_CHECK([UNBOUND], [unbound], [ub_ctx_create], [unbound.h], [])
+
AST_EXT_LIB_CHECK([UNIXODBC], [odbc], [SQLConnect], [sql.h], [])
AST_EXT_LIB_CHECK([OGG], [ogg], [ogg_sync_init], [])
diff --git a/include/asterisk/autoconfig.h.in b/include/asterisk/autoconfig.h.in
index a8293a2fe..8c7ead499 100644
--- a/include/asterisk/autoconfig.h.in
+++ b/include/asterisk/autoconfig.h.in
@@ -1065,6 +1065,9 @@
/* Define to 1 if you have the `truncl' function. */
#undef HAVE_TRUNCL
+/* Define to 1 if you have the unbound library. */
+#undef HAVE_UNBOUND
+
/* Define to 1 if you have the <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
index 000000000..1f67bb803
--- /dev/null
+++ b/include/asterisk/dns_core.h
@@ -0,0 +1,267 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <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
index 000000000..48fa26471
--- /dev/null
+++ b/include/asterisk/dns_internal.h
@@ -0,0 +1,145 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <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
index 000000000..5d7541aa5
--- /dev/null
+++ b/include/asterisk/dns_naptr.h
@@ -0,0 +1,89 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <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
index 000000000..c89fdfde7
--- /dev/null
+++ b/include/asterisk/dns_query_set.h
@@ -0,0 +1,136 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <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
index 000000000..c5c1af1ac
--- /dev/null
+++ b/include/asterisk/dns_recurring.h
@@ -0,0 +1,78 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <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
index 000000000..819ce7a59
--- /dev/null
+++ b/include/asterisk/dns_resolver.h
@@ -0,0 +1,142 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <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
index 000000000..ef25a908b
--- /dev/null
+++ b/include/asterisk/dns_srv.h
@@ -0,0 +1,71 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <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
index 000000000..736c85ee2
--- /dev/null
+++ b/include/asterisk/dns_tlsa.h
@@ -0,0 +1,72 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <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
index 000000000..394eaa514
--- /dev/null
+++ b/main/dns_core.c
@@ -0,0 +1,566 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <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
index 000000000..f4facddae
--- /dev/null
+++ b/main/dns_naptr.c
@@ -0,0 +1,65 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <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
index 000000000..45626d1b9
--- /dev/null
+++ b/main/dns_query_set.c
@@ -0,0 +1,93 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <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
index 000000000..3ebbab070
--- /dev/null
+++ b/main/dns_recurring.c
@@ -0,0 +1,149 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <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
index 000000000..eeba9a679
--- /dev/null
+++ b/main/dns_srv.c
@@ -0,0 +1,55 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <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
index 000000000..aa6f5308f
--- /dev/null
+++ b/main/dns_tlsa.c
@@ -0,0 +1,55 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <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
diff --git a/makeopts.in b/makeopts.in
index 96b031bdd..cc233109d 100644
--- a/makeopts.in
+++ b/makeopts.in
@@ -299,6 +299,9 @@ CRYPTO_LIB=@CRYPTO_LIB@
TONEZONE_INCLUDE=@TONEZONE_INCLUDE@
TONEZONE_LIB=@TONEZONE_LIB@
+UNBOUND_INCLUDE=@UNBOUND_INCLUDE@
+UNBOUND_LIB=@UNBOUND_LIB@
+
UNIXODBC_INCLUDE=@UNIXODBC_INCLUDE@
UNIXODBC_LIB=@UNIXODBC_LIB@
diff --git a/res/res_resolver_unbound.c b/res/res_resolver_unbound.c
new file mode 100644
index 000000000..43f2acdfd
--- /dev/null
+++ b/res/res_resolver_unbound.c
@@ -0,0 +1,1271 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Digium, Inc.
+ *
+ * Joshua Colp <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
index 000000000..13306adc8
--- /dev/null
+++ b/tests/test_dns.c
@@ -0,0 +1,1355 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Mark Michelson
+ *
+ * Mark Michelson <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
index 000000000..3d38c645b
--- /dev/null
+++ b/tests/test_dns_recurring.c
@@ -0,0 +1,648 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2015, Mark Michelson
+ *
+ * Mark Michelson <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");