diff --git a/src/api/netdb.c b/src/api/netdb.c index c6a4c9af9..2233524a3 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. @@ -329,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_LWIP - 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++; + } + 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; } @@ -428,4 +470,324 @@ 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: + * Rule 1: not implemented + * - Rule 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. + LWIP_DEBUGF(DNS_DEBUG, ("dns_select: selecting from %d candidates\n", cand_list_length)); + + // 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 |= 0x1 << dns_addr_get_scope(&mapped_addr); + has_source_precedence_label_flags |= 0x1 << 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); + if (!ip6_addr_isany_val(*ip6_addr)) { + has_ipv6_source_scope_flags |= 0x1 << dns_addr_get_scope(ip6_addr); + has_source_precedence_label_flags |= 0x1 << dns_get_precedence_label(ip6_addr); + } + } + } + + LWIP_DEBUGF(DNS_DEBUG, ("dns_select: precedence labels flags 0x%04x, ipv6 scopes flags 0x%04x, ipv4 scopes flags 0x%04x\n", + (unsigned int)has_source_precedence_label_flags, (unsigned int)has_ipv6_source_scope_flags, + (unsigned int)has_ipv4_source_scope_flags)); + + // Rule 1: Avoid unusable destinations - not implemented + + // 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 = (0x1 << 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 = (0x1 << cand_1_scope & has_ipv4_source_scope_flags) != 0; + + LWIP_DEBUGF(DNS_DEBUG, ("dns_select: rule 2, cand_0 scope (%d) match %d, cand_1 scope (%d) match %d\n", + cand_0_scope, cand_0_matching_scope, cand_1_scope, cand_1_matching_scope)); + + // - 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 = (0x1 << 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 = (0x1 << cand_1_label & has_source_precedence_label_flags) != 0; + + LWIP_DEBUGF(DNS_DEBUG, ("dns_select: rule 5, cand_0 label (%d) match %d, cand_1 label (%d) match %d\n", + cand_0_label, cand_0_matching_label, cand_1_label, cand_1_matching_label)); + + // 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); + + LWIP_DEBUGF(DNS_DEBUG, ("dns_select: rule 6, cand_0 precedence %d, cand_1 precedence %d\n", + cand_0_precedence, cand_1_precedence)); + + 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 */