diff --git a/CHANGES b/CHANGES index 4237c82e29..57a4ebb567 100644 --- a/CHANGES +++ b/CHANGES @@ -83,6 +83,10 @@ Core dedicated thread per consumer in certain cases. The initial settings for the thread pool can now be configured in 'stasis.conf'. + * A new core DNS API has been implemented which provides a common interface + for DNS functionality. Modules that use this functionality will require that + a DNS resolver module is loaded and available. + Functions ------------------ @@ -110,6 +114,19 @@ res_musiconhold over the channel-set musicclass. This allows separate hold-music from application (e.g. Queue or Dial) specified music. +res_resolver_unbound +------------------ + * Added a res_resolver_unbound module which uses the libunbound resolver library + to perform DNS resolution. This module requires the libunbound library to be + installed in order to be used. + +res_pjsip +------------------ + * A new SIP resolver using the core DNS API has been implemented. This relies on + external SIP resolver support in PJSIP which is only available as of PJSIP + 2.4. If this support is unavailable the existing built-in PJSIP SIP resolver + will be used instead. The new SIP resolver provides NAPTR support, improved + SRV support, and AAAA record support. CEL Backends ------------------ diff --git a/configure b/configure index 9a2630dc15..c539ee94bf 100755 --- a/configure +++ b/configure @@ -1,5 +1,5 @@ #! /bin/sh -# From configure.ac Revision: 432815 . +# From configure.ac Revision. # Guess values for system-dependent variables and create Makefiles. # Generated by GNU Autoconf 2.69 for asterisk trunk. # @@ -908,6 +908,10 @@ PBX_PORTAUDIO PORTAUDIO_DIR PORTAUDIO_INCLUDE PORTAUDIO_LIB +PBX_PJSIP_EXTERNAL_RESOLVER +PJSIP_EXTERNAL_RESOLVER_DIR +PJSIP_EXTERNAL_RESOLVER_INCLUDE +PJSIP_EXTERNAL_RESOLVER_LIB PBX_PJ_SSL_CERT_LOAD_FROM_FILES2 PJ_SSL_CERT_LOAD_FROM_FILES2_DIR PJ_SSL_CERT_LOAD_FROM_FILES2_INCLUDE @@ -10310,6 +10314,18 @@ PBX_PJ_SSL_CERT_LOAD_FROM_FILES2=0 +PJSIP_EXTERNAL_RESOLVER_DESCRIP="PJSIP External Resolver Support" +PJSIP_EXTERNAL_RESOLVER_OPTION=pjsip +PJSIP_EXTERNAL_RESOLVER_DIR=${PJPROJECT_DIR} + +PBX_PJSIP_EXTERNAL_RESOLVER=0 + + + + + + + PORTAUDIO_DESCRIP="PortAudio" PORTAUDIO_OPTION="portaudio" PBX_PORTAUDIO=0 @@ -24470,6 +24486,110 @@ fi +if test "x${PBX_PJSIP_EXTERNAL_RESOLVER}" != "x1" -a "${USE_PJSIP_EXTERNAL_RESOLVER}" != "no"; then + pbxlibdir="" + # if --with-PJSIP_EXTERNAL_RESOLVER=DIR has been specified, use it. + if test "x${PJSIP_EXTERNAL_RESOLVER_DIR}" != "x"; then + if test -d ${PJSIP_EXTERNAL_RESOLVER_DIR}/lib; then + pbxlibdir="-L${PJSIP_EXTERNAL_RESOLVER_DIR}/lib" + else + pbxlibdir="-L${PJSIP_EXTERNAL_RESOLVER_DIR}" + fi + fi + pbxfuncname="pjsip_endpt_set_ext_resolver" + if test "x${pbxfuncname}" = "x" ; then # empty lib, assume only headers + AST_PJSIP_EXTERNAL_RESOLVER_FOUND=yes + else + ast_ext_lib_check_save_CFLAGS="${CFLAGS}" + CFLAGS="${CFLAGS} $PJPROJECT_CFLAGS" + as_ac_Lib=`$as_echo "ac_cv_lib_pjsip_${pbxfuncname}" | $as_tr_sh` +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for ${pbxfuncname} in -lpjsip" >&5 +$as_echo_n "checking for ${pbxfuncname} in -lpjsip... " >&6; } +if eval \${$as_ac_Lib+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_check_lib_save_LIBS=$LIBS +LIBS="-lpjsip ${pbxlibdir} $PJPROJECT_LIBS $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_PJSIP_EXTERNAL_RESOLVER_FOUND=yes +else + AST_PJSIP_EXTERNAL_RESOLVER_FOUND=no +fi + + CFLAGS="${ast_ext_lib_check_save_CFLAGS}" + fi + + # now check for the header. + if test "${AST_PJSIP_EXTERNAL_RESOLVER_FOUND}" = "yes"; then + PJSIP_EXTERNAL_RESOLVER_LIB="${pbxlibdir} -lpjsip $PJPROJECT_LIBS" + # if --with-PJSIP_EXTERNAL_RESOLVER=DIR has been specified, use it. + if test "x${PJSIP_EXTERNAL_RESOLVER_DIR}" != "x"; then + PJSIP_EXTERNAL_RESOLVER_INCLUDE="-I${PJSIP_EXTERNAL_RESOLVER_DIR}/include" + fi + PJSIP_EXTERNAL_RESOLVER_INCLUDE="${PJSIP_EXTERNAL_RESOLVER_INCLUDE} $PJPROJECT_CFLAGS" + if test "xpjsip.h" = "x" ; then # no header, assume found + PJSIP_EXTERNAL_RESOLVER_HEADER_FOUND="1" + else # check for the header + ast_ext_lib_check_saved_CPPFLAGS="${CPPFLAGS}" + CPPFLAGS="${CPPFLAGS} ${PJSIP_EXTERNAL_RESOLVER_INCLUDE}" + ac_fn_c_check_header_mongrel "$LINENO" "pjsip.h" "ac_cv_header_pjsip_h" "$ac_includes_default" +if test "x$ac_cv_header_pjsip_h" = xyes; then : + PJSIP_EXTERNAL_RESOLVER_HEADER_FOUND=1 +else + PJSIP_EXTERNAL_RESOLVER_HEADER_FOUND=0 +fi + + + CPPFLAGS="${ast_ext_lib_check_saved_CPPFLAGS}" + fi + if test "x${PJSIP_EXTERNAL_RESOLVER_HEADER_FOUND}" = "x0" ; then + PJSIP_EXTERNAL_RESOLVER_LIB="" + PJSIP_EXTERNAL_RESOLVER_INCLUDE="" + else + if test "x${pbxfuncname}" = "x" ; then # only checking headers -> no library + PJSIP_EXTERNAL_RESOLVER_LIB="" + fi + PBX_PJSIP_EXTERNAL_RESOLVER=1 + cat >>confdefs.h <<_ACEOF +#define HAVE_PJSIP_EXTERNAL_RESOLVER 1 +_ACEOF + + fi + fi +fi + + + if test "x${PBX_POPT}" != "x1" -a "${USE_POPT}" != "no"; then pbxlibdir="" diff --git a/configure.ac b/configure.ac index afbb5afc1c..8a37075435 100644 --- a/configure.ac +++ b/configure.ac @@ -458,6 +458,7 @@ AST_EXT_LIB_SETUP_OPTIONAL([PJ_TRANSACTION_GRP_LOCK], [PJSIP Transaction Group L AST_EXT_LIB_SETUP_OPTIONAL([PJSIP_REPLACE_MEDIA_STREAM], [PJSIP Media Stream Replacement Support], [PJPROJECT], [pjsip]) AST_EXT_LIB_SETUP_OPTIONAL([PJSIP_GET_DEST_INFO], [pjsip_get_dest_info support], [PJPROJECT], [pjsip]) AST_EXT_LIB_SETUP_OPTIONAL([PJ_SSL_CERT_LOAD_FROM_FILES2], [pj_ssl_cert_load_from_files2 support], [PJPROJECT], [pjsip]) +AST_EXT_LIB_SETUP_OPTIONAL([PJSIP_EXTERNAL_RESOLVER], [PJSIP External Resolver Support], [PJPROJECT], [pjsip]) AST_EXT_LIB_SETUP([PORTAUDIO], [PortAudio], [portaudio]) AST_EXT_LIB_SETUP([PRI], [ISDN PRI], [pri]) AST_EXT_LIB_SETUP_OPTIONAL([PRI_SETUP_ACK_INBAND], [ISDN PRI progress inband ie in SETUP ACK], [PRI], [pri]) @@ -2124,6 +2125,7 @@ CPPFLAGS="${saved_cppflags}" AST_EXT_LIB_CHECK([PJSIP_GET_DEST_INFO], [pjsip], [pjsip_get_dest_info], [pjsip.h], [$PJPROJECT_LIBS], [$PJPROJECT_CFLAGS]) AST_EXT_LIB_CHECK([PJ_SSL_CERT_LOAD_FROM_FILES2], [pj], [pj_ssl_cert_load_from_files2], [pjlib.h], [$PJPROJECT_LIBS], [$PJPROJECT_CFLAGS]) +AST_EXT_LIB_CHECK([PJSIP_EXTERNAL_RESOLVER], [pjsip], [pjsip_endpt_set_ext_resolver], [pjsip.h], [$PJPROJECT_LIBS], [$PJPROJECT_CFLAGS]) AST_EXT_LIB_CHECK([POPT], [popt], [poptStrerror], [popt.h]) diff --git a/include/asterisk/autoconfig.h.in b/include/asterisk/autoconfig.h.in index 8c7ead4996..474fb8c317 100644 --- a/include/asterisk/autoconfig.h.in +++ b/include/asterisk/autoconfig.h.in @@ -578,6 +578,10 @@ /* Define if your system has the PJPROJECT libraries. */ #undef HAVE_PJPROJECT +/* Define to 1 if PJPROJECT has the PJSIP External Resolver Support feature. + */ +#undef HAVE_PJSIP_EXTERNAL_RESOLVER + /* Define to 1 if PJPROJECT has the pjsip_get_dest_info support feature. */ #undef HAVE_PJSIP_GET_DEST_INFO diff --git a/include/asterisk/dns_core.h b/include/asterisk/dns_core.h index 1f67bb8036..fe67e340d7 100644 --- a/include/asterisk/dns_core.h +++ b/include/asterisk/dns_core.h @@ -204,6 +204,15 @@ int ast_dns_record_get_ttl(const struct ast_dns_record *record); */ const char *ast_dns_record_get_data(const struct ast_dns_record *record); +/*! + * \brief Retrieve the size of the raw DNS record + * + * \param record The DNS record + * + * \return the size of the raw DNS record + */ +size_t ast_dns_record_get_data_size(const struct ast_dns_record *record); + /*! * \brief Get the next DNS record * diff --git a/include/asterisk/dns_internal.h b/include/asterisk/dns_internal.h index d518f90669..be8794ba99 100644 --- a/include/asterisk/dns_internal.h +++ b/include/asterisk/dns_internal.h @@ -23,6 +23,12 @@ * \author Joshua Colp */ +/*! \brief For AST_VECTOR */ +#include "asterisk/vector.h" + +/*! \brief For ast_dns_query_set_callback */ +#include "asterisk/dns_query_set.h" + /*! \brief Generic DNS record information */ struct ast_dns_record { /*! \brief Resource record type */ @@ -151,6 +157,30 @@ struct ast_dns_query_recurring { char name[0]; }; +/*! \brief A DNS query set query, which includes its state */ +struct dns_query_set_query { + /*! \brief Whether the query started successfully or not */ + unsigned int started; + /*! \brief THe query itself */ + struct ast_dns_query *query; +}; + +/*! \brief A set of DNS queries */ +struct ast_dns_query_set { + /*! \brief DNS queries */ + AST_VECTOR(, struct dns_query_set_query) queries; + /* \brief Whether the query set is in progress or not */ + int in_progress; + /*! \brief The total number of completed queries */ + int queries_completed; + /*! \brief The total number of cancelled queries */ + int queries_cancelled; + /*! \brief Callback to invoke upon completion */ + ast_dns_query_set_callback callback; + /*! \brief User-specific data */ + void *user_data; +}; + /*! \brief An active DNS query */ struct ast_dns_query_active { /*! \brief The underlying DNS query */ @@ -241,3 +271,25 @@ int dns_parse_short(unsigned char *cur, uint16_t *val); * \return The number of bytes consumed while parsing */ int dns_parse_string(char *cur, uint8_t *size, char **val); + +/*! + * \brief Allocate a DNS query (but do not start resolution) + * + * \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 + * \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 query must be released upon completion or cancellation using ao2_ref + */ +struct ast_dns_query *dns_query_alloc(const char *name, int rr_type, int rr_class, ast_dns_resolve_callback callback, void *data); diff --git a/include/asterisk/dns_query_set.h b/include/asterisk/dns_query_set.h index c89fdfde70..fac732ae0d 100644 --- a/include/asterisk/dns_query_set.h +++ b/include/asterisk/dns_query_set.h @@ -43,6 +43,8 @@ typedef void (*ast_dns_query_set_callback)(const struct ast_dns_query_set *query * * \retval non-NULL success * \retval NULL failure + * + * \note The query set must be released upon cancellation or completion using ao2_ref */ struct ast_dns_query_set *ast_dns_query_set_create(void); @@ -76,6 +78,8 @@ size_t ast_dns_query_set_num_queries(const struct ast_dns_query_set *query_set); * * \retval non-NULL success * \retval NULL failure + * + * \note The returned query is only valid for the lifetime of the query set itself */ struct ast_dns_query *ast_dns_query_set_get(const struct ast_dns_query_set *query_set, unsigned int index); @@ -106,29 +110,25 @@ void ast_dns_query_set_resolve_async(struct ast_dns_query_set *query_set, ast_dn * * \param query_set The query set * + * \retval 0 success + * \retval -1 failure + * * \note This function will return when all queries have been completed */ -void ast_query_set_resolve(struct ast_dns_query_set *query_set); +int 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 + * \retval 0 success (all queries have been cancelled) + * \retval -1 failure (some queries could not be cancelled) * * \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 diff --git a/main/dns_core.c b/main/dns_core.c index e66c71d62b..0b471db918 100644 --- a/main/dns_core.c +++ b/main/dns_core.c @@ -32,7 +32,6 @@ ASTERISK_REGISTER_FILE() #include "asterisk/linkedlists.h" -#include "asterisk/vector.h" #include "asterisk/astobj2.h" #include "asterisk/strings.h" #include "asterisk/sched.h" @@ -163,6 +162,11 @@ const char *ast_dns_record_get_data(const struct ast_dns_record *record) return record->data_ptr; } +size_t ast_dns_record_get_data_size(const struct ast_dns_record *record) +{ + return record->data_len; +} + const struct ast_dns_record *ast_dns_record_get_next(const struct ast_dns_record *record) { return AST_LIST_NEXT(record, list); @@ -186,9 +190,9 @@ static void dns_query_destroy(void *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 *dns_query_alloc(const char *name, int rr_type, int rr_class, ast_dns_resolve_callback callback, void *data) { - struct ast_dns_query_active *active; + struct ast_dns_query *query; if (ast_strlen_zero(name)) { ast_log(LOG_WARNING, "Could not perform asynchronous resolution, no name provided\n"); @@ -215,30 +219,42 @@ struct ast_dns_query_active *ast_dns_resolve_async(const char *name, int rr_type 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); + query = ao2_alloc_options(sizeof(*query) + strlen(name) + 1, dns_query_destroy, AO2_ALLOC_OPT_LOCK_NOLOCK); + if (!query) { 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 */ + query->callback = callback; + query->user_data = ao2_bump(data); + query->rr_type = rr_type; + query->rr_class = rr_class; + strcpy(query->name, name); /* SAFE */ AST_RWLIST_RDLOCK(&resolvers); - active->query->resolver = AST_RWLIST_FIRST(&resolvers); + query->resolver = AST_RWLIST_FIRST(&resolvers); AST_RWLIST_UNLOCK(&resolvers); - if (!active->query->resolver) { + if (!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(query, -1); + return NULL; + } + + return query; +} + +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; + + active = ao2_alloc_options(sizeof(*active), dns_query_active_destroy, AO2_ALLOC_OPT_LOCK_NOLOCK); + if (!active) { + return NULL; + } + + active->query = dns_query_alloc(name, rr_type, rr_class, callback, data); + if (!active->query) { ao2_ref(active, -1); return NULL; } diff --git a/main/dns_query_set.c b/main/dns_query_set.c index 852fa3e533..c7a4eb18eb 100644 --- a/main/dns_query_set.c +++ b/main/dns_query_set.c @@ -33,39 +33,117 @@ ASTERISK_REGISTER_FILE() #include "asterisk/vector.h" #include "asterisk/astobj2.h" +#include "asterisk/utils.h" +#include "asterisk/linkedlists.h" #include "asterisk/dns_core.h" #include "asterisk/dns_query_set.h" +#include "asterisk/dns_internal.h" +#include "asterisk/dns_resolver.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; -}; +/*! \brief The default number of expected queries to be added to the query set */ +#define DNS_QUERY_SET_EXPECTED_QUERY_COUNT 5 + +/*! \brief Release all queries held in a query set */ +static void dns_query_set_release(struct ast_dns_query_set *query_set) +{ + int idx; + + for (idx = 0; idx < AST_VECTOR_SIZE(&query_set->queries); ++idx) { + struct dns_query_set_query *query = AST_VECTOR_GET_ADDR(&query_set->queries, idx); + + ao2_ref(query->query, -1); + } + + AST_VECTOR_FREE(&query_set->queries); +} + +/*! \brief Destructor for DNS query set */ +static void dns_query_set_destroy(void *data) +{ + struct ast_dns_query_set *query_set = data; + + dns_query_set_release(query_set); + ao2_cleanup(query_set->user_data); +} struct ast_dns_query_set *ast_dns_query_set_create(void) { - return NULL; + struct ast_dns_query_set *query_set; + + query_set = ao2_alloc_options(sizeof(*query_set), dns_query_set_destroy, AO2_ALLOC_OPT_LOCK_NOLOCK); + if (!query_set) { + return NULL; + } + + if (AST_VECTOR_INIT(&query_set->queries, DNS_QUERY_SET_EXPECTED_QUERY_COUNT)) { + ao2_ref(query_set, -1); + return NULL; + } + + return query_set; +} + +/*! \brief Callback invoked upon completion of a DNS query */ +static void dns_query_set_callback(const struct ast_dns_query *query) +{ + struct ast_dns_query_set *query_set = ast_dns_query_get_data(query); + + if (ast_atomic_fetchadd_int(&query_set->queries_completed, +1) != (AST_VECTOR_SIZE(&query_set->queries) - 1)) { + return; + } + + /* All queries have been completed, invoke final callback */ + if (query_set->queries_cancelled != AST_VECTOR_SIZE(&query_set->queries)) { + query_set->callback(query_set); + } + + ao2_cleanup(query_set->user_data); + query_set->user_data = NULL; + + dns_query_set_release(query_set); } int ast_dns_query_set_add(struct ast_dns_query_set *query_set, const char *name, int rr_type, int rr_class) { - return -1; + struct dns_query_set_query query = { + .started = 0, + }; + + ast_assert(!query_set->in_progress); + if (query_set->in_progress) { + ast_log(LOG_ERROR, "Attempted to add additional query to query set '%p' after resolution has started\n", + query_set); + return -1; + } + + query.query = dns_query_alloc(name, rr_type, rr_class, dns_query_set_callback, query_set); + if (!query.query) { + return -1; + } + + AST_VECTOR_APPEND(&query_set->queries, query); + + return 0; } size_t ast_dns_query_set_num_queries(const struct ast_dns_query_set *query_set) { - return 0; + return AST_VECTOR_SIZE(&query_set->queries); } struct ast_dns_query *ast_dns_query_set_get(const struct ast_dns_query_set *query_set, unsigned int index) { - return NULL; + /* Only once all queries have been completed can results be retrieved */ + if (query_set->queries_completed != AST_VECTOR_SIZE(&query_set->queries)) { + return NULL; + } + + /* If the index exceeds the number of queries... no query for you */ + if (index >= AST_VECTOR_SIZE(&query_set->queries)) { + return NULL; + } + + return AST_VECTOR_GET_ADDR(&query_set->queries, index)->query; } void *ast_dns_query_set_get_data(const struct ast_dns_query_set *query_set) @@ -75,19 +153,104 @@ void *ast_dns_query_set_get_data(const struct ast_dns_query_set *query_set) void ast_dns_query_set_resolve_async(struct ast_dns_query_set *query_set, ast_dns_query_set_callback callback, void *data) { + int idx; + + ast_assert(!query_set->in_progress); + if (query_set->in_progress) { + ast_log(LOG_ERROR, "Attempted to start asynchronous resolution of query set '%p' when it has already started\n", + query_set); + return; + } + + query_set->in_progress = 1; query_set->callback = callback; query_set->user_data = ao2_bump(data); + + for (idx = 0; idx < AST_VECTOR_SIZE(&query_set->queries); ++idx) { + struct dns_query_set_query *query = AST_VECTOR_GET_ADDR(&query_set->queries, idx); + + if (!query->query->resolver->resolve(query->query)) { + query->started = 1; + continue; + } + + dns_query_set_callback(query->query); + } } -void ast_query_set_resolve(struct ast_dns_query_set *query_set) +/*! \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 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); } -int ast_dns_query_set_resolve_cancel(struct ast_dns_query_set *query_set) +/*! \brief Callback used to implement synchronous resolution */ +static void dns_synchronous_resolve_callback(const struct ast_dns_query_set *query_set) { - return -1; + struct dns_synchronous_resolve *synchronous = ast_dns_query_set_get_data(query_set); + + ast_mutex_lock(&synchronous->lock); + synchronous->completed = 1; + ast_cond_signal(&synchronous->cond); + ast_mutex_unlock(&synchronous->lock); } -void ast_dns_query_set_free(struct ast_dns_query_set *query_set) +int ast_query_set_resolve(struct ast_dns_query_set *query_set) { + struct dns_synchronous_resolve *synchronous; + + 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); + + ast_dns_query_set_resolve_async(query_set, dns_synchronous_resolve_callback, synchronous); + + /* 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(synchronous, -1); + + return 0; } + +int ast_dns_query_set_resolve_cancel(struct ast_dns_query_set *query_set) +{ + int idx; + size_t query_count = AST_VECTOR_SIZE(&query_set->queries); + + for (idx = 0; idx < AST_VECTOR_SIZE(&query_set->queries); ++idx) { + struct dns_query_set_query *query = AST_VECTOR_GET_ADDR(&query_set->queries, idx); + + if (query->started) { + if (!query->query->resolver->cancel(query->query)) { + query_set->queries_cancelled++; + dns_query_set_callback(query->query); + } + } else { + query_set->queries_cancelled++; + } + } + + return (query_set->queries_cancelled == query_count) ? 0 : -1; +} \ No newline at end of file diff --git a/res/res_pjsip.c b/res/res_pjsip.c index fcd8516b65..d04b09b40d 100644 --- a/res/res_pjsip.c +++ b/res/res_pjsip.c @@ -3480,8 +3480,6 @@ static int load_module(void) return AST_MODULE_LOAD_DECLINE; } - ast_sip_initialize_dns(); - pjsip_tsx_layer_init_module(ast_pjsip_endpoint); pjsip_ua_init_module(ast_pjsip_endpoint, NULL); @@ -3514,6 +3512,9 @@ static int load_module(void) return AST_MODULE_LOAD_DECLINE; } + ast_sip_initialize_resolver(); + ast_sip_initialize_dns(); + if (ast_sip_initialize_distributor()) { ast_log(LOG_ERROR, "Failed to register distributor module. Aborting load\n"); ast_res_pjsip_destroy_configuration(); diff --git a/res/res_pjsip/include/res_pjsip_private.h b/res/res_pjsip/include/res_pjsip_private.h index bf428d5c51..a8b94112bc 100644 --- a/res/res_pjsip/include/res_pjsip_private.h +++ b/res/res_pjsip/include/res_pjsip_private.h @@ -231,6 +231,12 @@ void ast_sip_destroy_system(void); */ void ast_sip_initialize_dns(void); +/*! + * \internal + * \brief Initialize our own resolver support + */ +void ast_sip_initialize_resolver(void); + /*! * \internal * \brief Initialize global configuration diff --git a/res/res_pjsip/pjsip_resolver.c b/res/res_pjsip/pjsip_resolver.c new file mode 100644 index 0000000000..e4cc51af1e --- /dev/null +++ b/res/res_pjsip/pjsip_resolver.c @@ -0,0 +1,669 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Digium, Inc. + * + * Joshua Colp + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +#include "asterisk.h" + +#include +#include + +#include + +#include "asterisk/astobj2.h" +#include "asterisk/dns_core.h" +#include "asterisk/dns_query_set.h" +#include "asterisk/dns_srv.h" +#include "asterisk/dns_naptr.h" +#include "asterisk/res_pjsip.h" +#include "include/res_pjsip_private.h" + +#ifdef HAVE_PJSIP_EXTERNAL_RESOLVER + +/*! \brief Structure which contains transport+port information for an active query */ +struct sip_target { + /*! \brief The transport to be used */ + pjsip_transport_type_e transport; + /*! \brief The port */ + int port; +}; + +/*! \brief The vector used for current targets */ +AST_VECTOR(targets, struct sip_target); + +/*! \brief Structure which keeps track of resolution */ +struct sip_resolve { + /*! \brief Addresses currently being resolved, indexed based on index of queries in query set */ + struct targets resolving; + /*! \brief Active queries */ + struct ast_dns_query_set *queries; + /*! \brief Current viable server addresses */ + pjsip_server_addresses addresses; + /*! \brief Callback to invoke upon completion */ + pjsip_resolver_callback *callback; + /*! \brief User provided data */ + void *token; +}; + +/*! \brief Our own defined transports, reduces the size of sip_available_transports */ +enum sip_resolver_transport { + SIP_RESOLVER_TRANSPORT_UDP, + SIP_RESOLVER_TRANSPORT_TCP, + SIP_RESOLVER_TRANSPORT_TLS, + SIP_RESOLVER_TRANSPORT_UDP6, + SIP_RESOLVER_TRANSPORT_TCP6, + SIP_RESOLVER_TRANSPORT_TLS6, +}; + +/*! \brief Available transports on the system */ +static int sip_available_transports[] = { + /* This is a list of transports with whether they are available as a valid transport + * stored. We use our own identifier as to reduce the size of sip_available_transports. + * As this array is only manipulated at startup it does not require a lock to protect + * it. + */ + [SIP_RESOLVER_TRANSPORT_UDP] = 0, + [SIP_RESOLVER_TRANSPORT_TCP] = 0, + [SIP_RESOLVER_TRANSPORT_TLS] = 0, + [SIP_RESOLVER_TRANSPORT_UDP6] = 0, + [SIP_RESOLVER_TRANSPORT_TCP6] = 0, + [SIP_RESOLVER_TRANSPORT_TLS6] = 0, +}; + +/*! + * \internal + * \brief Destroy resolution data + * + * \param data The resolution data to destroy + * + * \return Nothing + */ +static void sip_resolve_destroy(void *data) +{ + struct sip_resolve *resolve = data; + + AST_VECTOR_FREE(&resolve->resolving); + ao2_cleanup(resolve->queries); +} + +/*! + * \internal + * \brief Check whether a transport is available or not + * + * \param transport The PJSIP transport type + * + * \return 1 success (transport is available) + * \return 0 failure (transport is not available) + */ +static int sip_transport_is_available(enum pjsip_transport_type_e transport) +{ + enum sip_resolver_transport resolver_transport; + + if (transport == PJSIP_TRANSPORT_UDP) { + resolver_transport = SIP_RESOLVER_TRANSPORT_UDP; + } else if (transport == PJSIP_TRANSPORT_TCP) { + resolver_transport = SIP_RESOLVER_TRANSPORT_TCP; + } else if (transport == PJSIP_TRANSPORT_TLS) { + resolver_transport = SIP_RESOLVER_TRANSPORT_TLS; + } else if (transport == PJSIP_TRANSPORT_UDP6) { + resolver_transport = SIP_RESOLVER_TRANSPORT_UDP6; + } else if (transport == PJSIP_TRANSPORT_TCP6) { + resolver_transport = SIP_RESOLVER_TRANSPORT_TCP6; + } else if (transport == PJSIP_TRANSPORT_TLS6) { + resolver_transport = SIP_RESOLVER_TRANSPORT_TLS6; + } else { + return 0; + } + + return sip_available_transports[resolver_transport]; +} + +/*! + * \internal + * \brief Add a query to be resolved + * + * \param resolve The ongoing resolution + * \param name What to resolve + * \param rr_type The type of record to look up + * \param rr_class The type of class to look up + * \param transport The transport to use for any resulting records + * \param port The port to use for any resulting records - if not specified the + * default for the transport is used + * + * \retval 0 success + * \retval -1 failure + */ +static int sip_resolve_add(struct sip_resolve *resolve, const char *name, int rr_type, int rr_class, pjsip_transport_type_e transport, int port) +{ + struct sip_target target = { + .transport = transport, + .port = port, + }; + + if (!resolve->queries) { + resolve->queries = ast_dns_query_set_create(); + } + + if (!resolve->queries) { + return -1; + } + + if (!port) { + target.port = pjsip_transport_get_default_port_for_type(transport); + } + + if (AST_VECTOR_APPEND(&resolve->resolving, target)) { + return -1; + } + + ast_debug(2, "[%p] Added target '%s' with record type '%d', transport '%s', and port '%d'\n", + resolve, name, rr_type, pjsip_transport_get_type_name(transport), target.port); + + return ast_dns_query_set_add(resolve->queries, name, rr_type, rr_class); +} + +/*! + * \internal + * \brief Task used to invoke the user specific callback + * + * \param data The complete resolution + * + * \return Nothing + */ +static int sip_resolve_invoke_user_callback(void *data) +{ + struct sip_resolve *resolve = data; + int idx; + + for (idx = 0; idx < resolve->addresses.count; ++idx) { + /* This includes space for the IP address, [, ], :, and the port */ + char addr[PJ_INET6_ADDRSTRLEN + 10]; + + ast_debug(2, "[%p] Address '%d' is %s with transport '%s'\n", + resolve, idx, pj_sockaddr_print(&resolve->addresses.entry[idx].addr, addr, sizeof(addr), 3), + pjsip_transport_get_type_name(resolve->addresses.entry[idx].type)); + } + + ast_debug(2, "[%p] Invoking user callback with '%d' addresses\n", resolve, resolve->addresses.count); + resolve->callback(resolve->addresses.count ? PJ_SUCCESS : PJLIB_UTIL_EDNSNOANSWERREC, resolve->token, &resolve->addresses); + + ao2_ref(resolve, -1); + + return 0; +} + +/*! + * \internal + * \brief Handle a NAPTR record according to RFC3263 + * + * \param resolve The ongoing resolution + * \param record The NAPTR record itself + * \param service The service to look for + * \param transport The transport to use for resulting queries + * + * \retval 0 success + * \retval -1 failure (record not handled / supported) + */ +static int sip_resolve_handle_naptr(struct sip_resolve *resolve, const struct ast_dns_record *record, + const char *service, pjsip_transport_type_e transport) +{ + if (strcasecmp(ast_dns_naptr_get_service(record), service)) { + return -1; + } + + if (!sip_transport_is_available(transport) && + !sip_transport_is_available(transport + PJSIP_TRANSPORT_IPV6)) { + ast_debug(2, "[%p] NAPTR service %s skipped as transport is unavailable\n", + resolve, service); + return -1; + } + + if (strcasecmp(ast_dns_naptr_get_flags(record), "s")) { + ast_debug(2, "[%p] NAPTR service %s received with unsupported flags '%s'\n", + resolve, service, ast_dns_naptr_get_flags(record)); + return -1; + } + + if (ast_strlen_zero(ast_dns_naptr_get_replacement(record))) { + return -1; + } + + return sip_resolve_add(resolve, ast_dns_naptr_get_replacement(record), ns_t_srv, ns_c_in, + transport, 0); +} + +/*! + * \internal + * \brief Query set callback function, invoked when all queries have completed + * + * \param query_set The completed query set + * + * \return Nothing + */ +static void sip_resolve_callback(const struct ast_dns_query_set *query_set) +{ + struct sip_resolve *resolve = ast_dns_query_set_get_data(query_set); + struct ast_dns_query_set *queries = resolve->queries; + struct targets resolving; + int idx, address_count = 0, have_naptr = 0, have_srv = 0; + unsigned short order = 0; + int strict_order = 0; + + ast_debug(2, "[%p] All parallel queries completed\n", resolve); + + resolve->queries = NULL; + + /* This purposely steals the resolving list so we can add entries to the new one in + * the same loop and also have access to the old. + */ + resolving = resolve->resolving; + AST_VECTOR_INIT(&resolve->resolving, 0); + + /* The order of queries is what defines the preference order for the records within + * this specific query set. The preference order overall is defined as a result of + * drilling down from other records. Each completed query set replaces the results + * of the last. + */ + for (idx = 0; idx < ast_dns_query_set_num_queries(queries); ++idx) { + struct ast_dns_query *query = ast_dns_query_set_get(queries, idx); + struct ast_dns_result *result = ast_dns_query_get_result(query); + struct sip_target *target; + const struct ast_dns_record *record; + + if (!result) { + ast_debug(2, "[%p] No result information for target '%s' of type '%d'\n", resolve, + ast_dns_query_get_name(query), ast_dns_query_get_rr_type(query)); + continue; + } + + target = AST_VECTOR_GET_ADDR(&resolving, idx); + for (record = ast_dns_result_get_records(result); record; record = ast_dns_record_get_next(record)) { + + if (ast_dns_record_get_rr_type(record) == ns_t_a || + ast_dns_record_get_rr_type(record) == ns_t_aaaa) { + /* If NAPTR or SRV records exist the subsequent results from them take preference */ + if (have_naptr || have_srv) { + ast_debug(2, "[%p] %s record being skipped on target '%s' because NAPTR or SRV record exists\n", + resolve, ast_dns_record_get_rr_type(record) == ns_t_a ? "A" : "AAAA", + ast_dns_query_get_name(query)); + continue; + } + + /* PJSIP has a fixed maximum number of addresses that can exist, so limit ourselves to that */ + if (address_count == PJSIP_MAX_RESOLVED_ADDRESSES) { + break; + } + + resolve->addresses.entry[address_count].type = target->transport; + + /* Populate address information for the new address entry */ + if (ast_dns_record_get_rr_type(record) == ns_t_a) { + ast_debug(2, "[%p] A record received on target '%s'\n", resolve, ast_dns_query_get_name(query)); + resolve->addresses.entry[address_count].addr_len = sizeof(pj_sockaddr_in); + pj_sockaddr_init(pj_AF_INET(), &resolve->addresses.entry[address_count].addr, NULL, + target->port); + resolve->addresses.entry[address_count].addr.ipv4.sin_addr = *(struct pj_in_addr*)ast_dns_record_get_data(record); + } else { + ast_debug(2, "[%p] AAAA record received on target '%s'\n", resolve, ast_dns_query_get_name(query)); + resolve->addresses.entry[address_count].addr_len = sizeof(pj_sockaddr_in6); + pj_sockaddr_init(pj_AF_INET6(), &resolve->addresses.entry[address_count].addr, NULL, + target->port); + pj_memcpy(&resolve->addresses.entry[address_count].addr.ipv6.sin6_addr, ast_dns_record_get_data(record), + ast_dns_record_get_data_size(record)); + } + + address_count++; + } else if (ast_dns_record_get_rr_type(record) == ns_t_srv) { + if (have_naptr) { + ast_debug(2, "[%p] SRV record being skipped on target '%s' because NAPTR record exists\n", + resolve, ast_dns_query_get_name(query)); + continue; + } + + /* SRV records just create new queries for AAAA+A, nothing fancy */ + ast_debug(2, "[%p] SRV record received on target '%s'\n", resolve, ast_dns_query_get_name(query)); + + if (sip_transport_is_available(target->transport + PJSIP_TRANSPORT_IPV6)) { + sip_resolve_add(resolve, ast_dns_srv_get_host(record), ns_t_aaaa, ns_c_in, target->transport + PJSIP_TRANSPORT_IPV6, + ast_dns_srv_get_port(record)); + have_srv = 1; + } + + if (sip_transport_is_available(target->transport)) { + sip_resolve_add(resolve, ast_dns_srv_get_host(record), ns_t_a, ns_c_in, target->transport, + ast_dns_srv_get_port(record)); + have_srv = 1; + } + } else if (ast_dns_record_get_rr_type(record) == ns_t_naptr) { + int added = -1; + + ast_debug(2, "[%p] NAPTR record received on target '%s'\n", resolve, ast_dns_query_get_name(query)); + + if (strict_order && (ast_dns_naptr_get_order(record) != order)) { + ast_debug(2, "[%p] NAPTR record skipped because order '%hu' does not match strict order '%hu'\n", + resolve, ast_dns_naptr_get_order(record), order); + continue; + } + + if (target->transport == PJSIP_TRANSPORT_UNSPECIFIED || target->transport == PJSIP_TRANSPORT_UDP) { + added = sip_resolve_handle_naptr(resolve, record, "sip+d2u", PJSIP_TRANSPORT_UDP); + } + if (target->transport == PJSIP_TRANSPORT_UNSPECIFIED || target->transport == PJSIP_TRANSPORT_TCP) { + added = sip_resolve_handle_naptr(resolve, record, "sip+d2t", PJSIP_TRANSPORT_TCP); + } + if (target->transport == PJSIP_TRANSPORT_UNSPECIFIED || target->transport == PJSIP_TRANSPORT_TLS) { + added = sip_resolve_handle_naptr(resolve, record, "sips+d2t", PJSIP_TRANSPORT_TLS); + } + + /* If this record was successfully handled then we need to limit ourselves to this order */ + if (!added) { + have_naptr = 1; + strict_order = 1; + order = ast_dns_naptr_get_order(record); + } + } + } + } + + /* Update the server addresses count, this is not limited as it can never exceed the max allowed */ + resolve->addresses.count = address_count; + + /* Free the vector we stole as we are responsible for it */ + AST_VECTOR_FREE(&resolving); + + /* If additional queries were added start the resolution process again */ + if (resolve->queries) { + ast_debug(2, "[%p] New queries added, performing parallel resolution again\n", resolve); + ast_dns_query_set_resolve_async(resolve->queries, sip_resolve_callback, resolve); + ao2_ref(queries, -1); + return; + } + + ast_debug(2, "[%p] Resolution completed - %d viable targets\n", resolve, resolve->addresses.count); + + /* Push a task to invoke the callback, we do this so it is guaranteed to run in a PJSIP thread */ + ao2_ref(resolve, +1); + if (ast_sip_push_task(NULL, sip_resolve_invoke_user_callback, resolve)) { + ao2_ref(resolve, -1); + } + + ao2_ref(queries, -1); +} + +/*! + * \internal + * \brief Determine what address family a host may be if it is already an IP address + * + * \param host The host (which may be an IP address) + * + * \retval 6 The host is an IPv6 address + * \retval 4 The host is an IPv4 address + * \retval 0 The host is not an IP address + */ +static int sip_resolve_get_ip_addr_ver(const pj_str_t *host) +{ + pj_in_addr dummy; + pj_in6_addr dummy6; + + if (pj_inet_aton(host, &dummy) > 0) { + return 4; + } + + if (pj_inet_pton(pj_AF_INET6(), host, &dummy6) == PJ_SUCCESS) { + return 6; + } + + return 0; +} + +/*! + * \internal + * \brief Perform SIP resolution of a host + * + * \param resolver Configured resolver instance + * \param pool Memory pool to allocate things from + * \param target The target we are resolving + * \param token User data to pass to the resolver callback + * \param cb User resolver callback to invoke upon resolution completion + */ +static void sip_resolve(pjsip_resolver_t *resolver, pj_pool_t *pool, const pjsip_host_info *target, + void *token, pjsip_resolver_callback *cb) +{ + int ip_addr_ver; + pjsip_transport_type_e type = target->type; + struct sip_resolve *resolve; + char host[NI_MAXHOST]; + int res = 0; + + ast_copy_pj_str(host, &target->addr.host, sizeof(host)); + + ast_debug(2, "Performing SIP DNS resolution of target '%s'\n", host); + + /* If the provided target is already an address don't bother resolving */ + ip_addr_ver = sip_resolve_get_ip_addr_ver(&target->addr.host); + + /* Determine the transport to use if none has been explicitly specified */ + if (type == PJSIP_TRANSPORT_UNSPECIFIED) { + /* If we've been told to use a secure or reliable transport restrict ourselves to that */ +#if PJ_HAS_TCP + if (target->flag & PJSIP_TRANSPORT_SECURE) { + type = PJSIP_TRANSPORT_TLS; + } else if (target->flag & PJSIP_TRANSPORT_RELIABLE) { + type = PJSIP_TRANSPORT_TCP; + } else +#endif + /* According to the RFC otherwise if an explicit IP address OR an explicit port is specified + * we use UDP + */ + if (ip_addr_ver || target->addr.port) { + type = PJSIP_TRANSPORT_UDP; + } + + if (ip_addr_ver == 6) { + type = (pjsip_transport_type_e)((int) type + PJSIP_TRANSPORT_IPV6); + } + } + + ast_debug(2, "Transport type for target '%s' is '%s'\n", host, pjsip_transport_get_type_name(type)); + + /* If it's already an address call the callback immediately */ + if (ip_addr_ver) { + pjsip_server_addresses addresses = { + .entry[0].type = type, + .count = 1, + }; + + if (ip_addr_ver == 4) { + addresses.entry[0].addr_len = sizeof(pj_sockaddr_in); + pj_sockaddr_init(pj_AF_INET(), &addresses.entry[0].addr, NULL, 0); + pj_inet_aton(&target->addr.host, &addresses.entry[0].addr.ipv4.sin_addr); + } else { + addresses.entry[0].addr_len = sizeof(pj_sockaddr_in6); + pj_sockaddr_init(pj_AF_INET6(), &addresses.entry[0].addr, NULL, 0); + pj_inet_pton(pj_AF_INET6(), &target->addr.host, &addresses.entry[0].addr.ipv6.sin6_addr); + } + + pj_sockaddr_set_port(&addresses.entry[0].addr, !target->addr.port ? pjsip_transport_get_default_port_for_type(type) : target->addr.port); + + ast_debug(2, "Target '%s' is an IP address, skipping resolution\n", host); + + cb(PJ_SUCCESS, token, &addresses); + + return; + } + + resolve = ao2_alloc_options(sizeof(*resolve), sip_resolve_destroy, AO2_ALLOC_OPT_LOCK_NOLOCK); + if (!resolve) { + cb(PJ_ENOMEM, token, NULL); + return; + } + + resolve->callback = cb; + resolve->token = token; + + if (AST_VECTOR_INIT(&resolve->resolving, 2)) { + ao2_ref(resolve, -1); + cb(PJ_ENOMEM, token, NULL); + return; + } + + ast_debug(2, "[%p] Created resolution tracking for target '%s'\n", resolve, host); + + /* If no port has been specified we can do NAPTR + SRV */ + if (!target->addr.port) { + char srv[NI_MAXHOST]; + + res |= sip_resolve_add(resolve, host, ns_t_naptr, ns_c_in, type, 0); + + if ((type == PJSIP_TRANSPORT_TLS || type == PJSIP_TRANSPORT_UNSPECIFIED) && + (sip_transport_is_available(PJSIP_TRANSPORT_TLS) || + sip_transport_is_available(PJSIP_TRANSPORT_TLS6))) { + snprintf(srv, sizeof(srv), "_sips._tcp.%s", host); + res |= sip_resolve_add(resolve, srv, ns_t_srv, ns_c_in, PJSIP_TRANSPORT_TLS, 0); + } + if ((type == PJSIP_TRANSPORT_TCP || type == PJSIP_TRANSPORT_UNSPECIFIED) && + (sip_transport_is_available(PJSIP_TRANSPORT_TCP) || + sip_transport_is_available(PJSIP_TRANSPORT_TCP6))) { + snprintf(srv, sizeof(srv), "_sip._tcp.%s", host); + res |= sip_resolve_add(resolve, srv, ns_t_srv, ns_c_in, PJSIP_TRANSPORT_TCP, 0); + } + if ((type == PJSIP_TRANSPORT_UDP || type == PJSIP_TRANSPORT_UNSPECIFIED) && + (sip_transport_is_available(PJSIP_TRANSPORT_UDP) || + sip_transport_is_available(PJSIP_TRANSPORT_UDP6))) { + snprintf(srv, sizeof(srv), "_sip._udp.%s", host); + res |= sip_resolve_add(resolve, srv, ns_t_srv, ns_c_in, PJSIP_TRANSPORT_UDP, 0); + } + } + + if ((type == PJSIP_TRANSPORT_UNSPECIFIED && sip_transport_is_available(PJSIP_TRANSPORT_UDP6)) || + sip_transport_is_available(type + PJSIP_TRANSPORT_IPV6)) { + res |= sip_resolve_add(resolve, host, ns_t_aaaa, ns_c_in, (type == PJSIP_TRANSPORT_UNSPECIFIED ? PJSIP_TRANSPORT_UDP6 : type + PJSIP_TRANSPORT_IPV6), target->addr.port); + } + + if ((type == PJSIP_TRANSPORT_UNSPECIFIED && sip_transport_is_available(PJSIP_TRANSPORT_UDP)) || + sip_transport_is_available(type)) { + res |= sip_resolve_add(resolve, host, ns_t_a, ns_c_in, (type == PJSIP_TRANSPORT_UNSPECIFIED ? PJSIP_TRANSPORT_UDP : type), target->addr.port); + } + + if (res) { + ao2_ref(resolve, -1); + cb(PJ_ENOMEM, token, NULL); + return; + } + + ast_debug(2, "[%p] Starting initial resolution using parallel queries for target '%s'\n", resolve, host); + ast_dns_query_set_resolve_async(resolve->queries, sip_resolve_callback, resolve); + + ao2_ref(resolve, -1); +} + +/*! + * \internal + * \brief Determine if a specific transport is configured on the system + * + * \param pool A memory pool to allocate things from + * \param transport The type of transport to check + * \param name A friendly name to print in the verbose message + * + * \return Nothing + */ +static void sip_check_transport(pj_pool_t *pool, pjsip_transport_type_e transport, const char *name) +{ + pjsip_tpmgr_fla2_param prm; + enum sip_resolver_transport resolver_transport; + + pjsip_tpmgr_fla2_param_default(&prm); + prm.tp_type = transport; + + if (transport == PJSIP_TRANSPORT_UDP) { + resolver_transport = SIP_RESOLVER_TRANSPORT_UDP; + } else if (transport == PJSIP_TRANSPORT_TCP) { + resolver_transport = SIP_RESOLVER_TRANSPORT_TCP; + } else if (transport == PJSIP_TRANSPORT_TLS) { + resolver_transport = SIP_RESOLVER_TRANSPORT_TLS; + } else if (transport == PJSIP_TRANSPORT_UDP6) { + resolver_transport = SIP_RESOLVER_TRANSPORT_UDP6; + } else if (transport == PJSIP_TRANSPORT_TCP6) { + resolver_transport = SIP_RESOLVER_TRANSPORT_TCP6; + } else if (transport == PJSIP_TRANSPORT_TLS6) { + resolver_transport = SIP_RESOLVER_TRANSPORT_TLS6; + } else { + ast_verb(2, "'%s' is an unsupported SIP transport\n", name); + return; + } + + if (pjsip_tpmgr_find_local_addr2(pjsip_endpt_get_tpmgr(ast_sip_get_pjsip_endpoint()), + pool, &prm) == PJ_SUCCESS) { + ast_verb(2, "'%s' is an available SIP transport\n", name); + sip_available_transports[resolver_transport] = 1; + } else { + ast_verb(2, "'%s' is not an available SIP transport, disabling resolver support for it\n", + name); + } +} + +/*! \brief External resolver implementation for PJSIP */ +static pjsip_ext_resolver resolver = { + .resolve = sip_resolve, +}; + +/*! + * \internal + * \brief Task to determine available transports and set ourselves an external resolver + * + * \retval 0 success + * \retval -1 failure + */ +static int sip_replace_resolver(void *data) +{ + pj_pool_t *pool; + + + pool = pjsip_endpt_create_pool(ast_sip_get_pjsip_endpoint(), "Transport Availability", 256, 256); + if (!pool) { + return -1; + } + + /* Determine what transports are available on the system */ + sip_check_transport(pool, PJSIP_TRANSPORT_UDP, "UDP+IPv4"); + sip_check_transport(pool, PJSIP_TRANSPORT_TCP, "TCP+IPv4"); + sip_check_transport(pool, PJSIP_TRANSPORT_TLS, "TLS+IPv4"); + sip_check_transport(pool, PJSIP_TRANSPORT_UDP6, "UDP+IPv6"); + sip_check_transport(pool, PJSIP_TRANSPORT_TCP6, "TCP+IPv6"); + sip_check_transport(pool, PJSIP_TRANSPORT_TLS6, "TLS+IPv6"); + + pjsip_endpt_release_pool(ast_sip_get_pjsip_endpoint(), pool); + + /* Replace the PJSIP resolver with our own implementation */ + pjsip_endpt_set_ext_resolver(ast_sip_get_pjsip_endpoint(), &resolver); + return 0; +} + +void ast_sip_initialize_resolver(void) +{ + /* Replace the existing PJSIP resolver with our own implementation */ + ast_sip_push_task_synchronous(NULL, sip_replace_resolver, NULL); +} + +#else + +void ast_sip_initialize_resolver(void) +{ + /* External resolver support does not exist in the version of PJSIP in use */ + ast_log(LOG_NOTICE, "The version of PJSIP in use does not support external resolvers, using PJSIP provided resolver\n"); +} + +#endif diff --git a/tests/test_dns_query_set.c b/tests/test_dns_query_set.c new file mode 100644 index 0000000000..08829f59e9 --- /dev/null +++ b/tests/test_dns_query_set.c @@ -0,0 +1,365 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Digium, Inc. + * + * Joshua Colp + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*** MODULEINFO + TEST_FRAMEWORK + core + ***/ + +#include "asterisk.h" + +#include +#include + +#include "asterisk/test.h" +#include "asterisk/module.h" +#include "asterisk/vector.h" +#include "asterisk/dns_core.h" +#include "asterisk/dns_resolver.h" +#include "asterisk/dns_query_set.h" +#include "asterisk/dns_internal.h" + +struct query_set_data { + /*! Boolean indicator if query set has completed */ + int query_set_complete; + /*! Number of times resolve() method has been called */ + int resolves; + /*! Number of times resolve() method is allowed to be called */ + int resolves_allowed; + /*! Number of times cancel() method has been called */ + int cancel; + /*! Number of times cancel() method is allowed to be called */ + int cancel_allowed; + ast_mutex_t lock; + ast_cond_t cond; +}; + +static void query_set_data_destructor(void *obj) +{ + struct query_set_data *qsdata = obj; + + ast_mutex_destroy(&qsdata->lock); + ast_cond_destroy(&qsdata->cond); +} + +static struct query_set_data *query_set_data_alloc(void) +{ + struct query_set_data *qsdata; + + qsdata = ao2_alloc(sizeof(*qsdata), query_set_data_destructor); + if (!qsdata) { + return NULL; + } + + ast_mutex_init(&qsdata->lock); + ast_cond_init(&qsdata->cond, NULL); + + return qsdata; +} + +#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. If the allowed number of resolutions + * has not been reached then this will succeed, otherwise the + * query is expected to have been canceled. + * + * \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; + struct ast_dns_query_set *query_set = ast_dns_query_get_data(query); + struct query_set_data *qsdata = query_set->user_data; + + ast_assert(qsdata != NULL); + + ast_dns_resolver_set_result(query, 0, 0, ns_r_noerror, "asterisk.org", DNS_ANSWER, DNS_ANSWER_SIZE); + 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 query_set_resolve(struct ast_dns_query *query) +{ + struct ast_dns_query_set *query_set = ast_dns_query_get_data(query); + struct query_set_data *qsdata = query_set->user_data; + pthread_t resolver_thread; + + /* Only the queries which will not be canceled actually start a thread */ + if (qsdata->resolves++ < qsdata->cancel_allowed) { + return 0; + } + + 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 query_set_cancel(struct ast_dns_query *query) +{ + struct ast_dns_query_set *query_set = ast_dns_query_get_data(query); + struct query_set_data *qsdata = query_set->user_data; + int res = -1; + + if (qsdata->cancel++ < qsdata->cancel_allowed) { + res = 0; + } + + return res; +} + +static struct ast_dns_resolver query_set_resolver = { + .name = "query_set", + .priority = 0, + .resolve = query_set_resolve, + .cancel = query_set_cancel, +}; + +/*! + * \brief Callback which is invoked upon query set completion + * + * \param query_set The query set + */ +static void query_set_callback(const struct ast_dns_query_set *query_set) +{ + struct query_set_data *qsdata = ast_dns_query_set_get_data(query_set); + + ast_mutex_lock(&qsdata->lock); + qsdata->query_set_complete = 1; + ast_cond_signal(&qsdata->cond); + ast_mutex_unlock(&qsdata->lock); +} + +/*! + * \brief Framework for running a query set DNS test + * + * This function serves as a common way of testing various numbers of queries in a + * query set and optional canceling of them. + * + * \param test The test being run + * \param resolve The number of queries that should be allowed to complete resolution + * \param cancel The number of queries that should be allowed to be canceled + */ +static enum ast_test_result_state query_set_test(struct ast_test *test, int resolve, int cancel) +{ + int total = resolve + cancel; + RAII_VAR(struct ast_dns_query_set *, query_set, NULL, ao2_cleanup); + RAII_VAR(struct query_set_data *, qsdata, NULL, ao2_cleanup); + enum ast_test_result_state res = AST_TEST_PASS; + int idx; + struct timespec timeout; + + if (ast_dns_resolver_register(&query_set_resolver)) { + ast_test_status_update(test, "Failed to register query set DNS resolver\n"); + return AST_TEST_FAIL; + } + + qsdata = query_set_data_alloc(); + if (!qsdata) { + ast_test_status_update(test, "Failed to allocate data necessary for query set test\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + query_set = ast_dns_query_set_create(); + if (!query_set) { + ast_test_status_update(test, "Failed to create DNS query set\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + qsdata->resolves_allowed = resolve; + qsdata->cancel_allowed = cancel; + + for (idx = 0; idx < total; ++idx) { + if (ast_dns_query_set_add(query_set, "asterisk.org", ns_t_a, ns_c_in)) { + ast_test_status_update(test, "Failed to add query to DNS query set\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + } + + if (ast_dns_query_set_num_queries(query_set) != total) { + ast_test_status_update(test, "DNS query set does not contain the correct number of queries\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + ast_dns_query_set_resolve_async(query_set, query_set_callback, qsdata); + + if (cancel && (cancel == total)) { + if (ast_dns_query_set_resolve_cancel(query_set)) { + ast_test_status_update(test, "Failed to cancel DNS query set when it should be cancellable\n"); + res = AST_TEST_FAIL; + } + + if (qsdata->query_set_complete) { + ast_test_status_update(test, "Query set callback was invoked despite all queries being cancelled\n"); + res = AST_TEST_FAIL; + } + + goto cleanup; + } else if (cancel) { + if (!ast_dns_query_set_resolve_cancel(query_set)) { + ast_test_status_update(test, "Successfully cancelled DNS query set when it should not be possible\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + } + + clock_gettime(CLOCK_REALTIME, &timeout); + timeout.tv_sec += 10; + + ast_mutex_lock(&qsdata->lock); + while (!qsdata->query_set_complete) { + if (ast_cond_timedwait(&qsdata->cond, &qsdata->lock, &timeout) == ETIMEDOUT) { + break; + } + } + ast_mutex_unlock(&qsdata->lock); + + if (!qsdata->query_set_complete) { + ast_test_status_update(test, "Query set did not complete when it should have\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + for (idx = 0; idx < ast_dns_query_set_num_queries(query_set); ++idx) { + const struct ast_dns_query *query = ast_dns_query_set_get(query_set, idx); + + if (strcmp(ast_dns_query_get_name(query), "asterisk.org")) { + ast_test_status_update(test, "Query did not have expected name\n"); + res = AST_TEST_FAIL; + } + if (ast_dns_query_get_rr_type(query) != ns_t_a) { + ast_test_status_update(test, "Query did not have expected type\n"); + res = AST_TEST_FAIL; + } + if (ast_dns_query_get_rr_class(query) != ns_c_in) { + ast_test_status_update(test, "Query did not have expected class\n"); + res = AST_TEST_FAIL; + } + } + +cleanup: + ast_dns_resolver_unregister(&query_set_resolver); + return res; +} + +AST_TEST_DEFINE(query_set) +{ + switch (cmd) { + case TEST_INIT: + info->name = "query_set"; + info->category = "/main/dns/query_set/"; + info->summary = "Test nominal asynchronous DNS query set\n"; + info->description = + "This tests nominal query set in the following ways:\n" + "\t* Multiple queries are added to a query set\n" + "\t* The mock resolver is configured to respond to all queries\n" + "\t* Asynchronous resolution of the query set is started\n" + "\t* The mock resolver responds to all queries\n" + "\t* We ensure that the query set callback is invoked upon completion\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + return query_set_test(test, 4, 0); +} + +AST_TEST_DEFINE(query_set_nominal_cancel) +{ + switch (cmd) { + case TEST_INIT: + info->name = "query_set_nominal_cancel"; + info->category = "/main/dns/query_set/"; + info->summary = "Test nominal asynchronous DNS query set cancellation\n"; + info->description = + "This tests nominal query set cancellation in the following ways:\n" + "\t* Multiple queries are added to a query set\n" + "\t* The mock resolver is configured to NOT respond to any queries\n" + "\t* Asynchronous resolution of the query set is started\n" + "\t* The query set is canceled and is confirmed to return with success\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + return query_set_test(test, 0, 4); +} + +AST_TEST_DEFINE(query_set_off_nominal_cancel) +{ + switch (cmd) { + case TEST_INIT: + info->name = "query_set_off_nominal_cancel"; + info->category = "/main/dns/query_set/"; + info->summary = "Test off-nominal asynchronous DNS query set cancellation\n"; + info->description = + "This tests nominal query set cancellation in the following ways:\n" + "\t* Multiple queries are added to a query set\n" + "\t* The mock resolver is configured to respond to half the queries\n" + "\t* Asynchronous resolution of the query set is started\n" + "\t* The query set is canceled and is confirmed to return failure\n" + "\t* The query set callback is confirmed to run, since it could not be fully canceled\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + return query_set_test(test, 2, 2); +} + +static int unload_module(void) +{ + AST_TEST_UNREGISTER(query_set); + AST_TEST_UNREGISTER(query_set_nominal_cancel); + AST_TEST_UNREGISTER(query_set_off_nominal_cancel); + + return 0; +} + +static int load_module(void) +{ + AST_TEST_REGISTER(query_set); + AST_TEST_REGISTER(query_set_nominal_cancel); + AST_TEST_REGISTER(query_set_off_nominal_cancel); + + return AST_MODULE_LOAD_SUCCESS; +} + +AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "DNS query set tests");