diff --git a/include/asterisk/dns_internal.h b/include/asterisk/dns_internal.h index 48fa26471b4fe681767642a1d28764853ca41729..eb149245a17bc39ae79bebd9bc9a6e6d36e028dd 100644 --- a/include/asterisk/dns_internal.h +++ b/include/asterisk/dns_internal.h @@ -35,6 +35,7 @@ struct ast_dns_record { size_t data_len; /*! \brief Linked list information */ AST_LIST_ENTRY(ast_dns_record) list; + char *data_ptr; /*! \brief The raw DNS record */ char data[0]; }; @@ -51,6 +52,10 @@ struct ast_dns_srv_record { unsigned short weight; /*! \brief The port in the SRV record */ unsigned short port; + /*! \brief The running weight sum */ + unsigned int weight_sum; + /*! \brief Additional data */ + char data[0]; }; /*! \brief A NAPTR record */ @@ -80,11 +85,13 @@ struct ast_dns_result { /*! \brief Optional rcode, set if an error occurred */ unsigned int rcode; /*! \brief Records returned */ - AST_LIST_HEAD_NOLOCK(, ast_dns_record) records; + AST_LIST_HEAD_NOLOCK(dns_records, ast_dns_record) records; /*! \brief The canonical name */ const char *canonical; /*! \brief The raw DNS answer */ const char *answer; + /*! \brief The size of the raw DNS answer */ + size_t answer_size; /*! \brief Buffer for dynamic data */ char buf[0]; }; @@ -143,3 +150,24 @@ struct ast_sched_context; * \return scheduler context */ struct ast_sched_context *ast_dns_get_sched(void); + +/*! + * \brief Allocate and parse a DNS SRV record + * + * \param query The DNS query + * \param data This specific SRV record + * \param size The size of the SRV record + * + * \retval non-NULL success + * \retval NULL failure + */ +struct ast_dns_record *ast_dns_srv_alloc(struct ast_dns_query *query, const char *data, const size_t size); + +/*! + * \brief Sort the SRV records on a result + * + * \param result The DNS result + */ +void ast_dns_srv_sort(struct ast_dns_result *result); + + diff --git a/main/dns_core.c b/main/dns_core.c index 394eaa514bedda9f38c7dfadbe410897fb01b04c..4fd80193ef08ae0c427ee17c1f7471ee493787b3 100644 --- a/main/dns_core.c +++ b/main/dns_core.c @@ -43,6 +43,7 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") #include "asterisk/dns_resolver.h" #include "asterisk/dns_internal.h" +#include <netinet/in.h> #include <arpa/nameser.h> AST_RWLIST_HEAD_STATIC(resolvers, ast_dns_resolver); @@ -159,7 +160,7 @@ 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) { - return record->data; + return record->data_ptr; } const struct ast_dns_record *ast_dns_record_get_next(const struct ast_dns_record *record) @@ -406,10 +407,25 @@ int ast_dns_resolver_set_result(struct ast_dns_query *query, unsigned int secure buf_ptr += strlen(canonical) + 1; memcpy(buf_ptr, answer, answer_size); /* SAFE */ query->result->answer = buf_ptr; + query->result->answer_size = answer_size; return 0; } +static struct ast_dns_record *generic_record_alloc(struct ast_dns_query *query, const char *data, const size_t size) +{ + struct ast_dns_record *record; + + record = ast_calloc(1, sizeof(*record) + size); + if (!record) { + return NULL; + } + + record->data_ptr = record->data; + + return record; +} + int ast_dns_resolver_add_record(struct ast_dns_query *query, int rr_type, int rr_class, int ttl, const char *data, const size_t size) { struct ast_dns_record *record; @@ -444,7 +460,12 @@ int ast_dns_resolver_add_record(struct ast_dns_query *query, int rr_type, int rr return -1; } - record = ast_calloc(1, sizeof(*record) + size); + if (rr_type == ns_t_srv) { + record = ast_dns_srv_alloc(query, data, size); + } else { + record = generic_record_alloc(query, data, size); + } + if (!record) { return -1; } @@ -452,8 +473,8 @@ int ast_dns_resolver_add_record(struct ast_dns_query *query, int rr_type, int rr record->rr_type = rr_type; record->rr_class = rr_class; record->ttl = ttl; - memcpy(record->data, data, size); record->data_len = size; + memcpy(record->data_ptr, data, size); AST_LIST_INSERT_TAIL(&query->result->records, record, list); @@ -462,6 +483,10 @@ int ast_dns_resolver_add_record(struct ast_dns_query *query, int rr_type, int rr void ast_dns_resolver_completed(struct ast_dns_query *query) { + if (ast_dns_query_get_rr_type(query) == ns_t_srv) { + ast_dns_srv_sort(query->result); + } + query->callback(query); } diff --git a/main/dns_srv.c b/main/dns_srv.c index eeba9a6795cc7db860385817da82080378e5e0f2..7895073defef947367b61b74eff9228119e5d112 100644 --- a/main/dns_srv.c +++ b/main/dns_srv.c @@ -31,25 +31,205 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") +#include <netinet/in.h> +#include <arpa/nameser.h> +#include <resolv.h> + #include "asterisk/dns_core.h" #include "asterisk/dns_srv.h" +#include "asterisk/linkedlists.h" +#include "asterisk/dns_internal.h" +#include "asterisk/utils.h" + +struct ast_dns_record *ast_dns_srv_alloc(struct ast_dns_query *query, const char *data, const size_t size) +{ + uint16_t priority; + uint16_t weight; + uint16_t port; + const char *ptr; + char *srv_offset; + char *srv_search_base = (char *)query->result->answer; + size_t remaining_size = query->result->answer_size; + const char *end_of_record; + struct ast_dns_srv_record *srv; + int host_size; + char host[NI_MAXHOST] = ""; + + while (1) { + srv_offset = memchr(srv_search_base, data[0], remaining_size); + + ast_assert(srv_offset != NULL); + ast_assert(srv_search_base + remaining_size - srv_offset >= size); + + if (!memcmp(srv_offset, data, size)) { + ptr = srv_offset; + break; + } + + remaining_size -= srv_offset - srv_search_base; + srv_search_base = srv_offset + 1; + } + + ast_assert(ptr != NULL); + + end_of_record = ptr + size; + + /* PRIORITY */ + priority = ((unsigned char)(ptr[1]) << 0) | ((unsigned char)(ptr[0]) << 8); + ptr += 2; + + if (ptr >= end_of_record) { + return NULL; + } + + /* WEIGHT */ + weight = ((unsigned char)(ptr[1]) << 0) | ((unsigned char)(ptr[0]) << 8); + ptr += 2; + + if (ptr >= end_of_record) { + return NULL; + } + + /* PORT */ + port = ((unsigned char)(ptr[1]) << 0) | ((unsigned char)(ptr[0]) << 8); + ptr += 2; + + if (ptr >= end_of_record) { + return NULL; + } + + host_size = dn_expand((unsigned char *)query->result->answer, (unsigned char *) end_of_record, (unsigned char *) ptr, host, sizeof(host) - 1); + if (host_size < 0) { + ast_log(LOG_ERROR, "Failed to expand domain name: %s\n", strerror(errno)); + return NULL; + } + + if (!strcmp(host, ".")) { + return NULL; + } + + srv = ast_calloc(1, sizeof(*srv) + size + host_size + 1); + if (!srv) { + return NULL; + } + + srv->priority = priority; + srv->weight = weight; + srv->port = port; + + srv->host = srv->data + size; + strcpy((char *)srv->host, host); /* SAFE */ + ((char *)srv->host)[host_size] = '\0'; + + srv->generic.data_ptr = srv->data; + + return (struct ast_dns_record *)srv; +} + +/* This implementation was taken from the existing srv.c which, after reading the RFC, implements it + * as it should. + */ +void ast_dns_srv_sort(struct ast_dns_result *result) +{ + struct ast_dns_record *current; + struct dns_records newlist = AST_LIST_HEAD_NOLOCK_INIT_VALUE; + + while (AST_LIST_FIRST(&result->records)) { + unsigned short cur_priority = 0; + struct dns_records temp_list = AST_LIST_HEAD_NOLOCK_INIT_VALUE; + + /* Find the lowest current priority to work on */ + AST_LIST_TRAVERSE(&result->records, current, list) { + if (!cur_priority || ((struct ast_dns_srv_record *)current)->priority < cur_priority) { + cur_priority = ((struct ast_dns_srv_record *)current)->priority; + } + } + + /* Find all records which match this priority */ + AST_LIST_TRAVERSE_SAFE_BEGIN(&result->records, current, list) { + if (((struct ast_dns_srv_record *)current)->priority != cur_priority) { + continue; + } + + AST_LIST_REMOVE_CURRENT(list); + + /* Records with a weight of zero must always be at the head */ + if (((struct ast_dns_srv_record *)current)->weight == 0) { + AST_LIST_INSERT_HEAD(&temp_list, current, list); + } else { + AST_LIST_INSERT_TAIL(&temp_list, current, list); + } + } + AST_LIST_TRAVERSE_SAFE_END; + + /* Apply weighting - as each record is passed the sum of all previous weights (plus its own) is stored away, and then a random weight + * is calculated. The first record with a weight sum greater than the random weight is put in the new list and the whole thing starts + * once again. + */ + while (AST_LIST_FIRST(&temp_list)) { + unsigned int weight_sum = 0; + unsigned int random_weight; + + AST_LIST_TRAVERSE(&temp_list, current, list) { + ((struct ast_dns_srv_record *)current)->weight_sum = weight_sum += ((struct ast_dns_srv_record *)current)->weight; + } + + /* if all the remaining entries have weight == 0, + then just append them to the result list and quit */ + if (weight_sum == 0) { + AST_LIST_APPEND_LIST(&newlist, &temp_list, list); + break; + } + + random_weight = 1 + (unsigned int) ((float) weight_sum * (ast_random() / ((float) RAND_MAX + 1.0))); + + AST_LIST_TRAVERSE_SAFE_BEGIN(&temp_list, current, list) { + if (((struct ast_dns_srv_record *)current)->weight_sum < random_weight) { + continue; + } + + AST_LIST_MOVE_CURRENT(&newlist, list); + break; + } + AST_LIST_TRAVERSE_SAFE_END; + } + + } + + /* now that the new list has been ordered, + put it in place */ + + AST_LIST_APPEND_LIST(&result->records, &newlist, list); +} const char *ast_dns_srv_get_host(const struct ast_dns_record *record) { - return NULL; + struct ast_dns_srv_record *srv = (struct ast_dns_srv_record *) record; + + ast_assert(ast_dns_record_get_rr_type(record) == ns_t_srv); + return srv->host; } unsigned short ast_dns_srv_get_priority(const struct ast_dns_record *record) { - return 0; + struct ast_dns_srv_record *srv = (struct ast_dns_srv_record *) record; + + ast_assert(ast_dns_record_get_rr_type(record) == ns_t_srv); + return srv->priority; } unsigned short ast_dns_srv_get_weight(const struct ast_dns_record *record) { - return 0; + struct ast_dns_srv_record *srv = (struct ast_dns_srv_record *) record; + + ast_assert(ast_dns_record_get_rr_type(record) == ns_t_srv); + return srv->weight; } unsigned short ast_dns_srv_get_port(const struct ast_dns_record *record) { - return 0; -} \ No newline at end of file + struct ast_dns_srv_record *srv = (struct ast_dns_srv_record *) record; + + ast_assert(ast_dns_record_get_rr_type(record) == ns_t_srv); + return srv->port; +} diff --git a/res/res_resolver_unbound.c b/res/res_resolver_unbound.c index 43f2acdfd940381c9729dafe6cc10eb27cb6396c..94af28e7fb267de14192a1133f99e0dc2510bbe1 100644 --- a/res/res_resolver_unbound.c +++ b/res/res_resolver_unbound.c @@ -36,6 +36,10 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") #include "asterisk/config_options.h" #include "asterisk/test.h" +#ifdef TEST_FRAMEWORK +#include "asterisk/dns_srv.h" +#endif + /*** DOCUMENTATION <configInfo name="res_resolver_unbound" language="en_US"> <configFile name="resolver_unbound.conf"> @@ -1181,6 +1185,74 @@ AST_TEST_DEFINE(resolve_cancel_off_nominal) return AST_TEST_PASS; } + +AST_TEST_DEFINE(resolve_srv) +{ + RAII_VAR(struct unbound_resolver *, resolver, NULL, ao2_cleanup); + RAII_VAR(struct unbound_config *, cfg, NULL, ao2_cleanup); + RAII_VAR(struct ast_dns_result *, result, NULL, ast_dns_result_free); + const struct ast_dns_record *record; + static const char *DOMAIN1 = "taco.bananas"; + static const char *DOMAIN1_SRV = "taco.bananas 12345 IN SRV 10 20 5060 sip.taco.bananas"; + enum ast_test_result_state res = AST_TEST_PASS; + + switch (cmd) { + case TEST_INIT: + info->name = "resolve_srv"; + info->category = "/res/res_resolver_unbound/"; + info->summary = "Test synchronous SRV resolution using libunbound\n"; + info->description = "This test performs the following:\n" + "\t* Set one SRV record on one domain\n" + "\t* Perform an SRV lookup on the domain\n" + "\t* Ensure that the SRV record returned matches the expected value\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + cfg = ao2_global_obj_ref(globals); + resolver = ao2_bump(cfg->global->state->resolver); + + ub_ctx_zone_add(resolver->context, DOMAIN1, "static"); + ub_ctx_data_add(resolver->context, DOMAIN1_SRV); + + if (ast_dns_resolve(DOMAIN1, ns_t_srv, ns_c_in, &result)) { + ast_test_status_update(test, "Failed to synchronously resolve SRV for domain '%s'\n", DOMAIN1); + res = AST_TEST_FAIL; + goto cleanup; + } + + record = ast_dns_result_get_records(result); + if (ast_dns_srv_get_priority(record) != 10) { + ast_test_status_update(test, "SRV Record returned priority '%d' when we expected 10\n", ast_dns_srv_get_priority(record)); + res = AST_TEST_FAIL; + goto cleanup; + } + + if (ast_dns_srv_get_weight(record) != 20) { + ast_test_status_update(test, "SRV Record returned weight '%d' when we expected 20\n", ast_dns_srv_get_weight(record)); + res = AST_TEST_FAIL; + goto cleanup; + } + + if (ast_dns_srv_get_port(record) != 5060) { + ast_test_status_update(test, "SRV Record returned port '%d' when we expected 5060\n", ast_dns_srv_get_port(record)); + res = AST_TEST_FAIL; + goto cleanup; + } + + if (strcmp(ast_dns_srv_get_host(record), "sip.taco.bananas")) { + ast_test_status_update(test, "SRV Record returned host '%s' when we expected sip.taco.bananas\n", ast_dns_srv_get_host(record)); + res = AST_TEST_FAIL; + goto cleanup; + } + +cleanup: + ub_ctx_data_remove(resolver->context, DOMAIN1_SRV); + ub_ctx_zone_remove(resolver->context, DOMAIN1); + + return res; +} #endif static int reload_module(void) @@ -1202,6 +1274,7 @@ static int unload_module(void) AST_TEST_UNREGISTER(resolve_sync_off_nominal); AST_TEST_UNREGISTER(resolve_sync_off_nominal); AST_TEST_UNREGISTER(resolve_cancel_off_nominal); + AST_TEST_UNREGISTER(resolve_srv); return 0; } @@ -1258,6 +1331,7 @@ static int load_module(void) AST_TEST_REGISTER(resolve_sync_off_nominal); AST_TEST_REGISTER(resolve_async_off_nominal); AST_TEST_REGISTER(resolve_cancel_off_nominal); + AST_TEST_REGISTER(resolve_srv); return AST_MODULE_LOAD_SUCCESS; } diff --git a/tests/test_dns_srv.c b/tests/test_dns_srv.c new file mode 100644 index 0000000000000000000000000000000000000000..de79be1d94791b1868e875b3ef3138665ae50863 --- /dev/null +++ b/tests/test_dns_srv.c @@ -0,0 +1,697 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2015, Digium, Inc. + * + * Joshua Colp <jcolp@digium.com> + * Mark Michelson <mmichelson@digium.com> + * + * See http://www.asterisk.org for more information about + * the Asterisk project. Please do not directly contact + * any of the maintainers of this project for assistance; + * the project provides a web site, mailing lists and IRC + * channels for your use. + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*** MODULEINFO + <depend>TEST_FRAMEWORK</depend> + <support_level>core</support_level> + ***/ + +#include "asterisk.h" + +#include <arpa/nameser.h> + +#include "asterisk/test.h" +#include "asterisk/module.h" +#include "asterisk/dns_core.h" +#include "asterisk/dns_resolver.h" +#include "asterisk/dns_srv.h" + +#define DNS_HEADER_SIZE 96 + +const char DNS_HEADER[] = { + /* ID == 0 */ + 0x00, 0x00, + /* QR == 1, Opcode == 0, AA == 1, TC == 0, RD == 1 */ + 0x85, + /* RA == 1, Z == 0, RCODE == 0 */ + 0x80, + /* QDCOUNT == 1 */ + 0x00, 0x01, + /* ANCOUNT == 1 */ + 0x00, 0x00, + /* NSCOUNT == 0 */ + 0x00, 0x00, + /* ARCOUNT == 0 */ + 0x00, 0x00, +}; + +static int generate_dns_header(unsigned short num_records, char *buf) +{ + unsigned short net_num_records = htons(num_records); + + memcpy(buf, DNS_HEADER, ARRAY_LEN(DNS_HEADER)); + /* Overwrite the ANCOUNT with the actual number of answers */ + memcpy(&buf[6], &net_num_records, sizeof(num_records)); + + return ARRAY_LEN(DNS_HEADER); +} + +const char DNS_QUESTION [] = { + /* goose */ + 0x05, 0x67, 0x6f, 0x6f, 0x73, 0x65, + /* feathers */ + 0x08, 0x66, 0x65, 0x61, 0x74, 0x68, 0x65, 0x72, 0x73, + /* end label */ + 0x00, + /* SRV type */ + 0x00, 0x23, + /* IN class */ + 0x00, 0x01, +}; + +static int generate_dns_question(char *buf) +{ + memcpy(buf, DNS_QUESTION, ARRAY_LEN(DNS_QUESTION)); + return ARRAY_LEN(DNS_QUESTION); +} + +const char SRV_ANSWER [] = { + /* Domain points to name from question */ + 0xc0, 0x0c, + /* NAPTR type */ + 0x00, 0x23, + /* IN Class */ + 0x00, 0x01, + /* TTL (12345 by default) */ + 0x00, 0x00, 0x30, 0x39, +}; + +static int generate_dns_answer(int ttl, char *buf) +{ + int net_ttl = htonl(ttl); + + memcpy(buf, SRV_ANSWER, ARRAY_LEN(SRV_ANSWER)); + /* Overwrite TTL if one is provided */ + if (ttl) { + memcpy(&buf[6], &net_ttl, sizeof(int)); + } + + return ARRAY_LEN(SRV_ANSWER); +} + +static int write_dns_string(const char *string, char *buf) +{ + uint8_t len = strlen(string); + buf[0] = len; + if (len) { + memcpy(&buf[1], string, len); + } + + return len + 1; +} + +static int write_dns_domain(const char *string, char *buf) +{ + char *copy = ast_strdupa(string); + char *part; + char *ptr = buf; + + while ((part = strsep(©, "."))) { + ptr += write_dns_string(part, ptr); + } + ptr += write_dns_string("", ptr); + + return ptr - buf; +} + +struct srv_record { + uint16_t priority; + uint16_t weight; + uint16_t port; + const char *host; + unsigned int ignore_priority; + unsigned int ignore_weight; + unsigned int ignore_port; + unsigned int ignore_host; +}; + +static int generate_srv_record(struct srv_record *record, char *buf) +{ + uint16_t priority = htons(record->priority); + uint16_t weight = htons(record->weight); + uint16_t port = htons(record->port); + char *ptr = buf; + + if (!record->ignore_priority) { + memcpy(ptr, &priority, sizeof(priority)); + ptr += sizeof(priority); + } + + if (!record->ignore_weight) { + memcpy(ptr, &weight, sizeof(weight)); + ptr += sizeof(weight); + } + + if (!record->ignore_port) { + memcpy(ptr, &port, sizeof(port)); + ptr += sizeof(port); + } + + if (!record->ignore_host) { + ptr += write_dns_domain(record->host, ptr); + } + + return ptr - buf; +} + +static struct srv_record *test_records; +static int num_test_records; +static char ans_buffer[1024]; + +static void *srv_thread(void *dns_query) +{ + struct ast_dns_query *query = dns_query; + int i; + char *ptr = ans_buffer; + + ptr += generate_dns_header(num_test_records, ptr); + ptr += generate_dns_question(ptr); + + for (i = 0; i < num_test_records; ++i) { + unsigned short rdlength; + unsigned short net_rdlength; + + ptr += generate_dns_answer(0, ptr); + rdlength = generate_srv_record(&test_records[i], ptr + 2); + net_rdlength = htons(rdlength); + memcpy(ptr, &net_rdlength, 2); + ptr += 2; + ptr += rdlength; + } + + ast_dns_resolver_set_result(query, 0, 0, ns_r_noerror, "goose.feathers", ans_buffer, ptr - ans_buffer); + + for (i = 0; i < num_test_records; ++i) { + char record[128]; + ptr = record; + + ptr += generate_srv_record(&test_records[i], ptr); + ast_dns_resolver_add_record(query, ns_t_srv, ns_c_in, 12345, record, ptr - record); + } + + ast_dns_resolver_completed(query); + + ao2_ref(query, -1); + return NULL; +} + +static int srv_resolve(struct ast_dns_query *query) +{ + pthread_t thread; + + return ast_pthread_create_detached(&thread, NULL, srv_thread, ao2_bump(query)); +} + +static int srv_cancel(struct ast_dns_query *query) +{ + return -1; +} + +static struct ast_dns_resolver srv_resolver = { + .name = "srv_test", + .priority = 0, + .resolve = srv_resolve, + .cancel = srv_cancel, +}; + +static enum ast_test_result_state nominal_test(struct ast_test *test, struct srv_record *records, + int *srv_record_order, int num_records) +{ + RAII_VAR(struct ast_dns_result *, result, NULL, ast_dns_result_free); + const struct ast_dns_record *record; + enum ast_test_result_state res = AST_TEST_PASS; + int i; + + test_records = records; + num_test_records = num_records; + memset(ans_buffer, 0, sizeof(ans_buffer)); + + ast_dns_resolver_register(&srv_resolver); + + if (ast_dns_resolve("goose.feathers", ns_t_srv, ns_c_in, &result)) { + ast_test_status_update(test, "DNS resolution failed\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + if (!result) { + ast_test_status_update(test, "DNS resolution returned no result\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + i = 0; + for (record = ast_dns_result_get_records(result); record; record = ast_dns_record_get_next(record)) { + if (ast_dns_srv_get_priority(record) != records[srv_record_order[i]].priority) { + ast_test_status_update(test, "Unexpected priority in returned SRV record\n"); + res = AST_TEST_FAIL; + } + if (ast_dns_srv_get_weight(record) != records[srv_record_order[i]].weight) { + ast_test_status_update(test, "Unexpected weight in returned SRV record\n"); + res = AST_TEST_FAIL; + } + if (ast_dns_srv_get_port(record) != records[srv_record_order[i]].port) { + ast_test_status_update(test, "Unexpected port in returned SRV record\n"); + res = AST_TEST_FAIL; + } + if (strcmp(ast_dns_srv_get_host(record), records[srv_record_order[i]].host)) { + ast_test_status_update(test, "Unexpected host in returned SRV record\n"); + res = AST_TEST_FAIL; + } + ++i; + } + + if (i != num_records) { + ast_test_status_update(test, "Unexpected number of records returned in SRV lookup\n"); + res = AST_TEST_FAIL; + } + +cleanup: + + ast_dns_resolver_unregister(&srv_resolver); + + test_records = NULL; + num_test_records = 0; + memset(ans_buffer, 0, sizeof(ans_buffer)); + + return res; +} + +AST_TEST_DEFINE(srv_resolve_single_record) +{ + struct srv_record records[] = { + { 10, 10, 5060, "goose.down" }, + }; + + int srv_record_order[] = { 0, }; + + switch (cmd) { + case TEST_INIT: + info->name = "srv_resolve_single_record"; + info->category = "/main/dns/srv/"; + info->summary = "Test an SRV lookup which returns a single record"; + info->description = "This test defines a single SRV record and performs a\n" + "resolution of the domain to which they belong. The test ensures that all\n" + "fields of the SRV record are parsed correctly\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + return nominal_test(test, records, srv_record_order, ARRAY_LEN(records)); +} + +AST_TEST_DEFINE(srv_resolve_sort_priority) +{ + struct srv_record records[] = { + { 20, 10, 5060, "tacos" }, + { 10, 10, 5060, "goose.down" }, + }; + int srv_record_order[] = { 1, 0}; + + switch (cmd) { + case TEST_INIT: + info->name = "srv_resolve_sort_priority"; + info->category = "/main/dns/srv/"; + info->summary = "Test an SRV lookup which returns two records with differing priorities"; + info->description = "This test defines two SRV records with differing priorities and\n" + "performs a resolution of the domain to which they belong. The test ensures that\n" + "the two records are sorted according to priority and that all fields of the SRV\n" + "records are parsed correctly\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + return nominal_test(test, records, srv_record_order, ARRAY_LEN(records)); +} + +AST_TEST_DEFINE(srv_resolve_same_priority_zero_weight) +{ + struct srv_record records[] = { + { 10, 0, 5060, "tacos" }, + { 10, 10, 5060, "goose.down" }, + }; + int srv_record_order[] = { 1, 0}; + + switch (cmd) { + case TEST_INIT: + info->name = "srv_resolve_same_priority_zero_weight"; + info->category = "/main/dns/srv/"; + info->summary = "Test an SRV lookup which returns two records with same priority but different weights"; + info->description = "This test defines two SRV records with same priority but different weights and\n" + "performs a resolution of the domain to which they belong. The test ensures that\n" + "the record with zero weight comes last and that all fields of the SRV\n" + "records are parsed correctly\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + return nominal_test(test, records, srv_record_order, ARRAY_LEN(records)); +} + +AST_TEST_DEFINE(srv_resolve_same_priority_different_weights) +{ + struct srv_record records[] = { + { 10, 10, 5060, "tacos" }, + { 10, 20, 5060, "goose.down" }, + }; + + int srv_record_occurence[2] = { 0, }; + enum ast_test_result_state res = AST_TEST_PASS; + int count = 0; + + switch (cmd) { + case TEST_INIT: + info->name = "srv_resolve_same_priority_different_weights"; + info->category = "/main/dns/srv/"; + info->summary = "Test an SRV lookup which returns two records with same priority but different weights"; + info->description = "This test defines two SRV records with same priority but different weights and\n" + "performs a resolution of the domain to which they belong. The test ensures that\n" + "the record with higher weight occurs more often than the one of lesser weight\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + test_records = records; + num_test_records = ARRAY_LEN(records); + + ast_dns_resolver_register(&srv_resolver); + + for (count = 0; count < 100; count++) { + struct ast_dns_result *result; + const struct ast_dns_record *record; + int i; + + memset(ans_buffer, 0, sizeof(ans_buffer)); + + if (ast_dns_resolve("goose.feathers", ns_t_srv, ns_c_in, &result)) { + ast_test_status_update(test, "DNS resolution failed\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + if (!result) { + ast_test_status_update(test, "DNS resolution returned no result\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + record = ast_dns_result_get_records(result); + for (i = 0; i < ARRAY_LEN(records); i++) { + if (ast_dns_srv_get_priority(record) != records[i].priority) { + continue; + } + if (ast_dns_srv_get_weight(record) != records[i].weight) { + continue; + } + if (ast_dns_srv_get_port(record) != records[i].port) { + continue; + } + if (strcmp(ast_dns_srv_get_host(record), records[i].host)) { + continue; + } + + srv_record_occurence[i]++; + break; + } + + ast_dns_result_free(result); + } + + if (srv_record_occurence[0] > srv_record_occurence[1]) { + ast_test_status_update(test, "SRV sorting resulted in lesser weight being returned more often\n"); + res = AST_TEST_FAIL; + } + +cleanup: + + ast_dns_resolver_unregister(&srv_resolver); + + test_records = NULL; + num_test_records = 0; + memset(ans_buffer, 0, sizeof(ans_buffer)); + + return res; +} + +AST_TEST_DEFINE(srv_resolve_different_priorities_different_weights) +{ + struct srv_record records[] = { + { 10, 10, 5060, "tacos" }, + { 10, 20, 5060, "goose.down" }, + { 5, 80, 5060, "moo" }, + { 5, 10, 5060, "Canada" }, + }; + + int srv_record_priority[4] = { 5, 5, 10, 10 }; + int srv_record_occurence[4] = { 0, }; + enum ast_test_result_state res = AST_TEST_PASS; + int count = 0; + + switch (cmd) { + case TEST_INIT: + info->name = "srv_resolve_different_priorities_different_weights"; + info->category = "/main/dns/srv/"; + info->summary = "Test an SRV lookup which returns four records with different priority and different weights"; + info->description = "This test defines four SRV records, two with one priority and two with another priority,\n" + "and different weights and performs a resolution of the domain to which they belong.\n" + "The test ensures that the priorities are sorted properly and that the records with higher weight\n" + "occur more often than the ones of less weight.\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + test_records = records; + num_test_records = ARRAY_LEN(records); + + ast_dns_resolver_register(&srv_resolver); + + for (count = 0; count < 100; count++) { + struct ast_dns_result *result; + const struct ast_dns_record *record; + int i; + + memset(ans_buffer, 0, sizeof(ans_buffer)); + + if (ast_dns_resolve("goose.feathers", ns_t_srv, ns_c_in, &result)) { + ast_test_status_update(test, "DNS resolution failed\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + if (!result) { + ast_test_status_update(test, "DNS resolution returned no result\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + i = 0; + for (record = ast_dns_result_get_records(result); record; record = ast_dns_record_get_next(record)) { + if (ast_dns_srv_get_priority(record) != srv_record_priority[i]) { + ast_test_status_update(test, "Unexpected priority in returned SRV record\n"); + res = AST_TEST_FAIL; + } + i++; + } + + record = ast_dns_result_get_records(result); + for (i = 0; i < ARRAY_LEN(records); i++) { + if (ast_dns_srv_get_priority(record) != records[i].priority) { + continue; + } + if (ast_dns_srv_get_weight(record) != records[i].weight) { + continue; + } + if (ast_dns_srv_get_port(record) != records[i].port) { + continue; + } + if (strcmp(ast_dns_srv_get_host(record), records[i].host)) { + continue; + } + + srv_record_occurence[i]++; + break; + } + + ast_dns_result_free(result); + } + + if (srv_record_occurence[0] > srv_record_occurence[1]) { + ast_test_status_update(test, "SRV sorting resulted in lesser weight being returned more often for priority 10\n"); + res = AST_TEST_FAIL; + } + + if (srv_record_occurence[3] > srv_record_occurence[2]) { + ast_test_status_update(test, "SRV sorting resulted in lesser weight being returned more often for priority 5\n"); + res = AST_TEST_FAIL; + } + +cleanup: + + ast_dns_resolver_unregister(&srv_resolver); + + test_records = NULL; + num_test_records = 0; + memset(ans_buffer, 0, sizeof(ans_buffer)); + + return res; +} + +static enum ast_test_result_state invalid_record_test(struct ast_test *test, struct srv_record *records, + int num_records) +{ + RAII_VAR(struct ast_dns_result *, result, NULL, ast_dns_result_free); + const struct ast_dns_record *record; + enum ast_test_result_state res = AST_TEST_PASS; + + test_records = records; + num_test_records = num_records; + memset(ans_buffer, 0, sizeof(ans_buffer)); + + ast_dns_resolver_register(&srv_resolver); + + if (ast_dns_resolve("goose.feathers", ns_t_srv, ns_c_in, &result)) { + ast_test_status_update(test, "DNS resolution failed\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + if (!result) { + ast_test_status_update(test, "DNS resolution returned no result\n"); + res = AST_TEST_FAIL; + goto cleanup; + } + + record = ast_dns_result_get_records(result); + if (record) { + ast_test_status_update(test, "Unexpected record returned from SRV query\n"); + res = AST_TEST_FAIL; + } + +cleanup: + + ast_dns_resolver_unregister(&srv_resolver); + + test_records = NULL; + num_test_records = 0; + memset(ans_buffer, 0, sizeof(ans_buffer)); + + return res; +} + +AST_TEST_DEFINE(srv_resolve_record_missing_weight_port_host) +{ + struct srv_record records[] = { + { 10, 10, 5060, "tacos.com", 0, 1, 1, 1 }, + }; + + switch (cmd) { + case TEST_INIT: + info->name = "srv_resolve_record_missing_weight_port_host"; + info->category = "/main/dns/srv/"; + info->summary = "Test an SRV lookup which returns a single invalid record"; + info->description = "This test defines a single SRV record and performs a\n" + "resolution of the domain to which they belong. The test ensures that the\n" + "record is determined to be corrupt as it contains only a priority\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + return invalid_record_test(test, records, ARRAY_LEN(records)); +} + +AST_TEST_DEFINE(srv_resolve_record_missing_port_host) +{ + struct srv_record records[] = { + { 10, 10, 5060, "tacos.com", 0, 0, 1, 1 }, + }; + + switch (cmd) { + case TEST_INIT: + info->name = "srv_resolve_record_missing_port_host"; + info->category = "/main/dns/srv/"; + info->summary = "Test an SRV lookup which returns a single invalid record"; + info->description = "This test defines a single SRV record and performs a\n" + "resolution of the domain to which they belong. The test ensures that the\n" + "record is determined to be corrupt as it contains only a priority and weight\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + return invalid_record_test(test, records, ARRAY_LEN(records)); +} + +AST_TEST_DEFINE(srv_resolve_record_missing_host) +{ + struct srv_record records[] = { + { 10, 10, 5060, "tacos.com", 0, 0, 0, 1 }, + }; + + switch (cmd) { + case TEST_INIT: + info->name = "srv_resolve_record_missing_host"; + info->category = "/main/dns/srv/"; + info->summary = "Test an SRV lookup which returns a single invalid record"; + info->description = "This test defines a single SRV record and performs a\n" + "resolution of the domain to which they belong. The test ensures that the\n" + "record is determined to be corrupt as it contains only a priority, weight,\n" + "and port\n"; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + return invalid_record_test(test, records, ARRAY_LEN(records)); +} + +static int unload_module(void) +{ + AST_TEST_UNREGISTER(srv_resolve_single_record); + AST_TEST_UNREGISTER(srv_resolve_sort_priority); + AST_TEST_UNREGISTER(srv_resolve_same_priority_zero_weight); + AST_TEST_UNREGISTER(srv_resolve_same_priority_different_weights); + AST_TEST_UNREGISTER(srv_resolve_different_priorities_different_weights); + AST_TEST_UNREGISTER(srv_resolve_record_missing_weight_port_host); + AST_TEST_UNREGISTER(srv_resolve_record_missing_port_host); + AST_TEST_UNREGISTER(srv_resolve_record_missing_host); + + return 0; +} + +static int load_module(void) +{ + AST_TEST_REGISTER(srv_resolve_single_record); + AST_TEST_REGISTER(srv_resolve_sort_priority); + AST_TEST_REGISTER(srv_resolve_same_priority_zero_weight); + AST_TEST_REGISTER(srv_resolve_same_priority_different_weights); + AST_TEST_REGISTER(srv_resolve_different_priorities_different_weights); + AST_TEST_REGISTER(srv_resolve_record_missing_weight_port_host); + AST_TEST_REGISTER(srv_resolve_record_missing_port_host); + AST_TEST_REGISTER(srv_resolve_record_missing_host); + + return AST_MODULE_LOAD_SUCCESS; +} + +AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "DNS SRV Tests");