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.
This commit is contained in:
Beniamino Galvani 2026-01-28 15:48:54 +01:00
parent 2499e896ee
commit 7c7528edaf
4 changed files with 123 additions and 30 deletions

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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);

View file

@ -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;
/**