From 1589346de4f6f3a377795733548447c5163b5f0b Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Sat, 7 Mar 2026 10:54:37 +0100 Subject: [PATCH 1/3] libnm-glib-aux: add nm_ip6_addr_common_prefix_len() --- src/libnm-glib-aux/nm-inet-utils.c | 22 ++++++++ src/libnm-glib-aux/nm-inet-utils.h | 2 + .../tests/test-shared-general.c | 55 +++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/src/libnm-glib-aux/nm-inet-utils.c b/src/libnm-glib-aux/nm-inet-utils.c index f4f418cf18..32f3ca54c9 100644 --- a/src/libnm-glib-aux/nm-inet-utils.c +++ b/src/libnm-glib-aux/nm-inet-utils.c @@ -167,6 +167,28 @@ 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; +} + /*****************************************************************************/ gconstpointer diff --git a/src/libnm-glib-aux/nm-inet-utils.h b/src/libnm-glib-aux/nm-inet-utils.h index 999519f890..050562f8ac 100644 --- a/src/libnm-glib-aux/nm-inet-utils.h +++ b/src/libnm-glib-aux/nm-inet-utils.h @@ -280,6 +280,8 @@ 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); + /*****************************************************************************/ #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); From 7f2745f3b1301e65126611d4ff5bd5e2305f7f99 Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Sat, 7 Mar 2026 10:55:26 +0100 Subject: [PATCH 2/3] libnm-glib-aux: add nm_ip6_addr_rfc6724_label() --- src/libnm-glib-aux/nm-inet-utils.c | 64 ++++++++++++++++++++++++++++++ src/libnm-glib-aux/nm-inet-utils.h | 2 + 2 files changed, 66 insertions(+) diff --git a/src/libnm-glib-aux/nm-inet-utils.c b/src/libnm-glib-aux/nm-inet-utils.c index 32f3ca54c9..ead060bc39 100644 --- a/src/libnm-glib-aux/nm-inet-utils.c +++ b/src/libnm-glib-aux/nm-inet-utils.c @@ -189,6 +189,70 @@ nm_ip6_addr_common_prefix_len(const struct in6_addr *a, const struct in6_addr *b 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 050562f8ac..ab4361e63b 100644 --- a/src/libnm-glib-aux/nm-inet-utils.h +++ b/src/libnm-glib-aux/nm-inet-utils.h @@ -282,6 +282,8 @@ 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 */ From a03a2458197f00a7aa9527cef42e5e637840edf2 Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Sat, 7 Mar 2026 10:56:04 +0100 Subject: [PATCH 3/3] l3cfg: fix selection of the CLAT IPv6 prefix If the router advertises both ULA and GUA prefixes, the CLAT should select the one that better matches the NAT64 prefix when generating the additional IPv6 address, as recommended by Internet Draft draft-ietf-v6ops-claton. The current implementation just takes the first one, which can cause problems. For example, if the network is using a public NAT64 server, the NAT64 prefix is in the GUA range. Choosing a ULA as source address would not work. Fixes: f0e77a43542c ('Add support for CLAT to l3cfg') --- src/core/nm-l3cfg.c | 93 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 74 insertions(+), 19 deletions(-) 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