diff --git a/makefiles/pseudomodules.inc.mk b/makefiles/pseudomodules.inc.mk index 77826489b147..992828f0c280 100644 --- a/makefiles/pseudomodules.inc.mk +++ b/makefiles/pseudomodules.inc.mk @@ -215,6 +215,7 @@ PSEUDOMODULES += sock_async PSEUDOMODULES += sock_aux_local PSEUDOMODULES += sock_aux_rssi PSEUDOMODULES += sock_aux_timestamp +PSEUDOMODULES += sock_dns_cache PSEUDOMODULES += sock_dtls PSEUDOMODULES += sock_ip PSEUDOMODULES += sock_tcp diff --git a/sys/Makefile.dep b/sys/Makefile.dep index c6f9b9e39858..203ecd093fa4 100644 --- a/sys/Makefile.dep +++ b/sys/Makefile.dep @@ -602,6 +602,12 @@ ifneq (,$(filter sock_dns,$(USEMODULE))) USEMODULE += posix_headers endif +ifneq (,$(filter sock_dns_cache,$(USEMODULE))) + USEMODULE += sock_dns + USEMODULE += ztimer_msec + USEMODULE += checksum +endif + ifneq (,$(filter sock_util,$(USEMODULE))) USEMODULE += posix_inet USEMODULE += fmt diff --git a/sys/include/net/dns/msg.h b/sys/include/net/dns/msg.h index 98c700c74741..19f222239dee 100644 --- a/sys/include/net/dns/msg.h +++ b/sys/include/net/dns/msg.h @@ -104,13 +104,14 @@ size_t dns_msg_compose_query(void *dns_buf, const char *domain_name, * @param[in] family The address family used to compose the query for * this response (see @ref dns_msg_compose_query()) * @param[out] addr_out The IP address returned by the response. + * @param[out] ttl The live time of the entry in seconds * * @return Length of the @p addr_out on success. * @return -EBADMSG, when an address corresponding to @p family can not be found * in @p buf. */ int dns_msg_parse_reply(const uint8_t *buf, size_t len, int family, - void *addr_out); + void *addr_out, uint32_t *ttl); #ifdef __cplusplus } diff --git a/sys/net/application_layer/dns/msg.c b/sys/net/application_layer/dns/msg.c index 0b366ef6f7a5..a85794a41d9e 100644 --- a/sys/net/application_layer/dns/msg.c +++ b/sys/net/application_layer/dns/msg.c @@ -131,7 +131,7 @@ size_t dns_msg_compose_query(void *dns_buf, const char *domain_name, } int dns_msg_parse_reply(const uint8_t *buf, size_t len, int family, - void *addr_out) + void *addr_out, uint32_t *ttl) { const uint8_t *buflim = buf + len; const dns_hdr_t *hdr = (dns_hdr_t *)buf; @@ -162,7 +162,10 @@ int dns_msg_parse_reply(const uint8_t *buf, size_t len, int family, bufpos += RR_TYPE_LENGTH; uint16_t class = ntohs(_get_short(bufpos)); bufpos += RR_CLASS_LENGTH; - bufpos += RR_TTL_LENGTH; /* skip ttl */ + if (ttl) { + *ttl = byteorder_bebuftohl(bufpos); + } + bufpos += RR_TTL_LENGTH; unsigned addrlen = ntohs(_get_short(bufpos)); /* skip unwanted answers */ diff --git a/sys/net/application_layer/gcoap/dns.c b/sys/net/application_layer/gcoap/dns.c index 1d52dafd3262..c6638783a60a 100644 --- a/sys/net/application_layer/gcoap/dns.c +++ b/sys/net/application_layer/gcoap/dns.c @@ -712,7 +712,7 @@ static void _resp_handler(const gcoap_request_memo_t *memo, coap_pkt_t *pdu, case COAP_FORMAT_DNS_MESSAGE: case COAP_FORMAT_NONE: context->res = dns_msg_parse_reply(data, data_len, family, - context->addr_out); + context->addr_out, NULL); if ((ENABLE_DEBUG) && (context->res < 0)) { DEBUG("gcoap_dns: Unable to parse DNS reply: %d\n", context->res); diff --git a/sys/net/application_layer/sock_dns/Makefile b/sys/net/application_layer/sock_dns/Makefile index 48422e909a47..61406763ce12 100644 --- a/sys/net/application_layer/sock_dns/Makefile +++ b/sys/net/application_layer/sock_dns/Makefile @@ -1 +1,7 @@ +SRC += dns.c + +ifneq (,$(filter sock_dns_cache, $(USEMODULE))) + SRC += dns_cache.c +endif + include $(RIOTBASE)/Makefile.base diff --git a/sys/net/application_layer/sock_dns/dns.c b/sys/net/application_layer/sock_dns/dns.c index 86e96cfba565..8b857e6c820e 100644 --- a/sys/net/application_layer/sock_dns/dns.c +++ b/sys/net/application_layer/sock_dns/dns.c @@ -25,6 +25,7 @@ #include "net/dns/msg.h" #include "net/sock/udp.h" #include "net/sock/dns.h" +#include "dns_cache.h" /* min domain name length is 1, so minimum record length is 7 */ #define DNS_MIN_REPLY_LEN (unsigned)(sizeof(dns_hdr_t) + 7) @@ -67,6 +68,8 @@ void auto_init_sock_dns(void) int sock_dns_query(const char *domain_name, void *addr_out, int family) { + ssize_t res; + sock_udp_t sock_dns; static uint8_t dns_buf[CONFIG_DNS_MSG_LEN]; if (sock_dns_server.port == 0) { @@ -77,9 +80,12 @@ int sock_dns_query(const char *domain_name, void *addr_out, int family) return -ENOSPC; } - sock_udp_t sock_dns; + res = sock_dns_cache_query(domain_name, addr_out, family); + if (res) { + return res; + } - ssize_t res = sock_udp_create(&sock_dns, NULL, &sock_dns_server, 0); + res = sock_udp_create(&sock_dns, NULL, &sock_dns_server, 0); if (res) { goto out; } @@ -95,8 +101,10 @@ int sock_dns_query(const char *domain_name, void *addr_out, int family) res = sock_udp_recv(&sock_dns, dns_buf, sizeof(dns_buf), 1000000LU, NULL); if (res > 0) { if (res > (int)DNS_MIN_REPLY_LEN) { + uint32_t ttl; if ((res = dns_msg_parse_reply(dns_buf, res, family, - addr_out)) > 0) { + addr_out, &ttl)) > 0) { + sock_dns_cache_add(domain_name, addr_out, res, ttl); goto out; } } diff --git a/sys/net/application_layer/sock_dns/dns_cache.c b/sys/net/application_layer/sock_dns/dns_cache.c new file mode 100644 index 000000000000..8325d829c40a --- /dev/null +++ b/sys/net/application_layer/sock_dns/dns_cache.c @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2022 ML!PA Consulting GmbH + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup net_sock_dns + * @{ + * @file + * @brief DNS cache implementation + * @author Benjamin Valentin + * @} + */ + +#include "bitfield.h" +#include "checksum/fletcher32.h" +#include "net/sock/dns.h" +#include "time_units.h" +#include "dns_cache.h" +#include "ztimer.h" + +#define ENABLE_DEBUG 0 +#include "debug.h" + +static struct dns_cache_entry { + uint32_t hash; + uint32_t expires; + union { +#if IS_ACTIVE(SOCK_HAS_IPV4) + ipv4_addr_t v4; +#endif +#if IS_ACTIVE(SOCK_HAS_IPV6) + ipv6_addr_t v6; +#endif + } addr; +} cache[CONFIG_DNS_CACHE_SIZE]; + +#if IS_ACTIVE(SOCK_HAS_IPV4) && IS_ACTIVE(SOCK_HAS_IPV6) +BITFIELD(cache_is_v6, CONFIG_DNS_CACHE_SIZE); + +static inline uint8_t _get_len(unsigned idx) +{ + return bf_isset(cache_is_v6, idx) ? 16 : 4; +} +#elif IS_ACTIVE(SOCK_HAS_IPV4) +static inline uint8_t _get_len(unsigned idx) +{ + (void)idx; + return 4; +} +#elif IS_ACTIVE(SOCK_HAS_IPV6) +static inline uint8_t _get_len(unsigned idx) +{ + (void)idx; + return 16; +} +#endif + +static void _set_len(unsigned idx, uint8_t len) +{ +#if IS_ACTIVE(SOCK_HAS_IPV4) && IS_ACTIVE(SOCK_HAS_IPV6) + if (len == 16) { + bf_set(cache_is_v6, idx); + } else { + bf_unset(cache_is_v6, idx); + } +#else + (void)idx; + (void)len; +#endif +} + +static bool _is_empty(unsigned idx) +{ + const uint8_t len = _get_len(idx); + const uint8_t *addr = (void *)&cache[idx].addr; + for (unsigned i = 0; i < len; ++i) { + if (addr[i]) { + return false; + } + } + + return true; +} + +static void _set_empty(unsigned idx) +{ + memset(&cache[idx].addr, 0, _get_len(idx)); +} + +static uint8_t _addr_len(int family) +{ + switch (family) { +#if IS_ACTIVE(SOCK_HAS_IPV4) + case AF_INET: + return sizeof(ipv4_addr_t); +#endif +#if IS_ACTIVE(SOCK_HAS_IPV6) + case AF_INET6: + return sizeof(ipv6_addr_t); +#endif + case AF_UNSPEC: + return 0; + default: + return 255; + } +} + +static uint32_t _hash(const void *data, size_t len) +{ + return fletcher32(data, (len + 1) / 2); +} + +int sock_dns_cache_query(const char *domain_name, void *addr_out, int family) +{ + uint32_t now = ztimer_now(ZTIMER_MSEC) / MS_PER_SEC; + uint32_t hash = _hash(domain_name, strlen(domain_name)); + uint8_t addr_len = _addr_len(family); + + for (unsigned i = 0; i < CONFIG_DNS_CACHE_SIZE; ++i) { + /* empty slot */ + if (_is_empty(i)) { + continue; + } + /* TTL expired - invalidate slot */ + if (now > cache[i].expires) { + DEBUG("dns_cache[%u] expired\n", i); + _set_empty(i); + continue; + } + /* check if hash and length match */ + if (cache[i].hash == hash && (!addr_len || addr_len == _get_len(i))) { + DEBUG("dns_cache[%u] hit\n", i); + memcpy(addr_out, &cache[i].addr, _get_len(i)); + return _get_len(i); + } + } + DEBUG("dns_cache miss\n"); + + return 0; +} + +static void _add_entry(uint8_t i, uint32_t hash, const void *addr_out, + int addr_len, uint32_t expires) +{ + DEBUG("dns_cache[%u] add cache entry\n", i); + cache[i].hash = hash; + cache[i].expires = expires; + memcpy(&cache[i].addr, addr_out, addr_len); + _set_len(i, addr_len); +} + +void sock_dns_cache_add(const char *domain_name, const void *addr_out, + int addr_len, uint32_t ttl) +{ + uint32_t now = ztimer_now(ZTIMER_MSEC) / MS_PER_SEC; + uint32_t hash = _hash(domain_name, strlen(domain_name)); + uint32_t oldest = ttl; + int idx = -1; + + assert(addr_len == 4 || addr_len == 16); + DEBUG("dns_cache: lifetime of %s is %"PRIu32" s\n", domain_name, ttl); + + for (unsigned i = 0; i < CONFIG_DNS_CACHE_SIZE; ++i) { + if (now > cache[i].expires || _is_empty(i)) { + _add_entry(i, hash, addr_out, addr_len, now + ttl); + return; + } + if (cache[i].hash == hash && _get_len(i) == addr_len) { + DEBUG("dns_cache[%u] update ttl\n", i); + cache[i].expires = now + ttl; + return; + } + uint32_t _ttl = cache[i].expires - now; + if (_ttl < oldest) { + oldest = _ttl; + idx = i; + } + } + + if (idx >= 0) { + DEBUG("dns_cache: evict first entry to expire\n"); + _add_entry(idx, hash, addr_out, addr_len, now + ttl); + } +} diff --git a/sys/net/application_layer/sock_dns/dns_cache.h b/sys/net/application_layer/sock_dns/dns_cache.h new file mode 100644 index 000000000000..ffa05eba20da --- /dev/null +++ b/sys/net/application_layer/sock_dns/dns_cache.h @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2022 ML!PA Consulting GmbH + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup net_sock_dns + * + * @brief DNS cache + * + * @{ + * + * @file + * @brief DNS cache definitions + * + * This implements a simple DNS cache for A and AAAA entries. + * + * The cache eviction strategy is based on the remaining time to live + * of the cache entries, so the first entry to expire will be evicted. + * + * This is fine if there are only few cache entries and cache eviction + * is unlikely. + * If there is communication to many different hosts, the addition of a + * least-recently used counter could likely improve the behavior. + * + * @author Benjamin Valentin + */ + +#ifndef DNS_CACHE_H +#define DNS_CACHE_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Number of DNS cache entries + */ +#ifndef CONFIG_DNS_CACHE_SIZE +#define CONFIG_DNS_CACHE_SIZE 4 +#endif + +#if IS_USED(MODULE_SOCK_DNS_CACHE) || DOXYGEN +/** + * @brief Get IP address for a DNS name from the DNS cache + * + * @param[in] domain_name DNS name to resolve into address + * @param[out] addr_out buffer to write result into + * @param[in] family Either AF_INET, AF_INET6 or AF_UNSPEC + * + * @return the size of the resolved address on success + * @return <= 0 otherwise + */ +int sock_dns_cache_query(const char *domain_name, void *addr_out, int family); + +/** + * @brief Add an IP address for a DNS name to the DNS cache + * + * @param[in] domain_name DNS name to resolve into address + * @param[in] addr buffer containing the address + * @param[in] addr_len length of the address in bytes + * @param[in] ttl lifetime of the entry in seconds + */ +void sock_dns_cache_add(const char *domain_name, const void *addr, int addr_len, uint32_t ttl); +#else +static inline int sock_dns_cache_query(const char *domain_name, + void *addr_out, int family) +{ + (void)domain_name; + (void)addr_out; + (void)family; + return 0; +} + +static inline void sock_dns_cache_add(const char *domain_name, const void *addr, + int addr_len, uint32_t ttl) +{ + (void)domain_name; + (void)addr; + (void)addr_len; + (void)ttl; +} +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* DNS_CACHE_H */ +/** @} */ diff --git a/tests/unittests/tests-sock_dns_cache/Makefile b/tests/unittests/tests-sock_dns_cache/Makefile new file mode 100644 index 000000000000..48422e909a47 --- /dev/null +++ b/tests/unittests/tests-sock_dns_cache/Makefile @@ -0,0 +1 @@ +include $(RIOTBASE)/Makefile.base diff --git a/tests/unittests/tests-sock_dns_cache/Makefile.include b/tests/unittests/tests-sock_dns_cache/Makefile.include new file mode 100644 index 000000000000..1942dd65b01d --- /dev/null +++ b/tests/unittests/tests-sock_dns_cache/Makefile.include @@ -0,0 +1,2 @@ +USEMODULE += gnrc_ipv6 +USEMODULE += sock_dns_cache diff --git a/tests/unittests/tests-sock_dns_cache/tests-sock_dns_cache.c b/tests/unittests/tests-sock_dns_cache/tests-sock_dns_cache.c new file mode 100644 index 000000000000..96f3ce9f78d2 --- /dev/null +++ b/tests/unittests/tests-sock_dns_cache/tests-sock_dns_cache.c @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 ML!PA Consulting GmbH + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +#include +#include +#include "net/af.h" +#include "net/ipv6.h" +#include "ztimer.h" + +#include "tests-sock_dns_cache.h" +#include "../net/application_layer/sock_dns/dns_cache.h" + +static void test_dns_cache_add(void) +{ + ipv6_addr_t addr_in = IPV6_ADDR_ALL_NODES_IF_LOCAL; + ipv6_addr_t addr_out; + + TEST_ASSERT_EQUAL_INT(0, sock_dns_cache_query("example.com", &addr_out, AF_INET6)); + + /* add DNS entry, set it to expire in 1s */ + sock_dns_cache_add("example.com", &addr_in, sizeof(addr_in), 1); + TEST_ASSERT_EQUAL_INT(sizeof(addr_out), sock_dns_cache_query("example.com", &addr_out, AF_INET6)); + TEST_ASSERT_EQUAL_INT(0, memcmp(&addr_in, &addr_out, sizeof(addr_in))); + + TEST_ASSERT_EQUAL_INT(0, sock_dns_cache_query("example.com", &addr_out, AF_INET)); + TEST_ASSERT_EQUAL_INT(0, sock_dns_cache_query("alt.example.com", &addr_out, AF_INET6)); + TEST_ASSERT_EQUAL_INT(0, sock_dns_cache_query("example.comm", &addr_out, AF_INET6)); + TEST_ASSERT_EQUAL_INT(0, sock_dns_cache_query("example.co", &addr_out, AF_INET6)); + + ztimer_sleep(ZTIMER_USEC, 2000000); + TEST_ASSERT_EQUAL_INT(0, sock_dns_cache_query("example.com", &addr_out, AF_INET6)); +} + +Test *tests_sock_dns_cache_tests(void) +{ + EMB_UNIT_TESTFIXTURES(fixtures) { + new_TestFixture(test_dns_cache_add), + }; + + EMB_UNIT_TESTCALLER(sock_dns_cache_tests, NULL, NULL, fixtures); + + return (Test *)&sock_dns_cache_tests; +} + +void tests_sock_dns_cache(void) +{ + TESTS_RUN(tests_sock_dns_cache_tests()); +} diff --git a/tests/unittests/tests-sock_dns_cache/tests-sock_dns_cache.h b/tests/unittests/tests-sock_dns_cache/tests-sock_dns_cache.h new file mode 100644 index 000000000000..e19940513aea --- /dev/null +++ b/tests/unittests/tests-sock_dns_cache/tests-sock_dns_cache.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2022 ML!PA Consulting GmbH + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @addtogroup unittests + * @{ + * + * @file + * @brief Unittests for the ``sock_dns_cache`` module + * + * @author Benjamin Valentin + */ +#ifndef TESTS_SOCK_DNS_CACHE_H +#define TESTS_SOCK_DNS_CACHE_H + +#include "embUnit.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief The entry point of this test suite. + */ +void tests_sock_dns_cache(void); + +/** + * @brief Generates tests for sock_dns_cache + * + * @return embUnit tests if successful, NULL if not. + */ +Test *tests_sock_dns_cache_tests(void); + +#ifdef __cplusplus +} +#endif + +#endif /* TESTS_SOCK_DNS_CACHE_H */ +/** @} */