diff --git a/configs/samples/rtp.conf.sample b/configs/samples/rtp.conf.sample index 7c6d9f7f4c..d1429dc136 100644 --- a/configs/samples/rtp.conf.sample +++ b/configs/samples/rtp.conf.sample @@ -70,6 +70,25 @@ rtpend=20000 ; ; stunaddr= ; +; If a hostname is specified for stunaddr periodic DNS resolution is enabled +; based on the TTL returned in the queries. If the TTL returned is 0, it +; means that the next time the IP address is needed, it should be looked up +; again. I.E. Don't cache. Historically (and incorrectly) however, we +; stopped the periodic resolution and never re-resolved the hostname again. +; The stunaddr_reresolve_ttl_0 parameter can address this: +; +; Possible values: +; - 'no': This is the historical (and current default) behavior of not doing +; any further lookups and continuing to use the last successful result until +; Asterisk is restarted or rtp.conf is reloaded. +; - 'yes': Use the last cached result for the current call but trigger +; re-resolution in the background for the benefit of future calls. +; If the result of the background lookup is a ttl > 0, periodic resolution +; will be restarted otherwise the next call will use the new cached value +; and will trigger a background lookup again. +; +; stunaddr_reresolve_ttl_0 = no +; ; Some multihomed servers have IP interfaces that cannot reach the STUN ; server specified by stunaddr. Blacklist those interface subnets from ; trying to send a STUN packet to find the external IP address. @@ -85,6 +104,8 @@ rtpend=20000 ; to a specific IP address. Blacklisting is done via ACL infrastructure ; so it's possible to whitelist as well. ; + + ; stun_acl = named_acl ; stun_deny = 0.0.0.0/0 ; stun_permit = 1.2.3.4/32 diff --git a/main/dns_core.c b/main/dns_core.c index 009f12ad5e..3bd186e7c0 100644 --- a/main/dns_core.c +++ b/main/dns_core.c @@ -258,7 +258,8 @@ struct ast_dns_query_active *ast_dns_resolve_async(const char *name, int rr_type ao2_ref(active, -1); return NULL; } - + ast_debug(2, "Calling %s resolver for name: %s class: %d type: %d", active->query->resolver->name, + name, rr_class, rr_type); 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); diff --git a/main/dns_recurring.c b/main/dns_recurring.c index 0d97a0bc2c..396546cc6c 100644 --- a/main/dns_recurring.c +++ b/main/dns_recurring.c @@ -104,7 +104,16 @@ static void dns_query_recurring_resolution_callback(const struct ast_dns_query * /* It is impossible for this to be the last reference as the query has a reference to it */ ao2_ref(recurring, -1); } + ast_debug(2, "Rescheduled resolution for name: %s class: %d type: %d in %d seconds", + ast_dns_query_get_name(query), ast_dns_query_get_rr_class(query), + ast_dns_query_get_rr_type(query), ttl + EXTRA_TTL); + } else { + ast_debug(2, "TTL = 0 so not rescheduling resolution for name: %s class: %d type: %d", + ast_dns_query_get_name(query), ast_dns_query_get_rr_class(query), ast_dns_query_get_rr_type(query)); } + } else { + ast_debug(2, "Recurring resolution cancelled for name: %s class: %d type: %d", + ast_dns_query_get_name(query), ast_dns_query_get_rr_class(query), ast_dns_query_get_rr_type(query)); } ao2_replace(recurring->active, NULL); @@ -146,6 +155,9 @@ int ast_dns_resolve_recurring_cancel(struct ast_dns_query_recurring *recurring) ao2_lock(recurring); + ast_debug(2, "Cancelling recurring resolution for name: %s class: %d type: %d", + recurring->name, recurring->rr_class, recurring->rr_type); + recurring->cancelled = 1; AST_SCHED_DEL_UNREF(ast_dns_get_sched(), recurring->timer, ao2_ref(recurring, -1)); diff --git a/res/res_rtp_asterisk.c b/res/res_rtp_asterisk.c index 5df1d70c8b..a91a15b6fd 100644 --- a/res/res_rtp_asterisk.c +++ b/res/res_rtp_asterisk.c @@ -228,7 +228,6 @@ static int dtls_mtu = DEFAULT_DTLS_MTU; #ifdef HAVE_PJPROJECT static int icesupport = DEFAULT_ICESUPPORT; static int stun_software_attribute = DEFAULT_STUN_SOFTWARE_ATTRIBUTE; -static struct sockaddr_in stunaddr; static pj_str_t turnaddr; static int turnport = DEFAULT_TURN_PORT; static pj_str_t turnusername; @@ -244,9 +243,16 @@ static ast_rwlock_t ice_acl_lock = AST_RWLOCK_INIT_VALUE; static struct ast_acl_list *stun_acl = NULL; static ast_rwlock_t stun_acl_lock = AST_RWLOCK_INIT_VALUE; +static struct sockaddr_in stunaddr; /*! stunaddr recurring resolution */ static ast_rwlock_t stunaddr_lock = AST_RWLOCK_INIT_VALUE; static struct ast_dns_query_recurring *stunaddr_resolver = NULL; +/*! TTL from last successful query */ +static int stunaddr_ttl = 0; +/*! The current hostname if stunaddr isn't an IP address */ +static char *stun_hostname = NULL; +/*! Re-resolve hostname if TTL = 0? */ +static int stunaddr_reresolve_ttl_0 = 0; /*! \brief Pool factory used by pjlib to allocate memory. */ static pj_caching_pool cachingpool; @@ -697,7 +703,7 @@ static int __rtp_sendto(struct ast_rtp_instance *instance, void *buf, size_t siz #ifdef HAVE_PJPROJECT static void stunaddr_resolve_callback(const struct ast_dns_query *query); -static int store_stunaddr_resolved(const struct ast_dns_query *query); +static int store_stunaddr_resolved(const char *name, const struct ast_dns_result *result, int lock); #endif #if defined(HAVE_OPENSSL) && (OPENSSL_VERSION_NUMBER >= 0x10001000L) && !defined(OPENSSL_NO_SRTP) @@ -3759,6 +3765,8 @@ static void rtp_add_candidates_to_ice(struct ast_rtp_instance *instance, struct struct ast_ice_host_candidate *candidate; int af_inet_ok = 0, af_inet6_ok = 0; struct sockaddr_in stunaddr_copy; + int stunaddr_ttl_copy = 0; + char *stun_hostname_copy = NULL; if (ast_sockaddr_is_ipv4(addr)) { af_inet_ok = 1; @@ -3868,27 +3876,97 @@ static void rtp_add_candidates_to_ice(struct ast_rtp_instance *instance, struct freeifaddrs(ifa); } + /* + * Snap copies of the stun info with the stunaddr_lock held because + * recurring DNS lookup could be happening in another thread. + */ ast_rwlock_rdlock(&stunaddr_lock); memcpy(&stunaddr_copy, &stunaddr, sizeof(stunaddr)); + stunaddr_ttl_copy = stunaddr_ttl; + stun_hostname_copy = ast_strdupa(S_OR(stun_hostname, "")); ast_rwlock_unlock(&stunaddr_lock); /* If configured to use a STUN server to get our external mapped address do so */ - if (stunaddr_copy.sin_addr.s_addr && !stun_address_is_blacklisted(addr) && + if ( (!ast_strlen_zero(stun_hostname_copy) || stunaddr_copy.sin_addr.s_addr) + && !stun_address_is_blacklisted(addr) && (ast_sockaddr_is_ipv4(addr) || ast_sockaddr_is_any(addr)) && count < PJ_ICE_MAX_CAND) { struct sockaddr_in answer; int rsp; - ast_debug_category(3, AST_DEBUG_CATEGORY_ICE | AST_DEBUG_CATEGORY_STUN, + ast_debug_category(2, AST_DEBUG_CATEGORY_ICE | AST_DEBUG_CATEGORY_STUN, "(%p) ICE request STUN %s %s candidate\n", instance, transport == AST_TRANSPORT_UDP ? "UDP" : "TCP", component == AST_RTP_ICE_COMPONENT_RTP ? "RTP" : "RTCP"); /* * The instance should not be locked because we can block - * waiting for a STUN respone. + * waiting for stunaddr_lock and/or a STUN respone. */ ao2_unlock(instance); + + /* + * If stunaddr_ttl is 0 and stun_hostname is set, then "stunaddr" was set to a + * hostname in rtp.conf but the last attempt at resolution returned a TTL = 0 + * (don't cache) and periodic resolution was cancelled. Theoretically, as long + * as TTL = 0, we should do a synchronous lookup before every use of the address + * but historically (and incorrectly), we just kept on using the cached result + * forever. + * + * Now, if the "stunaddr_reresolve_ttl_0" parameter in rtp.conf set set to yes, + * we'll use the cached value for the current call setup (because we don't + * want to hold up the call for a synchronous DNS lookup) but restart periodic + * resolution. It's not obvious but restarting periodic resolution actually + * just triggers an asynchronous lookup which calls our stunaddr_resolve_callback + * then reschedules itself if the result has a TTL > 0. + * + * The bottom line is that it's not really an issue if THIS call setup attempt + * uses a stale cached entry if TTL = 0 as long as we trigger a re-resolution + * fairly quickly and keep doing it as long as TTL = 0. + * + */ + ast_debug_stun(2, "Checking stunaddr_reresolve_ttl_0: %s TTL: %d Host: %s resolver: %p\n", + AST_CLI_YESNO(stunaddr_reresolve_ttl_0), stunaddr_ttl_copy, stun_hostname_copy, + stunaddr_resolver); + + if (stunaddr_reresolve_ttl_0 && stunaddr_ttl_copy == 0 + && !ast_strlen_zero(stun_hostname_copy) && !stunaddr_resolver) { + + /* + * We need the write lock becausae we might be setting stunaddr_resolver. + */ + ast_rwlock_wrlock(&stunaddr_lock); + + /* + * Now that we have the write lock, check whether we still need to resolve. + * It's possible that another thread got here first. It's also possible that + * a reload changed the "stunaddr" parameter to an IP address (which clears + * stun_hostname) so we don't need resolution at all any more. + */ + if (stunaddr_ttl == 0 && !ast_strlen_zero(stun_hostname) && !stunaddr_resolver) { + ast_debug_stun(2, "Restarting recurring resolution for stun server '%s'\n", + stun_hostname); + /* + * This will return immediately after triggering an async lookup + * and scheduling the next lookup. stunaddr_resolve_callback will be + * called from another thread. + */ + stunaddr_resolver = ast_dns_resolve_recurring(stun_hostname, T_A, C_IN, + &stunaddr_resolve_callback, NULL); + if (!stunaddr_resolver) { + ast_log(LOG_ERROR, "Failed to setup recurring DNS resolution of stunaddr '%s'", + stun_hostname); + } + } else { + ast_debug_stun(2, "stun TTL: %d H: %s. Re-resolution skipped because another thread took care of it.\n", + stunaddr_ttl, stun_hostname); + } + + ast_rwlock_unlock(&stunaddr_lock); + } else { + ast_debug_stun(2, "stun TTL: %d H: %s. Re-resolution not needed or disabled.\n", stunaddr_ttl, stun_hostname); + } + rsp = ast_stun_request(component == AST_RTP_ICE_COMPONENT_RTCP ? rtp->rtcp->s : rtp->s, &stunaddr_copy, NULL, &answer); ao2_lock(instance); @@ -5649,7 +5727,7 @@ static int ast_rtp_write(struct ast_rtp_instance *instance, struct ast_frame *fr format = frame->subclass.format; if (ast_format_cmp(rtp->lasttxformat, format) == AST_FORMAT_CMP_NOT_EQUAL) { /* Oh dear, if the format changed we will have to set up a new smoother */ - ast_debug_rtp(1, "(%s) RTP ooh, format changed from %s to %s\n", + ast_debug_rtp(3, "(%s) RTP ooh, format changed from %s to %s\n", ast_rtp_instance_get_channel_id(instance), ast_format_get_name(rtp->lasttxformat), ast_format_get_name(frame->subclass.format)); @@ -9645,67 +9723,87 @@ static int ast_rtp_bundle(struct ast_rtp_instance *child, struct ast_rtp_instanc #ifdef HAVE_PJPROJECT static void stunaddr_resolve_callback(const struct ast_dns_query *query) { - const int lowest_ttl = ast_dns_result_get_lowest_ttl(ast_dns_query_get_result(query)); const char *stunaddr_name = ast_dns_query_get_name(query); - const char *stunaddr_resolved_str; - if (!store_stunaddr_resolved(query)) { - ast_log(LOG_WARNING, "Failed to resolve stunaddr '%s'. Cancelling recurring resolution.\n", stunaddr_name); - return; - } - - if (DEBUG_ATLEAST(2)) { - ast_rwlock_rdlock(&stunaddr_lock); - stunaddr_resolved_str = ast_inet_ntoa(stunaddr.sin_addr); - ast_rwlock_unlock(&stunaddr_lock); - - ast_debug_stun(2, "Resolved stunaddr '%s' to '%s'. Lowest TTL = %d.\n", - stunaddr_name, - stunaddr_resolved_str, - lowest_ttl); - } - - if (!lowest_ttl) { - ast_log(LOG_WARNING, "Resolution for stunaddr '%s' returned TTL = 0. Recurring resolution was cancelled.\n", ast_dns_query_get_name(query)); - } + /* Call store_stunaddr_resolved with locking enabled. */ + store_stunaddr_resolved(stunaddr_name, ast_dns_query_get_result(query), 1); } -static int store_stunaddr_resolved(const struct ast_dns_query *query) +static int store_stunaddr_resolved(const char *name, const struct ast_dns_result *result, int lock) { - const struct ast_dns_result *result = ast_dns_query_get_result(query); const struct ast_dns_record *record; + struct ast_dns_query_recurring *last_resolver = stunaddr_resolver; + /* + * According to https://datatracker.ietf.org/doc/html/rfc2181#section-5.2, + * It is an error if the TTLs in an RRset differ but if they do, we should + * use the lowest one. + */ + const int ttl = ast_dns_result_get_lowest_ttl(result); for (record = ast_dns_result_get_records(result); record; record = ast_dns_record_get_next(record)) { const size_t data_size = ast_dns_record_get_data_size(record); const unsigned char *data = (unsigned char *)ast_dns_record_get_data(record); const int rr_type = ast_dns_record_get_rr_type(record); + ast_debug_stun(2, "Record rr_type '%u' ttl: %d data_size '%zu' from DNS query for stunaddr '%s'\n", + rr_type, ttl, data_size, name); + if (rr_type == ns_t_a && data_size == 4) { - ast_rwlock_wrlock(&stunaddr_lock); + if (lock) { + ast_rwlock_wrlock(&stunaddr_lock); + } memcpy(&stunaddr.sin_addr, data, data_size); stunaddr.sin_family = AF_INET; - ast_rwlock_unlock(&stunaddr_lock); + stunaddr_ttl = ttl; + ast_debug_stun(2, "Resolved stunaddr '%s' to '%s'. TTL = %d.\n", name, + ast_inet_ntoa(stunaddr.sin_addr), stunaddr_ttl); + if (stunaddr_ttl == 0) { + ast_log(LOG_WARNING, "Resolution for stunaddr '%s' returned TTL = 0. Recurring resolution disabled.\n", name); + ao2_cleanup(stunaddr_resolver); + stunaddr_resolver = NULL; + } + if (lock) { + ast_rwlock_unlock(&stunaddr_lock); + } return 1; } else { - ast_debug_stun(3, "Unrecognized rr_type '%u' or data_size '%zu' from DNS query for stunaddr '%s'\n", - rr_type, data_size, ast_dns_query_get_name(query)); + ast_debug_stun(2, "Unrecognized rr_type '%u' or data_size '%zu' from DNS query for stunaddr '%s'\n", + rr_type, data_size, name); continue; } } + + ao2_cleanup(stunaddr_resolver); + stunaddr_resolver = NULL; + stunaddr_ttl = 0; + + if (stunaddr.sin_addr.s_addr) { + ast_log(LOG_WARNING, "Lookup of stunaddr '%s' failed.%s STUN continuing with server %s:%d\n", + name, last_resolver ? " Periodic resolution cancelled." : "", + ast_inet_ntoa(stunaddr.sin_addr), htons(stunaddr.sin_port)); + } else { + ast_log(LOG_WARNING, "Lookup of stunaddr '%s' failed. STUN disabled.\n", name); + } + return 0; } static void clean_stunaddr(void) { + ast_rwlock_wrlock(&stunaddr_lock); + ast_debug_stun(2, "Cleanup\n"); if (stunaddr_resolver) { + ast_debug_stun(2, "Cancelling recurring resolution for '%s'\n", stun_hostname); if (ast_dns_resolve_recurring_cancel(stunaddr_resolver)) { ast_log(LOG_ERROR, "Failed to cancel recurring DNS resolution of previous stunaddr.\n"); } ao2_ref(stunaddr_resolver, -1); stunaddr_resolver = NULL; } - ast_rwlock_wrlock(&stunaddr_lock); memset(&stunaddr, 0, sizeof(stunaddr)); + stunaddr_ttl = 0; + ast_free(stun_hostname); + stun_hostname = NULL; ast_rwlock_unlock(&stunaddr_lock); } #endif @@ -9809,6 +9907,8 @@ static char *handle_cli_rtp_settings(struct ast_cli_entry *e, int cmd, struct as { #ifdef HAVE_PJPROJECT struct sockaddr_in stunaddr_copy; + const char *stun_hostname_copy = NULL; + int stunaddr_ttl_copy = 0; #endif switch (cmd) { case CLI_INIT: @@ -9845,8 +9945,26 @@ static char *handle_cli_rtp_settings(struct ast_cli_entry *e, int cmd, struct as ast_rwlock_rdlock(&stunaddr_lock); memcpy(&stunaddr_copy, &stunaddr, sizeof(stunaddr)); + stun_hostname_copy = ast_strdupa(S_OR(stun_hostname, "")); + stunaddr_ttl_copy = stunaddr_ttl; ast_rwlock_unlock(&stunaddr_lock); - ast_cli(a->fd, " STUN address: %s:%d\n", ast_inet_ntoa(stunaddr_copy.sin_addr), htons(stunaddr_copy.sin_port)); + + ast_cli(a->fd, " STUN: %s\n", stunaddr_copy.sin_addr.s_addr ? "enbabled" : "disabled"); + if (ast_strlen_zero(stun_hostname_copy)) { + ast_cli(a->fd, " Address: %s:%d\n", ast_inet_ntoa(stunaddr_copy.sin_addr), + htons(stunaddr_copy.sin_port)); + } else { + ast_cli(a->fd, " Hostname: %s:%d\n", stun_hostname_copy, htons(stunaddr_copy.sin_port)); + ast_cli(a->fd, " Resolved Addr: %s:%d%s\n", ast_inet_ntoa(stunaddr_copy.sin_addr), + htons(stunaddr_copy.sin_port), + stunaddr_copy.sin_addr.s_addr ? stunaddr_resolver ? "" : " (possibly stale)" : " (lookup failed)"); + ast_cli(a->fd, " Last TTL: %d (periodic resolution %s)\n", stunaddr_ttl_copy, + stunaddr_resolver ? "enabled" : "disabled"); + ast_cli(a->fd, " Reresove TTL 0: %s\n", AST_CLI_YESNO(stunaddr_reresolve_ttl_0)); + } + if (stun_acl) { + ast_acl_output(a->fd, stun_acl, " "); + } #endif return CLI_SUCCESS; } @@ -10043,6 +10161,55 @@ static char *handle_cli_rtp_drop_incoming_packets(struct ast_cli_entry *e, int c } #endif +#ifdef HAVE_PJPROJECT +static char *handle_cli_rtp_refresh_stun(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + switch (cmd) { + case CLI_INIT: + e->command = "rtp resolve stun hostname"; + e->usage = + "Usage: rtp resolve stun hostname\n" + " Force a resolution of the STUN hostname (if set).\n"; + return NULL; + case CLI_GENERATE: + return NULL; + } + + if (a->argc != e->args) { + return CLI_SHOWUSAGE; + } + + ast_rwlock_wrlock(&stunaddr_lock); + if (ast_strlen_zero(stun_hostname)) { + if (stunaddr.sin_addr.s_addr) { + ast_cli(a->fd, "RTP STUN server specified as IP address '%s'. Resolution not required./n", + ast_inet_ntoa(stunaddr.sin_addr)); + } else { + ast_cli(a->fd, "RTP STUN disabled./n"); + } + } else { + if (stunaddr_resolver) { + ast_debug_stun(2, "Cancelling recurring resolution for '%s'\n", stun_hostname); + if (ast_dns_resolve_recurring_cancel(stunaddr_resolver)) { + ast_log(LOG_ERROR, "Failed to cancel recurring DNS resolution of previous stunaddr.\n"); + } + ao2_ref(stunaddr_resolver, -1); + stunaddr_resolver = NULL; + } + stunaddr_resolver = ast_dns_resolve_recurring(stun_hostname, T_A, C_IN, &stunaddr_resolve_callback, NULL); + if (!stunaddr_resolver) { + ast_cli(a->fd, "Failed to setup recurring DNS resolution of stunaddr '%s'", + stun_hostname); + } else { + ast_cli(a->fd, "Triggered background stun hostname resolution for '%s'. Run 'rtp show settings' to check results.\n", stun_hostname); + } + } + ast_rwlock_unlock(&stunaddr_lock); + + return CLI_SUCCESS; +} +#endif + static struct ast_cli_entry cli_rtp[] = { AST_CLI_DEFINE(handle_cli_rtp_set_debug, "Enable/Disable RTP debugging"), AST_CLI_DEFINE(handle_cli_rtp_settings, "Display RTP settings"), @@ -10051,6 +10218,9 @@ static struct ast_cli_entry cli_rtp[] = { #ifdef AST_DEVMODE AST_CLI_DEFINE(handle_cli_rtp_drop_incoming_packets, "Drop RTP incoming packets"), #endif +#ifdef HAVE_PJPROJECT + AST_CLI_DEFINE(handle_cli_rtp_refresh_stun, "Force a resolution of the STUN hostname"), +#endif }; static int rtp_reload(int reload, int by_external_config) @@ -10172,6 +10342,9 @@ static int rtp_reload(int reload, int by_external_config) if ((s = ast_variable_retrieve(cfg, "general", "stun_software_attribute"))) { stun_software_attribute = ast_true(s); } + if ((s = ast_variable_retrieve(cfg, "general", "stunaddr_reresolve_ttl_0"))) { + stunaddr_reresolve_ttl_0 = ast_true(s); + } if ((s = ast_variable_retrieve(cfg, "general", "stunaddr"))) { char *hostport, *host, *port; unsigned int port_parsed = STANDARD_STUN_PORT; @@ -10187,19 +10360,41 @@ static int rtp_reload(int reload, int by_external_config) } ast_rwlock_wrlock(&stunaddr_lock); ast_sockaddr_to_sin(&stunaddr_parsed, &stunaddr); + /* Set stunaddr_ttl = -1 to indicate no resolution required in the future */ + stunaddr_ttl = -1; ast_rwlock_unlock(&stunaddr_lock); } else if (ast_sockaddr_split_hostport(hostport, &host, &port, 0)) { if (port) { ast_parse_arg(port, PARSE_UINT32|PARSE_IN_RANGE, &port_parsed, 1, 65535); } - stunaddr.sin_port = htons(port_parsed); - stunaddr_resolver = ast_dns_resolve_recurring(host, T_A, C_IN, - &stunaddr_resolve_callback, NULL); - if (!stunaddr_resolver) { - ast_log(LOG_ERROR, "Failed to setup recurring DNS resolution of stunaddr '%s'", - host); + ast_rwlock_wrlock(&stunaddr_lock); + + stunaddr.sin_port = htons(port_parsed); + ast_free(stun_hostname); + stun_hostname = ast_strdup(host); + if (!stun_hostname) { + ast_log(LOG_ERROR, "Failed to set stun_hostname from '%s'", host); + } else { + stunaddr_resolver = ast_dns_resolve_recurring(host, T_A, C_IN, + &stunaddr_resolve_callback, NULL); + if (!stunaddr_resolver) { + ast_log(LOG_ERROR, "Failed to setup recurring DNS resolution of stunaddr '%s'", + host); + } else { + ast_debug_stun(2, "Attemping to start recurring stun hostname resolution for '%s'\n", stun_hostname); + } + /* + * Set stunaddr_ttl = 0 to indicate resolution is required. + * If a later query returns a positive ttl, great. We'll use the results + * of the last query until it expires. If it returns 0, we'll resolve + * every time we need it. + */ + stunaddr_ttl = 0; } + ast_rwlock_unlock(&stunaddr_lock); + + } else { ast_log(LOG_ERROR, "Failed to parse stunaddr '%s'", hostport); }