diff --git a/src/core/nm-l3cfg.c b/src/core/nm-l3cfg.c index 59fc6e4d7f..72288def8c 100644 --- a/src/core/nm-l3cfg.c +++ b/src/core/nm-l3cfg.c @@ -4140,6 +4140,53 @@ update_routes: } } +/** + * _clat_prefix_is_better: + * @best: current best candidate (or %NULL) + * @candidate: the new candidate prefix + * @nat64_pref: the NAT64 prefix + * + * Compare two SLAAC candidate prefixes to be used for CLAT, + * as recommended by draft-ietf-v6ops-claton Section 7. Apply + * rules 6 and 8 of the source address selection algorithm from + * RFC 6724, Section 5. + * + * Returns %TRUE if @candidate is better than @best. + */ +static gboolean +_clat_prefix_is_better(const NMPlatformIP6Address *best, + const NMPlatformIP6Address *candidate, + const struct in6_addr *nat64_pref) +{ + guint nat64_pref_label; + gboolean best_label_match; + gboolean cand_label_match; + guint best_prefix_len; + guint cand_prefix_len; + + if (!best) + return TRUE; + + /* Rule 6: prefer the address whose RFC 6724 label matches + * the label of the NAT64 prefix. */ + nat64_pref_label = nm_ip6_addr_rfc6724_label(nat64_pref); + best_label_match = nm_ip6_addr_rfc6724_label(&best->address) == nat64_pref_label; + cand_label_match = nm_ip6_addr_rfc6724_label(&candidate->address) == nat64_pref_label; + + if (cand_label_match && !best_label_match) + return TRUE; + else if (best_label_match && !cand_label_match) + return FALSE; + + /* Rule 8: longest matching prefix with the NAT64 prefix. */ + best_prefix_len = nm_ip6_addr_common_prefix_len(&best->address, nat64_pref); + cand_prefix_len = nm_ip6_addr_common_prefix_len(&candidate->address, nat64_pref); + if (cand_prefix_len != best_prefix_len) + return cand_prefix_len > best_prefix_len; + + return FALSE; +} + static void _l3cfg_update_clat_config(NML3Cfg *self, NML3ConfigData *l3cd, @@ -4206,29 +4253,37 @@ _l3cfg_update_clat_config(NML3Cfg *self, network_id = nm_l3_config_data_get_network_id(l3cd); if (!self->priv.p->clat_address_6_valid && network_id) { + const NMPlatformIP6Address *best_prefix = NULL; + + /* Select the best SLAAC prefix for the CLAT address per + * draft-ietf-v6ops-claton-14 Section 7 */ nm_l3_config_data_iter_ip6_address_for_each (&iter, l3cd, &ip6_entry) { if (ip6_entry->addr_source == NM_IP_CONFIG_SOURCE_NDISC && ip6_entry->plen == 64) { - ip6 = ip6_entry->address; - - nm_utils_ipv6_addr_set_stable_privacy(NM_UTILS_STABLE_TYPE_CLAT, - &ip6, - nm_l3cfg_get_ifname(self, TRUE), - network_id, - 0); - self->priv.p->clat_address_6 = (NMPlatformIP6Address) { - .ifindex = self->priv.ifindex, - .address = ip6, - .peer_address = ip6, - .addr_source = NM_IP_CONFIG_SOURCE_CLAT, - .plen = ip6_entry->plen, - }; - - _LOGT("clat: using IPv6 address %s", nm_inet6_ntop(&ip6, buf)); - - self->priv.p->clat_address_6_valid = TRUE; - break; + if (_clat_prefix_is_better(best_prefix, ip6_entry, &pref64)) + best_prefix = ip6_entry; } } + + if (best_prefix) { + ip6 = best_prefix->address; + + nm_utils_ipv6_addr_set_stable_privacy(NM_UTILS_STABLE_TYPE_CLAT, + &ip6, + nm_l3cfg_get_ifname(self, TRUE), + network_id, + 0); + self->priv.p->clat_address_6 = (NMPlatformIP6Address) { + .ifindex = self->priv.ifindex, + .address = ip6, + .peer_address = ip6, + .addr_source = NM_IP_CONFIG_SOURCE_CLAT, + .plen = best_prefix->plen, + }; + + _LOGT("clat: using IPv6 address %s", nm_inet6_ntop(&ip6, buf)); + + self->priv.p->clat_address_6_valid = TRUE; + } } /* Don't get a v4 address if we have no v6 address (otherwise, we could diff --git a/src/libnm-glib-aux/nm-inet-utils.c b/src/libnm-glib-aux/nm-inet-utils.c index f4f418cf18..ead060bc39 100644 --- a/src/libnm-glib-aux/nm-inet-utils.c +++ b/src/libnm-glib-aux/nm-inet-utils.c @@ -167,6 +167,92 @@ nm_ip6_addr_is_ula(const struct in6_addr *address) return (address->s6_addr32[0] & htonl(0xfe000000u)) == htonl(0xfc000000u); } +/** + * nm_ip6_addr_common_prefix_len: + * @a: first IPv6 address + * @b: second IPv6 address + * + * Returns: the number of leading bits that @a and @b have in common, + * from 0 to 128. + */ +guint +nm_ip6_addr_common_prefix_len(const struct in6_addr *a, const struct in6_addr *b) +{ + guint i; + + for (i = 0; i < 16; i++) { + guint8 diff = a->s6_addr[i] ^ b->s6_addr[i]; + + if (diff != 0) + return i * 8u + __builtin_clz((guint) diff) - 24u; + } + return 128; +} + +/** + * nm_ip6_addr_rfc6724_label: + * @addr: an IPv6 address + * + * Returns the label for @addr from the default policy table defined + * in RFC 6724, Section 2.1: + * + * Prefix Precedence Label + * ::1/128 50 0 + * ::/0 40 1 + * ::ffff:0:0/96 35 4 + * 2002::/16 30 2 + * 2001::/32 5 5 + * fc00::/7 3 13 + * ::/96 1 3 + * fec0::/10 1 11 + * 3ffe::/16 1 12 + * + * Returns: the label value (0-13). It can be used in the Source + * Address Selection algorithm to prefer a source whose label + * matches with the label of the destination. + */ +guint +nm_ip6_addr_rfc6724_label(const struct in6_addr *addr) +{ + /* Checked from most-specific to least-specific prefix length. */ + + /* ::1/128 (loopback) */ + if (IN6_IS_ADDR_LOOPBACK(addr)) + return 0; + + /* ::ffff:0:0/96 (IPv4-mapped) */ + if (IN6_IS_ADDR_V4MAPPED(addr)) + return 4; + + /* ::/96 (IPv4-compatible, deprecated) */ + if (addr->s6_addr32[0] == 0 && addr->s6_addr32[1] == 0 && addr->s6_addr32[2] == 0 + && !IN6_IS_ADDR_UNSPECIFIED(addr)) + return 3; + + /* 2001::/32 (Teredo) */ + if (addr->s6_addr32[0] == htonl(0x20010000u)) + return 5; + + /* 2002::/16 (6to4) */ + if ((addr->s6_addr32[0] & htonl(0xFFFF0000u)) == htonl(0x20020000u)) + return 2; + + /* 3ffe::/16 (deprecated 6bone) */ + if ((addr->s6_addr32[0] & htonl(0xFFFF0000u)) == htonl(0x3FFE0000u)) + return 12; + + /* fec0::/10 (deprecated site-local) */ + if ((addr->s6_addr32[0] & htonl(0xFFC00000u)) == htonl(0xFEC00000u)) + return 11; + + /* fc00::/7 (ULA) */ + if (nm_ip6_addr_is_ula(addr)) + return 13; + + /* ::/0 (default) */ + return 1; +} + /*****************************************************************************/ gconstpointer diff --git a/src/libnm-glib-aux/nm-inet-utils.h b/src/libnm-glib-aux/nm-inet-utils.h index 999519f890..ab4361e63b 100644 --- a/src/libnm-glib-aux/nm-inet-utils.h +++ b/src/libnm-glib-aux/nm-inet-utils.h @@ -280,6 +280,10 @@ nm_ip_addr_same_prefix(int addr_family, gconstpointer addr_a, gconstpointer addr gboolean nm_ip_addr_is_site_local(int addr_family, const void *address); gboolean nm_ip6_addr_is_ula(const struct in6_addr *address); +guint nm_ip6_addr_common_prefix_len(const struct in6_addr *a, const struct in6_addr *b); + +guint nm_ip6_addr_rfc6724_label(const struct in6_addr *addr); + /*****************************************************************************/ #define NM_IPV4LL_NETWORK ((in_addr_t) htonl(0xA9FE0000lu)) /* 169.254.0.0 */ diff --git a/src/libnm-glib-aux/tests/test-shared-general.c b/src/libnm-glib-aux/tests/test-shared-general.c index 931c25c667..95255c7e88 100644 --- a/src/libnm-glib-aux/tests/test-shared-general.c +++ b/src/libnm-glib-aux/tests/test-shared-general.c @@ -2380,6 +2380,60 @@ test_inet_utils(void) /*****************************************************************************/ +static void +test_ip6_addr_common_prefix_len(void) +{ + struct in6_addr a; + struct in6_addr b; + + /* identical addresses -> 128 */ + a = nmtst_inet6_from_string("2001:db8::1"); + b = nmtst_inet6_from_string("2001:db8::1"); + g_assert_cmpuint(nm_ip6_addr_common_prefix_len(&a, &b), ==, 128); + + /* completely different -> 0 */ + a = nmtst_inet6_from_string("8000::"); + b = nmtst_inet6_from_string("::"); + g_assert_cmpuint(nm_ip6_addr_common_prefix_len(&a, &b), ==, 0); + + /* first 64 bits in common, differ at bit 65 */ + a = nmtst_inet6_from_string("2001:db8:abcd:1234:8000::"); + b = nmtst_inet6_from_string("2001:db8:abcd:1234::"); + g_assert_cmpuint(nm_ip6_addr_common_prefix_len(&a, &b), ==, 64); + + /* same /48 prefix */ + a = nmtst_inet6_from_string("2001:db8:abcd::"); + b = nmtst_inet6_from_string("2001:db8:abcd:ffff::"); + g_assert_cmpuint(nm_ip6_addr_common_prefix_len(&a, &b), ==, 48); + + /* differ in 5th bit -> 4 common bits */ + a = nmtst_inet6_from_string("f800::"); + b = nmtst_inet6_from_string("f000::"); + g_assert_cmpuint(nm_ip6_addr_common_prefix_len(&a, &b), ==, 4); + + /* differ in 2nd bit -> 1 common bit */ + a = nmtst_inet6_from_string("c000::"); + b = nmtst_inet6_from_string("8000::"); + g_assert_cmpuint(nm_ip6_addr_common_prefix_len(&a, &b), ==, 1); + + /* both zero -> 128 */ + a = nmtst_inet6_from_string("::"); + b = nmtst_inet6_from_string("::"); + g_assert_cmpuint(nm_ip6_addr_common_prefix_len(&a, &b), ==, 128); + + /* first 120 bits in common, differ at MSB of last byte */ + a = nmtst_inet6_from_string("2001:db8::80"); + b = nmtst_inet6_from_string("2001:db8::"); + g_assert_cmpuint(nm_ip6_addr_common_prefix_len(&a, &b), ==, 120); + + /* first 127 bits in common, differ only in last bit */ + a = nmtst_inet6_from_string("2001:db8::fe"); + b = nmtst_inet6_from_string("2001:db8::ff"); + g_assert_cmpuint(nm_ip6_addr_common_prefix_len(&a, &b), ==, 127); +} + +/*****************************************************************************/ + static gboolean _inet_parse(int addr_family, const char *str, gboolean accept_legacy, gpointer out_addr) { @@ -2940,6 +2994,7 @@ main(int argc, char **argv) g_test_add_func("/general/test_path_simplify", test_path_simplify); g_test_add_func("/general/test_hostname_is_valid", test_hostname_is_valid); g_test_add_func("/general/test_inet_utils", test_inet_utils); + g_test_add_func("/general/test_ip6_addr_common_prefix_len", test_ip6_addr_common_prefix_len); g_test_add_func("/general/test_inet_parse_ip4_legacy", test_inet_parse_ip4_legacy); g_test_add_func("/general/test_garray", test_garray); g_test_add_func("/general/test_nm_prioq", test_nm_prioq);