diff --git a/.ci_scripts/install_packages.sh b/.ci_scripts/install_packages.sh index a7b016c806e62a7b608fe356c795bbafb80ec2ae..18e55e3e3245dcf10221727a137d67d0ed9b8b84 100755 --- a/.ci_scripts/install_packages.sh +++ b/.ci_scripts/install_packages.sh @@ -1,4 +1,4 @@ #!/bin/bash -apt update -apt install -y gcc make cmake libcunit1 libcunit1-dev net-tools valgrind cppcheck +apt-get update +DEBIAN_FRONTEND=noninteractive apt-get install -y gcc make cmake libcunit1 libcunit1-dev net-tools valgrind cppcheck diff --git a/CMakeLists.txt b/CMakeLists.txt index 73dbda5c19bcf5ca45f42976791e6c6688cc90a4..1fe705ee63da0590b55f28e30d6f29cf4c9b2d25 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,4 +31,6 @@ set(PARSERS header dns dhcp http igmp ssdp coap) # Subdirectories containing code add_subdirectory(src) -add_subdirectory(test) +IF( NOT OPENWRT_CROSSCOMPILING ) + add_subdirectory(test) +ENDIF() diff --git a/include/dns.h b/include/dns.h index 7b6975ea1b8835199ff93868ab879542929b9efd..d28d8dcf1df3551fdbc47f5c7481111a35a18a2d 100644 --- a/include/dns.h +++ b/include/dns.h @@ -1,5 +1,6 @@ /** - * @file include/dns.h + * @file include/parsers/dns.h + * @author François De Keersmaeker (francois.dekeersmaeker@uclouvain.be) * @brief DNS message parser * @date 2022-09-09 * @@ -15,6 +16,11 @@ #include <stdint.h> #include <stdbool.h> #include <string.h> +#include <unistd.h> +#include <errno.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> #include <arpa/inet.h> #include "packet_utils.h" #include "dns_map.h" @@ -71,7 +77,7 @@ typedef struct dns_header { */ typedef struct dns_question { char *qname; - dns_rr_type_t qtype; + uint16_t qtype; uint16_t qclass; } dns_question_t; @@ -89,7 +95,7 @@ typedef union { */ typedef struct dns_resource_record { char *name; - dns_rr_type_t rtype; + uint16_t rtype; uint16_t rclass; uint32_t ttl; uint16_t rdlength; @@ -187,10 +193,10 @@ dns_question_t* dns_get_question(dns_question_t *questions, uint16_t qdcount, ch /** * @brief Retrieve the IP addresses corresponding to a given domain name in a DNS Answers list. - * + * * Searches a DNS Answer list for a specific domain name and returns the corresponding IP address. * Processes each Answer recursively if the Answer Type is a CNAME. - * + * * @param answers DNS Answers list to search in * @param ancount number of Answers in the list * @param domain_name domain name to search for @@ -199,6 +205,37 @@ dns_question_t* dns_get_question(dns_question_t *questions, uint16_t qdcount, ch ip_list_t dns_get_ip_from_name(dns_resource_record_t *answers, uint16_t ancount, char *domain_name); +///// COMMUNICATE ///// + +/** + * @brief Convert domain name to message format. + * + * @param dst converted domain name + * @param src domain name to convert + */ +void dns_convert_qname(char *dst, char *src, uint16_t len); + +/** + * @brief Send a DNS query for the given domain name. + * + * @param qname domain name to query for + * @param sockfd socket file descriptor + * @param server_addr DNS server IPv4 address + * @return 0 if the query was sent successfully, -1 otherwise + */ +int dns_send_query(char *qname, int sockfd, struct sockaddr_in *server_addr); + +/** + * @brief Receive a DNS response. + * + * @param sockfd socket file descriptor + * @param server_addr DNS server IPv4 address + * @param dns_message allocated buffer which will be filled with the DNS response message, upon success + * @return 0 if DNS response was received successfully, -1 otherwise + */ +int dns_receive_response(int sockfd, struct sockaddr_in *server_addr, dns_message_t *dns_message); + + ///// DESTROY ///// /** diff --git a/src/dns.c b/src/dns.c index 5c3126517eaa2b5b23cf42f513f2fa13a07390cd..3b7c305047bcf4393161ad0e9beec17299dcd1d1 100644 --- a/src/dns.c +++ b/src/dns.c @@ -1,5 +1,6 @@ /** - * @file src/dns.c + * @file src/parsers/dns.c + * @author François De Keersmaeker (francois.dekeersmaeker@uclouvain.be) * @brief DNS message parser * @date 2022-09-09 * @@ -9,6 +10,13 @@ #include "dns.h" +// DNS message timeout +#define TIMEOUT 5 // Timeout value, in seconds +struct timeval timeout = { + .tv_sec = TIMEOUT, + .tv_usec = 0 +}; + ///// PARSING ///// @@ -191,7 +199,7 @@ dns_resource_record_t* dns_parse_rrs(uint16_t count, uint8_t *data, uint16_t *of // Parse domain name (rrs + i)->name = dns_parse_domain_name(data, offset); // Parse rtype, rclass and TTL - dns_rr_type_t rtype = ntohs(*((uint16_t *) (data + *offset))); + uint16_t rtype = ntohs(*((uint16_t *) (data + *offset))); (rrs + i)->rtype = rtype; (rrs + i)->rclass = ntohs(*((uint16_t *) (data + *offset + 2))) & DNS_CLASS_MASK; (rrs + i)->ttl = ntohl(*((uint32_t *) (data + *offset + 4))); @@ -199,7 +207,7 @@ dns_resource_record_t* dns_parse_rrs(uint16_t count, uint8_t *data, uint16_t *of uint16_t rdlength = ntohs(*((uint16_t *) (data + *offset + 8))); (rrs + i)->rdlength = rdlength; *offset += 10; - (rrs + i)->rdata = dns_parse_rdata(rtype, rdlength, data, offset); + (rrs + i)->rdata = dns_parse_rdata((dns_rr_type_t) rtype, rdlength, data, offset); } return rrs; } @@ -326,39 +334,48 @@ dns_question_t* dns_get_question(dns_question_t *questions, uint16_t qdcount, ch /** * @brief Retrieve the IP addresses corresponding to a given domain name in a DNS Answers list. - * + * * Searches a DNS Answer list for a specific domain name and returns the corresponding IP address. * Processes each Answer recursively if the Answer Type is a CNAME. - * + * * @param answers DNS Answers list to search in * @param ancount number of Answers in the list * @param domain_name domain name to search for * @return struct ip_list representing the list of corresponding IP addresses */ -ip_list_t dns_get_ip_from_name(dns_resource_record_t *answers, uint16_t ancount, char *domain_name) { +ip_list_t dns_get_ip_from_name(dns_resource_record_t *answers, uint16_t ancount, char *domain_name) +{ ip_list_t ip_list; ip_list.ip_count = 0; ip_list.ip_addresses = NULL; char *cname = domain_name; - for (uint16_t i = 0; i < ancount; i++) { - if (strcmp((answers + i)->name, cname) == 0) { + for (uint16_t i = 0; i < ancount; i++) + { + if (strcmp((answers + i)->name, cname) == 0) + { dns_rr_type_t rtype = (answers + i)->rtype; if (rtype == A || rtype == AAAA) { // Handle IP list length - if (ip_list.ip_addresses == NULL) { - ip_list.ip_addresses = (ip_addr_t *) malloc(sizeof(ip_addr_t)); - } else { + if (ip_list.ip_addresses == NULL) + { + ip_list.ip_addresses = (ip_addr_t *)malloc(sizeof(ip_addr_t)); + } + else + { void *realloc_ptr = realloc(ip_list.ip_addresses, (ip_list.ip_count + 1) * sizeof(ip_addr_t)); - if (realloc_ptr == NULL) { + if (realloc_ptr == NULL) + { // Handle realloc error free(ip_list.ip_addresses); fprintf(stderr, "Error reallocating memory for IP list.\n"); ip_list.ip_count = 0; ip_list.ip_addresses = NULL; return ip_list; - } else { - ip_list.ip_addresses = (ip_addr_t*) realloc_ptr; + } + else + { + ip_list.ip_addresses = (ip_addr_t *)realloc_ptr; } } // Handle IP version and value @@ -375,6 +392,152 @@ ip_list_t dns_get_ip_from_name(dns_resource_record_t *answers, uint16_t ancount, } +///// COMMUNICATE ///// + +/** + * @brief Convert domain name to message format. + * + * @param dst converted domain name + * @param src domain name to convert + */ +void dns_convert_qname(char *dst, char *src, uint16_t len) { + int lock = 0; // Points to the next index to fill in the output array + + // Start by iterating over each character in the input domain name + for (int i = 0; i < len; i++) + { + int segment_len = 0; // Length of the current segment + + // Count the length of the current segment until we hit a dot or the end of the string + while (*(src + i) != '.' && *(src + i) != '\0') + { + *(dst + lock + 1 + segment_len) = *(src + i); + segment_len++; + i++; + } + + *(dst + lock) = segment_len; // Prefix the segment with its length + lock += segment_len + 1; // Move the index past the current segment + } + + *(dst + lock) = '\0'; // Null terminate the DNS label format string +} + +/** + * @brief Send a DNS query for the given domain name. + * + * @param qname domain name to query for + * @param sockfd socket file descriptor + * @param server_addr DNS server IPv4 address + */ +int dns_send_query(char *qname, int sockfd, struct sockaddr_in *server_addr) { + // Buffer that will contain the message + uint16_t qname_len = strlen(qname); + uint16_t qname_labels_len = qname_len + sizeof(uint8_t) * 2; + uint16_t dns_questions_size = qname_labels_len + sizeof(uint16_t) * 2; + uint16_t dns_message_size = DNS_HEADER_SIZE + dns_questions_size; + uint8_t *buffer = (uint8_t *) malloc(dns_message_size); + + // Populate DNS Header fields + dns_header_t dns_header; + dns_header.id = htons((uint16_t) getpid()); + dns_header.flags = htons((uint16_t) (0b0000000100000000)); + dns_header.qr = 0; // Query: qr = 0 + dns_header.qdcount = htons((uint16_t) 1); // Only 1 question + dns_header.ancount = 0; + dns_header.nscount = 0; + dns_header.arcount = 0; + + // Populate DNS Question fields + dns_question_t dns_question; + dns_question.qname = (char *)malloc(qname_labels_len); + dns_convert_qname(dns_question.qname, qname, qname_len); + dns_question.qtype = htons((uint16_t) A); + dns_question.qclass = htons((uint16_t) 1); + + // Copy all DNS fields + memcpy(buffer, &dns_header, sizeof(uint16_t) * 2); + memcpy(buffer + sizeof(uint16_t) * 2, &(dns_header.qdcount), sizeof(uint16_t) * 4); + memcpy(buffer + DNS_HEADER_SIZE, dns_question.qname, qname_labels_len); + memcpy(buffer + DNS_HEADER_SIZE + qname_labels_len, &(dns_question.qtype), sizeof(uint16_t) * 2); + + // Set socket timeout + if (setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) < 0) + { + perror("Error setting socket send timeout"); + // Free memory + free(dns_question.qname); + free(buffer); + return -1; + } + + // Send DNS message + #ifdef DEBUG + printf("Sending DNS query for domain name %s to server %s\n", qname, inet_ntoa(server_addr->sin_addr)); + #endif /* DEBUG */ + if (sendto(sockfd, buffer, dns_message_size, 0, (struct sockaddr *)server_addr, sizeof(*server_addr)) < 0) + { + if (errno == EWOULDBLOCK || errno == EAGAIN) + { + printf("DNS query for %s timed out.\n", qname); + } else { + perror("Failed sending DNS query."); + } + // Free memory + free(dns_question.qname); + free(buffer); + return -1; + } + + // DNS query was sent successfully + free(dns_question.qname); + free(buffer); + return 0; +} + +/** + * @brief Receive a DNS response. + * + * @param sockfd socket file descriptor + * @param server_addr DNS server IPv4 address + * @param dns_message allocated buffer which will be filled with the DNS response message, upon success + * @return 0 if DNS response was received successfully, -1 otherwise + */ +int dns_receive_response(int sockfd, struct sockaddr_in *server_addr, dns_message_t* dns_message) +{ + // Receiving buffer + int bufsize = 65536; + uint8_t *buffer = (uint8_t *)malloc(bufsize); + + // Set socket timeout + if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) + { + perror("Error setting socket receive timeout"); + free(buffer); + return -1; + } + + // Await response + int n = recvfrom(sockfd, (char *)buffer, bufsize, 0, NULL, NULL); + if (n < 0) + { + if (errno == EWOULDBLOCK || errno == EAGAIN) + { + printf("DNS receive timed out\n"); + } else { + perror("Failed receiving DNS response."); + } + free(buffer); + return -1; + } + + // DNS response was received successfully, parse it + *dns_message = dns_parse_message(buffer); + + free(buffer); + return 0; +} + ///// DESTROY ///// /** @@ -410,7 +573,7 @@ static void dns_free_rrs(dns_resource_record_t *rrs, uint16_t count) { dns_resource_record_t rr = *(rrs + i); if (rr.rdlength > 0) { free(rr.name); - dns_free_rdata(rr.rdata, rr.rtype); + dns_free_rdata(rr.rdata, (dns_rr_type_t) rr.rtype); } } free(rrs); @@ -539,7 +702,7 @@ void dns_print_rr(char* section_name, dns_resource_record_t rr) { printf(" Class: %hd\n", rr.rclass); printf(" TTL [s]: %d\n", rr.ttl); printf(" Data length: %hd\n", rr.rdlength); - printf(" RDATA: %s\n", dns_rdata_to_str(rr.rtype, rr.rdlength, rr.rdata)); + printf(" RDATA: %s\n", dns_rdata_to_str((dns_rr_type_t) rr.rtype, rr.rdlength, rr.rdata)); } /** diff --git a/test/dns.c b/test/dns.c index 80a311155f603c0392d3b612bccd3cb117651ef3..03373a2674a0e6cd0b3b7f3d7266a1f6f31ee8ab 100644 --- a/test/dns.c +++ b/test/dns.c @@ -1,5 +1,6 @@ /** - * @file test/dns.c + * @file test/parsers/dns.c + * @author François De Keersmaeker (francois.dekeersmaeker@uclouvain.be) * @brief Unit tests for the DNS parser * @date 2022-09-09 * @@ -63,6 +64,27 @@ void compare_rrs(uint16_t count, dns_resource_record_t *actual, dns_resource_rec } } +/** + * @brief Unit test for the dns_convert_qname function. + */ +void test_dns_convert_qname() { + // Test parameters + char *qname = "www.google.com"; + uint8_t qname_len = strlen(qname); + char *expected = "\3www\6google\3com"; + uint8_t converted_len = qname_len + 2; + + // Execute function + char *actual = (char*) malloc(converted_len); + dns_convert_qname(actual, qname, qname_len); + + // Verify result + CU_ASSERT_STRING_EQUAL(actual, expected); + + // Clean up + free(actual); +} + /** * Unit test for the DNS parser. */ @@ -130,16 +152,16 @@ void test_dns_xiaomi() { CU_ASSERT_TRUE(dns_contains_full_domain_name(message.questions, message.header.qdcount, domain_name)); char *suffix = "api.io.mi.com"; CU_ASSERT_TRUE(dns_contains_suffix_domain_name(message.questions, message.header.qdcount, suffix, strlen(suffix))); - domain_name = "www.example.org"; + domain_name = "swag.framinem.org"; CU_ASSERT_FALSE(dns_contains_full_domain_name(message.questions, message.header.qdcount, domain_name)); - suffix = "example.org"; + suffix = "framinem.com"; CU_ASSERT_FALSE(dns_contains_suffix_domain_name(message.questions, message.header.qdcount, suffix, strlen(suffix))); // Get question from domain name domain_name = "business.smartcamera.api.io.mi.com"; dns_question_t *question_lookup = dns_get_question(message.questions, message.header.qdcount, domain_name); CU_ASSERT_PTR_NOT_NULL(question_lookup); - domain_name = "www.example.org"; + domain_name = "swag.framinem.org"; question_lookup = dns_get_question(message.questions, message.header.qdcount, domain_name); CU_ASSERT_PTR_NULL(question_lookup); @@ -150,7 +172,7 @@ void test_dns_xiaomi() { CU_ASSERT_EQUAL(ip_list.ip_count, 1); CU_ASSERT_STRING_EQUAL(ipv4_net_to_str(ip_list.ip_addresses->value.ipv4), ip_address); free(ip_list.ip_addresses); - domain_name = "www.example.org"; + domain_name = "swag.framinem.org"; ip_list = dns_get_ip_from_name(message.answers, message.header.ancount, domain_name); CU_ASSERT_EQUAL(ip_list.ip_count, 0); CU_ASSERT_PTR_NULL(ip_list.ip_addresses); @@ -278,16 +300,16 @@ void test_dns_office() { CU_ASSERT_TRUE(dns_contains_full_domain_name(message.questions, message.header.qdcount, domain_name)); char* suffix = "office.com"; CU_ASSERT_TRUE(dns_contains_suffix_domain_name(message.questions, message.header.qdcount, suffix, strlen(suffix))); - domain_name = "www.example.org"; + domain_name = "swag.framinem.org"; CU_ASSERT_FALSE(dns_contains_full_domain_name(message.questions, message.header.qdcount, domain_name)); - suffix = "example.org"; + suffix = "framinem.org"; CU_ASSERT_FALSE(dns_contains_suffix_domain_name(message.questions, message.header.qdcount, suffix, strlen(suffix))); // Get question from domain name domain_name = "outlook.office.com"; dns_question_t *question_lookup = dns_get_question(message.questions, message.header.qdcount, domain_name); CU_ASSERT_PTR_NOT_NULL(question_lookup); - domain_name = "www.example.org"; + domain_name = "swag.framinem.org"; question_lookup = dns_get_question(message.questions, message.header.qdcount, domain_name); CU_ASSERT_PTR_NULL(question_lookup); @@ -305,7 +327,7 @@ void test_dns_office() { CU_ASSERT_STRING_EQUAL(ipv4_net_to_str((ip_list.ip_addresses + i)->value.ipv4), ip_addresses[i]); } free(ip_list.ip_addresses); - domain_name = "www.example.org"; + domain_name = "swag.framinem.org"; ip_list = dns_get_ip_from_name(message.answers, message.header.ancount, domain_name); CU_ASSERT_EQUAL(ip_list.ip_count, 0); CU_ASSERT_PTR_NULL(ip_list.ip_addresses); @@ -314,6 +336,39 @@ void test_dns_office() { dns_free_message(message); } +/** + * @brief Test the `dns_send_query` and `dns_receive_response` functions. + */ +void test_dns_send_receive() { + // Initialize + int ret; + char *domain_name = "www.google.com"; + + // Open socket + int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + CU_ASSERT_TRUE(sockfd > 0); + + // Server address: network gateway + struct sockaddr_in server_addr; + memset(&server_addr, 0, sizeof(server_addr)); + server_addr.sin_family = AF_INET; + server_addr.sin_port = htons(53); + server_addr.sin_addr.s_addr = inet_addr("8.8.8.8"); + + // Send query for dummy domain name + ret = dns_send_query(domain_name, sockfd, &server_addr); + CU_ASSERT_EQUAL(ret, 0); + + // Receive response + dns_message_t dns_response; + ret = dns_receive_response(sockfd, &server_addr, &dns_response); + CU_ASSERT_EQUAL(ret ,0); + CU_ASSERT_STRING_EQUAL(dns_response.questions->qname, domain_name); + + // Free memory + dns_free_message(dns_response); +} + /** * Main function for the unit tests. */ @@ -325,8 +380,10 @@ int main(int argc, char const *argv[]) printf("Test suite: dns\n"); CU_pSuite suite = CU_add_suite("dns", NULL, NULL); // Run tests + CU_add_test(suite, "dns-convert-qname", test_dns_convert_qname); CU_add_test(suite, "dns-xiaomi", test_dns_xiaomi); CU_add_test(suite, "dns-office", test_dns_office); + CU_add_test(suite, "dns-send-receive", test_dns_send_receive); CU_basic_run_tests(); CU_cleanup_registry(); return 0;