2 * Asterisk -- An open source telephony toolkit.
4 * Copyright (C) 2015, Digium, Inc.
6 * Joshua Colp <jcolp@digium.com>
8 * See http://www.asterisk.org for more information about
9 * the Asterisk project. Please do not directly contact
10 * any of the maintainers of this project for assistance;
11 * the project provides a web site, mailing lists and IRC
12 * channels for your use.
14 * This program is free software, distributed under the terms of
15 * the GNU General Public License Version 2. See the LICENSE file
16 * at the top of the source tree.
22 #include <pjlib-util/errno.h>
24 #include <arpa/nameser.h>
26 #include "asterisk/astobj2.h"
27 #include "asterisk/dns_core.h"
28 #include "asterisk/dns_query_set.h"
29 #include "asterisk/dns_srv.h"
30 #include "asterisk/dns_naptr.h"
31 #include "asterisk/res_pjsip.h"
32 #include "include/res_pjsip_private.h"
34 #ifdef HAVE_PJSIP_EXTERNAL_RESOLVER
36 /*! \brief Structure which contains transport+port information for an active query */
38 /*! \brief The transport to be used */
39 pjsip_transport_type_e transport;
40 /*! \brief The port */
44 /*! \brief The vector used for current targets */
45 AST_VECTOR(targets, struct sip_target);
47 /*! \brief Structure which keeps track of resolution */
49 /*! \brief Addresses currently being resolved, indexed based on index of queries in query set */
50 struct targets resolving;
51 /*! \brief Active queries */
52 struct ast_dns_query_set *queries;
53 /*! \brief Current viable server addresses */
54 pjsip_server_addresses addresses;
55 /*! \brief Callback to invoke upon completion */
56 pjsip_resolver_callback *callback;
57 /*! \brief User provided data */
61 /*! \brief Our own defined transports, reduces the size of sip_available_transports */
62 enum sip_resolver_transport {
63 SIP_RESOLVER_TRANSPORT_UDP,
64 SIP_RESOLVER_TRANSPORT_TCP,
65 SIP_RESOLVER_TRANSPORT_TLS,
66 SIP_RESOLVER_TRANSPORT_UDP6,
67 SIP_RESOLVER_TRANSPORT_TCP6,
68 SIP_RESOLVER_TRANSPORT_TLS6,
71 /*! \brief Available transports on the system */
72 static int sip_available_transports[] = {
73 /* This is a list of transports with whether they are available as a valid transport
74 * stored. We use our own identifier as to reduce the size of sip_available_transports.
75 * As this array is only manipulated at startup it does not require a lock to protect
78 [SIP_RESOLVER_TRANSPORT_UDP] = 0,
79 [SIP_RESOLVER_TRANSPORT_TCP] = 0,
80 [SIP_RESOLVER_TRANSPORT_TLS] = 0,
81 [SIP_RESOLVER_TRANSPORT_UDP6] = 0,
82 [SIP_RESOLVER_TRANSPORT_TCP6] = 0,
83 [SIP_RESOLVER_TRANSPORT_TLS6] = 0,
88 * \brief Destroy resolution data
90 * \param data The resolution data to destroy
94 static void sip_resolve_destroy(void *data)
96 struct sip_resolve *resolve = data;
98 AST_VECTOR_FREE(&resolve->resolving);
99 ao2_cleanup(resolve->queries);
104 * \brief Check whether a transport is available or not
106 * \param transport The PJSIP transport type
108 * \return 1 success (transport is available)
109 * \return 0 failure (transport is not available)
111 static int sip_transport_is_available(enum pjsip_transport_type_e transport)
113 enum sip_resolver_transport resolver_transport;
115 if (transport == PJSIP_TRANSPORT_UDP) {
116 resolver_transport = SIP_RESOLVER_TRANSPORT_UDP;
117 } else if (transport == PJSIP_TRANSPORT_TCP) {
118 resolver_transport = SIP_RESOLVER_TRANSPORT_TCP;
119 } else if (transport == PJSIP_TRANSPORT_TLS) {
120 resolver_transport = SIP_RESOLVER_TRANSPORT_TLS;
121 } else if (transport == PJSIP_TRANSPORT_UDP6) {
122 resolver_transport = SIP_RESOLVER_TRANSPORT_UDP6;
123 } else if (transport == PJSIP_TRANSPORT_TCP6) {
124 resolver_transport = SIP_RESOLVER_TRANSPORT_TCP6;
125 } else if (transport == PJSIP_TRANSPORT_TLS6) {
126 resolver_transport = SIP_RESOLVER_TRANSPORT_TLS6;
131 return sip_available_transports[resolver_transport];
136 * \brief Add a query to be resolved
138 * \param resolve The ongoing resolution
139 * \param name What to resolve
140 * \param rr_type The type of record to look up
141 * \param rr_class The type of class to look up
142 * \param transport The transport to use for any resulting records
143 * \param port The port to use for any resulting records - if not specified the
144 * default for the transport is used
149 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)
151 struct sip_target target = {
152 .transport = transport,
156 if (!resolve->queries) {
157 resolve->queries = ast_dns_query_set_create();
160 if (!resolve->queries) {
165 target.port = pjsip_transport_get_default_port_for_type(transport);
168 if (AST_VECTOR_APPEND(&resolve->resolving, target)) {
172 ast_debug(2, "[%p] Added target '%s' with record type '%d', transport '%s', and port '%d'\n",
173 resolve, name, rr_type, pjsip_transport_get_type_name(transport), target.port);
175 return ast_dns_query_set_add(resolve->queries, name, rr_type, rr_class);
180 * \brief Task used to invoke the user specific callback
182 * \param data The complete resolution
186 static int sip_resolve_invoke_user_callback(void *data)
188 struct sip_resolve *resolve = data;
191 for (idx = 0; idx < resolve->addresses.count; ++idx) {
192 /* This includes space for the IP address, [, ], :, and the port */
193 char addr[PJ_INET6_ADDRSTRLEN + 10];
195 ast_debug(2, "[%p] Address '%d' is %s with transport '%s'\n",
196 resolve, idx, pj_sockaddr_print(&resolve->addresses.entry[idx].addr, addr, sizeof(addr), 3),
197 pjsip_transport_get_type_name(resolve->addresses.entry[idx].type));
200 ast_debug(2, "[%p] Invoking user callback with '%d' addresses\n", resolve, resolve->addresses.count);
201 resolve->callback(resolve->addresses.count ? PJ_SUCCESS : PJLIB_UTIL_EDNSNOANSWERREC, resolve->token, &resolve->addresses);
203 ao2_ref(resolve, -1);
210 * \brief Handle a NAPTR record according to RFC3263
212 * \param resolve The ongoing resolution
213 * \param record The NAPTR record itself
214 * \param service The service to look for
215 * \param transport The transport to use for resulting queries
218 * \retval -1 failure (record not handled / supported)
220 static int sip_resolve_handle_naptr(struct sip_resolve *resolve, const struct ast_dns_record *record,
221 const char *service, pjsip_transport_type_e transport)
223 if (strcasecmp(ast_dns_naptr_get_service(record), service)) {
227 if (!sip_transport_is_available(transport) &&
228 !sip_transport_is_available(transport + PJSIP_TRANSPORT_IPV6)) {
229 ast_debug(2, "[%p] NAPTR service %s skipped as transport is unavailable\n",
234 if (strcasecmp(ast_dns_naptr_get_flags(record), "s")) {
235 ast_debug(2, "[%p] NAPTR service %s received with unsupported flags '%s'\n",
236 resolve, service, ast_dns_naptr_get_flags(record));
240 if (ast_strlen_zero(ast_dns_naptr_get_replacement(record))) {
244 return sip_resolve_add(resolve, ast_dns_naptr_get_replacement(record), ns_t_srv, ns_c_in,
250 * \brief Query set callback function, invoked when all queries have completed
252 * \param query_set The completed query set
256 static void sip_resolve_callback(const struct ast_dns_query_set *query_set)
258 struct sip_resolve *resolve = ast_dns_query_set_get_data(query_set);
259 struct ast_dns_query_set *queries = resolve->queries;
260 struct targets resolving;
261 int idx, address_count = 0, have_naptr = 0, have_srv = 0;
262 unsigned short order = 0;
263 int strict_order = 0;
265 ast_debug(2, "[%p] All parallel queries completed\n", resolve);
267 resolve->queries = NULL;
269 /* This purposely steals the resolving list so we can add entries to the new one in
270 * the same loop and also have access to the old.
272 resolving = resolve->resolving;
273 AST_VECTOR_INIT(&resolve->resolving, 0);
275 /* The order of queries is what defines the preference order for the records within
276 * this specific query set. The preference order overall is defined as a result of
277 * drilling down from other records. Each completed query set replaces the results
280 for (idx = 0; idx < ast_dns_query_set_num_queries(queries); ++idx) {
281 struct ast_dns_query *query = ast_dns_query_set_get(queries, idx);
282 struct ast_dns_result *result = ast_dns_query_get_result(query);
283 struct sip_target *target;
284 const struct ast_dns_record *record;
287 ast_debug(2, "[%p] No result information for target '%s' of type '%d'\n", resolve,
288 ast_dns_query_get_name(query), ast_dns_query_get_rr_type(query));
292 target = AST_VECTOR_GET_ADDR(&resolving, idx);
293 for (record = ast_dns_result_get_records(result); record; record = ast_dns_record_get_next(record)) {
295 if (ast_dns_record_get_rr_type(record) == ns_t_a ||
296 ast_dns_record_get_rr_type(record) == ns_t_aaaa) {
297 /* If NAPTR or SRV records exist the subsequent results from them take preference */
298 if (have_naptr || have_srv) {
299 ast_debug(2, "[%p] %s record being skipped on target '%s' because NAPTR or SRV record exists\n",
300 resolve, ast_dns_record_get_rr_type(record) == ns_t_a ? "A" : "AAAA",
301 ast_dns_query_get_name(query));
305 /* PJSIP has a fixed maximum number of addresses that can exist, so limit ourselves to that */
306 if (address_count == PJSIP_MAX_RESOLVED_ADDRESSES) {
310 resolve->addresses.entry[address_count].type = target->transport;
312 /* Populate address information for the new address entry */
313 if (ast_dns_record_get_rr_type(record) == ns_t_a) {
314 ast_debug(2, "[%p] A record received on target '%s'\n", resolve, ast_dns_query_get_name(query));
315 resolve->addresses.entry[address_count].addr_len = sizeof(pj_sockaddr_in);
316 pj_sockaddr_init(pj_AF_INET(), &resolve->addresses.entry[address_count].addr, NULL,
318 resolve->addresses.entry[address_count].addr.ipv4.sin_addr = *(struct pj_in_addr*)ast_dns_record_get_data(record);
320 ast_debug(2, "[%p] AAAA record received on target '%s'\n", resolve, ast_dns_query_get_name(query));
321 resolve->addresses.entry[address_count].addr_len = sizeof(pj_sockaddr_in6);
322 pj_sockaddr_init(pj_AF_INET6(), &resolve->addresses.entry[address_count].addr, NULL,
324 pj_memcpy(&resolve->addresses.entry[address_count].addr.ipv6.sin6_addr, ast_dns_record_get_data(record),
325 ast_dns_record_get_data_size(record));
329 } else if (ast_dns_record_get_rr_type(record) == ns_t_srv) {
331 ast_debug(2, "[%p] SRV record being skipped on target '%s' because NAPTR record exists\n",
332 resolve, ast_dns_query_get_name(query));
336 /* SRV records just create new queries for AAAA+A, nothing fancy */
337 ast_debug(2, "[%p] SRV record received on target '%s'\n", resolve, ast_dns_query_get_name(query));
339 if (sip_transport_is_available(target->transport + PJSIP_TRANSPORT_IPV6)) {
340 sip_resolve_add(resolve, ast_dns_srv_get_host(record), ns_t_aaaa, ns_c_in, target->transport + PJSIP_TRANSPORT_IPV6,
341 ast_dns_srv_get_port(record));
345 if (sip_transport_is_available(target->transport)) {
346 sip_resolve_add(resolve, ast_dns_srv_get_host(record), ns_t_a, ns_c_in, target->transport,
347 ast_dns_srv_get_port(record));
350 } else if (ast_dns_record_get_rr_type(record) == ns_t_naptr) {
353 ast_debug(2, "[%p] NAPTR record received on target '%s'\n", resolve, ast_dns_query_get_name(query));
355 if (strict_order && (ast_dns_naptr_get_order(record) != order)) {
356 ast_debug(2, "[%p] NAPTR record skipped because order '%hu' does not match strict order '%hu'\n",
357 resolve, ast_dns_naptr_get_order(record), order);
361 if (target->transport == PJSIP_TRANSPORT_UNSPECIFIED || target->transport == PJSIP_TRANSPORT_UDP) {
362 added = sip_resolve_handle_naptr(resolve, record, "sip+d2u", PJSIP_TRANSPORT_UDP);
364 if (target->transport == PJSIP_TRANSPORT_UNSPECIFIED || target->transport == PJSIP_TRANSPORT_TCP) {
365 added = sip_resolve_handle_naptr(resolve, record, "sip+d2t", PJSIP_TRANSPORT_TCP);
367 if (target->transport == PJSIP_TRANSPORT_UNSPECIFIED || target->transport == PJSIP_TRANSPORT_TLS) {
368 added = sip_resolve_handle_naptr(resolve, record, "sips+d2t", PJSIP_TRANSPORT_TLS);
371 /* If this record was successfully handled then we need to limit ourselves to this order */
375 order = ast_dns_naptr_get_order(record);
381 /* Update the server addresses count, this is not limited as it can never exceed the max allowed */
382 resolve->addresses.count = address_count;
384 /* Free the vector we stole as we are responsible for it */
385 AST_VECTOR_FREE(&resolving);
387 /* If additional queries were added start the resolution process again */
388 if (resolve->queries) {
389 ast_debug(2, "[%p] New queries added, performing parallel resolution again\n", resolve);
390 ast_dns_query_set_resolve_async(resolve->queries, sip_resolve_callback, resolve);
391 ao2_ref(queries, -1);
395 ast_debug(2, "[%p] Resolution completed - %d viable targets\n", resolve, resolve->addresses.count);
397 /* Push a task to invoke the callback, we do this so it is guaranteed to run in a PJSIP thread */
398 ao2_ref(resolve, +1);
399 if (ast_sip_push_task(NULL, sip_resolve_invoke_user_callback, resolve)) {
400 ao2_ref(resolve, -1);
403 ao2_ref(queries, -1);
408 * \brief Determine what address family a host may be if it is already an IP address
410 * \param host The host (which may be an IP address)
412 * \retval 6 The host is an IPv6 address
413 * \retval 4 The host is an IPv4 address
414 * \retval 0 The host is not an IP address
416 static int sip_resolve_get_ip_addr_ver(const pj_str_t *host)
421 if (pj_inet_aton(host, &dummy) > 0) {
425 if (pj_inet_pton(pj_AF_INET6(), host, &dummy6) == PJ_SUCCESS) {
434 * \brief Perform SIP resolution of a host
436 * \param resolver Configured resolver instance
437 * \param pool Memory pool to allocate things from
438 * \param target The target we are resolving
439 * \param token User data to pass to the resolver callback
440 * \param cb User resolver callback to invoke upon resolution completion
442 static void sip_resolve(pjsip_resolver_t *resolver, pj_pool_t *pool, const pjsip_host_info *target,
443 void *token, pjsip_resolver_callback *cb)
446 pjsip_transport_type_e type = target->type;
447 struct sip_resolve *resolve;
448 char host[NI_MAXHOST];
451 ast_copy_pj_str(host, &target->addr.host, sizeof(host));
453 ast_debug(2, "Performing SIP DNS resolution of target '%s'\n", host);
455 /* If the provided target is already an address don't bother resolving */
456 ip_addr_ver = sip_resolve_get_ip_addr_ver(&target->addr.host);
458 /* Determine the transport to use if none has been explicitly specified */
459 if (type == PJSIP_TRANSPORT_UNSPECIFIED) {
460 /* If we've been told to use a secure or reliable transport restrict ourselves to that */
462 if (target->flag & PJSIP_TRANSPORT_SECURE) {
463 type = PJSIP_TRANSPORT_TLS;
464 } else if (target->flag & PJSIP_TRANSPORT_RELIABLE) {
465 type = PJSIP_TRANSPORT_TCP;
468 /* According to the RFC otherwise if an explicit IP address OR an explicit port is specified
471 if (ip_addr_ver || target->addr.port) {
472 type = PJSIP_TRANSPORT_UDP;
475 if (ip_addr_ver == 6) {
476 type = (pjsip_transport_type_e)((int) type + PJSIP_TRANSPORT_IPV6);
480 ast_debug(2, "Transport type for target '%s' is '%s'\n", host, pjsip_transport_get_type_name(type));
482 /* If it's already an address call the callback immediately */
484 pjsip_server_addresses addresses = {
485 .entry[0].type = type,
489 if (ip_addr_ver == 4) {
490 addresses.entry[0].addr_len = sizeof(pj_sockaddr_in);
491 pj_sockaddr_init(pj_AF_INET(), &addresses.entry[0].addr, NULL, 0);
492 pj_inet_aton(&target->addr.host, &addresses.entry[0].addr.ipv4.sin_addr);
494 addresses.entry[0].addr_len = sizeof(pj_sockaddr_in6);
495 pj_sockaddr_init(pj_AF_INET6(), &addresses.entry[0].addr, NULL, 0);
496 pj_inet_pton(pj_AF_INET6(), &target->addr.host, &addresses.entry[0].addr.ipv6.sin6_addr);
499 pj_sockaddr_set_port(&addresses.entry[0].addr, !target->addr.port ? pjsip_transport_get_default_port_for_type(type) : target->addr.port);
501 ast_debug(2, "Target '%s' is an IP address, skipping resolution\n", host);
503 cb(PJ_SUCCESS, token, &addresses);
508 resolve = ao2_alloc_options(sizeof(*resolve), sip_resolve_destroy, AO2_ALLOC_OPT_LOCK_NOLOCK);
510 cb(PJ_ENOMEM, token, NULL);
514 resolve->callback = cb;
515 resolve->token = token;
517 if (AST_VECTOR_INIT(&resolve->resolving, 2)) {
518 ao2_ref(resolve, -1);
519 cb(PJ_ENOMEM, token, NULL);
523 ast_debug(2, "[%p] Created resolution tracking for target '%s'\n", resolve, host);
525 /* If no port has been specified we can do NAPTR + SRV */
526 if (!target->addr.port) {
527 char srv[NI_MAXHOST];
529 res |= sip_resolve_add(resolve, host, ns_t_naptr, ns_c_in, type, 0);
531 if ((type == PJSIP_TRANSPORT_TLS || type == PJSIP_TRANSPORT_UNSPECIFIED) &&
532 (sip_transport_is_available(PJSIP_TRANSPORT_TLS) ||
533 sip_transport_is_available(PJSIP_TRANSPORT_TLS6))) {
534 snprintf(srv, sizeof(srv), "_sips._tcp.%s", host);
535 res |= sip_resolve_add(resolve, srv, ns_t_srv, ns_c_in, PJSIP_TRANSPORT_TLS, 0);
537 if ((type == PJSIP_TRANSPORT_TCP || type == PJSIP_TRANSPORT_UNSPECIFIED) &&
538 (sip_transport_is_available(PJSIP_TRANSPORT_TCP) ||
539 sip_transport_is_available(PJSIP_TRANSPORT_TCP6))) {
540 snprintf(srv, sizeof(srv), "_sip._tcp.%s", host);
541 res |= sip_resolve_add(resolve, srv, ns_t_srv, ns_c_in, PJSIP_TRANSPORT_TCP, 0);
543 if ((type == PJSIP_TRANSPORT_UDP || type == PJSIP_TRANSPORT_UNSPECIFIED) &&
544 (sip_transport_is_available(PJSIP_TRANSPORT_UDP) ||
545 sip_transport_is_available(PJSIP_TRANSPORT_UDP6))) {
546 snprintf(srv, sizeof(srv), "_sip._udp.%s", host);
547 res |= sip_resolve_add(resolve, srv, ns_t_srv, ns_c_in, PJSIP_TRANSPORT_UDP, 0);
551 if ((type == PJSIP_TRANSPORT_UNSPECIFIED && sip_transport_is_available(PJSIP_TRANSPORT_UDP6)) ||
552 sip_transport_is_available(type + PJSIP_TRANSPORT_IPV6)) {
553 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);
556 if ((type == PJSIP_TRANSPORT_UNSPECIFIED && sip_transport_is_available(PJSIP_TRANSPORT_UDP)) ||
557 sip_transport_is_available(type)) {
558 res |= sip_resolve_add(resolve, host, ns_t_a, ns_c_in, (type == PJSIP_TRANSPORT_UNSPECIFIED ? PJSIP_TRANSPORT_UDP : type), target->addr.port);
562 ao2_ref(resolve, -1);
563 cb(PJ_ENOMEM, token, NULL);
567 ast_debug(2, "[%p] Starting initial resolution using parallel queries for target '%s'\n", resolve, host);
568 ast_dns_query_set_resolve_async(resolve->queries, sip_resolve_callback, resolve);
570 ao2_ref(resolve, -1);
575 * \brief Determine if a specific transport is configured on the system
577 * \param pool A memory pool to allocate things from
578 * \param transport The type of transport to check
579 * \param name A friendly name to print in the verbose message
583 static void sip_check_transport(pj_pool_t *pool, pjsip_transport_type_e transport, const char *name)
585 pjsip_tpmgr_fla2_param prm;
586 enum sip_resolver_transport resolver_transport;
588 pjsip_tpmgr_fla2_param_default(&prm);
589 prm.tp_type = transport;
591 if (transport == PJSIP_TRANSPORT_UDP) {
592 resolver_transport = SIP_RESOLVER_TRANSPORT_UDP;
593 } else if (transport == PJSIP_TRANSPORT_TCP) {
594 resolver_transport = SIP_RESOLVER_TRANSPORT_TCP;
595 } else if (transport == PJSIP_TRANSPORT_TLS) {
596 resolver_transport = SIP_RESOLVER_TRANSPORT_TLS;
597 } else if (transport == PJSIP_TRANSPORT_UDP6) {
598 resolver_transport = SIP_RESOLVER_TRANSPORT_UDP6;
599 } else if (transport == PJSIP_TRANSPORT_TCP6) {
600 resolver_transport = SIP_RESOLVER_TRANSPORT_TCP6;
601 } else if (transport == PJSIP_TRANSPORT_TLS6) {
602 resolver_transport = SIP_RESOLVER_TRANSPORT_TLS6;
604 ast_verb(2, "'%s' is an unsupported SIP transport\n", name);
608 if (pjsip_tpmgr_find_local_addr2(pjsip_endpt_get_tpmgr(ast_sip_get_pjsip_endpoint()),
609 pool, &prm) == PJ_SUCCESS) {
610 ast_verb(2, "'%s' is an available SIP transport\n", name);
611 sip_available_transports[resolver_transport] = 1;
613 ast_verb(2, "'%s' is not an available SIP transport, disabling resolver support for it\n",
618 /*! \brief External resolver implementation for PJSIP */
619 static pjsip_ext_resolver resolver = {
620 .resolve = sip_resolve,
625 * \brief Task to determine available transports and set ourselves an external resolver
630 static int sip_replace_resolver(void *data)
635 pool = pjsip_endpt_create_pool(ast_sip_get_pjsip_endpoint(), "Transport Availability", 256, 256);
640 /* Determine what transports are available on the system */
641 sip_check_transport(pool, PJSIP_TRANSPORT_UDP, "UDP+IPv4");
642 sip_check_transport(pool, PJSIP_TRANSPORT_TCP, "TCP+IPv4");
643 sip_check_transport(pool, PJSIP_TRANSPORT_TLS, "TLS+IPv4");
644 sip_check_transport(pool, PJSIP_TRANSPORT_UDP6, "UDP+IPv6");
645 sip_check_transport(pool, PJSIP_TRANSPORT_TCP6, "TCP+IPv6");
646 sip_check_transport(pool, PJSIP_TRANSPORT_TLS6, "TLS+IPv6");
648 pjsip_endpt_release_pool(ast_sip_get_pjsip_endpoint(), pool);
650 /* Replace the PJSIP resolver with our own implementation */
651 pjsip_endpt_set_ext_resolver(ast_sip_get_pjsip_endpoint(), &resolver);
655 void ast_sip_initialize_resolver(void)
657 /* Replace the existing PJSIP resolver with our own implementation */
658 ast_sip_push_task_synchronous(NULL, sip_replace_resolver, NULL);
663 void ast_sip_initialize_resolver(void)
665 /* External resolver support does not exist in the version of PJSIP in use */
666 ast_log(LOG_NOTICE, "The version of PJSIP in use does not support external resolvers, using PJSIP provided resolver\n");