From 7c7528edaf6401b87d32247ab790343ba26ac8de Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Wed, 28 Jan 2026 15:48:54 +0100 Subject: [PATCH] ndisc: track multiple PREF64 options Previously the NMNDisc instance always used the last received NAT64 prefix. If a network advertises multiple NAT64 prefixes, NetworkManager would constantly flip between them. Change this and keep a list of valid PREF64. Most importantly, stick with the same PREF64 unless a new one appears from a router with higher priority, or the current PREF64 expires. --- src/core/ndisc/nm-lndp-ndisc.c | 30 ++++----- src/core/ndisc/nm-ndisc-private.h | 3 +- src/core/ndisc/nm-ndisc.c | 106 ++++++++++++++++++++++++++++-- src/core/ndisc/nm-ndisc.h | 14 ++-- 4 files changed, 123 insertions(+), 30 deletions(-) diff --git a/src/core/ndisc/nm-lndp-ndisc.c b/src/core/ndisc/nm-lndp-ndisc.c index 69cb733a77..c004e80136 100644 --- a/src/core/ndisc/nm-lndp-ndisc.c +++ b/src/core/ndisc/nm-lndp-ndisc.c @@ -162,7 +162,6 @@ receive_ra(struct ndp *ndp, struct ndp_msg *msg, gpointer user_data) int offset; int hop_limit; guint32 val; - gboolean pref64_found = FALSE; /* Router discovery is subject to the following RFC documents: * @@ -404,25 +403,22 @@ receive_ra(struct ndp *ndp, struct ndp_msg *msg, gpointer user_data) /* PREF64 */ ndp_msg_opt_for_each_offset (offset, msg, NDP_MSG_OPT_PREF64) { - struct in6_addr pref64_prefix = *ndp_msg_opt_pref64_prefix(msg, offset); - guint8 pref64_length = ndp_msg_opt_pref64_prefix_length(msg, offset); - gint64 expiry_msec = - _nm_ndisc_lifetime_to_expiry(now_msec, ndp_msg_opt_pref64_lifetime(msg, offset)); + NMNDiscPref64 pref64; + + pref64 = (NMNDiscPref64) { + .prefix = *ndp_msg_opt_pref64_prefix(msg, offset), + .plen = ndp_msg_opt_pref64_prefix_length(msg, offset), + .gateway = gateway.address, + .gateway_preference = gateway.preference, + .expiry_msec = + _nm_ndisc_lifetime_to_expiry(now_msec, ndp_msg_opt_pref64_lifetime(msg, offset)), + .gateway_expiry_msec = gateway.expiry_msec, + }; /* libndp should only return lengths defined in RFC 8781 */ - nm_assert(NM_IN_SET(pref64_length, 96, 64, 56, 48, 40, 32)); + nm_assert(NM_IN_SET(pref64.plen, 96, 64, 56, 48, 40, 32)); - /* Newer RA has more up to date information, prefer it: */ - if (!pref64_found) { - pref64_found = TRUE; - rdata->public.pref64.expiry_msec = 0; - } - - if (expiry_msec >= rdata->public.pref64.expiry_msec) { - rdata->public.pref64.network = pref64_prefix; - rdata->public.pref64.expiry_msec = expiry_msec; - rdata->public.pref64.plen = pref64_length; - rdata->public.pref64.valid = TRUE; + if (nm_ndisc_add_pref64(ndisc, &pref64, now_msec)) { changed |= NM_NDISC_CONFIG_PREF64; } } diff --git a/src/core/ndisc/nm-ndisc-private.h b/src/core/ndisc/nm-ndisc-private.h index 697700eb26..90ec032d77 100644 --- a/src/core/ndisc/nm-ndisc-private.h +++ b/src/core/ndisc/nm-ndisc-private.h @@ -14,7 +14,7 @@ struct _NMNDiscDataInternal { NMNDiscData public; GArray *gateways; GArray *addresses; - GArray *clat_addresses; + GArray *pref64; GArray *routes; GArray *dns_servers; GArray *dns_domains; @@ -29,6 +29,7 @@ gboolean nm_ndisc_add_gateway(NMNDisc *ndisc, const NMNDiscGateway *new_item, gi gboolean nm_ndisc_complete_and_add_address(NMNDisc *ndisc, const NMNDiscAddress *new_item, gint64 now_msec); gboolean nm_ndisc_add_route(NMNDisc *ndisc, const NMNDiscRoute *new_item, gint64 now_msec); +gboolean nm_ndisc_add_pref64(NMNDisc *ndisc, const NMNDiscPref64 *new_item, gint64 now_msec); gboolean nm_ndisc_add_dns_server(NMNDisc *ndisc, const NMNDiscDNSServer *new_item, gint64 now_msec); gboolean nm_ndisc_add_dns_domain(NMNDisc *ndisc, const NMNDiscDNSDomain *new_item, gint64 now_msec); diff --git a/src/core/ndisc/nm-ndisc.c b/src/core/ndisc/nm-ndisc.c index bbd2a14f9a..ed34f088a0 100644 --- a/src/core/ndisc/nm-ndisc.c +++ b/src/core/ndisc/nm-ndisc.c @@ -34,6 +34,7 @@ #define _SIZE_MAX_ROUTES 1000u #define _SIZE_MAX_DNS_SERVERS 64u #define _SIZE_MAX_DNS_DOMAINS 64u +#define _SIZE_MAX_PREF64 8u /*****************************************************************************/ @@ -212,8 +213,8 @@ nm_ndisc_data_to_l3cd(NMDedupMultiIndex *multi_idx, for (i = 0; i < rdata->dns_domains_n; i++) nm_l3_config_data_add_search(l3cd, AF_INET6, rdata->dns_domains[i].domain); - if (rdata->pref64.valid) { - nm_l3_config_data_set_pref64(l3cd, rdata->pref64.network, rdata->pref64.plen); + if (rdata->pref64_n > 0) { + nm_l3_config_data_set_pref64(l3cd, rdata->pref64[0].prefix, rdata->pref64[0].plen); } else { nm_l3_config_data_set_pref64_valid(l3cd, FALSE); } @@ -425,6 +426,7 @@ _data_complete(NMNDiscDataInternal *data) _SET(data, gateways); _SET(data, addresses); _SET(data, routes); + _SET(data, pref64); _SET(data, dns_servers); _SET(data, dns_domains); #undef _SET @@ -770,6 +772,59 @@ nm_ndisc_add_route(NMNDisc *ndisc, const NMNDiscRoute *new_item, gint64 now_msec return TRUE; } +gboolean +nm_ndisc_add_pref64(NMNDisc *ndisc, const NMNDiscPref64 *new_item, gint64 now_msec) +{ + NMNDiscDataInternal *rdata = &NM_NDISC_GET_PRIVATE(ndisc)->rdata; + guint i; + guint insert_idx = G_MAXUINT; + + for (i = 0; i < rdata->pref64->len;) { + NMNDiscPref64 *item = &nm_g_array_index(rdata->pref64, NMNDiscPref64, i); + + if (item->plen == new_item->plen && IN6_ARE_ADDR_EQUAL(&item->prefix, &new_item->prefix) + && IN6_ARE_ADDR_EQUAL(&item->gateway, &new_item->gateway)) { + if (new_item->expiry_msec <= now_msec) { + g_array_remove_index(rdata->pref64, i); + return TRUE; + } + + if (item->gateway_preference != new_item->gateway_preference) { + g_array_remove_index(rdata->pref64, i); + continue; + } + + item->gateway_expiry_msec = new_item->gateway_expiry_msec; + + if (item->expiry_msec == new_item->expiry_msec) + return FALSE; + + item->expiry_msec = new_item->expiry_msec; + return TRUE; + } + + /* Put before less preferable gateways. */ + if (_preference_to_priority(item->gateway_preference) + < _preference_to_priority(new_item->gateway_preference) + && insert_idx == G_MAXUINT) + insert_idx = i; + + i++; + } + + if (rdata->pref64->len >= _SIZE_MAX_PREF64) + return FALSE; + + if (new_item->expiry_msec <= now_msec) + return FALSE; + + g_array_insert_val(rdata->pref64, + insert_idx == G_MAXUINT ? rdata->pref64->len : insert_idx, + *new_item); + + return TRUE; +} + gboolean nm_ndisc_add_dns_server(NMNDisc *ndisc, const NMNDiscDNSServer *new_item, gint64 now_msec) { @@ -1410,6 +1465,17 @@ _config_changed_log(NMNDisc *ndisc, NMNDiscConfigMap changed) nm_icmpv6_router_pref_to_string(route->preference, str_pref, sizeof(str_pref)), get_exp(str_exp, now_msec, route)); } + for (i = 0; i < rdata->pref64->len; i++) { + const NMNDiscPref64 *pref64 = &nm_g_array_index(rdata->pref64, NMNDiscPref64, i); + char addrstr2[NM_INET_ADDRSTRLEN]; + + _LOGD(" pref64 %s/%u via %s exp %s", + nm_inet6_ntop(&pref64->prefix, addrstr), + pref64->plen, + nm_inet6_ntop(&pref64->gateway, addrstr2), + get_exp(str_exp, now_msec, pref64)); + } + for (i = 0; i < rdata->dns_servers->len; i++) { const NMNDiscDNSServer *dns_server = &nm_g_array_index(rdata->dns_servers, NMNDiscDNSServer, i); @@ -1538,13 +1604,39 @@ static void clean_pref64(NMNDisc *ndisc, gint64 now_msec, NMNDiscConfigMap *changed, gint64 *next_msec) { NMNDiscDataInternal *rdata = &NM_NDISC_GET_PRIVATE(ndisc)->rdata; + NMNDiscPref64 *arr; + guint i; + guint j; - if (!rdata->public.pref64.valid) + if (rdata->pref64->len == 0) return; - if (!expiry_next(now_msec, rdata->public.pref64.expiry_msec, next_msec)) { - rdata->public.pref64.valid = FALSE; - *changed |= NM_NDISC_CONFIG_PREF64; + + arr = &nm_g_array_first(rdata->pref64, NMNDiscPref64); + + for (i = 0, j = 0; i < rdata->pref64->len; i++) { + if (!expiry_next(now_msec, arr[i].expiry_msec, next_msec) + || !expiry_next(now_msec, + arr[i].gateway_expiry_msec, + next_msec)) { /* no gateway no party */ + if (i == 0) { + /* Emit the changed signal only when the first PREF64 expires, + * because only the first item is exported into the l3cd. Changes + * in other PREF64s are not relevant. */ + *changed |= NM_NDISC_CONFIG_PREF64; + } + continue; + } + + if (i != j) + arr[j] = arr[i]; + j++; } + + if (i != j) { + g_array_set_size(rdata->pref64, j); + } + + _array_set_size_max(rdata->pref64, _SIZE_MAX_PREF64); } static void @@ -1943,6 +2035,7 @@ nm_ndisc_init(NMNDisc *ndisc) rdata->gateways = g_array_new(FALSE, FALSE, sizeof(NMNDiscGateway)); rdata->addresses = g_array_new(FALSE, FALSE, sizeof(NMNDiscAddress)); rdata->routes = g_array_new(FALSE, FALSE, sizeof(NMNDiscRoute)); + rdata->pref64 = g_array_new(FALSE, FALSE, sizeof(NMNDiscPref64)); rdata->dns_servers = g_array_new(FALSE, FALSE, sizeof(NMNDiscDNSServer)); rdata->dns_domains = g_array_new(FALSE, FALSE, sizeof(NMNDiscDNSDomain)); g_array_set_clear_func(rdata->dns_domains, dns_domain_free); @@ -1975,6 +2068,7 @@ finalize(GObject *object) g_array_unref(rdata->gateways); g_array_unref(rdata->addresses); g_array_unref(rdata->routes); + g_array_unref(rdata->pref64); g_array_unref(rdata->dns_servers); g_array_unref(rdata->dns_domains); diff --git a/src/core/ndisc/nm-ndisc.h b/src/core/ndisc/nm-ndisc.h index 416c319117..5b2efde569 100644 --- a/src/core/ndisc/nm-ndisc.h +++ b/src/core/ndisc/nm-ndisc.h @@ -120,10 +120,12 @@ typedef struct _NMNDiscRoute { } NMNDiscRoute; typedef struct _NMNDiscPref64 { - struct in6_addr network; - gint64 expiry_msec; - guint8 plen; - bool valid : 1; + struct in6_addr prefix; + struct in6_addr gateway; + gint64 expiry_msec; + gint64 gateway_expiry_msec; + NMIcmpv6RouterPref gateway_preference; + guint8 plen; } NMNDiscPref64; typedef struct { @@ -196,16 +198,16 @@ typedef struct { guint gateways_n; guint addresses_n; guint routes_n; + guint pref64_n; guint dns_servers_n; guint dns_domains_n; const NMNDiscGateway *gateways; const NMNDiscAddress *addresses; const NMNDiscRoute *routes; + const NMNDiscPref64 *pref64; const NMNDiscDNSServer *dns_servers; const NMNDiscDNSDomain *dns_domains; - - NMNDiscPref64 pref64; } NMNDiscData; /**