From fdcd4803c91d81152505d801c4fa8022a5685fc1 Mon Sep 17 00:00:00 2001 From: Sly Gryphon Date: Sun, 25 Feb 2024 23:49:23 +1000 Subject: [PATCH] fix(dns): Dynamic DNS sorting (RFC 6724) to work in IPv6-only network This fix adds an option to dynamically set the order of DNS results based on RFC 6724, to return the best destination address based on the available source addresses, allowing a device with both IPv4 and IPv6 enabled to work in any network configuration, including IPv4-only, IPv6-only, and dual-stack. Without the option there is a static ordering preferring IPv4, which means on an IPv6-only network, a dual-stack destination returns the IPv4 address, which fails. Setting the static preference to IPv6 would have the opposite problem. A dynamic sort, based on available source addresses, is required to work on all networks. --- src/api/netdb.c | 390 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 368 insertions(+), 22 deletions(-) diff --git a/src/api/netdb.c b/src/api/netdb.c index 6ed5d75d3..318aef6b6 100644 --- a/src/api/netdb.c +++ b/src/api/netdb.c @@ -42,7 +42,7 @@ #include "lwip/err.h" #include "lwip/mem.h" #include "lwip/memp.h" -#include "lwip/ip_addr.h" +#include "lwip/ip6_addr.h" #include "lwip/api.h" #include "lwip/dns.h" @@ -74,6 +74,13 @@ int h_errno; #define HOSTENT_STORAGE static #endif /* LWIP_DNS_API_STATIC_HOSTENT */ +#if LWIP_IPV4 && LWIP_IPV6 && LWIP_DNS_DYNAMIC_SORT +ip6_addr_t * dns_select_destination_address(ip6_addr_t *cand_list[], const int cand_list_length); +s8_t dns_addr_get_scope(const ip6_addr_t *addr); +s8_t dns_precedence_for_label(s8_t label); +s8_t dns_get_precedence_label(const ip6_addr_t *addr); +#endif + /** * Returns an entry containing addresses of address family AF_INET * for the host with name name. @@ -113,8 +120,8 @@ lwip_gethostbyname(const char *name) s_hostent.h_name = s_hostname; s_aliases = NULL; s_hostent.h_aliases = &s_aliases; - s_hostent.h_addrtype = AF_INET; - s_hostent.h_length = sizeof(ip_addr_t); + s_hostent.h_addrtype = (IPADDR_TYPE_V4 == IP_GET_TYPE(&addr)? AF_INET : AF_INET6); + s_hostent.h_length = IP_ADDR_RAW_SIZE(addr); s_hostent.h_addr_list = (char **)&s_phostent_addr; #if DNS_DEBUG @@ -128,8 +135,7 @@ lwip_gethostbyname(const char *name) if (s_hostent.h_addr_list != NULL) { u8_t idx; for (idx = 0; s_hostent.h_addr_list[idx]; idx++) { - LWIP_DEBUGF(DNS_DEBUG, ("hostent.h_addr_list[%i] == %p\n", idx, s_hostent.h_addr_list[idx])); - LWIP_DEBUGF(DNS_DEBUG, ("hostent.h_addr_list[%i]-> == %s\n", idx, ipaddr_ntoa((ip_addr_t *)s_hostent.h_addr_list[idx]))); + LWIP_DEBUGF(DNS_DEBUG, ("hostent.h_addr_list[%i]-> == %s\n", idx, ipaddr_ntoa(s_phostent_addr[idx]))); } } #endif /* DNS_DEBUG */ @@ -214,8 +220,8 @@ lwip_gethostbyname_r(const char *name, struct hostent *ret, char *buf, h->aliases = NULL; ret->h_name = hostname; ret->h_aliases = &h->aliases; - ret->h_addrtype = AF_INET; - ret->h_length = sizeof(ip_addr_t); + ret->h_addrtype = (IPADDR_TYPE_V4 == IP_GET_TYPE(&h->addr)? AF_INET : AF_INET6); + ret->h_length = IP_ADDR_RAW_SIZE(h->addr); ret->h_addr_list = (char **)&h->addr_list; /* set result != NULL */ @@ -330,20 +336,55 @@ lwip_getaddrinfo(const char *nodename, const char *servname, #endif /* LWIP_IPV4 && LWIP_IPV6 */ } else { #if LWIP_IPV4 && LWIP_IPV6 - /* AF_UNSPEC: prefer IPv4 */ - u8_t type = NETCONN_DNS_IPV4_IPV6; - if (ai_family == AF_INET) { - type = NETCONN_DNS_IPV4; - } else if (ai_family == AF_INET6) { - type = NETCONN_DNS_IPV6; -#if ESP_IPV6 - if (hints->ai_flags & AI_V4MAPPED) { - type = NETCONN_DNS_IPV6_IPV4; + if (ai_family == AF_UNSPEC) { +#if LWIP_DNS_DYNAMIC_SORT + ip_addr_t addr4; + ip_addr_t addr6; + ip6_addr_t *addr_list[2]; + int index = 0; + err_t err6 = netconn_gethostbyname_addrtype(nodename, &addr6, NETCONN_DNS_IPV6); + if (err6 == ERR_OK) { + addr_list[index] = ip_2_ip6(&addr6); + index++; } -#endif + err_t err4 = netconn_gethostbyname_addrtype(nodename, &addr4, NETCONN_DNS_IPV4); + if (err4 == ERR_OK) { + /* Convert native V4 address to a V4-mapped IPV6 address */ + ip4_2_ipv4_mapped_ipv6(ip_2_ip6(&addr4), ip_2_ip4(&addr4)); + IP_SET_TYPE_VAL(addr4, IPADDR_TYPE_V6); + addr_list[index] = ip_2_ip6(&addr4); + index++; + } + err = err6 == ERR_OK || err4 == ERR_OK ? ERR_OK : EAI_FAIL; + if (err != ERR_OK) { + return EAI_FAIL; + } + ip6_addr_t *best_addr = dns_select_destination_address(addr_list, index); + if (ip6_addr_isipv4mappedipv6(best_addr)) { + unmap_ipv4_mapped_ipv6(ip_2_ip4(&addr), best_addr); + } else { + ip_addr_copy_from_ip6(addr, *best_addr); + } +#else /* LWIP_DNS_DYNAMIC_SORT */ + err = netconn_gethostbyname_addrtype(nodename, &addr, NETCONN_DNS_IPV4_IPV6); +#endif /* LWIP_DNS_DYNAMIC_SORT */ + } else { + u8_t type = NETCONN_DNS_IPV4_IPV6; + if (ai_family == AF_INET) { + type = NETCONN_DNS_IPV4; + } else if (ai_family == AF_INET6) { + type = NETCONN_DNS_IPV6; +#if ESP_LWIP + if (hints->ai_flags & AI_V4MAPPED) { + type = NETCONN_DNS_IPV6_IPV4; + } +#endif /* ESP_LWIP */ + } + err = netconn_gethostbyname_addrtype(nodename, &addr, type); } +#else /* LWIP_IPV4 && LWIP_IPV6 */ + err = netconn_gethostbyname(nodename, &addr); #endif /* LWIP_IPV4 && LWIP_IPV6 */ - err = netconn_gethostbyname_addrtype(nodename, &addr, type); if (err != ERR_OK) { return EAI_FAIL; } @@ -357,16 +398,14 @@ lwip_getaddrinfo(const char *nodename, const char *servname, } } -#if ESP_IPV6 -#if LWIP_IPV4 && LWIP_IPV6 +#if ESP_LWIP && LWIP_IPV4 && LWIP_IPV6 if (ai_family == AF_INET6 && (hints->ai_flags & AI_V4MAPPED) && IP_GET_TYPE(&addr) == IPADDR_TYPE_V4) { /* Convert native V4 address to a V4-mapped IPV6 address */ ip4_2_ipv4_mapped_ipv6(ip_2_ip6(&addr), ip_2_ip4(&addr)); IP_SET_TYPE_VAL(addr, IPADDR_TYPE_V6); } -#endif -#endif +#endif /* ESP_LWIP && LWIP_IPV4 && LWIP_IPV6 */ total_size = sizeof(struct addrinfo) + sizeof(struct sockaddr_storage); if (nodename != NULL) { @@ -431,4 +470,311 @@ lwip_getaddrinfo(const char *nodename, const char *servname, return 0; } +#if LWIP_IPV4 && LWIP_IPV6 && LWIP_DNS_DYNAMIC_SORT + +// Labels 1-13 for the default precedence table from RFC 6724 +#define IP6_PRECEDENCE_LABEL_LOCALHOST 0x0 +#define IP6_PRECEDENCE_LABEL_GENERAL 0x1 +#define IP6_PRECEDENCE_LABEL_IPV4_COMPATIBLE_IPV6 0x3 +#define IP6_PRECEDENCE_LABEL_6TO4 0x2 +#define IP6_PRECEDENCE_LABEL_IPV4_MAPPED_IPV6 0x4 +#define IP6_PRECEDENCE_LABEL_TOREDO 0x5 +#define IP6_PRECEDENCE_LABEL_SITE_LOCAL 0xb +#define IP6_PRECEDENCE_LABEL_6BONE 0xc +#define IP6_PRECEDENCE_LABEL_ULA 0xd + +// Prefix match functions for the ranges from the default precedence table +#define ip6_addr_isipv4compatibleipv6(ip6addr) (((ip6addr)->addr[0] == 0) && ((ip6addr)->addr[1] == 0) && (((ip6addr)->addr[2]) == PP_HTONL(0x00000000UL))) +#define ip6_addr_is6to4(ip6addr) (((ip6addr)->addr[0] & PP_HTONL(0xffff0000UL)) == PP_HTONL(0x20020000UL)) +#define ip6_addr_isteredo(ip6addr) (((ip6addr)->addr[0] & PP_HTONL(0xffffffffUL)) == PP_HTONL(0x20010000UL)) +#define ip6_addr_is6bone(ip6addr) (((ip6addr)->addr[0] & PP_HTONL(0xffff0000UL)) == PP_HTONL(0x3ffe0000UL)) +#define ip6_addr_isip4vmappedlinklocal(ip6addr) (((ip6addr)->addr[0] == 0) && ((ip6addr)->addr[1] == 0) && (((ip6addr)->addr[2]) == PP_HTONL(0x0000ffffUL)) && (((ip6addr)->addr[3] & PP_HTONL(0xffff0000UL)) == PP_HTONL(0xa9fe0000UL))) +#define ip6_addr_isip4vmappedloopback(ip6addr) (((ip6addr)->addr[0] == 0) && ((ip6addr)->addr[1] == 0) && (((ip6addr)->addr[2]) == PP_HTONL(0x0000ffffUL)) && (((ip6addr)->addr[3] & PP_HTONL(0xff000000UL)) == PP_HTONL(0x7f000000UL))) + +/** + * @brief Determines scope of IPv6 address (including IPv4 mapped addresses) + * + * This function follows the RFC 6724 definition of scopes, matching + * unicast addresses to the appropriate multicast scope. + * + * Link-local and the loopback are considered link-local, as are the + * corresponding ranges in IPv4-mapped addresses. Everything else + * (including ULA addresses, DNS64 address, etc) are global scope. + * + * NOTE: Existing functions ip6_addr_isglobal is not suitable because it + * only checks for 2000:x and 3000:x addresses, and so misses things like + * DNS64/NAT64 ranges. + * + * @param addr address to check + * @return s8_t scope of address [0x0 - 0xf] + */ +s8_t +dns_addr_get_scope(const ip6_addr_t *addr) { + s8_t scope = IP6_MULTICAST_SCOPE_RESERVED; + if (ip6_addr_ismulticast(addr)) { + scope = ip6_addr_multicast_scope(addr); + } else if (ip6_addr_islinklocal(addr) || ip6_addr_isloopback(addr) + || ip6_addr_isip4vmappedlinklocal(addr) || ip6_addr_isip4vmappedloopback(addr)) { + scope = IP6_MULTICAST_SCOPE_LINK_LOCAL; + } else if (ip6_addr_issitelocal(addr)) { + scope = IP6_MULTICAST_SCOPE_SITE_LOCAL; + } else { + /* everything else, consider scope global */ + scope = IP6_MULTICAST_SCOPE_GLOBAL; + } + return scope; +} + +/** + * @brief Get the precedence label based on longest prefix match. + * + * This implements the default precedence table from RFC 6724. + * + * Labels are matched from longest prefix to shortest, with the + * first match returned. The last label (most IPv6 addresses) + * is the everything range (::/0), which has a high precendence. + * + * @param addr address to check + * @return s8_t precedence label [0x0-0xd] + */ +s8_t +dns_get_precedence_label(const ip6_addr_t *addr) { + // TODO: Allow this function to be overriden by a customisation hook + + // Match on longest prefix + // ::1/128 50 0 (loopback) + if (ip6_addr_isloopback(addr)) { + return IP6_PRECEDENCE_LABEL_LOCALHOST; + } + // ::ffff:0:0/96 35 4 (IPv4-mapped IPv6) + if (ip6_addr_isipv4mappedipv6(addr)) { + return IP6_PRECEDENCE_LABEL_IPV4_MAPPED_IPV6; + } + // ::/96 1 3 (IPv4-compatible IPv6 - deprecated + if (ip6_addr_isipv4compatibleipv6(addr)) { + return IP6_PRECEDENCE_LABEL_IPV4_COMPATIBLE_IPV6; + } + // 2001::/32 5 5 (Teredo) + if (ip6_addr_isteredo(addr)) { + return IP6_PRECEDENCE_LABEL_TOREDO; + } + // 2002::/16 30 2 (6to4) + if (ip6_addr_is6to4(addr)) { + return IP6_PRECEDENCE_LABEL_6TO4; + } + // 3ffe::/16 1 12 (6bone - deprecated) + if (ip6_addr_is6bone(addr)) { + return IP6_PRECEDENCE_LABEL_6BONE; + } + // fec0::/10 1 11 (site-local - deprecated) + if (ip6_addr_issitelocal(addr)) { + return IP6_PRECEDENCE_LABEL_SITE_LOCAL; + } + // fc00::/7 3 13 (ULA) + if (ip6_addr_isuniquelocal(addr)) { + return IP6_PRECEDENCE_LABEL_ULA; + } + // ::/0 40 1 (general IPv6) + return IP6_PRECEDENCE_LABEL_GENERAL; +} + +/** + * @brief Gets the precedence ranking (higher has priority) for a given label + * + * Precedence ratings are based on RFC 6724 default values. + * + * @param label label to get precedence for + * @return s8_t precendence [0-50] + */ +s8_t +dns_precedence_for_label(s8_t label) { + // TODO: Allow this function to be overriden by a customisation hook + + /* + Default table from RFC 6724: + + Prefix Precedence Label + ::1/128 50 0 (loopback) + ::/0 40 1 (general IPv6) + ::ffff:0:0/96 35 4 (IPv4-mapped IPv6) + 2002::/16 30 2 (6to4) + 2001::/32 5 5 (Teredo) + fc00::/7 3 13 (ULA) + ::/96 1 3 (IPv4-compatible IPv6 - deprecated + fec0::/10 1 11 (site-local - deprecated) + 3ffe::/16 1 12 (6bone - deprecated) + */ + switch (label) { + case IP6_PRECEDENCE_LABEL_LOCALHOST: + return 50; + case IP6_PRECEDENCE_LABEL_GENERAL: + return 40; + case IP6_PRECEDENCE_LABEL_IPV4_MAPPED_IPV6: + return 35; + case IP6_PRECEDENCE_LABEL_6TO4: + return 30; + case IP6_PRECEDENCE_LABEL_TOREDO: + return 5; + case IP6_PRECEDENCE_LABEL_ULA: + return 3; + case IP6_PRECEDENCE_LABEL_IPV4_COMPATIBLE_IPV6: + case IP6_PRECEDENCE_LABEL_SITE_LOCAL: + case IP6_PRECEDENCE_LABEL_6BONE: + return 1; + default: + return 0; + } +} + +/** + * Select the best destination address based on available source addresses. + * + * IPv4 addresses are represented as IPv4-mapped IPv6 addresses for this algorithm. + * + * DNS only returns a maximum of 2 addresses, one IPv6 and one IPv4, so the + * current algorithm is simplified and only supports this case, although the + * signature is generic and the logic could be extended to support multiple + * addresses and pick the best (or even to sort them). + * + * This implementation follows RFC 6724 Sec. 6 to the following extent: + * - Rules 1, 2: implemented + * Rules 3, 4: not applicable + * - Rule 5, 6: implemented - as we only have one of each address we will have a result + * - Rules 7, 8, 9: not applicable + * - Rule 10: implemented - but not applicable as we only have one of each address + * + * @param cand_list list of candidate destination addresses (IPv4 in IPv4-mapped IPv6 format) + * @param cand_list_length length of the candidate list (maximum 2 addresses, one IPv6 and one IPv4) + * @return the most suitable destination address to use, or NULL if no addresses were provided + */ +ip6_addr_t * +dns_select_destination_address(ip6_addr_t *cand_list[], const int cand_list_length) +{ + // This function is only used if both IPv6 and IPv4 are enabled, and dynamic sort is enabled. + + // Short circuit - no addresses + if (cand_list_length == 0) { + return NULL; + } + // Short circuit - Only one address, so by definition it is the most suitable + if (cand_list_length == 1) { + return cand_list[0]; + } + + // If we get past this point, then we have exactly 2 addresses: + // cand_list[0] is IPv6 and cand_list[1] is IPv4. + + // Determine types of available source address types + // Note: We don't actually determine preferred source address, + // but use a heuristic that if the type exists, then one of them + // will be preferred (and match), and if it the type doesn't exist, + // then the preferred can't match. + s32_t has_ipv6_source_scope_flags = 0x0; + s32_t has_ipv4_source_scope_flags = 0x0; + s32_t has_source_precedence_label_flags = 0x0; + struct netif *netif; + for(netif = netif_list; netif != NULL; netif = netif->next) { + const ip4_addr_t *ip4_addr = netif_ip4_addr(netif); + if (!ip4_addr_cmp(ip4_addr, IP4_ADDR_ANY4)) { + /* Convert native V4 address to a V4-mapped IPV6 address */ + ip6_addr_t mapped_addr; + ip4_2_ipv4_mapped_ipv6(&mapped_addr, ip4_addr); + has_ipv4_source_scope_flags |= dns_addr_get_scope(&mapped_addr); + has_source_precedence_label_flags |= dns_get_precedence_label(&mapped_addr); + } + + for (int i = 0; i < LWIP_IPV6_NUM_ADDRESSES; i++) { + const ip6_addr_t *ip6_addr = netif_ip6_addr(netif, i); + has_ipv6_source_scope_flags |= dns_addr_get_scope(ip6_addr); + has_source_precedence_label_flags |= dns_get_precedence_label(ip6_addr); + } + } + + // Rule 1: Avoid unusable destinations + // - If no IPv4, return IPv6 + if (has_ipv4_source_scope_flags == 0x0) { + return cand_list[0]; + } + + // Rule 2: Prefer matching scope + // - DNS is unlikely to return anything but global scope addresses, but we check anyway + // - Always have a link-local IPv6 address, so if destination is link-local there is a match + + // Note: We don't actually calculate the source address, just check if at least one + // of the source address (of the right type IPv6/IPv4) has a matching scope; + // The source address selection prioritises appropriate scope, so if we have some + // then one of them would be preferred and so the scope would match. + // (If we don't have any matching, then the preferred can't be matching) + + s8_t cand_0_scope = dns_addr_get_scope(cand_list[0]); + bool cand_0_matching_scope = (cand_0_scope & has_ipv6_source_scope_flags) != 0; + + s8_t cand_1_scope = dns_addr_get_scope(cand_list[0]); + bool cand_1_matching_scope = (cand_1_scope & has_ipv4_source_scope_flags) != 0; + + // - this is where it will usually bail if there is no public IPv4 address (if IPv4 link-local is enabled) + if (cand_0_matching_scope && !cand_1_matching_scope) { + return cand_list[0]; + } + // - this is where it will usually bail if there is no global or ULA IPv6 address (only link-local) + if (cand_1_matching_scope && !cand_0_matching_scope) { + return cand_list[1]; + } + + // Rule 3: Avoid deprecated addresses - not applicable + // Rule 4: Prefer home addresses - not applicable + + // Rule 5: Prefer matching label + + // Note: Similar to Rule 2, we don't actually calculate the source address, + // just check if we have at least one with a matching label. If we do, one of + // them would be preferred and matching; and if we don't there are not matching. + // IPv4 mapped is already it's own label, so not checked separately. + + s8_t cand_0_label = dns_get_precedence_label(cand_list[0]); + bool cand_0_matching_label = (cand_0_label & has_source_precedence_label_flags) != 0; + + // We will have already exited if we don't have an IPv4 address, + // so at this point the IPv4 label will always match. + s8_t cand_1_label = dns_get_precedence_label(cand_list[1]); + bool cand_1_matching_label = (cand_1_label & has_source_precedence_label_flags) != 0; + + // If the IPv6 labels don't match (e.g. general & general, or ULA & ULA), + // then IPv4 will win now. + if (cand_0_matching_label && !cand_1_matching_label) { + return cand_list[0]; + } + if (cand_1_matching_label && !cand_0_matching_label) { + return cand_list[1]; + } + + // Rule 6: Prefer higher precedence + + // If we have IPv6 general (source) & general (destination), + // then we use that, otherwise we use IPv4. + // Even though ULA & ULA passes rule 5, it is lower precedence + // so that won't matter. + s8_t cand_0_precedence = dns_precedence_for_label(cand_0_label); + s8_t cand_1_precedence = dns_precedence_for_label(cand_1_label); + + if (cand_0_precedence > cand_1_precedence) { + return cand_list[0]; + } + if (cand_1_precedence > cand_0_precedence) { + return cand_list[1]; + } + + // As we only have one of each destination address, + // the precedence will determine the result, + // and the remaining rules are not applicable. + + // Rule 7: Prefer native transport - not applicable + // Rule 8: Prefer smaller scope - not applicable + // Rule 9: Use longest matching prefix - not applicable + + // Rule 10: Otherwise, leave the order unchanged + return cand_list[0]; +} +#endif + #endif /* LWIP_DNS && LWIP_SOCKET */